Compare commits
183 Commits
4da80c7089
...
lavoce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2163cec5e | ||
|
|
d5866ceae4 | ||
| 6fdb259246 | |||
| 0aad43c751 | |||
| d988d47e30 | |||
|
|
e08eaaa43e | ||
|
|
f6ca54f5b4 | ||
|
|
a9b8053bd8 | ||
| 600c101c68 | |||
|
|
14a6884f43 | ||
|
|
8e151ffe0b | ||
| 8f64843613 | |||
|
|
fe8f6d1002 | ||
|
|
5281e288a9 | ||
|
|
4ed05cc640 | ||
|
|
14e97fead2 | ||
|
|
0935d7c9f5 | ||
|
|
3266641f81 | ||
|
|
739c55c0bc | ||
|
|
5b4485d20d | ||
| 98057beb0f | |||
|
|
b342289888 | ||
| d03bbf2a41 | |||
|
|
ec094b8f55 | ||
| b6d941ba2d | |||
| 167abcaa43 | |||
| beb8ccbaee | |||
| 797f6c2383 | |||
| 016c77a586 | |||
| 36ebab7f9a | |||
| a5b1fbef74 | |||
|
|
cb0c53daea | ||
| 978e020305 | |||
| 4508738958 | |||
| eb23612444 | |||
| bee196e299 | |||
| d19f555c7c | |||
| 1d7253f9a1 | |||
| d8a1878ab1 | |||
| e4af2d6e18 | |||
| 8cf21643ea | |||
|
|
6b75f6f4b9 | ||
|
|
dc78add71d | ||
| 1ce922ff4c | |||
| 3a4b0f0748 | |||
| 4bffbb3798 | |||
| b9cdfcb1e9 | |||
| 49ba00d886 | |||
| cf1ccb0fd0 | |||
| fb790e5e37 | |||
| ea3adf40cc | |||
| 2ff50342e8 | |||
| 1f8ee62721 | |||
| 96d6367dbd | |||
| 8afff23ffe | |||
| 512282f367 | |||
| 4fab5df300 | |||
| 9e8191f8f8 | |||
| 13255f9713 | |||
| e23215b6c1 | |||
| a014d6b370 | |||
| 3225a0865e | |||
| 5703ff0e8d | |||
| 03be3a6a99 | |||
| fe5f081b92 | |||
| acaf1b3946 | |||
| 147171373c | |||
| f22e120204 | |||
| 1bc98de564 | |||
| 991a3eaa66 | |||
| 7a5a9aafd1 | |||
| 0694497f8d | |||
| c82d6d39c1 | |||
| edf20050db | |||
| 2e98dc168a | |||
| 1797058526 | |||
| 1c2ddca9d4 | |||
| 61ca7249cd | |||
| a98edbe658 | |||
| fbc5473f2b | |||
| 55a47c3a25 | |||
| 94e011e5c7 | |||
| db9b40f2fc | |||
| 5fdfb47f9e | |||
| 55c50f6f7f | |||
| ed4570e8dd | |||
| 572042ab53 | |||
| afcb85a323 | |||
| 14f8a5d472 | |||
| 309d191bce | |||
| 7e5105392c | |||
| 7e16bf63aa | |||
| 3e384f89b1 | |||
| b05e3fe5d9 | |||
| 1986368c1c | |||
| 1cd9cf765c | |||
| 908788f41d | |||
| 899695f548 | |||
| 7d2b18a94d | |||
| 1eab3fe845 | |||
| c4f290bfcb | |||
| f304a28493 | |||
| 73b5cd6e97 | |||
| 2d0c28bc48 | |||
| 1413d0ef33 | |||
| fde71818e2 | |||
| 85017cd88c | |||
| f1c7ae5e20 | |||
| 5989948bf9 | |||
| 6a21b65808 | |||
| b5fbf2f745 | |||
| 3b7ba28053 | |||
| f4caac55e6 | |||
| 34e38b3969 | |||
| 3198b71f7e | |||
| 8405568e85 | |||
| ecf59fa9c6 | |||
| 0e8078c29f | |||
| 1d06963f67 | |||
| 8de195d961 | |||
| da9cf0d554 | |||
| 8cf5878d46 | |||
| 7dd38aa50c | |||
| b2bcaa6b5f | |||
| 5822dbbc82 | |||
| de8f0ba2b6 | |||
| 08f8c4708f | |||
| 85db9e0a52 | |||
| 0916ea7103 | |||
| 5952858dca | |||
| 7064ec8587 | |||
| d2f9d6aacc | |||
| 7ef6d71be8 | |||
| d955cc7942 | |||
| 2ac707611f | |||
| 71f42f4149 | |||
| 4544e33d01 | |||
| 114ef96de8 | |||
| 259ee474aa | |||
| da14ed4e74 | |||
| 3738adf85a | |||
| 6b727c84d8 | |||
| 9a483aa873 | |||
| 1d408ef3c1 | |||
| e8c3f259bf | |||
| 7050d7ca84 | |||
| fd361f21cf | |||
| 47f7c7b682 | |||
| 39d8be10cc | |||
| 5a8e2dee2f | |||
| a86795fdf6 | |||
| a3e5fdd138 | |||
| 2abed31bde | |||
| c3fadb9382 | |||
| 7eabb2c7c8 | |||
| 50d040953f | |||
| dd874cbe9c | |||
| f2b652abe3 | |||
| b5c1888153 | |||
| cf1ad6d511 | |||
| a3d24a9426 | |||
| 3873ba5285 | |||
| 034cf636f9 | |||
| bddd249e07 | |||
| 1c3f80bc26 | |||
| 9ac2942ace | |||
| 5a1bd4e16f | |||
| 8c9ef41704 | |||
| 4f518dba9c | |||
| 893852f929 | |||
| 3a057f7ef0 | |||
| 1189115359 | |||
| 15d836c627 | |||
| 5baaf14bd9 | |||
| 873434ff84 | |||
| e9d047fdf3 | |||
| 4079466deb | |||
| 98e5ed250c | |||
| f52d61da62 | |||
| 6e256e3c42 | |||
| 784ffc5e87 | |||
| cb98d91577 | |||
| 988dcda0e2 |
@@ -22,7 +22,8 @@
|
||||
"exceljs": "^4.4.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^3.0.1",
|
||||
"jspdf": "^3.0.4",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"mqtt": "^5.14.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.2.0",
|
||||
@@ -30,6 +31,7 @@
|
||||
"react-icons": "^4.11.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-svg": "^16.3.0",
|
||||
"recharts": "^3.6.0",
|
||||
"sweetalert2": "^11.17.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
BIN
public/assets/defaultSparepartImg.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/assets/pupuk-indonesia-1.png
Normal file
|
After Width: | Height: | Size: 309 KiB |
BIN
public/assets/pupuk-indonesia-2.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
97
src/App.jsx
@@ -10,39 +10,50 @@ import Home from './pages/home/Home';
|
||||
import Blank from './pages/blank/Blank';
|
||||
|
||||
// Master
|
||||
import IndexDevice from './pages/master/device/IndexDevice';
|
||||
import IndexTag from './pages/master/tag/IndexTag';
|
||||
import IndexUnit from './pages/master/unit/IndexUnit';
|
||||
import IndexPlantSubSection from './pages/master/plantSubSection/IndexPlantSubSection';
|
||||
import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice';
|
||||
import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice';
|
||||
import IndexPlantSection from './pages/master/plantSection/IndexPlantSection';
|
||||
import IndexDevice from './pages/master/device/IndexDevice';
|
||||
import IndexUnit from './pages/master/unit/IndexUnit';
|
||||
import IndexTag from './pages/master/tag/IndexTag';
|
||||
import IndexStatus from './pages/master/status/IndexStatus';
|
||||
import IndexSparepart from './pages/master/sparepart/IndexSparepart';
|
||||
import IndexShift from './pages/master/shift/IndexShift';
|
||||
// Brand device
|
||||
import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice';
|
||||
import EditBrandDevice from './pages/master/brandDevice/EditBrandDevice';
|
||||
import ViewBrandDevice from './pages/master/brandDevice/ViewBrandDevice';
|
||||
import ViewFilePage from './pages/master/brandDevice/ViewFilePage';
|
||||
|
||||
// Jadwal Shift
|
||||
import IndexJadwalShift from './pages/jadwalShift/IndexJadwalShift';
|
||||
|
||||
// History
|
||||
import IndexTrending from './pages/history/trending/IndexTrending';
|
||||
import IndexReport from './pages/history/report/IndexReport';
|
||||
import IndexTrending from './pages/report/trending/IndexTrending';
|
||||
import IndexReport from './pages/report/report/IndexReport';
|
||||
|
||||
// Other Pages
|
||||
import IndexNotification from './pages/notification/IndexNotification';
|
||||
import IndexEventAlarm from './pages/eventAlarm/IndexEventAlarm';
|
||||
import IndexRole from './pages/role/IndexRole';
|
||||
import IndexUser from './pages/user/IndexUser';
|
||||
|
||||
// Shift Management
|
||||
import IndexMember from './pages/shiftManagement/member/IndexMember';
|
||||
import IndexContact from './pages/contact/IndexContact';
|
||||
import DetailNotificationTab from './pages/notificationDetail/IndexNotificationDetail';
|
||||
import IndexVerificationSparepart from './pages/verificationSparepart/IndexVerificationSparepart';
|
||||
|
||||
import SvgTest from './pages/home/SvgTest';
|
||||
import SvgOverview from './pages/home/SvgOverview';
|
||||
import SvgOverviewCompressor from './pages/home/SvgOverviewCompressor';
|
||||
import SvgCompressorA from './pages/home/SvgCompressorA';
|
||||
import SvgCompressorB from './pages/home/SvgCompressorB';
|
||||
import SvgCompressorC from './pages/home/SvgCompressorC';
|
||||
import SvgOverviewAirDryer from './pages/home/SvgOverviewAirDryer';
|
||||
import SvgAirDryerA from './pages/home/SvgAirDryerA';
|
||||
import SvgAirDryerB from './pages/home/SvgAirDryerB';
|
||||
import SvgAirDryerC from './pages/home/SvgAirDryerC';
|
||||
import IndexHistoryAlarm from './pages/history/alarm/IndexHistoryAlarm';
|
||||
import IndexHistoryEvent from './pages/history/event/IndexHistoryEvent';
|
||||
|
||||
// Image Viewer
|
||||
import ImageViewer from './Utils/ImageViewer';
|
||||
import RedirectWa from './pages/blank/RedirectWa';
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
@@ -53,6 +64,16 @@ const App = () => {
|
||||
<Route path="/signin" element={<SignIn />} />
|
||||
<Route path="/signup" element={<SignUp />} />
|
||||
<Route path="/svg" element={<SvgTest />} />
|
||||
<Route
|
||||
path="/notification-detail/:notificationId"
|
||||
element={<DetailNotificationTab />}
|
||||
/>
|
||||
<Route
|
||||
path="/verification-sparepart/:notificationId"
|
||||
element={<IndexVerificationSparepart />}
|
||||
/>
|
||||
|
||||
<Route path="/redirect" element={<RedirectWa />} />
|
||||
|
||||
{/* Protected Routes */}
|
||||
<Route path="/dashboard" element={<ProtectedRoute />}>
|
||||
@@ -60,11 +81,14 @@ const App = () => {
|
||||
<Route path="blank" element={<Blank />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/image-viewer/:fileName" element={<ImageViewer />} />
|
||||
|
||||
<Route path="/dashboard-svg" element={<ProtectedRoute />}>
|
||||
<Route path="overview" element={<SvgOverview />} />
|
||||
<Route path="overview-compressor" element={<SvgOverviewCompressor />} />
|
||||
<Route path="compressor-a" element={<SvgCompressorA />} />
|
||||
<Route path="compressor-b" element={<SvgCompressorB />} />
|
||||
<Route path="compressor-c" element={<SvgCompressorC />} />
|
||||
<Route path="overview-airdryer" element={<SvgOverviewAirDryer />} />
|
||||
<Route path="airdryer-a" element={<SvgAirDryerA />} />
|
||||
<Route path="airdryer-b" element={<SvgAirDryerB />} />
|
||||
<Route path="airdryer-c" element={<SvgAirDryerC />} />
|
||||
@@ -74,28 +98,42 @@ const App = () => {
|
||||
<Route path="device" element={<IndexDevice />} />
|
||||
<Route path="tag" element={<IndexTag />} />
|
||||
<Route path="unit" element={<IndexUnit />} />
|
||||
<Route path="brand-device" element={<IndexBrandDevice />} />
|
||||
<Route path="brand-device/add" element={<AddBrandDevice />} />
|
||||
<Route path="plant-section" element={<IndexPlantSection />} />
|
||||
<Route path="sparepart" element={<IndexSparepart />} />
|
||||
<Route path="plant-sub-section" element={<IndexPlantSubSection />} />
|
||||
<Route path="shift" element={<IndexShift />} />
|
||||
<Route path="status" element={<IndexStatus />} />
|
||||
|
||||
{/* Brand Device Routes */}
|
||||
<Route path="brand-device" element={<IndexBrandDevice />} />
|
||||
<Route path="brand-device/add" element={<AddBrandDevice />} />
|
||||
<Route path="brand-device/edit/:id" element={<EditBrandDevice />} />
|
||||
<Route path="brand-device/view/:id" element={<ViewBrandDevice />} />
|
||||
<Route
|
||||
path="brand-device/edit/:id/files/:fileType/:fileName"
|
||||
element={<ViewFilePage />}
|
||||
/>
|
||||
<Route
|
||||
path="brand-device/view/:id/files/:fileType/:fileName"
|
||||
element={<ViewFilePage />}
|
||||
/>
|
||||
<Route
|
||||
path="brand-device/view/temp/files/:fileName"
|
||||
element={<ViewFilePage />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/jadwal-shift" element={<ProtectedRoute />}>
|
||||
<Route index element={<IndexJadwalShift />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/history" element={<ProtectedRoute />}>
|
||||
<Route path="/report" element={<ProtectedRoute />}>
|
||||
<Route path="trending" element={<IndexTrending />} />
|
||||
<Route path="report" element={<IndexReport />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/notification" element={<ProtectedRoute />}>
|
||||
<Route index element={<IndexNotification />} />
|
||||
<Route path="/history" element={<ProtectedRoute />}>
|
||||
<Route path="alarm" element={<IndexHistoryAlarm />} />
|
||||
<Route path="event" element={<IndexHistoryEvent />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/event-alarm" element={<ProtectedRoute />}>
|
||||
<Route index element={<IndexEventAlarm />} />
|
||||
<Route path="/notification" element={<ProtectedRoute />}>
|
||||
<Route index element={<IndexNotification />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/role" element={<ProtectedRoute />}>
|
||||
@@ -106,11 +144,14 @@ const App = () => {
|
||||
<Route index element={<IndexUser />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/shift-management" element={<ProtectedRoute />}>
|
||||
<Route path="member" element={<IndexMember />} />
|
||||
<Route path="/contact" element={<ProtectedRoute />}>
|
||||
<Route index element={<IndexContact />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/jadwal-shift" element={<ProtectedRoute />}>
|
||||
<Route index element={<IndexJadwalShift />} />
|
||||
</Route>
|
||||
|
||||
{/* Catch-all */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
248
src/Utils/ImageViewer.jsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { getFileUrl, getFolderFromFileType } from '../api/file-uploads';
|
||||
|
||||
const ImageViewer = () => {
|
||||
const { fileName } = useParams();
|
||||
const [fileUrl, setFileUrl] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [isImage, setIsImage] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fileName) {
|
||||
setError('No file specified');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const decodedFileName = decodeURIComponent(fileName);
|
||||
const fileExtension = decodedFileName.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
|
||||
setIsImage(imageExtensions.includes(fileExtension));
|
||||
|
||||
const folder = getFolderFromFileType(fileExtension);
|
||||
|
||||
const url = getFileUrl(folder, decodedFileName);
|
||||
setFileUrl(url);
|
||||
|
||||
document.title = `File Viewer - ${decodedFileName}`;
|
||||
} catch (error) {
|
||||
|
||||
setError('Failed to load file');
|
||||
}
|
||||
}, [fileName]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (!isImage) return;
|
||||
|
||||
if (e.key === '+' || e.key === '=') {
|
||||
setZoom(prev => Math.min(prev + 0.1, 3));
|
||||
} else if (e.key === '-' || e.key === '_') {
|
||||
setZoom(prev => Math.max(prev - 0.1, 0.1));
|
||||
} else if (e.key === '0') {
|
||||
setZoom(1);
|
||||
} else if (e.key === 'Escape') {
|
||||
window.close();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isImage]);
|
||||
|
||||
|
||||
const handleWheel = (e) => {
|
||||
if (!isImage || !e.ctrlKey) return;
|
||||
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
||||
setZoom(prev => Math.min(Math.max(prev + delta, 0.1), 3));
|
||||
};
|
||||
|
||||
const handleZoomIn = () => setZoom(prev => Math.min(prev + 0.1, 3));
|
||||
const handleZoomOut = () => setZoom(prev => Math.max(prev - 0.1, 0.1));
|
||||
const handleResetZoom = () => setZoom(1);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
backgroundColor: '#f5f5f5'
|
||||
}}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<h1>Error</h1>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (!isImage) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
backgroundColor: '#f5f5f5'
|
||||
}}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<h1>File Type Not Supported</h1>
|
||||
<p>Image viewer only supports image files.</p>
|
||||
<p>Please use direct file preview for PDFs and other documents.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
backgroundColor: '#000',
|
||||
overflow: 'hidden',
|
||||
position: 'relative'
|
||||
}}
|
||||
onWheel={handleWheel}
|
||||
>
|
||||
|
||||
{isImage && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
padding: '10px',
|
||||
borderRadius: '8px',
|
||||
zIndex: 1000
|
||||
}}>
|
||||
<button
|
||||
onClick={handleZoomOut}
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
color: '#fff',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
title="Zoom Out (-)"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span style={{
|
||||
color: '#fff',
|
||||
padding: '8px 12px',
|
||||
minWidth: '60px',
|
||||
textAlign: 'center',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
{Math.round(zoom * 100)}%
|
||||
</span>
|
||||
<button
|
||||
onClick={handleZoomIn}
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
color: '#fff',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
title="Zoom In (+)"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
onClick={handleResetZoom}
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
color: '#fff',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
title="Reset Zoom (0)"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{isImage && fileUrl ? (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
<img
|
||||
src={fileUrl}
|
||||
alt={decodeURIComponent(fileName)}
|
||||
style={{
|
||||
maxWidth: 'none',
|
||||
maxHeight: 'none',
|
||||
transform: `scale(${zoom})`,
|
||||
transformOrigin: 'center',
|
||||
transition: 'transform 0.1s ease-out',
|
||||
cursor: zoom > 1 ? 'move' : 'default'
|
||||
}}
|
||||
onError={() => setError('Failed to load image')}
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
) : isImage ? (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
color: '#fff',
|
||||
fontFamily: 'Arial, sans-serif'
|
||||
}}>
|
||||
<p>Loading image...</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
{isImage && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
left: '20px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
color: '#fff',
|
||||
padding: '10px 15px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '12px',
|
||||
zIndex: 1000
|
||||
}}>
|
||||
<div>Mouse wheel + Ctrl: Zoom</div>
|
||||
<div>Keyboard: +/− Zoom, 0: Reset, ESC: Close</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageViewer;
|
||||
56
src/api/contact.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { SendRequest } from '../components/Global/ApiRequest';
|
||||
|
||||
const getAllContact = async (queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `contact?${queryParams.toString()}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getContactById = async (id) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `contact/${id}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createContact = async (queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'post',
|
||||
prefix: `contact`,
|
||||
params: queryParams,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateContact = async (id, queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'put',
|
||||
prefix: `contact/${id}`,
|
||||
params: queryParams,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const deleteContact = async (id) => {
|
||||
const response = await SendRequest({
|
||||
method: 'delete',
|
||||
prefix: `contact/${id}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export {
|
||||
getAllContact,
|
||||
getContactById,
|
||||
createContact,
|
||||
updateContact,
|
||||
deleteContact,
|
||||
};
|
||||
135
src/api/file-uploads.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { SendRequest } from '../components/Global/ApiRequest';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_SERVER;
|
||||
|
||||
// Get file from uploads directory
|
||||
const getFile = async (folder, filename) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
throw new Error('No authentication token found');
|
||||
}
|
||||
|
||||
const response = await axios.get(`${API_BASE_URL}/file-uploads/${folder}/${encodeURIComponent(filename)}`, {
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token.replace(/"/g, '')}`
|
||||
}
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Download file as blob with proper handling
|
||||
const downloadFile = async (folder, filename) => {
|
||||
try {
|
||||
const response = await getFile(folder, filename);
|
||||
|
||||
const blob = new Blob([response], {
|
||||
type: 'application/octet-stream'
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
return { success: true, filename };
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Get file info (metadata)
|
||||
const getFileInfo = async (folder, filename) => {
|
||||
const response = await SendRequest({
|
||||
method: 'head',
|
||||
prefix: `file-uploads/${folder}/${encodeURIComponent(filename)}`
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
|
||||
return {
|
||||
contentType: response.headers?.['content-type'],
|
||||
contentLength: response.headers?.['content-length'],
|
||||
lastModified: response.headers?.['last-modified'],
|
||||
filename: filename,
|
||||
folder: folder
|
||||
};
|
||||
};
|
||||
|
||||
// Get file URL for iframe
|
||||
const getFileUrl = (folder, filename) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
return `${API_BASE_URL}/file-uploads/${folder}/${encodeURIComponent(filename)}?token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
return `${API_BASE_URL}/file-uploads/${folder}/${encodeURIComponent(filename)}`;
|
||||
};
|
||||
|
||||
// Check if file exists
|
||||
const checkFileExists = async (folder, filename) => {
|
||||
const response = await SendRequest({
|
||||
method: 'head',
|
||||
prefix: `file-uploads/${folder}/${encodeURIComponent(filename)}`
|
||||
});
|
||||
|
||||
if (response.error && response.statusCode === 404) {
|
||||
return false;
|
||||
} else if (response.error) {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const getFileType = (filename) => {
|
||||
const ext = filename.split('.').pop().toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const pdfExtensions = ['pdf'];
|
||||
|
||||
if (imageExtensions.includes(ext)) {
|
||||
return 'image';
|
||||
} else if (pdfExtensions.includes(ext)) {
|
||||
return 'pdf';
|
||||
}
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
// Upload file to server
|
||||
const uploadFile = async (file, folder) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('folder', folder);
|
||||
|
||||
const response = await SendRequest({
|
||||
method: 'post',
|
||||
prefix: 'file-uploads',
|
||||
params: formData
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getFolderFromFileType = (fileType) => {
|
||||
return fileType === 'pdf' ? 'pdf' : 'images';
|
||||
};
|
||||
|
||||
export {
|
||||
getFile,
|
||||
downloadFile,
|
||||
getFileInfo,
|
||||
getFileUrl,
|
||||
checkFileExists,
|
||||
getFileType,
|
||||
getFolderFromFileType,
|
||||
uploadFile
|
||||
};
|
||||
54
src/api/history-value.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { SendRequest } from '../components/Global/ApiRequest';
|
||||
|
||||
const getAllHistoryAlarm = async (queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `history/alarm?${queryParams.toString()}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getAllHistoryEvent = async (queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `history/event?${queryParams.toString()}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getAllHistoryValueReport = async (queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `history/value-report?${queryParams.toString()}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getAllHistoryValueReportPivot = async (queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `history/value-report-pivot?${queryParams.toString()}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getAllHistoryValueTrendingPivot = async (queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `history/value-trending?${queryParams.toString()}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export {
|
||||
getAllHistoryAlarm,
|
||||
getAllHistoryEvent,
|
||||
getAllHistoryValueReport,
|
||||
getAllHistoryValueReportPivot,
|
||||
getAllHistoryValueTrendingPivot,
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { SendRequest } from '../components/Global/ApiRequest';
|
||||
const getAllJadwalShift = async (queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `jadwal-shift?${queryParams.toString()}`,
|
||||
prefix: `user-schedule?${queryParams.toString()}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
@@ -12,7 +12,7 @@ const getAllJadwalShift = async (queryParams) => {
|
||||
const getJadwalShiftById = async (id) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `jadwal-shift/${id}`,
|
||||
prefix: `user-schedule/${id}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
@@ -21,7 +21,7 @@ const getJadwalShiftById = async (id) => {
|
||||
const createJadwalShift = async (queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'post',
|
||||
prefix: `jadwal-shift`,
|
||||
prefix: `user-schedule`,
|
||||
params: queryParams,
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ const createJadwalShift = async (queryParams) => {
|
||||
const updateJadwalShift = async (id, queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'put',
|
||||
prefix: `jadwal-shift/${id}`,
|
||||
prefix: `user-schedule/${id}`,
|
||||
params: queryParams,
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ const updateJadwalShift = async (id, queryParams) => {
|
||||
const deleteJadwalShift = async (id) => {
|
||||
const response = await SendRequest({
|
||||
method: 'delete',
|
||||
prefix: `jadwal-shift/${id}`,
|
||||
prefix: `user-schedule/${id}`,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -47,4 +47,63 @@ const deleteBrand = async (id) => {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export { getAllBrands, getBrandById, createBrand, updateBrand, deleteBrand };
|
||||
const getErrorCodesByBrandId = async (brandId, queryParams) => {
|
||||
const query = queryParams ? `?${queryParams.toString()}` : '';
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `error-code/brand/${brandId}${query}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getErrorCodeById = async (id) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `error-code/${id}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createErrorCode = async (brandId, queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'post',
|
||||
prefix: `error-code/brand/${brandId}`,
|
||||
params: queryParams,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateErrorCode = async (brandId, errorCodeId, queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'put',
|
||||
prefix: `error-code/brand/${brandId}/${errorCodeId}`,
|
||||
params: queryParams,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const deleteErrorCode = async (brandId, errorCode) => {
|
||||
const response = await SendRequest({
|
||||
method: 'delete',
|
||||
prefix: `error-code/brand/${brandId}/${errorCode}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export {
|
||||
getAllBrands,
|
||||
getBrandById,
|
||||
createBrand,
|
||||
updateBrand,
|
||||
deleteBrand,
|
||||
getErrorCodesByBrandId,
|
||||
getErrorCodeById,
|
||||
createErrorCode,
|
||||
updateErrorCode,
|
||||
deleteErrorCode
|
||||
};
|
||||
|
||||
105
src/api/notification.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { SendRequest } from '../components/Global/ApiRequest';
|
||||
|
||||
const getAllNotification = async (queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `notification?${queryParams.toString()}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getNotificationById = async (id) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `notification/${id}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getNotificationDetail = async (id) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `notification/${id}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Create new notification log
|
||||
const createNotificationLog = async (data) => {
|
||||
const response = await SendRequest({
|
||||
method: 'post',
|
||||
prefix: 'notification-log',
|
||||
params: data,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Get notification logs by notification_error_id
|
||||
const getNotificationLogByNotificationId = async (notificationId) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `notification-log/notification_error/${notificationId}`,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// update is_read status
|
||||
const updateIsRead = async (notificationId) => {
|
||||
const response = await SendRequest({
|
||||
method: 'put',
|
||||
prefix: `notification/${notificationId}`,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Resend notification to specific user
|
||||
const resendNotificationToUser = async (notificationId, userId) => {
|
||||
const response = await SendRequest({
|
||||
method: 'post',
|
||||
prefix: `notification/${notificationId}/resend/${userId}`,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Resend Chat by User
|
||||
const resendChatByUser = async (notificationId, userPhone) => {
|
||||
const response = await SendRequest({
|
||||
method: 'post',
|
||||
prefix: `notification-user/resend/${notificationId}/${userPhone}`,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Resend Chat All User
|
||||
const resendChatAllUser = async (notificationId) => {
|
||||
const response = await SendRequest({
|
||||
method: 'post',
|
||||
prefix: `notification/resend/${notificationId}`,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Searching
|
||||
const searchData = async (queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `notification?criteria=${queryParams}`,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export {
|
||||
getAllNotification,
|
||||
getNotificationById,
|
||||
getNotificationDetail,
|
||||
createNotificationLog,
|
||||
getNotificationLogByNotificationId,
|
||||
updateIsRead,
|
||||
resendNotificationToUser,
|
||||
resendChatByUser,
|
||||
resendChatAllUser,
|
||||
searchData,
|
||||
};
|
||||
50
src/api/sparepart.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { SendRequest } from '../components/Global/ApiRequest';
|
||||
|
||||
const getAllSparepart = async (queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `sparepart?${queryParams.toString()}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getSparepartById = async (id) => {
|
||||
const response = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `sparepart/${id}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createSparepart = async (queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'post',
|
||||
prefix: `sparepart`,
|
||||
params: queryParams,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateSparepart = async (id, queryParams) => {
|
||||
const response = await SendRequest({
|
||||
method: 'put',
|
||||
prefix: `sparepart/${id}`,
|
||||
params: queryParams,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const deleteSparepart = async (id) => {
|
||||
const response = await SendRequest({
|
||||
method: 'delete',
|
||||
prefix: `sparepart/${id}`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export { getAllSparepart, getSparepartById, createSparepart, updateSparepart, deleteSparepart };
|
||||
BIN
src/assets/bg-cod-1.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
@@ -26,27 +26,29 @@
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 490.992px 230.229px;" d="M 646.097 240.002 L 676.271 240.002"/>
|
||||
</g>
|
||||
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 443.701px 171.141px;" d="M 443.542 155.983 L 443.859 186.298"/>
|
||||
<g transform="matrix(0.826913, 0, 0, 0.698383, 0.443817, 3.138935)">
|
||||
<rect x="752" y="355.455" width="42.438" height="3.527" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
|
||||
<rect x="756.328" y="359.271" width="34.034" height="53.968" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
|
||||
<rect x="756.146" y="352.019" width="34.034" height="3.38" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
|
||||
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="773.446" cy="384.7" rx="11.751" ry="11.009"/>
|
||||
<g>
|
||||
<rect x="622.282" y="251.383" width="35.093" height="2.463" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);"/>
|
||||
<g>
|
||||
<rect x="625.861" y="254.048" width="28.143" height="37.69" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);"/>
|
||||
<rect x="625.711" y="248.983" width="28.143" height="2.361" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);"/>
|
||||
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);" cx="640.016" cy="271.807" rx="9.717" ry="7.689"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="461.861" y="211.956" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px;" x="561" y="309.954" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp SP</text>
|
||||
<rect x="461.861" y="221.924" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="564.279" y="330.561" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="609.476" y="330.521" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
|
||||
<rect x="461.424" y="242.149" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="567.471" y="352.188" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp</text>
|
||||
<rect x="461.424" y="252.117" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="563.75" y="373.795" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="Dry1_HeatTempCelsius">####.##</text>
|
||||
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="608.947" y="373.755" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
|
||||
<rect x="535.456" y="242.272" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="659" y="352.363" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp</text>
|
||||
<rect x="535.456" y="252.24" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="653.279" y="373.97" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="698.476" y="373.93" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
|
||||
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="698.476" y="373.93" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°C</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1; font-weight: bold;" x="748" y="347.676" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER</text>
|
||||
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 678.512px 258.693px;" d="M 678.467 229.321 L 678.558 288.066" transform="matrix(0, 1.184039, -0.844567, 0, -0.000022, -0.000005)"/>
|
||||
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 703.162px 309.166px;" d="M 703.004 258.049 L 703.32 360.282"/>
|
||||
@@ -108,12 +110,12 @@
|
||||
<rect x="427.269" y="377.282" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="532.167" y="545.681" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dew Temp</text>
|
||||
<rect x="427.269" y="387.25" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.446" y="567.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="567.643" y="567.248" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°C</text>
|
||||
<rect x="427.27" y="412.201" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="532.168" y="595.681" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dew Temp</text>
|
||||
<rect x="427.27" y="422.169" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.447" y="617.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="567.644" y="617.248" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1; text-anchor: middle; font-weight: bolder;" x="602.463" y="573.003" transform="matrix(0.826913, 0, 0, 0.698383, 24.207672, -7.192523)">AIR<tspan x="602.4630126953125" dy="1em"></tspan>OUTLET</text>
|
||||
<g transform="matrix(-0.387768, 0, 0, -0.200385, 743.634644, -199.991287)" style="transform-origin: 72.2405px 412.5px;">
|
||||
@@ -177,17 +179,17 @@
|
||||
<rect x="43.443" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="53.987" y="423.091" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">RUN HOUR</text>
|
||||
<rect x="126.135" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="424.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="424.066" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
|
||||
<rect x="43.443" y="308.191" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; stroke-width: 1; font-weight: bold;" x="53.987" y="461.382" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">PURGE HOUR</text>
|
||||
<rect x="126.135" y="308.191" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="463.397" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="463.357" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
|
||||
<rect x="43.443" y="333.129" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; font-weight: 700; white-space: pre;" x="53.987" y="498.091" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER HOUR</text>
|
||||
<rect x="126.135" y="333.129" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="499.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="499.066" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
|
||||
<rect x="43.65" y="360.147" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(248, 213, 14);"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; font-weight: 700; white-space: pre; stroke-width: 1;" x="54.237" y="536.777" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Alarm Info</text>
|
||||
@@ -205,15 +207,15 @@
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HTD</text>
|
||||
<rect x="43.443" y="214.051" width="165.383" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; font-weight: 700; white-space: pre; stroke-width: 1;" x="53.987" y="322.585" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Step</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="177.05" y="323.6" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="233" y="323.56" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">s</text>
|
||||
<rect x="43.443" y="241.422" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; font-weight: 700; white-space: pre; stroke-width: 1;" x="54.237" y="364.271" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Cycle Timer</text>
|
||||
<rect x="126.341" y="241.068" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="168.775" y="365.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225.25" y="365.246" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">s</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; font-weight: 700; white-space: pre; stroke-width: 1;" x="141.894" y="324.069" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Time</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="92.151" y="325.554" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">##</text>
|
||||
|
||||
<rect x="870.356" y="142.816" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="1060.06" y="224.103" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dryer Status</text>
|
||||
<rect x="870.356" y="170.304" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
@@ -227,12 +229,33 @@
|
||||
<rect x="870.356" y="344.9" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.702" cy="366.997" rx="20.673" ry="17.46"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="380.451" y="296.591" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="334.165" cy="232.104" rx="13.582" ry="12.517"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="336.418" cy="237.483" rx="13.582" ry="12.517"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="379.214" y="423.395" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.623" cy="320.662" rx="13.582" ry="12.517"/>
|
||||
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.418" cy="321.016" rx="13.582" ry="12.517"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.623" cy="320.662" rx="13.582" ry="12.517" id="c_4021"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="897.237" y="299.014" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.502" cy="233.796" rx="13.582" ry="12.517"/>
|
||||
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.772" cy="233.876" rx="13.582" ry="12.517"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="896" y="425.818" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="762.96" cy="322.354" rx="13.582" ry="12.517"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 35px; stroke-width: 1; font-weight: bold;" x="348.875" y="78.242" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">AIR DRYER UNIT A (01-CL-10532-A)</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, 1.386371, 4.000207)">HTLS</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, -1.613663, 3.937793)">BLWR</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.447" y="617.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_4005">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.446" y="567.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_4004">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="653.279" y="373.97" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_4001">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="563.75" y="373.795" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_4002">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="564.279" y="330.561" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_4003">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="424.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_4009">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="463.397" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_4010">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="499.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_4011">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="177.05" y="323.6" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_4008">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="168.775" y="365.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_4007">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="92.151" y="325.554" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_4006">##</text>
|
||||
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="336.418" cy="237.483" rx="13.582" ry="12.517" id="c_4018"/>
|
||||
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(255, 172, 63);" cx="640.283" cy="271.689" rx="9.717" ry="7.689" id="c_4019"/>
|
||||
<ellipse style="fill: rgb(63, 255, 69); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.254" cy="192.696" rx="20.673" ry="17.46" id="c_4016"/>
|
||||
<ellipse style="fill: rgb(255, 159, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.352" cy="279.12" rx="20.673" ry="17.46" id="c_4017"/>
|
||||
<ellipse style="fill: rgb(255, 63, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.352" cy="366.862" rx="20.673" ry="17.46" id="c_4020"/>
|
||||
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="762.685" cy="322.259" rx="13.582" ry="12.517" id="c_4018"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="762.031" cy="234.094" rx="13.582" ry="12.517" id="c_4021"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
@@ -3,7 +3,7 @@
|
||||
<defs>
|
||||
<bx:grid x="0" y="0" width="25" height="25"/>
|
||||
</defs>
|
||||
<rect y="10.407" width="972.648" height="439.023" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" x="12.119"/>
|
||||
<rect y="10.407" width="972.648" height="440.159" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" x="12.119"/>
|
||||
<g transform="matrix(0.826913, 0, 0, 0.698383, 74.03907, 53.375034)">
|
||||
<ellipse style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);" cx="315" cy="183.068" rx="45" ry="45"/>
|
||||
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="315" cy="449.112" rx="45" ry="45"/>
|
||||
@@ -26,27 +26,26 @@
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 490.992px 230.229px;" d="M 646.097 240.002 L 676.271 240.002"/>
|
||||
</g>
|
||||
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 443.701px 171.141px;" d="M 443.542 155.983 L 443.859 186.298"/>
|
||||
<g transform="matrix(0.826913, 0, 0, 0.698383, 0.443817, 3.138935)">
|
||||
<rect x="752" y="355.455" width="42.438" height="3.527" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
|
||||
<rect x="756.328" y="359.271" width="34.034" height="53.968" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
|
||||
<rect x="756.146" y="352.019" width="34.034" height="3.38" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
|
||||
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="773.446" cy="384.7" rx="11.751" ry="11.009"/>
|
||||
<g>
|
||||
<rect x="622.282" y="251.383" width="35.093" height="2.463" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);"/>
|
||||
<g>
|
||||
<rect x="625.861" y="254.048" width="28.143" height="37.69" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);"/>
|
||||
<rect x="625.711" y="248.983" width="28.143" height="2.361" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);"/>
|
||||
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);" cx="640.016" cy="271.807" rx="9.717" ry="7.689"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="461.861" y="211.956" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px;" x="561" y="309.954" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp SP</text>
|
||||
<rect x="461.861" y="221.924" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="564.279" y="330.561" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="609.476" y="330.521" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
|
||||
<rect x="461.424" y="242.149" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="567.471" y="352.188" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp</text>
|
||||
<rect x="461.424" y="252.117" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="563.75" y="373.795" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="Dry1_HeatTempCelsius">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="608.947" y="373.755" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
|
||||
<rect x="535.456" y="242.272" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="659" y="352.363" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp</text>
|
||||
<rect x="535.456" y="252.24" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="653.279" y="373.97" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="698.476" y="373.93" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="698.476" y="373.93" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°C</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1; font-weight: bold;" x="748" y="347.676" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER</text>
|
||||
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 678.512px 258.693px;" d="M 678.467 229.321 L 678.558 288.066" transform="matrix(0, 1.184039, -0.844567, 0, -0.000022, -0.000005)"/>
|
||||
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 703.162px 309.166px;" d="M 703.004 258.049 L 703.32 360.282"/>
|
||||
@@ -108,12 +107,10 @@
|
||||
<rect x="427.269" y="377.282" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="532.167" y="545.681" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dew Temp</text>
|
||||
<rect x="427.269" y="387.25" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.446" y="567.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="567.643" y="567.248" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°C</text>
|
||||
<rect x="427.27" y="412.201" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="532.168" y="595.681" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dew Temp</text>
|
||||
<rect x="427.27" y="422.169" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.447" y="617.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="567.644" y="617.248" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1; text-anchor: middle; font-weight: bolder;" x="602.463" y="573.003" transform="matrix(0.826913, 0, 0, 0.698383, 24.207672, -7.192523)">AIR<tspan x="602.4630126953125" dy="1em"></tspan>OUTLET</text>
|
||||
<g transform="matrix(-0.387768, 0, 0, -0.200385, 743.634644, -199.991287)" style="transform-origin: 72.2405px 412.5px;">
|
||||
@@ -177,17 +174,14 @@
|
||||
<rect x="43.443" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="53.987" y="423.091" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">RUN HOUR</text>
|
||||
<rect x="126.135" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="424.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="424.066" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
|
||||
<rect x="43.443" y="308.191" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; stroke-width: 1; font-weight: bold;" x="53.987" y="461.382" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">PURGE HOUR</text>
|
||||
<rect x="126.135" y="308.191" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="463.397" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="463.357" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
|
||||
<rect x="43.443" y="333.129" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; font-weight: 700; white-space: pre;" x="53.987" y="498.091" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER HOUR</text>
|
||||
<rect x="126.135" y="333.129" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="499.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="499.066" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
|
||||
<rect x="43.65" y="360.147" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(248, 213, 14);"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; font-weight: 700; white-space: pre; stroke-width: 1;" x="54.237" y="536.777" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Alarm Info</text>
|
||||
@@ -205,15 +199,12 @@
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HTD</text>
|
||||
<rect x="43.443" y="214.051" width="165.383" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; font-weight: 700; white-space: pre; stroke-width: 1;" x="53.987" y="322.585" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Step</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="177.05" y="323.6" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="233" y="323.56" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">s</text>
|
||||
<rect x="43.443" y="241.422" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; font-weight: 700; white-space: pre; stroke-width: 1;" x="54.237" y="364.271" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Cycle Timer</text>
|
||||
<rect x="126.341" y="241.068" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="168.775" y="365.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225.25" y="365.246" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">s</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; font-weight: 700; white-space: pre; stroke-width: 1;" x="141.894" y="324.069" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Time</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="92.151" y="325.554" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">##</text>
|
||||
<rect x="870.356" y="142.816" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="1060.06" y="224.103" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dryer Status</text>
|
||||
<rect x="870.356" y="170.304" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
@@ -227,12 +218,33 @@
|
||||
<rect x="870.356" y="344.9" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.702" cy="366.997" rx="20.673" ry="17.46"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="380.451" y="296.591" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="334.165" cy="232.104" rx="13.582" ry="12.517"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="336.418" cy="237.483" rx="13.582" ry="12.517"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="379.214" y="423.395" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.623" cy="320.662" rx="13.582" ry="12.517"/>
|
||||
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.418" cy="321.016" rx="13.582" ry="12.517"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="897.237" y="299.014" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.502" cy="233.796" rx="13.582" ry="12.517"/>
|
||||
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.772" cy="233.876" rx="13.582" ry="12.517"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="896" y="425.818" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="762.96" cy="322.354" rx="13.582" ry="12.517"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 35px; stroke-width: 1; font-weight: bold;" x="348.875" y="78.242" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">AIR DRYER UNIT B (01-CL-10535-B)</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, 1.386371, 4.000207)">HTLS</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, -1.613663, 3.937793)">BLWR</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.447" y="617.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_5005">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.446" y="567.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_5004">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="653.279" y="373.97" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_5001">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="563.75" y="373.795" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_5002">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="564.279" y="330.561" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_5003">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="424.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_5009">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="463.397" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_5010">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="499.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_5011">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="177.05" y="323.6" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_5008">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="168.775" y="365.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_5007">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="92.151" y="325.554" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_5006">##</text>
|
||||
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="336.418" cy="237.483" rx="13.582" ry="12.517" id="c_5018"/>
|
||||
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(255, 172, 63);" cx="640.283" cy="271.689" rx="9.717" ry="7.689" id="c_5019"/>
|
||||
<ellipse style="fill: rgb(63, 255, 69); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.254" cy="192.696" rx="20.673" ry="17.46" id="c_5016"/>
|
||||
<ellipse style="fill: rgb(255, 159, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.352" cy="279.12" rx="20.673" ry="17.46" id="c_5017"/>
|
||||
<ellipse style="fill: rgb(255, 63, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.352" cy="366.862" rx="20.673" ry="17.46" id="c_5020"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.623" cy="320.662" rx="13.582" ry="12.517" id="c_5021"/>
|
||||
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="762.685" cy="322.259" rx="13.582" ry="12.517" id="c_5018"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="762.031" cy="234.094" rx="13.582" ry="12.517" id="c_5021"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
@@ -3,7 +3,7 @@
|
||||
<defs>
|
||||
<bx:grid x="0" y="0" width="25" height="25"/>
|
||||
</defs>
|
||||
<rect y="10.407" width="972.648" height="439.023" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" x="12.119"/>
|
||||
<rect y="10.407" width="972.648" height="440.159" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" x="12.119"/>
|
||||
<g transform="matrix(0.826913, 0, 0, 0.698383, 74.03907, 53.375034)">
|
||||
<ellipse style="stroke: rgb(0, 0, 0); fill: rgb(243, 243, 243);" cx="315" cy="183.068" rx="45" ry="45"/>
|
||||
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="315" cy="449.112" rx="45" ry="45"/>
|
||||
@@ -26,27 +26,26 @@
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 490.992px 230.229px;" d="M 646.097 240.002 L 676.271 240.002"/>
|
||||
</g>
|
||||
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 443.701px 171.141px;" d="M 443.542 155.983 L 443.859 186.298"/>
|
||||
<g transform="matrix(0.826913, 0, 0, 0.698383, 0.443817, 3.138935)">
|
||||
<rect x="752" y="355.455" width="42.438" height="3.527" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
|
||||
<rect x="756.328" y="359.271" width="34.034" height="53.968" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
|
||||
<rect x="756.146" y="352.019" width="34.034" height="3.38" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
|
||||
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);" cx="773.446" cy="384.7" rx="11.751" ry="11.009"/>
|
||||
<g>
|
||||
<rect x="622.282" y="251.383" width="35.093" height="2.463" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);"/>
|
||||
<g>
|
||||
<rect x="625.861" y="254.048" width="28.143" height="37.69" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);"/>
|
||||
<rect x="625.711" y="248.983" width="28.143" height="2.361" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);"/>
|
||||
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);" cx="640.016" cy="271.807" rx="9.717" ry="7.689"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="461.861" y="211.956" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px;" x="561" y="309.954" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp SP</text>
|
||||
<rect x="461.861" y="221.924" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="564.279" y="330.561" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="609.476" y="330.521" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
|
||||
<rect x="461.424" y="242.149" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="567.471" y="352.188" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp</text>
|
||||
<rect x="461.424" y="252.117" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="563.75" y="373.795" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="Dry1_HeatTempCelsius">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="608.947" y="373.755" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
|
||||
<rect x="535.456" y="242.272" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="659" y="352.363" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Heater Temp</text>
|
||||
<rect x="535.456" y="252.24" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="653.279" y="373.97" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="698.476" y="373.93" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="698.476" y="373.93" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°C</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1; font-weight: bold;" x="748" y="347.676" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER</text>
|
||||
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 678.512px 258.693px;" d="M 678.467 229.321 L 678.558 288.066" transform="matrix(0, 1.184039, -0.844567, 0, -0.000022, -0.000005)"/>
|
||||
<path style="fill: none; stroke: rgb(0, 0, 0); stroke-width: 1.525; transform-origin: 703.162px 309.166px;" d="M 703.004 258.049 L 703.32 360.282"/>
|
||||
@@ -108,12 +107,10 @@
|
||||
<rect x="427.269" y="377.282" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="532.167" y="545.681" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dew Temp</text>
|
||||
<rect x="427.269" y="387.25" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.446" y="567.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="567.643" y="567.248" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°C</text>
|
||||
<rect x="427.27" y="412.201" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="532.168" y="595.681" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dew Temp</text>
|
||||
<rect x="427.27" y="422.169" width="62.018" height="17.46" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.447" y="617.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="567.644" y="617.248" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°F</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1; text-anchor: middle; font-weight: bolder;" x="602.463" y="573.003" transform="matrix(0.826913, 0, 0, 0.698383, 24.207672, -7.192523)">AIR<tspan x="602.4630126953125" dy="1em"></tspan>OUTLET</text>
|
||||
<g transform="matrix(-0.387768, 0, 0, -0.200385, 743.634644, -199.991287)" style="transform-origin: 72.2405px 412.5px;">
|
||||
@@ -177,17 +174,14 @@
|
||||
<rect x="43.443" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="53.987" y="423.091" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">RUN HOUR</text>
|
||||
<rect x="126.135" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="424.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="424.066" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
|
||||
<rect x="43.443" y="308.191" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; stroke-width: 1; font-weight: bold;" x="53.987" y="461.382" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">PURGE HOUR</text>
|
||||
<rect x="126.135" y="308.191" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="463.397" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="463.357" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
|
||||
<rect x="43.443" y="333.129" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; font-weight: 700; white-space: pre;" x="53.987" y="498.091" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER HOUR</text>
|
||||
<rect x="126.135" y="333.129" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="499.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225" y="499.066" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">H</text>
|
||||
<rect x="43.65" y="360.147" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(248, 213, 14);"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; font-weight: 700; white-space: pre; stroke-width: 1;" x="54.237" y="536.777" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Alarm Info</text>
|
||||
@@ -205,15 +199,12 @@
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HTD</text>
|
||||
<rect x="43.443" y="214.051" width="165.383" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; font-weight: 700; white-space: pre; stroke-width: 1;" x="53.987" y="322.585" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Step</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="177.05" y="323.6" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="233" y="323.56" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">s</text>
|
||||
<rect x="43.443" y="241.422" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; font-weight: 700; white-space: pre; stroke-width: 1;" x="54.237" y="364.271" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Cycle Timer</text>
|
||||
<rect x="126.341" y="241.068" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="168.775" y="365.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="225.25" y="365.246" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">s</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 13px; font-weight: 700; white-space: pre; stroke-width: 1;" x="141.894" y="324.069" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Time</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="92.151" y="325.554" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">##</text>
|
||||
<rect x="870.356" y="142.816" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="1060.06" y="224.103" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">Dryer Status</text>
|
||||
<rect x="870.356" y="170.304" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
@@ -227,12 +218,34 @@
|
||||
<rect x="870.356" y="344.9" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.702" cy="366.997" rx="20.673" ry="17.46"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="380.451" y="296.591" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="334.165" cy="232.104" rx="13.582" ry="12.517"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="336.418" cy="237.483" rx="13.582" ry="12.517"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="379.214" y="423.395" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.623" cy="320.662" rx="13.582" ry="12.517"/>
|
||||
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.418" cy="321.016" rx="13.582" ry="12.517"/>
|
||||
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="897.237" y="299.014" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.502" cy="233.796" rx="13.582" ry="12.517"/>
|
||||
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.772" cy="233.876" rx="13.582" ry="12.517"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="896" y="425.818" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="762.96" cy="322.354" rx="13.582" ry="12.517"/>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 35px; stroke-width: 1; font-weight: bold;" x="348.875" y="78.242" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">AIR DRYER UNIT C (01-CL-10539-C)</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, 1.386371, 4.000207)">HTLS</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, -1.613663, 3.937793)">BLWR</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.447" y="617.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_6005">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="522.446" y="567.288" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_6004">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="653.279" y="373.97" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_6001">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="563.75" y="373.795" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_6002">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="564.279" y="330.561" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_6003">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="424.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_6009">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="463.397" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_6010">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="159.05" y="499.106" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_6011">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="177.05" y="323.6" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_6008">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="168.775" y="365.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_6007">####.##</text>
|
||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="92.151" y="325.554" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)" id="c_6006">##</text>
|
||||
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="336.418" cy="237.483" rx="13.582" ry="12.517" id="c_6018"/>
|
||||
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(255, 172, 63);" cx="640.283" cy="271.689" rx="9.717" ry="7.689" id="c_6019"/>
|
||||
<ellipse style="fill: rgb(63, 255, 69); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.254" cy="192.696" rx="20.673" ry="17.46" id="c_6016"/>
|
||||
<ellipse style="fill: rgb(255, 159, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.352" cy="279.12" rx="20.673" ry="17.46" id="c_6017"/>
|
||||
<ellipse style="fill: rgb(255, 63, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="911.352" cy="366.862" rx="20.673" ry="17.46" id="c_6020"/>
|
||||
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="762.685" cy="322.259" rx="13.582" ry="12.517" id="c_6018"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.623" cy="320.662" rx="13.582" ry="12.517" id="c_6021"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="762.031" cy="234.094" rx="13.582" ry="12.517" id="c_6021"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
1983
src/assets/svg/compressorA_rev.svg
Normal file
|
After Width: | Height: | Size: 177 KiB |
1983
src/assets/svg/compressorB_rev.svg
Normal file
|
After Width: | Height: | Size: 177 KiB |
1983
src/assets/svg/compressorC_rev.svg
Normal file
|
After Width: | Height: | Size: 177 KiB |
443
src/assets/svg/overview-airdryer.svg
Normal file
@@ -0,0 +1,443 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com" viewBox="0 0 950 500">
|
||||
<defs>
|
||||
<bx:grid x="0" y="0" width="25" height="25"/>
|
||||
</defs>
|
||||
<rect x="12.226" y="12.005" width="924.818" height="476.396" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0);"/>
|
||||
<rect x="25" y="75" width="900" height="400" style="stroke: rgb(0, 0, 0); fill: rgb(255, 255, 255);"/>
|
||||
<rect x="50" y="100.548" width="100.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
|
||||
<rect x="50" y="125" width="100.168" height="50" style="stroke: rgb(0, 0, 0); stroke-width: 0.693; fill: rgb(244, 248, 248);"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.693;" cx="125.124" cy="137.355" rx="11.269" ry="10.987"/>
|
||||
<g transform="matrix(1.13391, 0, 0, 1.234446, -9.410634, 162.99009)" style="">
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 363.181 109.151 L 363.181 110.989 L 460.721 110.989 L 469.183 107.313 L 469.183 105.958 L 460.721 109.151 L 363.181 109.151 Z"/>
|
||||
<path style="fill: rgb(115, 135, 166); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 361.061 109.151 L 361.061 53.909 L 462.836 53.909 L 462.836 109.151 L 361.061 109.151 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 445.801 105.475 L 445.801 57.586 L 454.374 57.586 L 454.374 105.475 L 445.801 105.475 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 433.101 105.475 L 433.101 57.586 L 441.571 57.586 L 441.571 105.475 L 433.101 105.475 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 420.411 105.475 L 420.411 57.586 L 428.871 57.586 L 428.871 105.475 L 420.411 105.475 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 407.721 105.475 L 407.721 57.586 L 416.181 57.586 L 416.181 105.475 L 407.721 105.475 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 395.021 105.475 L 395.021 57.586 L 403.481 57.586 L 403.481 105.475 L 395.021 105.475 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 382.221 105.475 L 382.221 57.586 L 390.791 57.586 L 390.791 105.475 L 382.221 105.475 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 369.521 105.475 L 369.521 57.586 L 377.991 57.586 L 377.991 105.475 L 369.521 105.475 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.641 81.579 L 371.641 76.064 L 375.871 76.064 L 375.871 81.579 L 371.641 81.579 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.531 94.446 L 371.531 88.931 L 375.871 88.931 L 375.871 94.446 L 371.531 94.446 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.641 87.093 L 371.641 83.417 L 375.981 83.417 L 375.981 87.093 L 371.641 87.093 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.641 99.96 L 371.641 96.284 L 375.871 96.284 L 375.871 99.96 L 371.641 99.96 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 81.579 L 384.331 76.064 L 388.561 76.064 L 388.561 81.579 L 384.331 81.579 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 94.446 L 384.331 88.931 L 388.561 88.931 L 388.561 94.446 L 384.331 94.446 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 87.093 L 384.331 83.417 L 388.671 83.417 L 388.671 87.093 L 384.331 87.093 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 99.96 L 384.331 96.284 L 388.561 96.284 L 388.561 99.96 L 384.331 99.96 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.031 81.579 L 397.031 76.064 L 401.371 76.064 L 401.371 81.579 L 397.031 81.579 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.031 94.446 L 397.031 88.931 L 401.261 88.931 L 401.261 94.446 L 397.031 94.446 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.141 87.093 L 397.141 83.417 L 401.371 83.417 L 401.371 87.093 L 397.141 87.093 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.031 99.96 L 397.031 96.284 L 401.371 96.284 L 401.371 99.96 L 397.031 99.96 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 385.451 105.475 L 385.451 101.798 L 387.561 101.798 L 387.561 105.475 L 385.451 105.475 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 372.751 105.475 L 372.751 101.798 L 374.871 101.798 L 374.871 105.475 L 372.751 105.475 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 372.751 61.262 L 372.751 57.586 L 374.871 57.586 L 374.871 61.262 L 372.751 61.262 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 385.451 61.262 L 385.451 57.586 L 387.561 57.586 L 387.561 61.262 L 385.451 61.262 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 398.141 61.262 L 398.141 57.586 L 400.261 57.586 L 400.261 61.262 L 398.141 61.262 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 398.141 105.475 L 398.141 101.798 L 400.261 101.798 L 400.261 105.475 L 398.141 105.475 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 80.611 L 451.141 80.611 L 451.141 82.449 L 449.031 82.449 L 449.031 80.611 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 76.935 L 451.141 76.935 L 451.141 78.87 L 449.031 78.87 L 449.031 76.935 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 84.287 L 451.141 84.287 L 451.141 86.126 L 449.031 86.126 L 449.031 84.287 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 73.258 L 451.141 73.258 L 451.141 75.097 L 449.031 75.097 L 449.031 73.258 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 87.964 L 451.141 87.964 L 451.141 89.802 L 449.031 89.802 L 449.031 87.964 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 69.582 L 451.141 69.582 L 451.141 71.42 L 449.031 71.42 L 449.031 69.582 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 91.64 L 451.141 91.64 L 451.141 93.478 L 449.031 93.478 L 449.031 91.64 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 80.611 L 438.451 80.611 L 438.451 82.449 L 436.331 82.449 L 436.331 80.611 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 76.935 L 438.451 76.935 L 438.451 78.87 L 436.331 78.87 L 436.331 76.935 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 84.287 L 438.451 84.287 L 438.451 86.126 L 436.331 86.126 L 436.331 84.287 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 73.258 L 438.451 73.258 L 438.451 75.097 L 436.331 75.097 L 436.331 73.258 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 87.964 L 438.451 87.964 L 438.451 89.802 L 436.331 89.802 L 436.331 87.964 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 69.582 L 438.451 69.582 L 438.451 71.42 L 436.331 71.42 L 436.331 69.582 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 91.64 L 438.451 91.64 L 438.451 93.478 L 436.331 93.478 L 436.331 91.64 Z"/>
|
||||
<path style="fill: rgb(89, 109, 140); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 462.836 53.909 L 471.299 50.233 L 471.299 105.475 L 462.836 109.151 L 462.836 53.909 Z"/>
|
||||
<path style="fill: rgb(191, 211, 242); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 462.836 53.909 L 471.299 50.233 L 369.521 50.233 L 361.061 53.909 L 462.836 53.909 Z"/>
|
||||
</g>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre;" x="56" y="116.982">AirDryer A</text>
|
||||
<rect x="50" y="150" width="100" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0);"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.368" y="141.716">On/Off</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="120.949" y="168.123">H</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.007" y="167.208">####</text>
|
||||
<rect x="49.832" y="225.548" width="100.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
|
||||
<rect x="49.832" y="250" width="100.168" height="50" style="stroke: rgb(0, 0, 0); stroke-width: 0.693; fill: rgb(244, 248, 248);"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.693;" cx="124.956" cy="262.355" rx="11.269" ry="10.987"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="55.832" y="241.982">AirDryer B</text>
|
||||
<rect x="49.832" y="275" width="100" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.2" y="266.716">On/Off</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="120.781" y="293.123">H</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="55.839" y="292.208">####</text>
|
||||
<rect x="49.832" y="350.548" width="100.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
|
||||
<rect x="49.832" y="375" width="100.168" height="50" style="stroke: rgb(0, 0, 0); stroke-width: 0.693; fill: rgb(244, 248, 248);"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.693;" cx="124.956" cy="387.355" rx="11.269" ry="10.987"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="55.832" y="366.982">AirDryer C</text>
|
||||
<rect x="49.832" y="400" width="100" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.2" y="391.716">On/Off</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="120.781" y="418.123">H</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="55.839" y="417.208">####</text>
|
||||
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 4, 255);" d="M 250 125 L 350 125 L 350 275 L 400 275"/>
|
||||
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 4, 255);" d="M 250 250 L 350 250 L 350 275 L 400 275"/>
|
||||
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 4, 255);" d="M 250 375 L 350 375 L 350 275 L 400 275"/>
|
||||
<g transform="matrix(1, 0, 0, 1, 49.999999, -99.999998)" style="">
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 292.93 L 717.857 292.651 L 717.857 292.442 L 717.857 292.163 L 717.857 291.954 L 717.857 291.744 L 717.857 291.535 L 717.857 291.396 L 717.914 291.186 L 717.914 291.046 L 717.972 290.907 L 718.029 290.837 L 718.086 290.698 L 718.143 290.558 L 718.201 290.488 L 718.258 290.419 L 718.372 290.349 L 718.487 290.279 L 718.602 290.209 L 718.716 290.14 L 718.888 290.14 L 719.06 290.07 L 719.231 290.07 L 719.46 290 L 719.632 290 L 719.919 290 L 720.147 290 L 720.434 290 L 720.72 290 L 721.063 290 L 721.407 290 L 721.751 290 L 722.151 290 L 770.706 290 L 771.106 290 L 771.45 290 L 771.794 290 L 772.137 290 L 772.423 290 L 772.71 290 L 772.939 290 L 773.225 290 L 773.397 290.07 L 773.626 290.07 L 773.798 290.14 L 773.969 290.14 L 774.141 290.209 L 774.256 290.279 L 774.37 290.349 L 774.485 290.419 L 774.599 290.488 L 774.657 290.558 L 774.714 290.698 L 774.771 290.837 L 774.828 290.977 L 774.886 291.117 L 774.943 291.256 L 774.943 291.465 L 775 291.674 L 775 291.884 L 775 292.093 L 775 292.303 L 775 292.581 L 775 292.86 L 775 293.209 L 775 293.488 L 775 335.558 L 775 335.907 L 775 336.256 L 775 336.604 L 775 336.884 L 775 337.163 L 775 337.442 L 775 337.72 L 775 337.93 L 774.943 338.14 L 774.943 338.349 L 774.943 338.488 L 774.886 338.628 L 774.886 338.767 L 774.828 338.907 L 774.771 339.047 L 774.714 339.117 L 774.657 339.256 L 774.599 339.326 L 774.485 339.396 L 774.427 339.465 L 774.313 339.465 L 774.198 339.535 L 774.084 339.535 L 773.969 339.604 L 773.798 339.604 L 773.683 339.604 L 773.511 339.604 L 773.339 339.674 L 773.168 339.674 L 772.939 339.674 L 772.71 339.674 L 772.481 339.674 L 721.579 339.674 L 721.235 339.674 L 720.892 339.674 L 720.663 339.674 L 720.377 339.674 L 720.09 339.604 L 719.861 339.604 L 719.69 339.604 L 719.46 339.604 L 719.289 339.535 L 719.117 339.535 L 718.945 339.465 L 718.831 339.465 L 718.659 339.396 L 718.544 339.326 L 718.43 339.256 L 718.372 339.117 L 718.258 339.047 L 718.201 338.907 L 718.143 338.767 L 718.086 338.628 L 718.029 338.488 L 717.972 338.349 L 717.914 338.14 L 717.914 337.93 L 717.914 337.72 L 717.857 337.442 L 717.857 337.163 L 717.857 336.884 L 717.857 336.604 L 717.857 336.256 L 717.857 335.907 L 717.857 335.558 L 717.857 292.93 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 732.114 339.674 L 760.743 339.674 L 760.743 342.884 L 732.114 342.884 L 732.114 339.674 Z"/>
|
||||
<path style="fill: rgb(67, 67, 67); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 722.151 299.419 L 722.151 299.07 L 722.151 298.791 L 722.151 298.442 L 722.151 298.163 L 722.151 297.954 L 722.151 297.675 L 722.209 297.465 L 722.209 297.256 L 722.209 297.046 L 722.266 296.837 L 722.323 296.698 L 722.323 296.558 L 722.381 296.419 L 722.495 296.279 L 722.553 296.209 L 722.61 296.07 L 722.725 296 L 722.838 295.93 L 722.953 295.86 L 723.067 295.791 L 723.239 295.791 L 723.354 295.721 L 723.526 295.721 L 723.754 295.651 L 723.926 295.651 L 724.155 295.651 L 724.385 295.651 L 724.671 295.651 L 724.9 295.651 L 725.243 295.651 L 725.53 295.651 L 725.873 295.651 L 767.557 295.651 L 767.843 295.651 L 768.129 295.651 L 768.358 295.651 L 768.587 295.651 L 768.816 295.651 L 769.045 295.721 L 769.217 295.721 L 769.389 295.791 L 769.561 295.791 L 769.675 295.86 L 769.79 295.86 L 769.962 295.93 L 770.076 296 L 770.133 296.07 L 770.247 296.139 L 770.305 296.209 L 770.362 296.349 L 770.419 296.419 L 770.477 296.558 L 770.534 296.698 L 770.591 296.837 L 770.591 296.907 L 770.649 297.046 L 770.649 297.256 L 770.649 297.396 L 770.706 297.604 L 770.706 297.744 L 770.706 297.954 L 770.706 298.163 L 770.706 298.442 L 770.706 298.651 L 770.706 298.861 L 770.706 327.744 L 770.706 328.093 L 770.706 328.372 L 770.706 328.581 L 770.706 328.861 L 770.763 329.07 L 770.763 329.279 L 770.763 329.488 L 770.763 329.628 L 770.763 329.838 L 770.763 329.977 L 770.706 330.117 L 770.706 330.186 L 770.649 330.326 L 770.649 330.465 L 770.591 330.535 L 770.534 330.604 L 770.477 330.674 L 770.419 330.744 L 770.305 330.814 L 770.19 330.883 L 770.076 330.883 L 769.962 330.883 L 769.79 330.953 L 769.618 330.953 L 769.446 330.953 L 769.274 331.023 L 769.045 331.023 L 768.759 331.023 L 768.53 331.023 L 768.244 331.023 L 767.9 331.023 L 767.557 331.023 L 725.873 331.023 L 725.53 331.023 L 725.243 331.023 L 724.9 331.023 L 724.671 331.023 L 724.385 331.023 L 724.155 331.023 L 723.926 331.023 L 723.754 331.023 L 723.526 331.023 L 723.354 331.023 L 723.239 331.023 L 723.067 330.953 L 722.953 330.953 L 722.838 330.883 L 722.725 330.814 L 722.61 330.744 L 722.553 330.674 L 722.495 330.604 L 722.381 330.465 L 722.323 330.326 L 722.323 330.186 L 722.266 330.046 L 722.209 329.838 L 722.209 329.628 L 722.209 329.419 L 722.151 329.209 L 722.151 328.93 L 722.151 328.651 L 722.151 328.302 L 722.151 327.954 L 722.151 327.604 L 722.151 327.186 L 722.151 299.419 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 350 L 775 350 L 775 347.558 L 760.743 342.884 L 732.114 342.884 L 717.857 347.558 L 717.857 350 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 770.133 334.023 L 770.133 337.512 L 756.391 337.512 L 756.391 334.023 L 770.133 334.023 Z"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 347.558 L 775 347.558"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 675 285.361 L 703.571 285.361 L 703.571 350 L 675 350 L 675 285.361 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 287.463 L 701.169 287.463 L 701.169 350 L 677.317 350 L 677.317 287.463 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 290.616 L 701.169 290.616 L 701.169 297.898 L 677.317 297.898 L 677.317 290.616 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 297.898 L 701.169 297.898 L 701.169 305.181 L 677.317 305.181 L 677.317 297.898 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 293.694 L 698.767 293.694 L 698.767 294.745 L 679.72 294.745 L 679.72 293.694 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 300 L 698.767 300 L 698.767 303.078 L 679.72 303.078 L 679.72 300 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 275 L 698.767 275 L 703.571 285.361 L 675 285.361 L 679.72 275 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 695.163 300 L 701.169 300 L 701.169 303.078 L 695.163 303.078 L 695.163 300 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 300 L 698.767 300 L 698.767 303.078 L 697.566 303.078 L 697.566 300 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 291.592 L 692.847 291.592 L 692.847 292.643 L 697.566 292.643 L 697.566 291.592 Z"/>
|
||||
<circle style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="890.673" cy="114.148" r="1.051" transform="matrix(1.142857, 0, 0, 1.000013, -320.346635, 178.493448)"/>
|
||||
</g>
|
||||
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(6, 255, 0);" d="M 525 250 L 575 250 L 575 200 L 725 200"/>
|
||||
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(6, 255, 0);" d="M 525 250 L 575 250 L 575 400 L 725 400"/>
|
||||
<rect x="399.832" y="149.864" width="125.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
|
||||
<rect x="400" y="175" width="125" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="425.352" y="167.628">PLC AirDryer</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="426.101" y="192.839">IP : 192.168.0.3</text>
|
||||
<rect x="725" y="99.864" width="125.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
|
||||
<rect x="725.168" y="125" width="125" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="759.52" y="117.628">PC Station</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="751.269" y="142.839">IP : 192.168.0.2</text>
|
||||
<g transform="matrix(1, 0, 0, 1, 49.999999, 99.999998)" style="">
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 292.93 L 717.857 292.651 L 717.857 292.442 L 717.857 292.163 L 717.857 291.954 L 717.857 291.744 L 717.857 291.535 L 717.857 291.396 L 717.914 291.186 L 717.914 291.046 L 717.972 290.907 L 718.029 290.837 L 718.086 290.698 L 718.143 290.558 L 718.201 290.488 L 718.258 290.419 L 718.372 290.349 L 718.487 290.279 L 718.602 290.209 L 718.716 290.14 L 718.888 290.14 L 719.06 290.07 L 719.231 290.07 L 719.46 290 L 719.632 290 L 719.919 290 L 720.147 290 L 720.434 290 L 720.72 290 L 721.063 290 L 721.407 290 L 721.751 290 L 722.151 290 L 770.706 290 L 771.106 290 L 771.45 290 L 771.794 290 L 772.137 290 L 772.423 290 L 772.71 290 L 772.939 290 L 773.225 290 L 773.397 290.07 L 773.626 290.07 L 773.798 290.14 L 773.969 290.14 L 774.141 290.209 L 774.256 290.279 L 774.37 290.349 L 774.485 290.419 L 774.599 290.488 L 774.657 290.558 L 774.714 290.698 L 774.771 290.837 L 774.828 290.977 L 774.886 291.117 L 774.943 291.256 L 774.943 291.465 L 775 291.674 L 775 291.884 L 775 292.093 L 775 292.303 L 775 292.581 L 775 292.86 L 775 293.209 L 775 293.488 L 775 335.558 L 775 335.907 L 775 336.256 L 775 336.604 L 775 336.884 L 775 337.163 L 775 337.442 L 775 337.72 L 775 337.93 L 774.943 338.14 L 774.943 338.349 L 774.943 338.488 L 774.886 338.628 L 774.886 338.767 L 774.828 338.907 L 774.771 339.047 L 774.714 339.117 L 774.657 339.256 L 774.599 339.326 L 774.485 339.396 L 774.427 339.465 L 774.313 339.465 L 774.198 339.535 L 774.084 339.535 L 773.969 339.604 L 773.798 339.604 L 773.683 339.604 L 773.511 339.604 L 773.339 339.674 L 773.168 339.674 L 772.939 339.674 L 772.71 339.674 L 772.481 339.674 L 721.579 339.674 L 721.235 339.674 L 720.892 339.674 L 720.663 339.674 L 720.377 339.674 L 720.09 339.604 L 719.861 339.604 L 719.69 339.604 L 719.46 339.604 L 719.289 339.535 L 719.117 339.535 L 718.945 339.465 L 718.831 339.465 L 718.659 339.396 L 718.544 339.326 L 718.43 339.256 L 718.372 339.117 L 718.258 339.047 L 718.201 338.907 L 718.143 338.767 L 718.086 338.628 L 718.029 338.488 L 717.972 338.349 L 717.914 338.14 L 717.914 337.93 L 717.914 337.72 L 717.857 337.442 L 717.857 337.163 L 717.857 336.884 L 717.857 336.604 L 717.857 336.256 L 717.857 335.907 L 717.857 335.558 L 717.857 292.93 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 732.114 339.674 L 760.743 339.674 L 760.743 342.884 L 732.114 342.884 L 732.114 339.674 Z"/>
|
||||
<path style="fill: rgb(67, 67, 67); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 722.151 299.419 L 722.151 299.07 L 722.151 298.791 L 722.151 298.442 L 722.151 298.163 L 722.151 297.954 L 722.151 297.675 L 722.209 297.465 L 722.209 297.256 L 722.209 297.046 L 722.266 296.837 L 722.323 296.698 L 722.323 296.558 L 722.381 296.419 L 722.495 296.279 L 722.553 296.209 L 722.61 296.07 L 722.725 296 L 722.838 295.93 L 722.953 295.86 L 723.067 295.791 L 723.239 295.791 L 723.354 295.721 L 723.526 295.721 L 723.754 295.651 L 723.926 295.651 L 724.155 295.651 L 724.385 295.651 L 724.671 295.651 L 724.9 295.651 L 725.243 295.651 L 725.53 295.651 L 725.873 295.651 L 767.557 295.651 L 767.843 295.651 L 768.129 295.651 L 768.358 295.651 L 768.587 295.651 L 768.816 295.651 L 769.045 295.721 L 769.217 295.721 L 769.389 295.791 L 769.561 295.791 L 769.675 295.86 L 769.79 295.86 L 769.962 295.93 L 770.076 296 L 770.133 296.07 L 770.247 296.139 L 770.305 296.209 L 770.362 296.349 L 770.419 296.419 L 770.477 296.558 L 770.534 296.698 L 770.591 296.837 L 770.591 296.907 L 770.649 297.046 L 770.649 297.256 L 770.649 297.396 L 770.706 297.604 L 770.706 297.744 L 770.706 297.954 L 770.706 298.163 L 770.706 298.442 L 770.706 298.651 L 770.706 298.861 L 770.706 327.744 L 770.706 328.093 L 770.706 328.372 L 770.706 328.581 L 770.706 328.861 L 770.763 329.07 L 770.763 329.279 L 770.763 329.488 L 770.763 329.628 L 770.763 329.838 L 770.763 329.977 L 770.706 330.117 L 770.706 330.186 L 770.649 330.326 L 770.649 330.465 L 770.591 330.535 L 770.534 330.604 L 770.477 330.674 L 770.419 330.744 L 770.305 330.814 L 770.19 330.883 L 770.076 330.883 L 769.962 330.883 L 769.79 330.953 L 769.618 330.953 L 769.446 330.953 L 769.274 331.023 L 769.045 331.023 L 768.759 331.023 L 768.53 331.023 L 768.244 331.023 L 767.9 331.023 L 767.557 331.023 L 725.873 331.023 L 725.53 331.023 L 725.243 331.023 L 724.9 331.023 L 724.671 331.023 L 724.385 331.023 L 724.155 331.023 L 723.926 331.023 L 723.754 331.023 L 723.526 331.023 L 723.354 331.023 L 723.239 331.023 L 723.067 330.953 L 722.953 330.953 L 722.838 330.883 L 722.725 330.814 L 722.61 330.744 L 722.553 330.674 L 722.495 330.604 L 722.381 330.465 L 722.323 330.326 L 722.323 330.186 L 722.266 330.046 L 722.209 329.838 L 722.209 329.628 L 722.209 329.419 L 722.151 329.209 L 722.151 328.93 L 722.151 328.651 L 722.151 328.302 L 722.151 327.954 L 722.151 327.604 L 722.151 327.186 L 722.151 299.419 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 350 L 775 350 L 775 347.558 L 760.743 342.884 L 732.114 342.884 L 717.857 347.558 L 717.857 350 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 770.133 334.023 L 770.133 337.512 L 756.391 337.512 L 756.391 334.023 L 770.133 334.023 Z"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 347.558 L 775 347.558"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 675 285.361 L 703.571 285.361 L 703.571 350 L 675 350 L 675 285.361 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 287.463 L 701.169 287.463 L 701.169 350 L 677.317 350 L 677.317 287.463 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 290.616 L 701.169 290.616 L 701.169 297.898 L 677.317 297.898 L 677.317 290.616 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 297.898 L 701.169 297.898 L 701.169 305.181 L 677.317 305.181 L 677.317 297.898 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 293.694 L 698.767 293.694 L 698.767 294.745 L 679.72 294.745 L 679.72 293.694 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 300 L 698.767 300 L 698.767 303.078 L 679.72 303.078 L 679.72 300 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 275 L 698.767 275 L 703.571 285.361 L 675 285.361 L 679.72 275 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 695.163 300 L 701.169 300 L 701.169 303.078 L 695.163 303.078 L 695.163 300 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 300 L 698.767 300 L 698.767 303.078 L 697.566 303.078 L 697.566 300 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 291.592 L 692.847 291.592 L 692.847 292.643 L 697.566 292.643 L 697.566 291.592 Z"/>
|
||||
<circle style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="890.673" cy="114.148" r="1.051" transform="matrix(1.142857, 0, 0, 1.000013, -320.346635, 178.493448)"/>
|
||||
</g>
|
||||
<rect x="724.832" y="299.864" width="125.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
|
||||
<rect x="725" y="325" width="125" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="760.352" y="317.628">PC Server</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="751.1" y="317.975" transform="matrix(1, 0, 0, 1, -3.000031, 24.863983)">IP : xxx.xxx.xx.xx<tspan x="751.0999755859375" dy="1em"></tspan></text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 30px; white-space: pre; stroke-width: 1;" x="316.458" y="50.984">OVERVIEW AIR DRYER</text>
|
||||
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 0, 0);" d="M 625 75 L 625 114.125 L 625 264.125 L 625 475"/>
|
||||
<g transform="matrix(0.999999, 0, 0, 0.888921, -1058.006891, 44.212168)" style="">
|
||||
<g id="Group_Base" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M4.392,105.741h62.834v2.253H4.392V105.741z"/>
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M0,112.498l1.014-1.126H2.14v-11.261H1.014L0,98.984h4.392v13.514H0z "/>
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M71.957,98.984l-1.127,1.127h-1.125v11.261h1.125l1.127,1.126h-4.504 V98.984H71.957z"/>
|
||||
</g>
|
||||
<g id="Group_Supports" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M11.148,89.977h49.547v13.514H11.148V89.977z"/>
|
||||
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M4.392,76.463h4.504v27.026H4.392V76.463z"/>
|
||||
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M62.948,76.463h4.278v27.026h-4.278V76.463z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,102.363h5.631v-6.758h-5.631h-0.337l-0.451,0.338 l-0.338,0.339l-0.338,0.563l-0.338,0.563l-0.225,0.563l-0.112,0.563l-0.113,0.45l0.113,0.451l0.112,0.563l0.225,0.563l0.338,0.563 l0.338,0.449l0.338,0.338l0.451,0.338L15.652,102.363z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,102.363h-5.631v-6.758h5.631h0.451l0.338,0.338l0.449,0.339 l0.338,0.563l0.226,0.563l0.226,0.563l0.225,0.563v0.448v0.451l-0.225,0.563l-0.226,0.563l-0.226,0.563l-0.338,0.449l-0.449,0.338 l-0.338,0.338L56.191,102.363z"/>
|
||||
</g>
|
||||
<g id="Group_Pipes" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="341.6875" y1="-275.623" x2="346.1914" y2="-275.623" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_1_)" d="M56.191,6.871h-4.504v4.504h4.504V6.871z"/>
|
||||
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,11.375V6.871h-4.504v4.504H56.191z"/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="305.6523" y1="-275.623" x2="310.1572" y2="-275.623" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_2_)" d="M20.157,6.871h-4.504v4.504h4.504V6.871z"/>
|
||||
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M20.157,11.375V6.871h-4.504v4.504H20.157z"/>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="323.6699" y1="-283.5059" x2="328.1738" y2="-283.5059" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_3_)" stroke="#4C4C4C" stroke-width="0.25" d="M38.174,6.871H33.67v20.27h4.504V6.871z"/>
|
||||
<path fill="#999999" d="M58.443,6.871V4.619H13.4v2.252H58.443z"/>
|
||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="325.9219" y1="-271.0625" x2="325.9219" y2="-273.2832" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_4_)" stroke="#4C4C4C" stroke-width="0.25" d="M13.4,6.871h45.043V4.619H13.4V6.871"/>
|
||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="325.9219" y1="-362.064" x2="325.9219" y2="-368.8364" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_5_)" stroke="#4C4C4C" stroke-width="0.25" d="M21.283,95.605h29.277v6.758H21.283V95.605z"/>
|
||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="322.5439" y1="-355.3496" x2="329.3008" y2="-355.3496" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_6_)" stroke="#4C4C4C" stroke-width="0.25" d="M39.301,95.605V78.715h-6.757v16.893l0.112,0.676l0.226,0.563 l0.225,0.563l0.45,0.45l0.563,0.449l0.563,0.338l0.563,0.227l0.675,0.111l0.677-0.111l0.676-0.227l0.563-0.338l0.451-0.449 l0.449-0.45l0.339-0.563l0.112-0.563L39.301,95.605z"/>
|
||||
</g>
|
||||
<g id="Group_Column2" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="298.8965" y1="-318.8652" x2="316.9141" y2="-318.8652" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#B2B2B2"/>
|
||||
<stop offset="0.5" style="stop-color:#E5E5E5"/>
|
||||
<stop offset="1" style="stop-color:#B2B2B2"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_7_)" d="M26.914,13.628V94.48H8.896V13.628c0,0,2.204-3.378,9.009-3.378 C25.176,10.25,26.914,13.628,26.914,13.628"/>
|
||||
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,13.628V94.48H8.896V13.628c0,0,1.972-3.378,9.043-3.378 C25.442,10.25,26.914,13.628,26.914,13.628z"/>
|
||||
</g>
|
||||
<g id="Group_Column1" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="334.9316" y1="-318.7939" x2="352.9482" y2="-318.7939" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#B2B2B2"/>
|
||||
<stop offset="0.5" style="stop-color:#E5E5E5"/>
|
||||
<stop offset="1" style="stop-color:#B2B2B2"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_8_)" d="M62.948,13.628V94.48H44.932V13.628c0,0,1.692-3.52,8.67-3.52 C61.661,10.108,62.948,13.628,62.948,13.628z"/>
|
||||
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M62.948,13.628V94.48H44.932V13.628c0,0,1.668-3.52,8.67-3.52 C61.589,10.108,62.948,13.628,62.948,13.628z"/>
|
||||
</g>
|
||||
<g id="Group_Boards" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<circle fill="#666666" stroke="#4C4C4C" stroke-width="0.25" cx="31.981" cy="24.325" r="3.941"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M22.409,27.141h27.026v33.783H22.409V27.141z"/>
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M25.787,31.645h20.27v21.396h-20.27V31.645z"/>
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M25.787,57.545h20.27v21.17h-20.27V57.545z"/>
|
||||
<path fill="#7F7F7F" stroke="#4C4C4C" stroke-width="0.25" d="M37.049,60.924h6.756v7.656h-6.756V60.924z"/>
|
||||
</g>
|
||||
<g id="Group_Points" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,55.293h3.378v4.504h-3.378V55.293z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,58.671h3.378v4.505h-3.378V58.671z"/>
|
||||
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,58.671h3.378v1.126h-3.378V58.671z"/>
|
||||
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,55.293h3.378v1.126h-3.378V55.293z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,47.41h3.378v4.504h-3.378V47.41z"/>
|
||||
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,47.41h3.378v1.126h-3.378V47.41z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M52.813,55.293h3.379v4.504h-3.379V55.293z"/>
|
||||
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M52.813,55.293h3.379v1.126h-3.379V55.293z"/>
|
||||
</g>
|
||||
<g id="Group_Connectors" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,1.917h0.676v43.692h-0.676V1.917z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,45.608h-7.207v-0.45h7.207V45.608z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,44.032h-7.207v-0.676h7.207V44.032z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,41.78h-7.207v-0.676h7.207V41.78z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,2.367H23.085v-0.45h30.179V2.367z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M23.085,2.367h0.45v2.252h-0.45V2.367z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,22.636h-40.54v-0.675h40.54V22.636z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M55.516,22.636h0.676v6.757h-0.676V22.636z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,22.636h0.676v6.757h-0.676V22.636z"/>
|
||||
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M27.364,0.002l1.577,1.464l-2.478,2.478l-1.576-1.577L27.364,0.002z"/>
|
||||
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M31.418,1.466l1.576-1.464l2.252,2.365L33.67,3.943L31.418,1.466z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.999999, 0, 0, 0.888921, -1061.049982, 169.212228)" style="">
|
||||
<g id="group-1" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M4.392,105.741h62.834v2.253H4.392V105.741z"/>
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M0,112.498l1.014-1.126H2.14v-11.261H1.014L0,98.984h4.392v13.514H0z "/>
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M71.957,98.984l-1.127,1.127h-1.125v11.261h1.125l1.127,1.126h-4.504 V98.984H71.957z"/>
|
||||
</g>
|
||||
<g id="group-2" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M11.148,89.977h49.547v13.514H11.148V89.977z"/>
|
||||
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M4.392,76.463h4.504v27.026H4.392V76.463z"/>
|
||||
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M62.948,76.463h4.278v27.026h-4.278V76.463z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,102.363h5.631v-6.758h-5.631h-0.337l-0.451,0.338 l-0.338,0.339l-0.338,0.563l-0.338,0.563l-0.225,0.563l-0.112,0.563l-0.113,0.45l0.113,0.451l0.112,0.563l0.225,0.563l0.338,0.563 l0.338,0.449l0.338,0.338l0.451,0.338L15.652,102.363z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,102.363h-5.631v-6.758h5.631h0.451l0.338,0.338l0.449,0.339 l0.338,0.563l0.226,0.563l0.226,0.563l0.225,0.563v0.448v0.451l-0.225,0.563l-0.226,0.563l-0.226,0.563l-0.338,0.449l-0.449,0.338 l-0.338,0.338L56.191,102.363z"/>
|
||||
</g>
|
||||
<g id="group-3" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<linearGradient id="gradient-1" gradientUnits="userSpaceOnUse" x1="341.6875" y1="-275.623" x2="346.1914" y2="-275.623" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-1)" d="M56.191,6.871h-4.504v4.504h4.504V6.871z"/>
|
||||
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,11.375V6.871h-4.504v4.504H56.191z"/>
|
||||
<linearGradient id="gradient-2" gradientUnits="userSpaceOnUse" x1="305.6523" y1="-275.623" x2="310.1572" y2="-275.623" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-2)" d="M20.157,6.871h-4.504v4.504h4.504V6.871z"/>
|
||||
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M20.157,11.375V6.871h-4.504v4.504H20.157z"/>
|
||||
<linearGradient id="gradient-3" gradientUnits="userSpaceOnUse" x1="323.6699" y1="-283.5059" x2="328.1738" y2="-283.5059" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-3)" stroke="#4C4C4C" stroke-width="0.25" d="M38.174,6.871H33.67v20.27h4.504V6.871z"/>
|
||||
<path fill="#999999" d="M58.443,6.871V4.619H13.4v2.252H58.443z"/>
|
||||
<linearGradient id="gradient-4" gradientUnits="userSpaceOnUse" x1="325.9219" y1="-271.0625" x2="325.9219" y2="-273.2832" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-4)" stroke="#4C4C4C" stroke-width="0.25" d="M13.4,6.871h45.043V4.619H13.4V6.871"/>
|
||||
<linearGradient id="gradient-5" gradientUnits="userSpaceOnUse" x1="325.9219" y1="-362.064" x2="325.9219" y2="-368.8364" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-5)" stroke="#4C4C4C" stroke-width="0.25" d="M21.283,95.605h29.277v6.758H21.283V95.605z"/>
|
||||
<linearGradient id="gradient-6" gradientUnits="userSpaceOnUse" x1="322.5439" y1="-355.3496" x2="329.3008" y2="-355.3496" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-6)" stroke="#4C4C4C" stroke-width="0.25" d="M39.301,95.605V78.715h-6.757v16.893l0.112,0.676l0.226,0.563 l0.225,0.563l0.45,0.45l0.563,0.449l0.563,0.338l0.563,0.227l0.675,0.111l0.677-0.111l0.676-0.227l0.563-0.338l0.451-0.449 l0.449-0.45l0.339-0.563l0.112-0.563L39.301,95.605z"/>
|
||||
</g>
|
||||
<g id="group-4" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<linearGradient id="gradient-7" gradientUnits="userSpaceOnUse" x1="298.8965" y1="-318.8652" x2="316.9141" y2="-318.8652" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#B2B2B2"/>
|
||||
<stop offset="0.5" style="stop-color:#E5E5E5"/>
|
||||
<stop offset="1" style="stop-color:#B2B2B2"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-7)" d="M26.914,13.628V94.48H8.896V13.628c0,0,2.204-3.378,9.009-3.378 C25.176,10.25,26.914,13.628,26.914,13.628"/>
|
||||
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,13.628V94.48H8.896V13.628c0,0,1.972-3.378,9.043-3.378 C25.442,10.25,26.914,13.628,26.914,13.628z"/>
|
||||
</g>
|
||||
<g id="group-5" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<linearGradient id="gradient-8" gradientUnits="userSpaceOnUse" x1="334.9316" y1="-318.7939" x2="352.9482" y2="-318.7939" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#B2B2B2"/>
|
||||
<stop offset="0.5" style="stop-color:#E5E5E5"/>
|
||||
<stop offset="1" style="stop-color:#B2B2B2"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-8)" d="M62.948,13.628V94.48H44.932V13.628c0,0,1.692-3.52,8.67-3.52 C61.661,10.108,62.948,13.628,62.948,13.628z"/>
|
||||
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M62.948,13.628V94.48H44.932V13.628c0,0,1.668-3.52,8.67-3.52 C61.589,10.108,62.948,13.628,62.948,13.628z"/>
|
||||
</g>
|
||||
<g id="group-6" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<circle fill="#666666" stroke="#4C4C4C" stroke-width="0.25" cx="31.981" cy="24.325" r="3.941"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M22.409,27.141h27.026v33.783H22.409V27.141z"/>
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M25.787,31.645h20.27v21.396h-20.27V31.645z"/>
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M25.787,57.545h20.27v21.17h-20.27V57.545z"/>
|
||||
<path fill="#7F7F7F" stroke="#4C4C4C" stroke-width="0.25" d="M37.049,60.924h6.756v7.656h-6.756V60.924z"/>
|
||||
</g>
|
||||
<g id="group-7" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,55.293h3.378v4.504h-3.378V55.293z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,58.671h3.378v4.505h-3.378V58.671z"/>
|
||||
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,58.671h3.378v1.126h-3.378V58.671z"/>
|
||||
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,55.293h3.378v1.126h-3.378V55.293z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,47.41h3.378v4.504h-3.378V47.41z"/>
|
||||
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,47.41h3.378v1.126h-3.378V47.41z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M52.813,55.293h3.379v4.504h-3.379V55.293z"/>
|
||||
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M52.813,55.293h3.379v1.126h-3.379V55.293z"/>
|
||||
</g>
|
||||
<g id="group-8" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,1.917h0.676v43.692h-0.676V1.917z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,45.608h-7.207v-0.45h7.207V45.608z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,44.032h-7.207v-0.676h7.207V44.032z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,41.78h-7.207v-0.676h7.207V41.78z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,2.367H23.085v-0.45h30.179V2.367z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M23.085,2.367h0.45v2.252h-0.45V2.367z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,22.636h-40.54v-0.675h40.54V22.636z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M55.516,22.636h0.676v6.757h-0.676V22.636z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,22.636h0.676v6.757h-0.676V22.636z"/>
|
||||
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M27.364,0.002l1.577,1.464l-2.478,2.478l-1.576-1.577L27.364,0.002z"/>
|
||||
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M31.418,1.466l1.576-1.464l2.252,2.365L33.67,3.943L31.418,1.466z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.999999, 0, 0, 0.888921, -1061.04986, 294.212174)" style="">
|
||||
<g id="group-9" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M4.392,105.741h62.834v2.253H4.392V105.741z"/>
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M0,112.498l1.014-1.126H2.14v-11.261H1.014L0,98.984h4.392v13.514H0z "/>
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M71.957,98.984l-1.127,1.127h-1.125v11.261h1.125l1.127,1.126h-4.504 V98.984H71.957z"/>
|
||||
</g>
|
||||
<g id="group-10" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M11.148,89.977h49.547v13.514H11.148V89.977z"/>
|
||||
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M4.392,76.463h4.504v27.026H4.392V76.463z"/>
|
||||
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M62.948,76.463h4.278v27.026h-4.278V76.463z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,102.363h5.631v-6.758h-5.631h-0.337l-0.451,0.338 l-0.338,0.339l-0.338,0.563l-0.338,0.563l-0.225,0.563l-0.112,0.563l-0.113,0.45l0.113,0.451l0.112,0.563l0.225,0.563l0.338,0.563 l0.338,0.449l0.338,0.338l0.451,0.338L15.652,102.363z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,102.363h-5.631v-6.758h5.631h0.451l0.338,0.338l0.449,0.339 l0.338,0.563l0.226,0.563l0.226,0.563l0.225,0.563v0.448v0.451l-0.225,0.563l-0.226,0.563l-0.226,0.563l-0.338,0.449l-0.449,0.338 l-0.338,0.338L56.191,102.363z"/>
|
||||
</g>
|
||||
<g id="group-11" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<linearGradient id="gradient-9" gradientUnits="userSpaceOnUse" x1="341.6875" y1="-275.623" x2="346.1914" y2="-275.623" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-9)" d="M56.191,6.871h-4.504v4.504h4.504V6.871z"/>
|
||||
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,11.375V6.871h-4.504v4.504H56.191z"/>
|
||||
<linearGradient id="gradient-10" gradientUnits="userSpaceOnUse" x1="305.6523" y1="-275.623" x2="310.1572" y2="-275.623" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-10)" d="M20.157,6.871h-4.504v4.504h4.504V6.871z"/>
|
||||
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M20.157,11.375V6.871h-4.504v4.504H20.157z"/>
|
||||
<linearGradient id="gradient-11" gradientUnits="userSpaceOnUse" x1="323.6699" y1="-283.5059" x2="328.1738" y2="-283.5059" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-11)" stroke="#4C4C4C" stroke-width="0.25" d="M38.174,6.871H33.67v20.27h4.504V6.871z"/>
|
||||
<path fill="#999999" d="M58.443,6.871V4.619H13.4v2.252H58.443z"/>
|
||||
<linearGradient id="gradient-12" gradientUnits="userSpaceOnUse" x1="325.9219" y1="-271.0625" x2="325.9219" y2="-273.2832" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-12)" stroke="#4C4C4C" stroke-width="0.25" d="M13.4,6.871h45.043V4.619H13.4V6.871"/>
|
||||
<linearGradient id="gradient-13" gradientUnits="userSpaceOnUse" x1="325.9219" y1="-362.064" x2="325.9219" y2="-368.8364" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-13)" stroke="#4C4C4C" stroke-width="0.25" d="M21.283,95.605h29.277v6.758H21.283V95.605z"/>
|
||||
<linearGradient id="gradient-14" gradientUnits="userSpaceOnUse" x1="322.5439" y1="-355.3496" x2="329.3008" y2="-355.3496" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#999999"/>
|
||||
<stop offset="0.5" style="stop-color:#CCCCCC"/>
|
||||
<stop offset="1" style="stop-color:#999999"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-14)" stroke="#4C4C4C" stroke-width="0.25" d="M39.301,95.605V78.715h-6.757v16.893l0.112,0.676l0.226,0.563 l0.225,0.563l0.45,0.45l0.563,0.449l0.563,0.338l0.563,0.227l0.675,0.111l0.677-0.111l0.676-0.227l0.563-0.338l0.451-0.449 l0.449-0.45l0.339-0.563l0.112-0.563L39.301,95.605z"/>
|
||||
</g>
|
||||
<g id="group-12" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<linearGradient id="gradient-15" gradientUnits="userSpaceOnUse" x1="298.8965" y1="-318.8652" x2="316.9141" y2="-318.8652" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#B2B2B2"/>
|
||||
<stop offset="0.5" style="stop-color:#E5E5E5"/>
|
||||
<stop offset="1" style="stop-color:#B2B2B2"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-15)" d="M26.914,13.628V94.48H8.896V13.628c0,0,2.204-3.378,9.009-3.378 C25.176,10.25,26.914,13.628,26.914,13.628"/>
|
||||
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,13.628V94.48H8.896V13.628c0,0,1.972-3.378,9.043-3.378 C25.442,10.25,26.914,13.628,26.914,13.628z"/>
|
||||
</g>
|
||||
<g id="group-13" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<linearGradient id="gradient-16" gradientUnits="userSpaceOnUse" x1="334.9316" y1="-318.7939" x2="352.9482" y2="-318.7939" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
|
||||
<stop offset="0" style="stop-color:#B2B2B2"/>
|
||||
<stop offset="0.5" style="stop-color:#E5E5E5"/>
|
||||
<stop offset="1" style="stop-color:#B2B2B2"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#gradient-16)" d="M62.948,13.628V94.48H44.932V13.628c0,0,1.692-3.52,8.67-3.52 C61.661,10.108,62.948,13.628,62.948,13.628z"/>
|
||||
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M62.948,13.628V94.48H44.932V13.628c0,0,1.668-3.52,8.67-3.52 C61.589,10.108,62.948,13.628,62.948,13.628z"/>
|
||||
</g>
|
||||
<g id="group-14" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<circle fill="#666666" stroke="#4C4C4C" stroke-width="0.25" cx="31.981" cy="24.325" r="3.941"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M22.409,27.141h27.026v33.783H22.409V27.141z"/>
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M25.787,31.645h20.27v21.396h-20.27V31.645z"/>
|
||||
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M25.787,57.545h20.27v21.17h-20.27V57.545z"/>
|
||||
<path fill="#7F7F7F" stroke="#4C4C4C" stroke-width="0.25" d="M37.049,60.924h6.756v7.656h-6.756V60.924z"/>
|
||||
</g>
|
||||
<g id="group-15" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,55.293h3.378v4.504h-3.378V55.293z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,58.671h3.378v4.505h-3.378V58.671z"/>
|
||||
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,58.671h3.378v1.126h-3.378V58.671z"/>
|
||||
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,55.293h3.378v1.126h-3.378V55.293z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,47.41h3.378v4.504h-3.378V47.41z"/>
|
||||
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,47.41h3.378v1.126h-3.378V47.41z"/>
|
||||
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M52.813,55.293h3.379v4.504h-3.379V55.293z"/>
|
||||
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M52.813,55.293h3.379v1.126h-3.379V55.293z"/>
|
||||
</g>
|
||||
<g id="group-16" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,1.917h0.676v43.692h-0.676V1.917z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,45.608h-7.207v-0.45h7.207V45.608z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,44.032h-7.207v-0.676h7.207V44.032z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,41.78h-7.207v-0.676h7.207V41.78z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,2.367H23.085v-0.45h30.179V2.367z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M23.085,2.367h0.45v2.252h-0.45V2.367z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,22.636h-40.54v-0.675h40.54V22.636z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M55.516,22.636h0.676v6.757h-0.676V22.636z"/>
|
||||
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,22.636h0.676v6.757h-0.676V22.636z"/>
|
||||
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M27.364,0.002l1.577,1.464l-2.478,2.478l-1.576-1.577L27.364,0.002z"/>
|
||||
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M31.418,1.466l1.576-1.464l2.252,2.365L33.67,3.943L31.418,1.466z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 56 KiB |
251
src/assets/svg/overview-compressor.svg
Normal file
@@ -0,0 +1,251 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com" viewBox="0 0 950 500">
|
||||
<defs>
|
||||
<bx:grid x="0" y="0" width="25" height="25"/>
|
||||
</defs>
|
||||
<rect x="12.226" y="12.005" width="924.818" height="462.995" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0);"/>
|
||||
<rect x="25" y="75" width="900" height="375" style="stroke: rgb(0, 0, 0); fill: rgb(255, 255, 255);"/>
|
||||
<rect x="50" y="100.548" width="100.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
|
||||
<rect x="50" y="125" width="100.168" height="50" style="stroke: rgb(0, 0, 0); stroke-width: 0.693; fill: rgb(244, 248, 248);"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.693;" cx="125.124" cy="137.355" rx="11.269" ry="10.987"/>
|
||||
<g transform="matrix(1.116151, 0, 0, 1.116686, -10.678418, 46.611243)" style="">
|
||||
<path style="fill: rgb(204, 204, 204); stroke-width: 1;" d="M 166.356 47.81 L 278.348 47.81 L 278.348 114.973 L 166.356 114.973 L 166.356 47.81 Z"/>
|
||||
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 166.356 95.477 L 278.348 67.396 L 278.348 114.973 L 166.356 114.973 L 166.356 95.477 Z"/>
|
||||
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 166.356 98.16 L 278.348 70.258 L 278.348 114.973 L 166.356 114.973 L 166.356 98.16 Z"/>
|
||||
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 166.356 102.453 L 278.348 74.371 L 278.348 114.973 L 166.356 114.973 L 166.356 102.453 Z"/>
|
||||
<path style="fill: rgb(204, 204, 204); stroke-width: 1;" d="M 168.486 49.688 L 276.106 49.688 L 276.106 109.249 L 168.486 109.249 L 168.486 49.688 Z"/>
|
||||
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 222.296 91.9 L 276.106 67.038 L 276.106 109.249 L 222.296 109.249 L 222.296 91.9 Z"/>
|
||||
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 168.486 92.079 L 222.296 67.038 L 222.296 109.249 L 168.486 109.249 L 168.486 92.079 Z"/>
|
||||
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 222.296 94.583 L 276.106 69.542 L 276.106 109.249 L 222.296 109.249 L 222.296 94.583 Z"/>
|
||||
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 168.486 94.583 L 222.296 69.542 L 222.296 109.249 L 168.486 109.249 L 168.486 94.583 Z"/>
|
||||
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 222.296 98.16 L 276.106 73.298 L 276.106 109.249 L 222.296 109.249 L 222.296 98.16 Z"/>
|
||||
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 168.486 98.16 L 222.296 73.298 L 222.296 109.249 L 168.486 109.249 L 168.486 98.16 Z"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 166.356 47.81 L 278.348 47.81 L 278.348 114.973 L 166.356 114.973 L 166.356 47.81"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 222.296 49.688 L 276.106 49.688 L 276.106 109.249 L 222.296 109.249 L 222.296 49.688"/>
|
||||
<path style="fill: rgb(153, 153, 153); stroke-width: 1;" d="M 240.906 110.323 L 259.739 110.323 L 259.739 113.9 L 240.906 113.9 L 240.906 110.323 Z"/>
|
||||
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 169.607 108.713 L 169.159 108.176 L 169.607 107.64 L 170.504 107.64 L 170.952 108.176 L 170.504 108.713 L 169.607 108.713 Z"/>
|
||||
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 169.607 68.111 L 169.159 67.575 L 169.607 66.859 L 170.504 66.859 L 170.952 67.575 L 170.504 68.111 L 169.607 68.111 Z"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 168.486 49.688 L 222.296 49.688 L 222.296 109.249 L 168.486 109.249 L 168.486 49.688"/>
|
||||
<path style="fill: rgb(229, 229, 229); stroke-width: 1;" d="M 229.247 51.119 L 235.749 51.119 L 235.749 59.973 L 229.247 59.973 L 229.247 51.119 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 224.762 51.119 L 226.332 51.119 L 226.332 55.233 L 224.762 55.233 L 224.762 51.119 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 272.519 51.119 L 274.313 51.119 L 274.313 55.233 L 272.519 55.233 L 272.519 51.119 Z"/>
|
||||
<path style="fill: rgb(51, 51, 51); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 168.486 49.688 L 222.296 49.688 L 222.296 66.501 L 168.486 66.501 L 168.486 49.688 Z"/>
|
||||
<path style="fill: rgb(229, 229, 229); stroke-width: 1;" d="M 185.526 60.778 L 192.028 60.778 L 192.028 63.103 L 185.526 63.103 L 185.526 60.778 Z"/>
|
||||
<path style="fill: rgb(178, 178, 178); stroke-width: 1;" d="M 200.324 62.745 L 198.978 61.672 L 198.978 60.152 L 200.324 59.079 L 202.117 59.079 L 203.687 60.152 L 203.687 61.672 L 202.117 62.745 L 200.324 62.745 Z"/>
|
||||
<path style="fill: rgb(178, 178, 178); stroke-width: 1;" d="M 207.947 62.745 L 206.602 61.672 L 206.602 60.152 L 207.947 59.079 L 209.74 59.079 L 211.086 60.152 L 211.086 61.672 L 209.74 62.745 L 207.947 62.745 Z"/>
|
||||
<path style="fill: rgb(102, 102, 102); stroke-width: 1;" d="M 176.109 60.778 L 182.611 60.778 L 182.611 63.103 L 176.109 63.103 L 176.109 60.778 Z"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 179.024 54.339 L 179.024 59.079 L 176.782 59.079"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 183.06 54.339 L 183.06 59.079 L 180.93 59.079"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 187.32 54.339 L 187.32 59.079 L 184.853 59.079"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 191.355 54.339 L 191.355 59.079 L 188.889 59.079"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 188.889 59.079 L 188.889 54.339 L 191.355 54.339"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 184.853 59.079 L 184.853 54.339 L 187.32 54.339"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 180.93 59.079 L 180.93 54.339 L 183.06 54.339"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 176.782 59.079 L 176.782 54.339 L 179.024 54.339"/>
|
||||
<path style="fill: rgb(127, 127, 127); stroke-width: 1;" d="M 197.857 66.859 L 222.296 104.241 L 222.296 94.404 L 204.135 66.859 L 197.857 66.859 Z"/>
|
||||
<path style="fill: rgb(102, 102, 102); stroke-width: 1;" d="M 204.135 66.859 L 222.296 94.404 L 222.296 84.566 L 210.637 66.859 L 204.135 66.859 Z"/>
|
||||
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 210.637 66.859 L 222.296 84.566 L 222.296 74.729 L 217.139 66.859 L 210.637 66.859 Z"/>
|
||||
<path style="fill: none; stroke: rgb(255, 255, 255); stroke-width: 2;" d="M 222.969 50.046 L 275.434 50.046"/>
|
||||
</g>
|
||||
<g transform="matrix(1.13391, 0, 0, 1.234446, -9.410634, 162.99009)" style="">
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 363.181 109.151 L 363.181 110.989 L 460.721 110.989 L 469.183 107.313 L 469.183 105.958 L 460.721 109.151 L 363.181 109.151 Z"/>
|
||||
<path style="fill: rgb(115, 135, 166); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 361.061 109.151 L 361.061 53.909 L 462.836 53.909 L 462.836 109.151 L 361.061 109.151 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 445.801 105.475 L 445.801 57.586 L 454.374 57.586 L 454.374 105.475 L 445.801 105.475 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 433.101 105.475 L 433.101 57.586 L 441.571 57.586 L 441.571 105.475 L 433.101 105.475 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 420.411 105.475 L 420.411 57.586 L 428.871 57.586 L 428.871 105.475 L 420.411 105.475 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 407.721 105.475 L 407.721 57.586 L 416.181 57.586 L 416.181 105.475 L 407.721 105.475 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 395.021 105.475 L 395.021 57.586 L 403.481 57.586 L 403.481 105.475 L 395.021 105.475 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 382.221 105.475 L 382.221 57.586 L 390.791 57.586 L 390.791 105.475 L 382.221 105.475 Z"/>
|
||||
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 369.521 105.475 L 369.521 57.586 L 377.991 57.586 L 377.991 105.475 L 369.521 105.475 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.641 81.579 L 371.641 76.064 L 375.871 76.064 L 375.871 81.579 L 371.641 81.579 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.531 94.446 L 371.531 88.931 L 375.871 88.931 L 375.871 94.446 L 371.531 94.446 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.641 87.093 L 371.641 83.417 L 375.981 83.417 L 375.981 87.093 L 371.641 87.093 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.641 99.96 L 371.641 96.284 L 375.871 96.284 L 375.871 99.96 L 371.641 99.96 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 81.579 L 384.331 76.064 L 388.561 76.064 L 388.561 81.579 L 384.331 81.579 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 94.446 L 384.331 88.931 L 388.561 88.931 L 388.561 94.446 L 384.331 94.446 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 87.093 L 384.331 83.417 L 388.671 83.417 L 388.671 87.093 L 384.331 87.093 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 99.96 L 384.331 96.284 L 388.561 96.284 L 388.561 99.96 L 384.331 99.96 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.031 81.579 L 397.031 76.064 L 401.371 76.064 L 401.371 81.579 L 397.031 81.579 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.031 94.446 L 397.031 88.931 L 401.261 88.931 L 401.261 94.446 L 397.031 94.446 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.141 87.093 L 397.141 83.417 L 401.371 83.417 L 401.371 87.093 L 397.141 87.093 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.031 99.96 L 397.031 96.284 L 401.371 96.284 L 401.371 99.96 L 397.031 99.96 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 385.451 105.475 L 385.451 101.798 L 387.561 101.798 L 387.561 105.475 L 385.451 105.475 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 372.751 105.475 L 372.751 101.798 L 374.871 101.798 L 374.871 105.475 L 372.751 105.475 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 372.751 61.262 L 372.751 57.586 L 374.871 57.586 L 374.871 61.262 L 372.751 61.262 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 385.451 61.262 L 385.451 57.586 L 387.561 57.586 L 387.561 61.262 L 385.451 61.262 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 398.141 61.262 L 398.141 57.586 L 400.261 57.586 L 400.261 61.262 L 398.141 61.262 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 398.141 105.475 L 398.141 101.798 L 400.261 101.798 L 400.261 105.475 L 398.141 105.475 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 80.611 L 451.141 80.611 L 451.141 82.449 L 449.031 82.449 L 449.031 80.611 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 76.935 L 451.141 76.935 L 451.141 78.87 L 449.031 78.87 L 449.031 76.935 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 84.287 L 451.141 84.287 L 451.141 86.126 L 449.031 86.126 L 449.031 84.287 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 73.258 L 451.141 73.258 L 451.141 75.097 L 449.031 75.097 L 449.031 73.258 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 87.964 L 451.141 87.964 L 451.141 89.802 L 449.031 89.802 L 449.031 87.964 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 69.582 L 451.141 69.582 L 451.141 71.42 L 449.031 71.42 L 449.031 69.582 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 91.64 L 451.141 91.64 L 451.141 93.478 L 449.031 93.478 L 449.031 91.64 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 80.611 L 438.451 80.611 L 438.451 82.449 L 436.331 82.449 L 436.331 80.611 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 76.935 L 438.451 76.935 L 438.451 78.87 L 436.331 78.87 L 436.331 76.935 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 84.287 L 438.451 84.287 L 438.451 86.126 L 436.331 86.126 L 436.331 84.287 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 73.258 L 438.451 73.258 L 438.451 75.097 L 436.331 75.097 L 436.331 73.258 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 87.964 L 438.451 87.964 L 438.451 89.802 L 436.331 89.802 L 436.331 87.964 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 69.582 L 438.451 69.582 L 438.451 71.42 L 436.331 71.42 L 436.331 69.582 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 91.64 L 438.451 91.64 L 438.451 93.478 L 436.331 93.478 L 436.331 91.64 Z"/>
|
||||
<path style="fill: rgb(89, 109, 140); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 462.836 53.909 L 471.299 50.233 L 471.299 105.475 L 462.836 109.151 L 462.836 53.909 Z"/>
|
||||
<path style="fill: rgb(191, 211, 242); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 462.836 53.909 L 471.299 50.233 L 369.521 50.233 L 361.061 53.909 L 462.836 53.909 Z"/>
|
||||
</g>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre;" x="56" y="116.982">COMPRESSOR A</text>
|
||||
<rect x="50" y="150" width="100" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0);"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.368" y="141.716">On/Off</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="120.949" y="168.123">H</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.007" y="167.208">####</text>
|
||||
<rect x="49.832" y="225.548" width="100.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
|
||||
<rect x="49.832" y="250" width="100.168" height="50" style="stroke: rgb(0, 0, 0); stroke-width: 0.693; fill: rgb(244, 248, 248);"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.693;" cx="124.956" cy="262.355" rx="11.269" ry="10.987"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="55.832" y="241.982">COMPRESSOR B</text>
|
||||
<rect x="49.832" y="275" width="100" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.2" y="266.716">On/Off</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="120.781" y="293.123">H</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="55.839" y="292.208">####</text>
|
||||
<rect x="49.832" y="350.548" width="100.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
|
||||
<rect x="49.832" y="375" width="100.168" height="50" style="stroke: rgb(0, 0, 0); stroke-width: 0.693; fill: rgb(244, 248, 248);"/>
|
||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.693;" cx="124.956" cy="387.355" rx="11.269" ry="10.987"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="55.832" y="366.982">COMPRESSOR C</text>
|
||||
<rect x="49.832" y="400" width="100" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.2" y="391.716">On/Off</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="120.781" y="418.123">H</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="55.839" y="417.208">####</text>
|
||||
<g transform="matrix(1.116151, 0, 0, 1.116686, -10.678391, 171.611261)" style="">
|
||||
<path style="fill: rgb(204, 204, 204); stroke-width: 1;" d="M 166.356 47.81 L 278.348 47.81 L 278.348 114.973 L 166.356 114.973 L 166.356 47.81 Z"/>
|
||||
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 166.356 95.477 L 278.348 67.396 L 278.348 114.973 L 166.356 114.973 L 166.356 95.477 Z"/>
|
||||
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 166.356 98.16 L 278.348 70.258 L 278.348 114.973 L 166.356 114.973 L 166.356 98.16 Z"/>
|
||||
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 166.356 102.453 L 278.348 74.371 L 278.348 114.973 L 166.356 114.973 L 166.356 102.453 Z"/>
|
||||
<path style="fill: rgb(204, 204, 204); stroke-width: 1;" d="M 168.486 49.688 L 276.106 49.688 L 276.106 109.249 L 168.486 109.249 L 168.486 49.688 Z"/>
|
||||
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 222.296 91.9 L 276.106 67.038 L 276.106 109.249 L 222.296 109.249 L 222.296 91.9 Z"/>
|
||||
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 168.486 92.079 L 222.296 67.038 L 222.296 109.249 L 168.486 109.249 L 168.486 92.079 Z"/>
|
||||
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 222.296 94.583 L 276.106 69.542 L 276.106 109.249 L 222.296 109.249 L 222.296 94.583 Z"/>
|
||||
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 168.486 94.583 L 222.296 69.542 L 222.296 109.249 L 168.486 109.249 L 168.486 94.583 Z"/>
|
||||
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 222.296 98.16 L 276.106 73.298 L 276.106 109.249 L 222.296 109.249 L 222.296 98.16 Z"/>
|
||||
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 168.486 98.16 L 222.296 73.298 L 222.296 109.249 L 168.486 109.249 L 168.486 98.16 Z"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 166.356 47.81 L 278.348 47.81 L 278.348 114.973 L 166.356 114.973 L 166.356 47.81"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 222.296 49.688 L 276.106 49.688 L 276.106 109.249 L 222.296 109.249 L 222.296 49.688"/>
|
||||
<path style="fill: rgb(153, 153, 153); stroke-width: 1;" d="M 240.906 110.323 L 259.739 110.323 L 259.739 113.9 L 240.906 113.9 L 240.906 110.323 Z"/>
|
||||
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 169.607 108.713 L 169.159 108.176 L 169.607 107.64 L 170.504 107.64 L 170.952 108.176 L 170.504 108.713 L 169.607 108.713 Z"/>
|
||||
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 169.607 68.111 L 169.159 67.575 L 169.607 66.859 L 170.504 66.859 L 170.952 67.575 L 170.504 68.111 L 169.607 68.111 Z"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 168.486 49.688 L 222.296 49.688 L 222.296 109.249 L 168.486 109.249 L 168.486 49.688"/>
|
||||
<path style="fill: rgb(229, 229, 229); stroke-width: 1;" d="M 229.247 51.119 L 235.749 51.119 L 235.749 59.973 L 229.247 59.973 L 229.247 51.119 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 224.762 51.119 L 226.332 51.119 L 226.332 55.233 L 224.762 55.233 L 224.762 51.119 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 272.519 51.119 L 274.313 51.119 L 274.313 55.233 L 272.519 55.233 L 272.519 51.119 Z"/>
|
||||
<path style="fill: rgb(51, 51, 51); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 168.486 49.688 L 222.296 49.688 L 222.296 66.501 L 168.486 66.501 L 168.486 49.688 Z"/>
|
||||
<path style="fill: rgb(229, 229, 229); stroke-width: 1;" d="M 185.526 60.778 L 192.028 60.778 L 192.028 63.103 L 185.526 63.103 L 185.526 60.778 Z"/>
|
||||
<path style="fill: rgb(178, 178, 178); stroke-width: 1;" d="M 200.324 62.745 L 198.978 61.672 L 198.978 60.152 L 200.324 59.079 L 202.117 59.079 L 203.687 60.152 L 203.687 61.672 L 202.117 62.745 L 200.324 62.745 Z"/>
|
||||
<path style="fill: rgb(178, 178, 178); stroke-width: 1;" d="M 207.947 62.745 L 206.602 61.672 L 206.602 60.152 L 207.947 59.079 L 209.74 59.079 L 211.086 60.152 L 211.086 61.672 L 209.74 62.745 L 207.947 62.745 Z"/>
|
||||
<path style="fill: rgb(102, 102, 102); stroke-width: 1;" d="M 176.109 60.778 L 182.611 60.778 L 182.611 63.103 L 176.109 63.103 L 176.109 60.778 Z"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 179.024 54.339 L 179.024 59.079 L 176.782 59.079"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 183.06 54.339 L 183.06 59.079 L 180.93 59.079"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 187.32 54.339 L 187.32 59.079 L 184.853 59.079"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 191.355 54.339 L 191.355 59.079 L 188.889 59.079"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 188.889 59.079 L 188.889 54.339 L 191.355 54.339"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 184.853 59.079 L 184.853 54.339 L 187.32 54.339"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 180.93 59.079 L 180.93 54.339 L 183.06 54.339"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 176.782 59.079 L 176.782 54.339 L 179.024 54.339"/>
|
||||
<path style="fill: rgb(127, 127, 127); stroke-width: 1;" d="M 197.857 66.859 L 222.296 104.241 L 222.296 94.404 L 204.135 66.859 L 197.857 66.859 Z"/>
|
||||
<path style="fill: rgb(102, 102, 102); stroke-width: 1;" d="M 204.135 66.859 L 222.296 94.404 L 222.296 84.566 L 210.637 66.859 L 204.135 66.859 Z"/>
|
||||
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 210.637 66.859 L 222.296 84.566 L 222.296 74.729 L 217.139 66.859 L 210.637 66.859 Z"/>
|
||||
<path style="fill: none; stroke: rgb(255, 255, 255); stroke-width: 2;" d="M 222.969 50.046 L 275.434 50.046"/>
|
||||
</g>
|
||||
<g transform="matrix(1.116151, 0, 0, 1.116686, -10.678387, 296.611283)" style="">
|
||||
<path style="fill: rgb(204, 204, 204); stroke-width: 1;" d="M 166.356 47.81 L 278.348 47.81 L 278.348 114.973 L 166.356 114.973 L 166.356 47.81 Z"/>
|
||||
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 166.356 95.477 L 278.348 67.396 L 278.348 114.973 L 166.356 114.973 L 166.356 95.477 Z"/>
|
||||
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 166.356 98.16 L 278.348 70.258 L 278.348 114.973 L 166.356 114.973 L 166.356 98.16 Z"/>
|
||||
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 166.356 102.453 L 278.348 74.371 L 278.348 114.973 L 166.356 114.973 L 166.356 102.453 Z"/>
|
||||
<path style="fill: rgb(204, 204, 204); stroke-width: 1;" d="M 168.486 49.688 L 276.106 49.688 L 276.106 109.249 L 168.486 109.249 L 168.486 49.688 Z"/>
|
||||
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 222.296 91.9 L 276.106 67.038 L 276.106 109.249 L 222.296 109.249 L 222.296 91.9 Z"/>
|
||||
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 168.486 92.079 L 222.296 67.038 L 222.296 109.249 L 168.486 109.249 L 168.486 92.079 Z"/>
|
||||
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 222.296 94.583 L 276.106 69.542 L 276.106 109.249 L 222.296 109.249 L 222.296 94.583 Z"/>
|
||||
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 168.486 94.583 L 222.296 69.542 L 222.296 109.249 L 168.486 109.249 L 168.486 94.583 Z"/>
|
||||
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 222.296 98.16 L 276.106 73.298 L 276.106 109.249 L 222.296 109.249 L 222.296 98.16 Z"/>
|
||||
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 168.486 98.16 L 222.296 73.298 L 222.296 109.249 L 168.486 109.249 L 168.486 98.16 Z"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 166.356 47.81 L 278.348 47.81 L 278.348 114.973 L 166.356 114.973 L 166.356 47.81"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 222.296 49.688 L 276.106 49.688 L 276.106 109.249 L 222.296 109.249 L 222.296 49.688"/>
|
||||
<path style="fill: rgb(153, 153, 153); stroke-width: 1;" d="M 240.906 110.323 L 259.739 110.323 L 259.739 113.9 L 240.906 113.9 L 240.906 110.323 Z"/>
|
||||
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 169.607 108.713 L 169.159 108.176 L 169.607 107.64 L 170.504 107.64 L 170.952 108.176 L 170.504 108.713 L 169.607 108.713 Z"/>
|
||||
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 169.607 68.111 L 169.159 67.575 L 169.607 66.859 L 170.504 66.859 L 170.952 67.575 L 170.504 68.111 L 169.607 68.111 Z"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 168.486 49.688 L 222.296 49.688 L 222.296 109.249 L 168.486 109.249 L 168.486 49.688"/>
|
||||
<path style="fill: rgb(229, 229, 229); stroke-width: 1;" d="M 229.247 51.119 L 235.749 51.119 L 235.749 59.973 L 229.247 59.973 L 229.247 51.119 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 224.762 51.119 L 226.332 51.119 L 226.332 55.233 L 224.762 55.233 L 224.762 51.119 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 272.519 51.119 L 274.313 51.119 L 274.313 55.233 L 272.519 55.233 L 272.519 51.119 Z"/>
|
||||
<path style="fill: rgb(51, 51, 51); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 168.486 49.688 L 222.296 49.688 L 222.296 66.501 L 168.486 66.501 L 168.486 49.688 Z"/>
|
||||
<path style="fill: rgb(229, 229, 229); stroke-width: 1;" d="M 185.526 60.778 L 192.028 60.778 L 192.028 63.103 L 185.526 63.103 L 185.526 60.778 Z"/>
|
||||
<path style="fill: rgb(178, 178, 178); stroke-width: 1;" d="M 200.324 62.745 L 198.978 61.672 L 198.978 60.152 L 200.324 59.079 L 202.117 59.079 L 203.687 60.152 L 203.687 61.672 L 202.117 62.745 L 200.324 62.745 Z"/>
|
||||
<path style="fill: rgb(178, 178, 178); stroke-width: 1;" d="M 207.947 62.745 L 206.602 61.672 L 206.602 60.152 L 207.947 59.079 L 209.74 59.079 L 211.086 60.152 L 211.086 61.672 L 209.74 62.745 L 207.947 62.745 Z"/>
|
||||
<path style="fill: rgb(102, 102, 102); stroke-width: 1;" d="M 176.109 60.778 L 182.611 60.778 L 182.611 63.103 L 176.109 63.103 L 176.109 60.778 Z"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 179.024 54.339 L 179.024 59.079 L 176.782 59.079"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 183.06 54.339 L 183.06 59.079 L 180.93 59.079"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 187.32 54.339 L 187.32 59.079 L 184.853 59.079"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 191.355 54.339 L 191.355 59.079 L 188.889 59.079"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 188.889 59.079 L 188.889 54.339 L 191.355 54.339"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 184.853 59.079 L 184.853 54.339 L 187.32 54.339"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 180.93 59.079 L 180.93 54.339 L 183.06 54.339"/>
|
||||
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 176.782 59.079 L 176.782 54.339 L 179.024 54.339"/>
|
||||
<path style="fill: rgb(127, 127, 127); stroke-width: 1;" d="M 197.857 66.859 L 222.296 104.241 L 222.296 94.404 L 204.135 66.859 L 197.857 66.859 Z"/>
|
||||
<path style="fill: rgb(102, 102, 102); stroke-width: 1;" d="M 204.135 66.859 L 222.296 94.404 L 222.296 84.566 L 210.637 66.859 L 204.135 66.859 Z"/>
|
||||
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 210.637 66.859 L 222.296 84.566 L 222.296 74.729 L 217.139 66.859 L 210.637 66.859 Z"/>
|
||||
<path style="fill: none; stroke: rgb(255, 255, 255); stroke-width: 2;" d="M 222.969 50.046 L 275.434 50.046"/>
|
||||
</g>
|
||||
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 4, 255);" d="M 300 125 L 350 125 L 350 275 L 400 275"/>
|
||||
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 4, 255);" d="M 300 250 L 350 250 L 350 275 L 400 275"/>
|
||||
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 4, 255);" d="M 300 375 L 350 375 L 350 275 L 400 275"/>
|
||||
<g transform="matrix(1, 0, 0, 1, 49.999999, -99.999998)" style="">
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 292.93 L 717.857 292.651 L 717.857 292.442 L 717.857 292.163 L 717.857 291.954 L 717.857 291.744 L 717.857 291.535 L 717.857 291.396 L 717.914 291.186 L 717.914 291.046 L 717.972 290.907 L 718.029 290.837 L 718.086 290.698 L 718.143 290.558 L 718.201 290.488 L 718.258 290.419 L 718.372 290.349 L 718.487 290.279 L 718.602 290.209 L 718.716 290.14 L 718.888 290.14 L 719.06 290.07 L 719.231 290.07 L 719.46 290 L 719.632 290 L 719.919 290 L 720.147 290 L 720.434 290 L 720.72 290 L 721.063 290 L 721.407 290 L 721.751 290 L 722.151 290 L 770.706 290 L 771.106 290 L 771.45 290 L 771.794 290 L 772.137 290 L 772.423 290 L 772.71 290 L 772.939 290 L 773.225 290 L 773.397 290.07 L 773.626 290.07 L 773.798 290.14 L 773.969 290.14 L 774.141 290.209 L 774.256 290.279 L 774.37 290.349 L 774.485 290.419 L 774.599 290.488 L 774.657 290.558 L 774.714 290.698 L 774.771 290.837 L 774.828 290.977 L 774.886 291.117 L 774.943 291.256 L 774.943 291.465 L 775 291.674 L 775 291.884 L 775 292.093 L 775 292.303 L 775 292.581 L 775 292.86 L 775 293.209 L 775 293.488 L 775 335.558 L 775 335.907 L 775 336.256 L 775 336.604 L 775 336.884 L 775 337.163 L 775 337.442 L 775 337.72 L 775 337.93 L 774.943 338.14 L 774.943 338.349 L 774.943 338.488 L 774.886 338.628 L 774.886 338.767 L 774.828 338.907 L 774.771 339.047 L 774.714 339.117 L 774.657 339.256 L 774.599 339.326 L 774.485 339.396 L 774.427 339.465 L 774.313 339.465 L 774.198 339.535 L 774.084 339.535 L 773.969 339.604 L 773.798 339.604 L 773.683 339.604 L 773.511 339.604 L 773.339 339.674 L 773.168 339.674 L 772.939 339.674 L 772.71 339.674 L 772.481 339.674 L 721.579 339.674 L 721.235 339.674 L 720.892 339.674 L 720.663 339.674 L 720.377 339.674 L 720.09 339.604 L 719.861 339.604 L 719.69 339.604 L 719.46 339.604 L 719.289 339.535 L 719.117 339.535 L 718.945 339.465 L 718.831 339.465 L 718.659 339.396 L 718.544 339.326 L 718.43 339.256 L 718.372 339.117 L 718.258 339.047 L 718.201 338.907 L 718.143 338.767 L 718.086 338.628 L 718.029 338.488 L 717.972 338.349 L 717.914 338.14 L 717.914 337.93 L 717.914 337.72 L 717.857 337.442 L 717.857 337.163 L 717.857 336.884 L 717.857 336.604 L 717.857 336.256 L 717.857 335.907 L 717.857 335.558 L 717.857 292.93 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 732.114 339.674 L 760.743 339.674 L 760.743 342.884 L 732.114 342.884 L 732.114 339.674 Z"/>
|
||||
<path style="fill: rgb(67, 67, 67); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 722.151 299.419 L 722.151 299.07 L 722.151 298.791 L 722.151 298.442 L 722.151 298.163 L 722.151 297.954 L 722.151 297.675 L 722.209 297.465 L 722.209 297.256 L 722.209 297.046 L 722.266 296.837 L 722.323 296.698 L 722.323 296.558 L 722.381 296.419 L 722.495 296.279 L 722.553 296.209 L 722.61 296.07 L 722.725 296 L 722.838 295.93 L 722.953 295.86 L 723.067 295.791 L 723.239 295.791 L 723.354 295.721 L 723.526 295.721 L 723.754 295.651 L 723.926 295.651 L 724.155 295.651 L 724.385 295.651 L 724.671 295.651 L 724.9 295.651 L 725.243 295.651 L 725.53 295.651 L 725.873 295.651 L 767.557 295.651 L 767.843 295.651 L 768.129 295.651 L 768.358 295.651 L 768.587 295.651 L 768.816 295.651 L 769.045 295.721 L 769.217 295.721 L 769.389 295.791 L 769.561 295.791 L 769.675 295.86 L 769.79 295.86 L 769.962 295.93 L 770.076 296 L 770.133 296.07 L 770.247 296.139 L 770.305 296.209 L 770.362 296.349 L 770.419 296.419 L 770.477 296.558 L 770.534 296.698 L 770.591 296.837 L 770.591 296.907 L 770.649 297.046 L 770.649 297.256 L 770.649 297.396 L 770.706 297.604 L 770.706 297.744 L 770.706 297.954 L 770.706 298.163 L 770.706 298.442 L 770.706 298.651 L 770.706 298.861 L 770.706 327.744 L 770.706 328.093 L 770.706 328.372 L 770.706 328.581 L 770.706 328.861 L 770.763 329.07 L 770.763 329.279 L 770.763 329.488 L 770.763 329.628 L 770.763 329.838 L 770.763 329.977 L 770.706 330.117 L 770.706 330.186 L 770.649 330.326 L 770.649 330.465 L 770.591 330.535 L 770.534 330.604 L 770.477 330.674 L 770.419 330.744 L 770.305 330.814 L 770.19 330.883 L 770.076 330.883 L 769.962 330.883 L 769.79 330.953 L 769.618 330.953 L 769.446 330.953 L 769.274 331.023 L 769.045 331.023 L 768.759 331.023 L 768.53 331.023 L 768.244 331.023 L 767.9 331.023 L 767.557 331.023 L 725.873 331.023 L 725.53 331.023 L 725.243 331.023 L 724.9 331.023 L 724.671 331.023 L 724.385 331.023 L 724.155 331.023 L 723.926 331.023 L 723.754 331.023 L 723.526 331.023 L 723.354 331.023 L 723.239 331.023 L 723.067 330.953 L 722.953 330.953 L 722.838 330.883 L 722.725 330.814 L 722.61 330.744 L 722.553 330.674 L 722.495 330.604 L 722.381 330.465 L 722.323 330.326 L 722.323 330.186 L 722.266 330.046 L 722.209 329.838 L 722.209 329.628 L 722.209 329.419 L 722.151 329.209 L 722.151 328.93 L 722.151 328.651 L 722.151 328.302 L 722.151 327.954 L 722.151 327.604 L 722.151 327.186 L 722.151 299.419 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 350 L 775 350 L 775 347.558 L 760.743 342.884 L 732.114 342.884 L 717.857 347.558 L 717.857 350 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 770.133 334.023 L 770.133 337.512 L 756.391 337.512 L 756.391 334.023 L 770.133 334.023 Z"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 347.558 L 775 347.558"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 675 285.361 L 703.571 285.361 L 703.571 350 L 675 350 L 675 285.361 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 287.463 L 701.169 287.463 L 701.169 350 L 677.317 350 L 677.317 287.463 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 290.616 L 701.169 290.616 L 701.169 297.898 L 677.317 297.898 L 677.317 290.616 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 297.898 L 701.169 297.898 L 701.169 305.181 L 677.317 305.181 L 677.317 297.898 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 293.694 L 698.767 293.694 L 698.767 294.745 L 679.72 294.745 L 679.72 293.694 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 300 L 698.767 300 L 698.767 303.078 L 679.72 303.078 L 679.72 300 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 275 L 698.767 275 L 703.571 285.361 L 675 285.361 L 679.72 275 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 695.163 300 L 701.169 300 L 701.169 303.078 L 695.163 303.078 L 695.163 300 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 300 L 698.767 300 L 698.767 303.078 L 697.566 303.078 L 697.566 300 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 291.592 L 692.847 291.592 L 692.847 292.643 L 697.566 292.643 L 697.566 291.592 Z"/>
|
||||
<circle style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="890.673" cy="114.148" r="1.051" transform="matrix(1.142857, 0, 0, 1.000013, -320.346635, 178.493448)"/>
|
||||
</g>
|
||||
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(6, 255, 0);" d="M 525 250 L 575 250 L 575 200 L 725 200"/>
|
||||
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(6, 255, 0);" d="M 525 250 L 575 250 L 575 375 L 725 375"/>
|
||||
<rect x="399.832" y="149.864" width="125.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
|
||||
<rect x="400" y="175" width="125" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="417.352" y="167.628">PLC Compressor</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="426.101" y="192.839">IP : 192.168.0.1</text>
|
||||
<rect x="725" y="99.864" width="125.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
|
||||
<rect x="725.168" y="125" width="125" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="759.52" y="117.628">PC Station</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="751.269" y="142.839">IP : 192.168.0.2</text>
|
||||
<g transform="matrix(1, 0, 0, 1, 49.999999, 74.999996)" style="">
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 292.93 L 717.857 292.651 L 717.857 292.442 L 717.857 292.163 L 717.857 291.954 L 717.857 291.744 L 717.857 291.535 L 717.857 291.396 L 717.914 291.186 L 717.914 291.046 L 717.972 290.907 L 718.029 290.837 L 718.086 290.698 L 718.143 290.558 L 718.201 290.488 L 718.258 290.419 L 718.372 290.349 L 718.487 290.279 L 718.602 290.209 L 718.716 290.14 L 718.888 290.14 L 719.06 290.07 L 719.231 290.07 L 719.46 290 L 719.632 290 L 719.919 290 L 720.147 290 L 720.434 290 L 720.72 290 L 721.063 290 L 721.407 290 L 721.751 290 L 722.151 290 L 770.706 290 L 771.106 290 L 771.45 290 L 771.794 290 L 772.137 290 L 772.423 290 L 772.71 290 L 772.939 290 L 773.225 290 L 773.397 290.07 L 773.626 290.07 L 773.798 290.14 L 773.969 290.14 L 774.141 290.209 L 774.256 290.279 L 774.37 290.349 L 774.485 290.419 L 774.599 290.488 L 774.657 290.558 L 774.714 290.698 L 774.771 290.837 L 774.828 290.977 L 774.886 291.117 L 774.943 291.256 L 774.943 291.465 L 775 291.674 L 775 291.884 L 775 292.093 L 775 292.303 L 775 292.581 L 775 292.86 L 775 293.209 L 775 293.488 L 775 335.558 L 775 335.907 L 775 336.256 L 775 336.604 L 775 336.884 L 775 337.163 L 775 337.442 L 775 337.72 L 775 337.93 L 774.943 338.14 L 774.943 338.349 L 774.943 338.488 L 774.886 338.628 L 774.886 338.767 L 774.828 338.907 L 774.771 339.047 L 774.714 339.117 L 774.657 339.256 L 774.599 339.326 L 774.485 339.396 L 774.427 339.465 L 774.313 339.465 L 774.198 339.535 L 774.084 339.535 L 773.969 339.604 L 773.798 339.604 L 773.683 339.604 L 773.511 339.604 L 773.339 339.674 L 773.168 339.674 L 772.939 339.674 L 772.71 339.674 L 772.481 339.674 L 721.579 339.674 L 721.235 339.674 L 720.892 339.674 L 720.663 339.674 L 720.377 339.674 L 720.09 339.604 L 719.861 339.604 L 719.69 339.604 L 719.46 339.604 L 719.289 339.535 L 719.117 339.535 L 718.945 339.465 L 718.831 339.465 L 718.659 339.396 L 718.544 339.326 L 718.43 339.256 L 718.372 339.117 L 718.258 339.047 L 718.201 338.907 L 718.143 338.767 L 718.086 338.628 L 718.029 338.488 L 717.972 338.349 L 717.914 338.14 L 717.914 337.93 L 717.914 337.72 L 717.857 337.442 L 717.857 337.163 L 717.857 336.884 L 717.857 336.604 L 717.857 336.256 L 717.857 335.907 L 717.857 335.558 L 717.857 292.93 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 732.114 339.674 L 760.743 339.674 L 760.743 342.884 L 732.114 342.884 L 732.114 339.674 Z"/>
|
||||
<path style="fill: rgb(67, 67, 67); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 722.151 299.419 L 722.151 299.07 L 722.151 298.791 L 722.151 298.442 L 722.151 298.163 L 722.151 297.954 L 722.151 297.675 L 722.209 297.465 L 722.209 297.256 L 722.209 297.046 L 722.266 296.837 L 722.323 296.698 L 722.323 296.558 L 722.381 296.419 L 722.495 296.279 L 722.553 296.209 L 722.61 296.07 L 722.725 296 L 722.838 295.93 L 722.953 295.86 L 723.067 295.791 L 723.239 295.791 L 723.354 295.721 L 723.526 295.721 L 723.754 295.651 L 723.926 295.651 L 724.155 295.651 L 724.385 295.651 L 724.671 295.651 L 724.9 295.651 L 725.243 295.651 L 725.53 295.651 L 725.873 295.651 L 767.557 295.651 L 767.843 295.651 L 768.129 295.651 L 768.358 295.651 L 768.587 295.651 L 768.816 295.651 L 769.045 295.721 L 769.217 295.721 L 769.389 295.791 L 769.561 295.791 L 769.675 295.86 L 769.79 295.86 L 769.962 295.93 L 770.076 296 L 770.133 296.07 L 770.247 296.139 L 770.305 296.209 L 770.362 296.349 L 770.419 296.419 L 770.477 296.558 L 770.534 296.698 L 770.591 296.837 L 770.591 296.907 L 770.649 297.046 L 770.649 297.256 L 770.649 297.396 L 770.706 297.604 L 770.706 297.744 L 770.706 297.954 L 770.706 298.163 L 770.706 298.442 L 770.706 298.651 L 770.706 298.861 L 770.706 327.744 L 770.706 328.093 L 770.706 328.372 L 770.706 328.581 L 770.706 328.861 L 770.763 329.07 L 770.763 329.279 L 770.763 329.488 L 770.763 329.628 L 770.763 329.838 L 770.763 329.977 L 770.706 330.117 L 770.706 330.186 L 770.649 330.326 L 770.649 330.465 L 770.591 330.535 L 770.534 330.604 L 770.477 330.674 L 770.419 330.744 L 770.305 330.814 L 770.19 330.883 L 770.076 330.883 L 769.962 330.883 L 769.79 330.953 L 769.618 330.953 L 769.446 330.953 L 769.274 331.023 L 769.045 331.023 L 768.759 331.023 L 768.53 331.023 L 768.244 331.023 L 767.9 331.023 L 767.557 331.023 L 725.873 331.023 L 725.53 331.023 L 725.243 331.023 L 724.9 331.023 L 724.671 331.023 L 724.385 331.023 L 724.155 331.023 L 723.926 331.023 L 723.754 331.023 L 723.526 331.023 L 723.354 331.023 L 723.239 331.023 L 723.067 330.953 L 722.953 330.953 L 722.838 330.883 L 722.725 330.814 L 722.61 330.744 L 722.553 330.674 L 722.495 330.604 L 722.381 330.465 L 722.323 330.326 L 722.323 330.186 L 722.266 330.046 L 722.209 329.838 L 722.209 329.628 L 722.209 329.419 L 722.151 329.209 L 722.151 328.93 L 722.151 328.651 L 722.151 328.302 L 722.151 327.954 L 722.151 327.604 L 722.151 327.186 L 722.151 299.419 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 350 L 775 350 L 775 347.558 L 760.743 342.884 L 732.114 342.884 L 717.857 347.558 L 717.857 350 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 770.133 334.023 L 770.133 337.512 L 756.391 337.512 L 756.391 334.023 L 770.133 334.023 Z"/>
|
||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 347.558 L 775 347.558"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 675 285.361 L 703.571 285.361 L 703.571 350 L 675 350 L 675 285.361 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 287.463 L 701.169 287.463 L 701.169 350 L 677.317 350 L 677.317 287.463 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 290.616 L 701.169 290.616 L 701.169 297.898 L 677.317 297.898 L 677.317 290.616 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 297.898 L 701.169 297.898 L 701.169 305.181 L 677.317 305.181 L 677.317 297.898 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 293.694 L 698.767 293.694 L 698.767 294.745 L 679.72 294.745 L 679.72 293.694 Z"/>
|
||||
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 300 L 698.767 300 L 698.767 303.078 L 679.72 303.078 L 679.72 300 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 275 L 698.767 275 L 703.571 285.361 L 675 285.361 L 679.72 275 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 695.163 300 L 701.169 300 L 701.169 303.078 L 695.163 303.078 L 695.163 300 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 300 L 698.767 300 L 698.767 303.078 L 697.566 303.078 L 697.566 300 Z"/>
|
||||
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 291.592 L 692.847 291.592 L 692.847 292.643 L 697.566 292.643 L 697.566 291.592 Z"/>
|
||||
<circle style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="890.673" cy="114.148" r="1.051" transform="matrix(1.142857, 0, 0, 1.000013, -320.346635, 178.493448)"/>
|
||||
</g>
|
||||
<rect x="724.832" y="275" width="125.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
|
||||
<rect x="725" y="300.136" width="125" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="760.352" y="292.764">PC Server</text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="751.1" y="317.975" transform="matrix(1, 0, 0, 1, -3, 0)">IP : xxx.xxx.xx.xx<tspan x="751.0999755859375" dy="1em"></tspan></text>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 30px; white-space: pre; stroke-width: 1;" x="312.458" y="50.984">OVERVIEW COMPRESSOR</text>
|
||||
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 0, 0);" d="M 625 75 L 625 114.125 L 625 264.125 L 625 450"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
@@ -30,18 +30,18 @@ instance.interceptors.response.use(
|
||||
originalRequest._retry = true;
|
||||
|
||||
try {
|
||||
console.log('🔄 Refresh token dipanggil...');
|
||||
// console.log('🔄 Refresh token dipanggil...');
|
||||
const refreshRes = await refreshApi.post('/auth/refresh-token');
|
||||
|
||||
const newAccessToken = refreshRes.data.data.accessToken;
|
||||
localStorage.setItem('token', newAccessToken);
|
||||
console.log('✅ Token refreshed successfully');
|
||||
// console.log('✅ Token refreshed successfully');
|
||||
|
||||
// update token di header
|
||||
instance.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;
|
||||
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
|
||||
|
||||
console.log('🔁 Retrying original request...');
|
||||
// console.log('🔁 Retrying original request...');
|
||||
return instance(originalRequest);
|
||||
} catch (refreshError) {
|
||||
console.error(
|
||||
@@ -70,24 +70,35 @@ async function ApiRequest({ method = 'GET', params = {}, prefix = '/', token = t
|
||||
},
|
||||
};
|
||||
|
||||
const rawToken = localStorage.getItem('token');
|
||||
const tokenRedirect = sessionStorage.getItem('token_redirect');
|
||||
|
||||
let rawToken = '';
|
||||
|
||||
if (tokenRedirect !== null) {
|
||||
rawToken = tokenRedirect;
|
||||
// console.log(`sessionStorage: ${tokenRedirect}`);
|
||||
} else {
|
||||
rawToken = localStorage.getItem('token');
|
||||
// console.log(`localStorage: ${rawToken}`);
|
||||
}
|
||||
|
||||
if (token && rawToken) {
|
||||
const cleanToken = rawToken.replace(/"/g, '');
|
||||
request.headers['Authorization'] = `Bearer ${cleanToken}`;
|
||||
console.log('🔐 Sending request with token:', cleanToken.substring(0, 20) + '...');
|
||||
// console.log('🔐 Sending request with token:', cleanToken.substring(0, 20) + '...');
|
||||
} else {
|
||||
console.warn('⚠️ No token found in localStorage');
|
||||
}
|
||||
|
||||
console.log('📤 API Request:', { method, url: prefix, hasToken: !!rawToken });
|
||||
// console.log('📤 API Request:', { method, url: prefix, hasToken: !!rawToken });
|
||||
|
||||
try {
|
||||
const response = await instance(request);
|
||||
console.log('✅ API Response:', {
|
||||
url: prefix,
|
||||
status: response.status,
|
||||
statusCode: response.data?.statusCode,
|
||||
});
|
||||
// console.log('✅ API Response:', {
|
||||
// url: prefix,
|
||||
// status: response.status,
|
||||
// statusCode: response.data?.statusCode,
|
||||
// });
|
||||
return { ...response, error: false };
|
||||
} catch (error) {
|
||||
const status = error?.response?.status || 500;
|
||||
@@ -132,17 +143,10 @@ async function cekError(status, message = '') {
|
||||
const SendRequest = async (queryParams) => {
|
||||
try {
|
||||
const response = await ApiRequest(queryParams);
|
||||
console.log('📦 SendRequest response:', {
|
||||
hasError: response.error,
|
||||
status: response.status,
|
||||
statusCode: response.data?.statusCode,
|
||||
data: response.data,
|
||||
});
|
||||
|
||||
// If ApiRequest returned error flag, return error structure
|
||||
if (response.error) {
|
||||
const errorMsg = response.data?.message || response.statusText || 'Request failed';
|
||||
console.error('❌ SendRequest error response:', errorMsg);
|
||||
|
||||
// Return consistent error structure instead of empty array
|
||||
return {
|
||||
|
||||
@@ -12,11 +12,12 @@ const CardList = ({
|
||||
showEditModal,
|
||||
showDeleteDialog,
|
||||
cardColor,
|
||||
fieldColor,
|
||||
}) => {
|
||||
const getCardStyle = () => {
|
||||
const color = cardColor ?? '#F3EDEA'; // Orange color
|
||||
const getCardStyle = (color) => {
|
||||
const colorStyle = color ?? '#F3EDEA'; // Orange color
|
||||
return {
|
||||
border: `2px solid ${color}`,
|
||||
border: `2px solid ${colorStyle}`,
|
||||
borderRadius: '8px',
|
||||
textAlign: 'center', // Center text
|
||||
};
|
||||
@@ -47,48 +48,55 @@ const CardList = ({
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={getTitleStyle(item.color ?? cardColor)}>
|
||||
<span
|
||||
style={getTitleStyle(fieldColor ? item[fieldColor] : cardColor)}
|
||||
>
|
||||
{item[header]}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
style={getCardStyle()}
|
||||
style={getCardStyle(fieldColor ? item[fieldColor] : cardColor)}
|
||||
actions={[
|
||||
<Space
|
||||
size="middle"
|
||||
style={{ display: 'flex', justifyContent: 'center' }}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
showPreviewModal && (
|
||||
<EyeOutlined
|
||||
style={{ color: '#1890ff' }}
|
||||
icon={<EyeOutlined />}
|
||||
key="preview"
|
||||
onClick={() => showPreviewModal(item)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
),
|
||||
showEditModal && (
|
||||
<EditOutlined
|
||||
style={{ color: '#faad14' }}
|
||||
icon={<EditOutlined />}
|
||||
key="edit"
|
||||
onClick={() => showEditModal(item)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
),
|
||||
showDeleteDialog && (
|
||||
<DeleteOutlined
|
||||
style={{ color: '#ff1818' }}
|
||||
key="delete"
|
||||
onClick={() => showDeleteDialog(item)}
|
||||
/>
|
||||
</Space>,
|
||||
]}
|
||||
),
|
||||
].filter(Boolean)} // <== Hapus elemen yang undefined
|
||||
>
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
{column.map((itemCard, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{!itemCard.hidden && itemCard.title !== 'No' && itemCard.title !== 'Aksi' && (
|
||||
{!itemCard.hidden &&
|
||||
itemCard.title !== 'No' &&
|
||||
itemCard.title !== 'Action' && (
|
||||
<p style={{ margin: '8px 0' }}>
|
||||
<Text strong>{itemCard.title}:</Text>{' '}
|
||||
{itemCard.render
|
||||
? itemCard.render(item[itemCard.dataIndex], item, index)
|
||||
: item[itemCard.dataIndex] || item[itemCard.key] || '-'
|
||||
}
|
||||
? itemCard.render(
|
||||
item[itemCard.dataIndex],
|
||||
item,
|
||||
index
|
||||
)
|
||||
: item[itemCard.dataIndex] ||
|
||||
item[itemCard.key] ||
|
||||
'-'}
|
||||
</p>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
||||
48
src/components/Global/DateRealTime.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Typography } from 'antd';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const JamRealtimeAntd = () => {
|
||||
const [waktu, setWaktu] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setWaktu(new Date());
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Format custom manual untuk konsistensi
|
||||
const formatWaktuLengkap = (date) => {
|
||||
const hari = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
|
||||
const bulan = [
|
||||
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
|
||||
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
|
||||
];
|
||||
|
||||
const namaHari = hari[date.getDay()];
|
||||
const tanggal = date.getDate().toString().padStart(2, '0');
|
||||
const namaBulan = bulan[date.getMonth()];
|
||||
const tahun = date.getFullYear();
|
||||
|
||||
const jam = date.getHours().toString().padStart(2, '0');
|
||||
const menit = date.getMinutes().toString().padStart(2, '0');
|
||||
const detik = date.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
return `${namaHari}, ${tanggal} ${namaBulan} ${tahun} ${jam}:${menit}:${detik}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Text style={{
|
||||
fontSize: '25px',
|
||||
// fontWeight: 'bold',
|
||||
color: '#1BAA56'
|
||||
}}>
|
||||
{formatWaktuLengkap(waktu)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default JamRealtimeAntd;
|
||||
@@ -2,7 +2,16 @@
|
||||
import mqtt from 'mqtt';
|
||||
|
||||
const mqttUrl = `${import.meta.env.VITE_MQTT_SERVER ?? 'ws://localhost:1884'}`;
|
||||
const topics = ['PIU_GGCP/Devices/PB'];
|
||||
const topics = [
|
||||
'PIU_COD/AIR_DRYER/OVERVIEW',
|
||||
'PIU_COD/AIR_DRYER/AIR_DRYER_A',
|
||||
'PIU_COD/AIR_DRYER/AIR_DRYER_B',
|
||||
'PIU_COD/AIR_DRYER/AIR_DRYER_C',
|
||||
'PIU_COD/COMPRESSOR/OVERVIEW',
|
||||
'PIU_COD/COMPRESSOR/COMPRESSOR_A',
|
||||
'PIU_COD/COMPRESSOR/COMPRESSOR_B',
|
||||
'PIU_COD/COMPRESSOR/COMPRESSOR_C'
|
||||
];
|
||||
const options = {
|
||||
keepalive: 30,
|
||||
clientId: 'react_mqtt_' + Math.random().toString(16).substr(2, 8),
|
||||
@@ -66,7 +75,8 @@ const listenMessage = (callback) => {
|
||||
|
||||
const setValSvg = (listenTopic, svg) => {
|
||||
client.on('message', (topic, message) => {
|
||||
if (topic == listenTopic) {
|
||||
// console.log(topic ,' = ', listenTopic);
|
||||
if (topic === listenTopic) {
|
||||
const objChanel = JSON.parse(message);
|
||||
|
||||
Object.entries(objChanel).forEach(([key, value]) => {
|
||||
@@ -78,7 +88,7 @@ const setValSvg = (listenTopic, svg) => {
|
||||
} else if (value === false) {
|
||||
el.style.display = 'none';
|
||||
} else if (!isNaN(value)) {
|
||||
el.textContent = Number(value ?? 0.0);
|
||||
el.textContent = Number(value ?? 0.0).toFixed(2);
|
||||
} else {
|
||||
el.textContent = value;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { memo, useState, useEffect, useRef } from 'react';
|
||||
import { Table, Pagination, Row, Col, Card, Grid, Button, Typography, Tag, Segmented } from 'antd';
|
||||
import { AppstoreOutlined, TableOutlined } from '@ant-design/icons';
|
||||
import { MacCommandOutlined, TableOutlined } from '@ant-design/icons';
|
||||
import CardList from './CardList';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -17,6 +17,12 @@ const TableList = memo(function TableList({
|
||||
showEditModal,
|
||||
showDeleteDialog,
|
||||
cardColor,
|
||||
fieldColor,
|
||||
firstLoad = true,
|
||||
columnDynamic = false,
|
||||
cardComponent, // New prop for custom card component
|
||||
onStockUpdate, // Prop to pass to card component
|
||||
onGetData, // Callback to execute when data is received
|
||||
}) {
|
||||
const [gridLoading, setGridLoading] = useState(false);
|
||||
|
||||
@@ -29,12 +35,21 @@ const TableList = memo(function TableList({
|
||||
total_page: 1,
|
||||
});
|
||||
|
||||
const [viewMode, setViewMode] = useState('card');
|
||||
const [columnsDynamic, setColumnsDynamic] = useState(columns);
|
||||
|
||||
const [viewMode, setViewMode] = useState('table');
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const [renderCount, setRenderCount] = useState(firstLoad ? 1 : 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (renderCount < 1) {
|
||||
setRenderCount(renderCount + 1);
|
||||
return;
|
||||
} else {
|
||||
filter(1, pagination.current_limit);
|
||||
}
|
||||
}, [triger]);
|
||||
|
||||
const filter = async (currentPage, pageSize) => {
|
||||
@@ -48,7 +63,57 @@ const TableList = memo(function TableList({
|
||||
const param = new URLSearchParams({ ...paging, ...queryParams });
|
||||
const resData = await getData(param);
|
||||
|
||||
setData(resData?.data ?? []);
|
||||
if (columnDynamic && resData) {
|
||||
const columnsApi = resData[columnDynamic] ?? '';
|
||||
|
||||
// Pisahkan string menjadi array kolom
|
||||
const colArray = columnsApi.split(',').map((c) => c.trim());
|
||||
|
||||
// Kolom default datetime di awal
|
||||
const defaultColumns = [
|
||||
{
|
||||
title: 'No',
|
||||
key: 'no',
|
||||
width: '5%',
|
||||
align: 'center',
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
title: 'Datetime',
|
||||
dataIndex: 'datetime',
|
||||
key: 'datetime',
|
||||
width: '15%',
|
||||
// render: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
];
|
||||
|
||||
// Buat kolom numerik dengan format 4 angka di belakang koma
|
||||
const numericColumns = colArray.map((colName) => ({
|
||||
title: colName,
|
||||
dataIndex: colName,
|
||||
key: colName,
|
||||
align: 'right',
|
||||
width: 'auto',
|
||||
render: (value) => {
|
||||
if (typeof value === 'number') {
|
||||
return value.toFixed(4);
|
||||
}
|
||||
return value ?? '-';
|
||||
},
|
||||
}));
|
||||
|
||||
// Gabungkan default + API columns
|
||||
setColumnsDynamic([...defaultColumns, ...numericColumns]);
|
||||
}
|
||||
|
||||
const fetchedData = resData?.data ?? [];
|
||||
|
||||
// Panggil callback jika disediakan
|
||||
if (onGetData && typeof onGetData === 'function') {
|
||||
onGetData(fetchedData);
|
||||
}
|
||||
|
||||
setData(fetchedData);
|
||||
|
||||
const pagingData = resData?.paging;
|
||||
|
||||
@@ -62,6 +127,8 @@ const TableList = memo(function TableList({
|
||||
}));
|
||||
}
|
||||
|
||||
setGridLoading(false);
|
||||
|
||||
if (resData) {
|
||||
setTimeout(() => {
|
||||
setGridLoading(false);
|
||||
@@ -85,35 +152,41 @@ const TableList = memo(function TableList({
|
||||
|
||||
const isMobile = !screens.md; // kalau kurang dari md (768px) dianggap mobile
|
||||
|
||||
// Use the custom card component if provided, otherwise default to CardList
|
||||
const CardViewComponent = cardComponent || CardList;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Segmented
|
||||
options={[
|
||||
{ value: 'card', icon: <AppstoreOutlined /> },
|
||||
{ value: 'table', icon: <TableOutlined /> },
|
||||
{ value: 'card', icon: <MacCommandOutlined /> },
|
||||
]}
|
||||
value={viewMode}
|
||||
onChange={setViewMode}
|
||||
/>
|
||||
{(isMobile && mobile) || viewMode === 'card' ? (
|
||||
<CardList
|
||||
<CardViewComponent
|
||||
cardColor={cardColor}
|
||||
fieldColor={fieldColor}
|
||||
data={data}
|
||||
column={columns}
|
||||
column={columnsDynamic}
|
||||
header={header}
|
||||
showPreviewModal={showPreviewModal}
|
||||
showEditModal={showEditModal}
|
||||
showDeleteDialog={showDeleteDialog}
|
||||
onStockUpdate={onStockUpdate}
|
||||
/>
|
||||
) : (
|
||||
<Row gutter={24}>
|
||||
<Row gutter={24} style={{ marginTop: '16px' }}>
|
||||
<Table
|
||||
rowSelection={rowSelection || null}
|
||||
columns={columns}
|
||||
columns={columnsDynamic}
|
||||
dataSource={data.map((item, index) => ({ ...item, key: index }))}
|
||||
pagination={false}
|
||||
loading={gridLoading}
|
||||
scroll={{ y: 520 }}
|
||||
size="small"
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
@@ -141,3 +214,4 @@ const TableList = memo(function TableList({
|
||||
});
|
||||
|
||||
export default TableList;
|
||||
|
||||
|
||||
@@ -20,36 +20,65 @@ html body {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Custom Orange Sidebar Menu Styles */
|
||||
.custom-orange-menu.ant-menu-dark .ant-menu-item-selected {
|
||||
/* Custom green Sidebar Menu Styles */
|
||||
.custom-green-menu.ant-menu-dark .ant-menu-item-selected {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.custom-orange-menu.ant-menu-dark .ant-menu-item-selected::after {
|
||||
.custom-green-menu.ant-menu-dark .ant-menu-item-selected::after {
|
||||
border-right-color: white !important;
|
||||
}
|
||||
|
||||
.custom-orange-menu.ant-menu-dark .ant-menu-item:hover,
|
||||
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-title:hover {
|
||||
.custom-green-menu.ant-menu-dark .ant-menu-item:hover,
|
||||
.custom-green-menu.ant-menu-dark .ant-menu-submenu-title:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title {
|
||||
.custom-green-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.custom-orange-menu.ant-menu-dark.ant-menu-inline .ant-menu-sub {
|
||||
.custom-green-menu.ant-menu-dark.ant-menu-inline .ant-menu-sub {
|
||||
background: rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.custom-orange-menu.ant-menu-dark .ant-menu-item,
|
||||
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-title {
|
||||
.custom-green-menu.ant-menu-dark .ant-menu-item,
|
||||
.custom-green-menu.ant-menu-dark .ant-menu-submenu-title {
|
||||
color: rgba(255, 255, 255, 0.9) !important;
|
||||
}
|
||||
|
||||
.custom-orange-menu.ant-menu-dark .ant-menu-item-active,
|
||||
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title {
|
||||
.custom-green-menu.ant-menu-dark .ant-menu-item-active,
|
||||
.custom-green-menu.ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/*start styling for scrollbar menu */
|
||||
.custom-menu-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
.custom-menu-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border-radius: 10px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
.custom-menu-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, #1BAA56 0%, rgb(5, 75, 34) 100%);
|
||||
border-radius: 10px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.custom-menu-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(180deg, #2bc56d 0%, rgb(8, 94, 43) 100%);
|
||||
}
|
||||
.custom-menu-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #1BAA56 transparent;
|
||||
}
|
||||
/* Hilangkan panah atas/bawah dengan important */
|
||||
.custom-menu-scrollbar::-webkit-scrollbar-button {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
/*end styling for scrollbar menu */
|
||||
@@ -5,6 +5,7 @@ import handleLogOut from '../Utils/Auth/Logout';
|
||||
import { useBreadcrumb } from './LayoutBreadcrumb';
|
||||
import { decryptData } from '../components/Global/Formatter';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DateRealTime from '../components/Global/DateRealTime';
|
||||
|
||||
const { Link, Text } = Typography;
|
||||
const { Header } = Layout;
|
||||
@@ -17,7 +18,7 @@ const LayoutHeader = () => {
|
||||
const { token } = theme.useToken() || {};
|
||||
const colorBgContainer = token?.colorBgContainer || '#fff';
|
||||
const colorBorder = token?.colorBorder || '#d9d9d9';
|
||||
const colorText = token?.colorText || '#000';
|
||||
const colorText = token?.colorText || '#1BAA56';
|
||||
|
||||
// Ambil data user dari localStorage
|
||||
let userData = null;
|
||||
@@ -40,15 +41,8 @@ const LayoutHeader = () => {
|
||||
// console.log('User data di header:', userData?.user);
|
||||
|
||||
// Role handling
|
||||
const roleNameDefault =
|
||||
userData?.user?.approval ||
|
||||
userData?.user?.partner_name ||
|
||||
userData?.user?.role_name ||
|
||||
'Guest';
|
||||
|
||||
let roleName = roleNameDefault;
|
||||
const userName =
|
||||
userData?.user?.name || userData?.user?.username || userData?.user?.user_name || 'User';
|
||||
let roleName = userData?.user?.role_name || 'Guest';
|
||||
const userName = userData?.user?.name || userData?.user?.username || userData?.user?.user_name || 'User';
|
||||
|
||||
// Override jika Super Admin
|
||||
if (
|
||||
@@ -73,8 +67,9 @@ const LayoutHeader = () => {
|
||||
paddingBottom: 20,
|
||||
paddingLeft: 24,
|
||||
paddingRight: 24,
|
||||
minHeight: 100,
|
||||
// minHeight: 100,
|
||||
boxSizing: 'border-box',
|
||||
boxShadow: '5px 0 10px rgba(0, 0, 0, 0.4)'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -87,16 +82,39 @@ const LayoutHeader = () => {
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: colorText,
|
||||
fontSize: 16,
|
||||
color: '#1BAA56',
|
||||
fontSize: 26,
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
Login AS {roleName}
|
||||
{/* Login AS {roleName} */}
|
||||
CALL OF DUTY
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
{/* <Text
|
||||
style={{
|
||||
color: '#000000',
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
> */}
|
||||
{/* Login AS {roleName} */}
|
||||
{/* Kamis, 04 November 2025 16:35:00 */}
|
||||
{/* </Text> */}
|
||||
<DateRealTime/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -117,7 +135,7 @@ const LayoutHeader = () => {
|
||||
>
|
||||
<UserOutlined style={{ fontSize: 16, color: colorText }} />
|
||||
<Text style={{ marginLeft: 8, color: colorText }} strong>
|
||||
{userName}
|
||||
{userName} @ {roleName}
|
||||
</Text>
|
||||
</Button>
|
||||
<Link
|
||||
|
||||
@@ -29,6 +29,10 @@ import {
|
||||
DesktopOutlined,
|
||||
NodeExpandOutlined,
|
||||
GroupOutlined,
|
||||
SlidersOutlined,
|
||||
SnippetsOutlined,
|
||||
ContactsOutlined,
|
||||
ToolOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -49,51 +53,70 @@ const allItems = [
|
||||
label: 'Dashboard',
|
||||
children: [
|
||||
{
|
||||
key: 'dashboard-svg-overview',
|
||||
key: 'dashboard-svg-compressor',
|
||||
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/dashboard-svg/overview">Overview</Link>,
|
||||
label: 'Compressor',
|
||||
children: [
|
||||
{
|
||||
key: 'dashboard-svg-compressor-overview',
|
||||
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/dashboard-svg/overview-compressor">Overview</Link>,
|
||||
},
|
||||
{
|
||||
key: 'dashboard-svg-compressor-a',
|
||||
key: 'dashboard-svg-compressor-compressor-a',
|
||||
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/dashboard-svg/compressor-a">Compressor A</Link>,
|
||||
},
|
||||
{
|
||||
key: 'dashboard-svg-compressor-b',
|
||||
key: 'dashboard-svg-compressor-compressor-b',
|
||||
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/dashboard-svg/compressor-b">Compressor B</Link>,
|
||||
},
|
||||
{
|
||||
key: 'dashboard-svg-compressor-c',
|
||||
key: 'dashboard-svg-compressor-compressor-c',
|
||||
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/dashboard-svg/compressor-c">Compressor C</Link>,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'dashboard-svg-airdryer-a',
|
||||
key: 'dashboard-svg-airdryer',
|
||||
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
|
||||
label: 'Air Dryer',
|
||||
children: [
|
||||
{
|
||||
key: 'dashboard-svg-airdryer-overview',
|
||||
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/dashboard-svg/overview-airdryer">Overview</Link>,
|
||||
},
|
||||
{
|
||||
key: 'dashboard-svg-airdryer-airdryer-a',
|
||||
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/dashboard-svg/airdryer-a">Air Dryer A</Link>,
|
||||
},
|
||||
{
|
||||
key: 'dashboard-svg-airdryer-b',
|
||||
key: 'dashboard-svg-airdryer-airdryer-b',
|
||||
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/dashboard-svg/airdryer-b">Air Dryer B</Link>,
|
||||
},
|
||||
{
|
||||
key: 'dashboard-svg-airdryer-c',
|
||||
key: 'dashboard-svg-airdryer-airdryer-c',
|
||||
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/dashboard-svg/airdryer-c">Air Dryer C</Link>,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'master',
|
||||
icon: <DatabaseOutlined style={{ fontSize: '19px' }} />,
|
||||
label: 'Master',
|
||||
children: [
|
||||
{
|
||||
key: 'master-plant-section',
|
||||
key: 'master-plant-sub-section',
|
||||
icon: <ProductOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/master/plant-section">Plant Section</Link>,
|
||||
label: <Link to="/master/plant-sub-section">Plant Sub Section</Link>,
|
||||
},
|
||||
{
|
||||
key: 'master-brand-device',
|
||||
@@ -121,9 +144,31 @@ const allItems = [
|
||||
label: <Link to="/master/status">Status</Link>,
|
||||
},
|
||||
{
|
||||
key: 'master-shift',
|
||||
icon: <ClockCircleOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/master/shift">Shift</Link>,
|
||||
key: 'master-sparepart',
|
||||
icon: <ToolOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/master/sparepart">Sparepart</Link>,
|
||||
},
|
||||
// {
|
||||
// key: 'master-shift',
|
||||
// icon: <ClockCircleOutlined style={{ fontSize: '19px' }} />,
|
||||
// label: <Link to="/master/shift">Shift</Link>,
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'report',
|
||||
icon: <SnippetsOutlined style={{ fontSize: '19px' }} />,
|
||||
label: 'Report',
|
||||
children: [
|
||||
{
|
||||
key: 'report-trending',
|
||||
icon: <LineChartOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/report/trending">Trending</Link>,
|
||||
},
|
||||
{
|
||||
key: 'report-report',
|
||||
icon: <FileTextOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/report/report">Report</Link>,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -133,32 +178,32 @@ const allItems = [
|
||||
label: 'History',
|
||||
children: [
|
||||
{
|
||||
key: 'history-trending',
|
||||
icon: <LineChartOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/history/trending">Trending</Link>,
|
||||
key: 'history-alarm',
|
||||
icon: <AlertOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/history/alarm">Alarm</Link>,
|
||||
},
|
||||
{
|
||||
key: 'history-report',
|
||||
icon: <FileTextOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/history/report">Report</Link>,
|
||||
key: 'history-event',
|
||||
icon: <SlidersOutlined style={{ fontSize: '19px' }} />,
|
||||
label: <Link to="/history/event">Event</Link>,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'contact',
|
||||
icon: <ContactsOutlined style={{ fontSize: '19px' }} />,
|
||||
label: (
|
||||
<Link to="/contact" className="fontMenus">
|
||||
Contact
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'notification',
|
||||
icon: <BellOutlined style={{ fontSize: '19px' }} />,
|
||||
label: (
|
||||
<Link to="/notification" className="fontMenus">
|
||||
Notifikasi
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'event-alarm',
|
||||
icon: <AlertOutlined style={{ fontSize: '19px' }} />,
|
||||
label: (
|
||||
<Link to="/event-alarm" className="fontMenus">
|
||||
Event Alarm
|
||||
Notification
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
@@ -180,15 +225,15 @@ const allItems = [
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'jadwal-shift',
|
||||
icon: <CalendarOutlined style={{ fontSize: '19px' }} />,
|
||||
label: (
|
||||
<Link to="/jadwal-shift" className="fontMenus">
|
||||
Jadwal Shift
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// key: 'jadwal-shift',
|
||||
// icon: <CalendarOutlined style={{ fontSize: '19px' }} />,
|
||||
// label: (
|
||||
// <Link to="/jadwal-shift" className="fontMenus">
|
||||
// Jadwal Shift
|
||||
// </Link>
|
||||
// ),
|
||||
// },
|
||||
];
|
||||
|
||||
const LayoutMenu = () => {
|
||||
@@ -206,44 +251,84 @@ const LayoutMenu = () => {
|
||||
if (pathname === '/user') return 'user';
|
||||
if (pathname === '/role') return 'role';
|
||||
if (pathname === '/notification') return 'notification';
|
||||
if (pathname === '/event-alarm') return 'event-alarm';
|
||||
if (pathname === '/jadwal-shift') return 'jadwal-shift';
|
||||
if (pathname === '/dashboard-svg') return 'dashboard-svg';
|
||||
if (pathname === '/contact') return 'contact';
|
||||
|
||||
// Handle master routes
|
||||
if (pathname.startsWith('/master/')) {
|
||||
const subPath = pathParts[1];
|
||||
return `master-${subPath}`;
|
||||
// Convert kebab-case to the actual menu keys
|
||||
const masterKeyMap = {
|
||||
'plant-sub-section': 'master-plant-sub-section',
|
||||
'brand-device': 'master-brand-device',
|
||||
device: 'master-device',
|
||||
unit: 'master-unit',
|
||||
tag: 'master-tag',
|
||||
status: 'master-status',
|
||||
sparepart: 'master-sparepart',
|
||||
shift: 'master-shift',
|
||||
};
|
||||
return masterKeyMap[subPath] || `master-${subPath}`;
|
||||
}
|
||||
|
||||
// Handle master routes
|
||||
// Handle dashboard svg routes
|
||||
if (pathname.startsWith('/dashboard-svg/')) {
|
||||
const subPath = pathParts[1];
|
||||
// Map specific routes to their menu keys
|
||||
if (subPath === 'overview-compressor') return 'dashboard-svg-compressor-overview';
|
||||
if (subPath === 'compressor-a') return 'dashboard-svg-compressor-compressor-a';
|
||||
if (subPath === 'compressor-b') return 'dashboard-svg-compressor-compressor-b';
|
||||
if (subPath === 'compressor-c') return 'dashboard-svg-compressor-compressor-c';
|
||||
if (subPath === 'overview-airdryer') return 'dashboard-svg-airdryer-overview';
|
||||
if (subPath === 'airdryer-a') return 'dashboard-svg-airdryer-airdryer-a';
|
||||
if (subPath === 'airdryer-b') return 'dashboard-svg-airdryer-airdryer-b';
|
||||
if (subPath === 'airdryer-c') return 'dashboard-svg-airdryer-airdryer-c';
|
||||
|
||||
return `dashboard-svg-${subPath}`;
|
||||
}
|
||||
|
||||
// Handle report routes
|
||||
if (pathname.startsWith('/report/')) {
|
||||
const subPath = pathParts[1];
|
||||
const reportKeyMap = {
|
||||
trending: 'report-trending',
|
||||
report: 'report-report',
|
||||
};
|
||||
return reportKeyMap[subPath] || `report-${subPath}`;
|
||||
}
|
||||
|
||||
// Handle history routes
|
||||
if (pathname.startsWith('/history/')) {
|
||||
const subPath = pathParts[1];
|
||||
return `history-${subPath}`;
|
||||
}
|
||||
|
||||
// Handle shift management routes
|
||||
if (pathname.startsWith('/shift-management/')) {
|
||||
const subPath = pathParts[1];
|
||||
return `shift-${subPath}`;
|
||||
const historyKeyMap = {
|
||||
alarm: 'history-alarm',
|
||||
event: 'history-event',
|
||||
};
|
||||
return historyKeyMap[subPath] || `history-${subPath}`;
|
||||
}
|
||||
|
||||
return 'home'; // default
|
||||
};
|
||||
|
||||
// Function to get parent key from menu key
|
||||
const getParentKey = (key) => {
|
||||
if (key.startsWith('master-')) return 'master';
|
||||
if (key.startsWith('dashboard-svg-')) return 'dashboard-svg';
|
||||
if (key.startsWith('history-')) return 'history';
|
||||
if (key.startsWith('shift-')) return 'shift-management';
|
||||
return null;
|
||||
// Function to get parent keys from menu key
|
||||
const getParentKeys = (key) => {
|
||||
const parentKeys = [];
|
||||
|
||||
if (key.startsWith('dashboard-svg-compressor-')) {
|
||||
parentKeys.push('dashboard-svg', 'dashboard-svg-compressor');
|
||||
} else if (key.startsWith('dashboard-svg-airdryer-')) {
|
||||
parentKeys.push('dashboard-svg', 'dashboard-svg-airdryer');
|
||||
} else if (key.startsWith('dashboard-svg-')) {
|
||||
parentKeys.push('dashboard-svg');
|
||||
} else if (key.startsWith('master-')) {
|
||||
parentKeys.push('master');
|
||||
} else if (key.startsWith('report-')) {
|
||||
parentKeys.push('report');
|
||||
} else if (key.startsWith('history-')) {
|
||||
parentKeys.push('history');
|
||||
}
|
||||
|
||||
return parentKeys;
|
||||
};
|
||||
|
||||
// Update selected and open keys when route changes
|
||||
@@ -251,11 +336,11 @@ const LayoutMenu = () => {
|
||||
const currentKey = getMenuKeyFromPath(location.pathname);
|
||||
setSelectedKeys([currentKey]);
|
||||
|
||||
const parentKey = getParentKey(currentKey);
|
||||
const parentKeys = getParentKeys(currentKey);
|
||||
|
||||
// If current menu has parent, open it. Otherwise, close all dropdowns
|
||||
if (parentKey) {
|
||||
setStateOpenKeys([parentKey]);
|
||||
// Always keep the parent menus open when a child is selected
|
||||
if (parentKeys.length > 0) {
|
||||
setStateOpenKeys(parentKeys);
|
||||
} else {
|
||||
setStateOpenKeys([]);
|
||||
}
|
||||
@@ -281,17 +366,28 @@ const LayoutMenu = () => {
|
||||
|
||||
const onOpenChange = (openKeys) => {
|
||||
const currentOpenKey = openKeys.find((key) => stateOpenKeys.indexOf(key) === -1);
|
||||
|
||||
// If clicking on a menu that was previously closed
|
||||
if (currentOpenKey !== undefined) {
|
||||
const repeatIndex = openKeys
|
||||
.filter((key) => key !== currentOpenKey)
|
||||
.findIndex((key) => levelKeys[key] === levelKeys[currentOpenKey]);
|
||||
|
||||
setStateOpenKeys(
|
||||
openKeys
|
||||
.filter((_, index) => index !== repeatIndex)
|
||||
.filter((key) => levelKeys[key] <= levelKeys[currentOpenKey])
|
||||
);
|
||||
} else {
|
||||
setStateOpenKeys(openKeys);
|
||||
// If clicking on a menu that was previously open, close only that menu
|
||||
// but keep other parent menus open if they have active children
|
||||
const currentKey = getMenuKeyFromPath(location.pathname);
|
||||
const necessaryParentKeys = getParentKeys(currentKey);
|
||||
|
||||
// Filter out only the menus that are necessary to keep open
|
||||
const filteredOpenKeys = openKeys.filter((key) => necessaryParentKeys.includes(key));
|
||||
|
||||
setStateOpenKeys(filteredOpenKeys);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -300,27 +396,17 @@ const LayoutMenu = () => {
|
||||
|
||||
const karyawan = () => {
|
||||
return allItems
|
||||
.filter(
|
||||
(item) => item.key !== 'setting'
|
||||
// tambahkan menu jika terdapat menu yang di sembunyikan dari user karyawan
|
||||
// && item.key !== 'master'
|
||||
// && item.key !== 'master'
|
||||
)
|
||||
.filter((item) => item.key !== 'setting')
|
||||
.map((item) => {
|
||||
if (item.key === 'master') {
|
||||
return {
|
||||
...item,
|
||||
// buka command dibawah jika terdapat sub menu yang di sembunyikan
|
||||
// children: item.children.filter(
|
||||
// child => child.key !== 'master-product'
|
||||
// tambahkan menu jika terdapat menu yang di sembunyikan dari user karyawan
|
||||
// && child.key !== 'master-service'
|
||||
// )
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
const items = isAdmin === 1 ? allItems : karyawan();
|
||||
|
||||
return (
|
||||
@@ -336,7 +422,7 @@ const LayoutMenu = () => {
|
||||
border: 'none',
|
||||
}}
|
||||
theme="dark"
|
||||
className="custom-orange-menu"
|
||||
className="custom-green-menu"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ const { Sider } = Layout;
|
||||
const LayoutSidebar = () => {
|
||||
return (
|
||||
<Sider
|
||||
width={300}
|
||||
width={255}
|
||||
breakpoint="lg"
|
||||
collapsedWidth="0"
|
||||
onBreakpoint={(broken) => {
|
||||
@@ -17,17 +17,37 @@ const LayoutSidebar = () => {
|
||||
// console.log(collapsed, type);
|
||||
}}
|
||||
style={{
|
||||
background: 'linear-gradient(180deg, #FF8C42 0%, #FF6B35 100%)',
|
||||
overflow: 'auto',
|
||||
background: 'linear-gradient(180deg, #1BAA56 0%,rgb(5, 75, 34) 100%)',
|
||||
// overflow: 'auto',
|
||||
height: '100vh',
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
borderTopRightRadius: '30px',
|
||||
borderBottomRightRadius: '30px',
|
||||
boxShadow: '5px 0 10px rgba(0, 0, 0, 0.4)',
|
||||
zIndex: 9999
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Logo section - fixed height */}
|
||||
<div style={{flexShrink: 0,minHeight: '64px'}}>
|
||||
<LayoutLogo />
|
||||
</div>
|
||||
|
||||
{/* Menu section - scrollable */}
|
||||
<div style={{flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column'}}>
|
||||
<div className="custom-menu-scrollbar" style={{flex: 1, overflowY: 'auto', overflowX: 'hidden', backgroundColor: 'transparent'}}>
|
||||
<LayoutMenu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Sider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ const MainLayout = ({ children }) => {
|
||||
<LayoutSidebar />
|
||||
<Layout
|
||||
style={{
|
||||
marginLeft: isDesktop ? '300px' : '0',
|
||||
marginLeft: isDesktop ? '250px' : '0',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Flex, Input, Form, Button, Card, Space, Image } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { NotifAlert } from '../../components/Global/ToastNotif';
|
||||
import { SendRequest } from '../../components/Global/ApiRequest';
|
||||
import bg_cod from 'assets/bg_cod.jpg';
|
||||
import bg_cod from 'assets/bg-cod-1.jpg';
|
||||
import logo from 'assets/freepik/LOGOPIU.png';
|
||||
|
||||
const SignIn = () => {
|
||||
|
||||
49
src/pages/blank/RedirectWa.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { verifyRedirect } from '../../api/auth';
|
||||
import { encryptData } from '../../components/Global/Formatter';
|
||||
import NotFound from './NotFound';
|
||||
import Waiting from './Waiting';
|
||||
import NotificationDetailTab from '../notificationDetail/IndexNotificationDetail';
|
||||
|
||||
export default function RedirectWa() {
|
||||
const [idData, setIdData] = useState(0);
|
||||
const [ready, setReady] = useState(0);
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
// URLSearchParams untuk ambil query
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const token = queryParams.get('token');
|
||||
|
||||
const handleInitForm = async (encodedToken) => {
|
||||
const originalToken = decodeURIComponent(encodedToken);
|
||||
// console.log(originalToken);
|
||||
|
||||
const response = await verifyRedirect({
|
||||
tokenRedirect: originalToken,
|
||||
});
|
||||
|
||||
console.log('tes', response);
|
||||
|
||||
const tokenResult = JSON.stringify(response.data?.data?.accessToken);
|
||||
|
||||
sessionStorage.setItem('token_redirect', tokenResult);
|
||||
response.data.auth = true;
|
||||
sessionStorage.setItem('session', encryptData(response?.data));
|
||||
|
||||
setIdData(response.data.data.idData);
|
||||
|
||||
setReady(1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleInitForm(token);
|
||||
}, [idData]);
|
||||
|
||||
if (ready == 0) return <Waiting />;
|
||||
|
||||
if (idData === 0) return <NotFound />;
|
||||
|
||||
return <NotificationDetailTab id={idData} />;
|
||||
}
|
||||
72
src/pages/contact/IndexContact.jsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ListContact from './component/ListContact';
|
||||
import DetailContact from './component/DetailContact';
|
||||
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
|
||||
import { Typography } from 'antd';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const IndexContact = memo(function IndexContact() {
|
||||
const navigate = useNavigate();
|
||||
const { setBreadcrumbItems } = useBreadcrumb();
|
||||
|
||||
const [actionMode, setActionMode] = useState('list');
|
||||
const [selectedData, setSelectedData] = useState(null);
|
||||
const [readOnly, setReadOnly] = useState(false);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [contactType, setContactType] = useState('operator');
|
||||
|
||||
const setMode = (param) => {
|
||||
setShowModal(param !== 'list');
|
||||
setReadOnly(param === 'preview');
|
||||
setActionMode(param);
|
||||
};
|
||||
|
||||
const handleContactSaved = (contactData, actionMode) => {
|
||||
setLastSavedContact({ contactData, actionMode });
|
||||
|
||||
// Clear after processing
|
||||
setTimeout(() => setLastSavedContact(null), 100);
|
||||
};
|
||||
|
||||
const [lastSavedContact, setLastSavedContact] = useState(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
setBreadcrumbItems([
|
||||
{ title: <Text strong style={{ fontSize: '14px' }}>• Contact</Text> },
|
||||
]);
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, [navigate, setBreadcrumbItems]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ListContact
|
||||
actionMode={actionMode}
|
||||
setActionMode={setMode}
|
||||
selectedData={selectedData}
|
||||
setSelectedData={setSelectedData}
|
||||
readOnly={readOnly}
|
||||
lastSavedContact={lastSavedContact}
|
||||
setContactType={setContactType}
|
||||
/>
|
||||
<DetailContact
|
||||
setActionMode={setMode}
|
||||
selectedData={selectedData}
|
||||
setSelectedData={setSelectedData}
|
||||
readOnly={readOnly}
|
||||
showModal={showModal}
|
||||
actionMode={actionMode}
|
||||
onContactSaved={handleContactSaved}
|
||||
contactType={contactType}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default IndexContact;
|
||||
272
src/pages/contact/component/DetailContact.jsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import { Modal, Input, Button, Switch, ConfigProvider, Typography, Divider, Select } from 'antd';
|
||||
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
||||
import { validateRun } from '../../../Utils/validate';
|
||||
import { createContact, updateContact } from '../../../api/contact';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const DetailContact = memo(function DetailContact(props) {
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
|
||||
const defaultData = {
|
||||
id: '',
|
||||
name: '',
|
||||
phone: '',
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
const [formData, setFormData] = useState(defaultData);
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
// Validasi untuk field phone - hanya angka yang diperbolehkan
|
||||
if (name === 'phone') {
|
||||
const cleanedValue = value.replace(/[^0-9+\-\s()]/g, '');
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: cleanedValue,
|
||||
}));
|
||||
} else {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleStatusToggle = (checked) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
is_active: checked,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setConfirmLoading(true);
|
||||
|
||||
// Validation rules
|
||||
const validationRules = [
|
||||
{ field: 'name', label: 'Contact Name', required: true },
|
||||
{ field: 'phone', label: 'Phone', required: true },
|
||||
];
|
||||
|
||||
if (
|
||||
validateRun(formData, validationRules, (errorMessages) => {
|
||||
NotifOk({ icon: 'warning', title: 'Peringatan', message: errorMessages });
|
||||
setConfirmLoading(false);
|
||||
})
|
||||
)
|
||||
return;
|
||||
|
||||
// Custom validation untuk name - minimal 3 karakter
|
||||
if (formData.name && formData.name.length < 3) {
|
||||
NotifOk({
|
||||
icon: 'warning',
|
||||
title: 'Peringatan',
|
||||
message: 'Nama contact minimal 3 karakter',
|
||||
});
|
||||
setConfirmLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Custom validation untuk phone - Indonesian phone format
|
||||
const phoneRegex = /^(?:\+62|0)8\d{7,10}$/;
|
||||
if (formData.phone && !phoneRegex.test(formData.phone.replace(/[\s\-\(\)]/g, ''))) {
|
||||
NotifOk({
|
||||
icon: 'warning',
|
||||
title: 'Peringatan',
|
||||
message: 'Nomor telepon harus format Indonesia (+628XXXXXXXXX atau 08XXXXXXXXX)',
|
||||
});
|
||||
setConfirmLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const contactData = {
|
||||
contact_name: formData.name,
|
||||
contact_phone: formData.phone.replace(/[\s\-\(\)]/g, ''), // Clean phone number
|
||||
is_active: formData.is_active,
|
||||
};
|
||||
|
||||
let response;
|
||||
if (props.actionMode === 'edit') {
|
||||
response = await updateContact(
|
||||
props.selectedData.contact_id || props.selectedData.id,
|
||||
contactData
|
||||
);
|
||||
} else {
|
||||
response = await createContact(contactData);
|
||||
}
|
||||
|
||||
NotifAlert({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: `Data Contact "${formData.name}" berhasil ${
|
||||
props.actionMode === 'add' ? 'ditambahkan' : 'diperbarui'
|
||||
}.`,
|
||||
});
|
||||
|
||||
props.onContactSaved?.(response.data, props.actionMode);
|
||||
handleCancel();
|
||||
} catch (error) {
|
||||
console.error('Save failed:', error);
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: error.response?.data?.message || 'Terjadi kesalahan saat menyimpan data.',
|
||||
});
|
||||
} finally {
|
||||
setConfirmLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
props.setActionMode('list');
|
||||
props.setSelectedData(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.showModal) {
|
||||
if (props.actionMode === 'edit' && props.selectedData) {
|
||||
setFormData({
|
||||
name: props.selectedData.contact_name || props.selectedData.name,
|
||||
phone: props.selectedData.contact_phone || props.selectedData.phone,
|
||||
is_active:
|
||||
props.selectedData.is_active || props.selectedData.status === 'active',
|
||||
});
|
||||
} else if (props.actionMode === 'add') {
|
||||
setFormData({
|
||||
name: '',
|
||||
phone: '',
|
||||
is_active: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [props.showModal, props.actionMode, props.selectedData]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${
|
||||
props.actionMode === 'add'
|
||||
? 'Tambah'
|
||||
: props.actionMode === 'edit'
|
||||
? 'Edit'
|
||||
: 'Detail'
|
||||
} Kontak`}
|
||||
open={props.showModal}
|
||||
onCancel={handleCancel}
|
||||
footer={[
|
||||
<React.Fragment key="modal-footer">
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: 'white',
|
||||
defaultColor: '#23A55A',
|
||||
defaultBorderColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button onClick={handleCancel}>{props.readOnly ? 'Tutup' : 'Batal'}</Button>
|
||||
</ConfigProvider>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: '#23a55a',
|
||||
defaultColor: '#FFFFFF',
|
||||
defaultBorderColor: '#23a55a',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!props.readOnly && (
|
||||
<Button loading={confirmLoading} onClick={handleSave}>
|
||||
Simpan
|
||||
</Button>
|
||||
)}
|
||||
</ConfigProvider>
|
||||
</React.Fragment>,
|
||||
]}
|
||||
>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
{/* Status field only show in add mode*/}
|
||||
{props.actionMode === 'add' && (
|
||||
<>
|
||||
<div>
|
||||
<div>
|
||||
<Text strong>Status</Text>
|
||||
</div>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', marginTop: '8px' }}
|
||||
>
|
||||
<div style={{ marginRight: '8px' }}>
|
||||
<Switch
|
||||
disabled={props.readOnly}
|
||||
style={{
|
||||
backgroundColor: formData.is_active
|
||||
? '#23A55A'
|
||||
: '#bfbfbf',
|
||||
}}
|
||||
checked={formData.is_active}
|
||||
onChange={handleStatusToggle}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text>{formData.is_active ? 'Active' : 'Inactive'}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Name</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Input
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter Name"
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Phone</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Input
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter Phone Number"
|
||||
readOnly={props.readOnly}
|
||||
maxLength={15}
|
||||
style={{ color: formData.is_active ? '#000000' : '#ff4d4f' }}
|
||||
/>
|
||||
</div>
|
||||
{/* Contact Type */}
|
||||
{/* <div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Contact Type</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Select
|
||||
value={formData.contact_type || undefined}
|
||||
onChange={handleContactTypeChange}
|
||||
placeholder="Select Contact Type"
|
||||
disabled={props.readOnly}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Select.Option value="operator">Operator</Select.Option>
|
||||
<Select.Option value="gudang">Gudang</Select.Option>
|
||||
</Select>
|
||||
</div> */}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default DetailContact;
|
||||
487
src/pages/contact/component/ListContact.jsx
Normal file
@@ -0,0 +1,487 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { Button, Row, Col, Input, Tabs, Space, ConfigProvider, Card, Tag, Switch } from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
SearchOutlined,
|
||||
UserOutlined,
|
||||
PhoneOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { NotifAlert, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
||||
import { getAllContact, deleteContact, updateContact } from '../../../api/contact';
|
||||
|
||||
const ContactCard = memo(function ContactCard({
|
||||
contact,
|
||||
showEditModal,
|
||||
showDeleteModal,
|
||||
onStatusToggle,
|
||||
}) {
|
||||
const handleStatusToggle = async (checked) => {
|
||||
try {
|
||||
const updatedContact = {
|
||||
contact_name: contact.contact_name || contact.name,
|
||||
contact_phone: contact.contact_phone || contact.phone,
|
||||
is_active: checked,
|
||||
contact_type: contact.contact_type,
|
||||
};
|
||||
|
||||
await updateContact(contact.contact_id || contact.id, updatedContact);
|
||||
|
||||
NotifAlert({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: `Status "${contact.contact_name || contact.name}" berhasil diperbarui.`,
|
||||
});
|
||||
|
||||
// Refresh contacts list
|
||||
onStatusToggle && onStatusToggle();
|
||||
} catch (error) {
|
||||
console.error('Error updating contact status:', error);
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'Gagal memperbarui status kontak',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Col xs={24} sm={12} md={8} lg={6}>
|
||||
<div
|
||||
className="contact-card"
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
height: '100%',
|
||||
padding: '16px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
border: '1px solid #e8e8e8',
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* Type Badge - Top Left */}
|
||||
{/* <div style={{ position: 'absolute', top: 0, left: 0, zIndex: 1 }}>
|
||||
<Tag
|
||||
color={
|
||||
contact.contact_type === 'operator'
|
||||
? 'blue'
|
||||
: contact.contact_type === 'gudang'
|
||||
? 'orange'
|
||||
: 'default'
|
||||
}
|
||||
style={{ fontSize: '11px' }}
|
||||
>
|
||||
{contact.contact_type === 'operator' ? 'Operator' : contact.contact_type === 'gudang' ? 'Gudang' : 'Unknown'}
|
||||
</Tag>
|
||||
</div> */}
|
||||
|
||||
{/* Status Slider - Top Right */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
zIndex: 1,
|
||||
padding: '4px 8px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<Switch
|
||||
checked={contact.status === 'active'}
|
||||
onChange={handleStatusToggle}
|
||||
style={{
|
||||
backgroundColor:
|
||||
contact.status === 'active' ? '#52c41a' : '#d9d9d9',
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: contact.status === 'active' ? '#52c41a' : '#ff4d4f',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{contact.status === 'active' ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
flex: 1,
|
||||
paddingTop: '28px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="avatar"
|
||||
style={{
|
||||
width: 55,
|
||||
height: 55,
|
||||
borderRadius: '50%',
|
||||
backgroundColor:
|
||||
contact.status === 'active' ? '#52c41a' : '#ff4d4f',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<UserOutlined style={{ color: 'white', fontSize: '25px' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
marginBottom: '4px',
|
||||
color: '#262626',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{contact.contact_name || contact.name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
<PhoneOutlined style={{ marginRight: 6, color: '#1890ff' }} />
|
||||
<span
|
||||
style={{
|
||||
color: contact.status === 'active' ? '#262626' : '#262626',
|
||||
}}
|
||||
>
|
||||
{contact.contact_phone || contact.phone}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Edit and Delete Buttons - Bottom Right */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '8px',
|
||||
marginTop: '8px',
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: '#fff7e6',
|
||||
borderColor: '#faad14',
|
||||
color: '#faad14',
|
||||
padding: '2px 6px',
|
||||
fontSize: '11px',
|
||||
height: '24px',
|
||||
}}
|
||||
icon={
|
||||
<EditOutlined style={{ color: '#faad14', fontSize: '11px' }} />
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
showEditModal(contact);
|
||||
}}
|
||||
>
|
||||
Edit info
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
danger
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: '#fff1f0',
|
||||
borderColor: 'red',
|
||||
padding: '2px 6px',
|
||||
fontSize: '11px',
|
||||
height: '24px',
|
||||
}}
|
||||
icon={<DeleteOutlined style={{ fontSize: '11px' }} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
showDeleteModal(contact);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
});
|
||||
|
||||
const ListContact = memo(function ListContact(props) {
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
const [filteredContacts, setFilteredContacts] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Default filter object matching plantSection pattern
|
||||
const defaultFilter = { criteria: '' };
|
||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||
|
||||
// Fetch contacts from API
|
||||
const fetchContacts = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Build search parameters matching database pattern
|
||||
const searchParams = { ...formDataFilter };
|
||||
|
||||
// Add specific filters if not "all"
|
||||
if (activeTab !== 'all') {
|
||||
if (activeTab === 'operator') {
|
||||
searchParams.code = 'operator';
|
||||
} else if (activeTab === 'gudang') {
|
||||
searchParams.code = 'gudang';
|
||||
}
|
||||
}
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
Object.entries(searchParams).forEach(([key, value]) => {
|
||||
if (value !== '' && value !== null && value !== undefined) {
|
||||
queryParams.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
const response = await getAllContact(queryParams);
|
||||
setFilteredContacts(response.data || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching contacts:', error);
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'Gagal memuat data kontak',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch contacts on component mount
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
navigate('/signin');
|
||||
return;
|
||||
}
|
||||
fetchContacts();
|
||||
}, []);
|
||||
|
||||
// Refetch when filters change
|
||||
useEffect(() => {
|
||||
fetchContacts();
|
||||
}, [formDataFilter, activeTab]);
|
||||
|
||||
// Listen for saved contact data
|
||||
useEffect(() => {
|
||||
if (props.lastSavedContact) {
|
||||
fetchContacts();
|
||||
}
|
||||
}, [props.lastSavedContact]);
|
||||
|
||||
const getFilteredContacts = () => {
|
||||
return filteredContacts;
|
||||
};
|
||||
|
||||
const showEditModal = (param) => {
|
||||
props.setSelectedData(param);
|
||||
props.setActionMode('edit');
|
||||
};
|
||||
|
||||
const showAddModal = () => {
|
||||
props.setSelectedData(null);
|
||||
props.setActionMode('add');
|
||||
|
||||
props.setContactType?.(activeTab);
|
||||
};
|
||||
|
||||
const showDeleteModal = (contact) => {
|
||||
NotifConfirmDialog({
|
||||
icon: 'question',
|
||||
title: 'Konfirmasi Hapus',
|
||||
message: `Kontak "${contact.contact_name || contact.name}" akan dihapus?`,
|
||||
onConfirm: () => handleDelete(contact),
|
||||
onCancel: () => props.setSelectedData(null),
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (contact) => {
|
||||
try {
|
||||
await deleteContact(contact.contact_id || contact.id);
|
||||
NotifAlert({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: `Kontak "${contact.contact_name || contact.name}" berhasil dihapus.`,
|
||||
});
|
||||
// Refetch contacts after deletion
|
||||
fetchContacts();
|
||||
} catch (error) {
|
||||
console.error('Error deleting contact:', error);
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'Gagal menghapus kontak',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card>
|
||||
<Row>
|
||||
<Col xs={24}>
|
||||
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
||||
<Col xs={24} sm={24} md={12} lg={12}>
|
||||
<Input.Search
|
||||
placeholder="Search by name..."
|
||||
value={formDataFilter.criteria}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setFormDataFilter({ criteria: value });
|
||||
if (value === '') {
|
||||
setFormDataFilter(defaultFilter);
|
||||
}
|
||||
}}
|
||||
onSearch={(value) => setFormDataFilter({ criteria: value })}
|
||||
allowClear={{
|
||||
clearIcon: (
|
||||
<span onClick={() => setFormDataFilter(defaultFilter)}>
|
||||
✕
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
enterButton={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
style={{
|
||||
backgroundColor: '#23A55A',
|
||||
borderColor: '#23A55A',
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
}
|
||||
size="large"
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Space wrap size="small">
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: 'white',
|
||||
defaultColor: '#23A55A',
|
||||
defaultBorderColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => showAddModal()}
|
||||
size="large"
|
||||
>
|
||||
Add Contact
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col xs={24} style={{ marginTop: '16px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
{/* Tabs */}
|
||||
{/* <Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
size="large"
|
||||
items={[
|
||||
{
|
||||
key: 'all',
|
||||
label: 'All',
|
||||
},
|
||||
{
|
||||
key: 'operator',
|
||||
label: 'Operator',
|
||||
},
|
||||
{
|
||||
key: 'gudang',
|
||||
label: 'Gudang',
|
||||
},
|
||||
]}
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
{getFilteredContacts().length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<span style={{ color: '#8c8c8c' }}>
|
||||
{loading ? 'Loading contacts...' : 'No contacts found'}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Row gutter={[16, 16]}>
|
||||
{getFilteredContacts().map((contact) => (
|
||||
<ContactCard
|
||||
key={contact.contact_id || contact.id}
|
||||
contact={{
|
||||
...contact,
|
||||
id: contact.contact_id || contact.id,
|
||||
name: contact.contact_name || contact.name,
|
||||
phone: contact.contact_phone || contact.phone,
|
||||
status: contact.is_active ? 'active' : 'inactive',
|
||||
}}
|
||||
showEditModal={showEditModal}
|
||||
showDeleteModal={showDeleteModal}
|
||||
onStatusToggle={fetchContacts}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default ListContact;
|
||||
@@ -1,72 +0,0 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
|
||||
import { Form, Typography } from 'antd';
|
||||
import ListEventAlarm from './component/ListEventAlarm';
|
||||
import DetailEventAlarm from './component/DetailEventAlarm';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const IndexEventAlarm = memo(function IndexEventAlarm() {
|
||||
const navigate = useNavigate();
|
||||
const { setBreadcrumbItems } = useBreadcrumb();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [actionMode, setActionMode] = useState('list');
|
||||
const [selectedData, setSelectedData] = useState(null);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
setBreadcrumbItems([
|
||||
{
|
||||
title: (
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
• Event Alarm
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, [navigate, setBreadcrumbItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (actionMode === 'preview') {
|
||||
setIsModalVisible(true);
|
||||
if (selectedData) {
|
||||
form.setFieldsValue(selectedData);
|
||||
}
|
||||
} else {
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
}
|
||||
}, [actionMode, selectedData, form]);
|
||||
|
||||
const handleCancel = () => {
|
||||
setActionMode('list');
|
||||
setSelectedData(null);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ListEventAlarm
|
||||
actionMode={actionMode}
|
||||
setActionMode={setActionMode}
|
||||
selectedData={selectedData}
|
||||
setSelectedData={setSelectedData}
|
||||
/>
|
||||
<DetailEventAlarm
|
||||
visible={isModalVisible}
|
||||
onCancel={handleCancel}
|
||||
form={form}
|
||||
selectedData={selectedData}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default IndexEventAlarm;
|
||||
@@ -1,58 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
import { Modal, Divider, Descriptions } from 'antd';
|
||||
|
||||
const DetailEventAlarm = memo(function DetailEventAlarm({ visible, onCancel, selectedData }) {
|
||||
return (
|
||||
<Modal
|
||||
title="Detail Event Alarm"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={onCancel}
|
||||
okText="Tutup"
|
||||
cancelButtonProps={{ style: { display: 'none' } }}
|
||||
width={700}
|
||||
>
|
||||
{selectedData && (
|
||||
<div>
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="Tanggal" span={2}>
|
||||
{selectedData.tanggal}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Plant Sub Section" span={2}>
|
||||
{selectedData.plant_sub_section}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Device">
|
||||
{selectedData.device}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Tag">
|
||||
{selectedData.tag}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Engineer" span={2}>
|
||||
{selectedData.engineer}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
|
||||
{/* Additional Info */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: '16px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#f6f9ff',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #d6e4ff',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '12px', color: '#595959' }}>
|
||||
<strong>Catatan:</strong> Event alarm ini telah tercatat dalam sistem untuk
|
||||
monitoring dan analisis lebih lanjut.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default DetailEventAlarm;
|
||||
@@ -1,246 +0,0 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { Button, Row, Col, Card, Input } from 'antd';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import TableList from '../../../components/Global/TableList';
|
||||
|
||||
// Dummy data untuk riwayat alarm
|
||||
const initialAlarmsData = [
|
||||
{
|
||||
alarm_id: 1,
|
||||
tanggal: '2025-01-15 08:30:00',
|
||||
plant_sub_section: 'Plant A - Section 1',
|
||||
device: 'Device 001',
|
||||
tag: 'TEMP-001',
|
||||
engineer: 'Pras',
|
||||
},
|
||||
{
|
||||
alarm_id: 2,
|
||||
tanggal: '2025-01-15 09:15:00',
|
||||
plant_sub_section: 'Plant B - Section 2',
|
||||
device: 'Device 002',
|
||||
tag: 'PRESS-002',
|
||||
engineer: 'Bagus',
|
||||
},
|
||||
{
|
||||
alarm_id: 3,
|
||||
tanggal: '2025-01-15 10:00:00',
|
||||
plant_sub_section: 'Plant A - Section 3',
|
||||
device: 'Device 003',
|
||||
tag: 'FLOW-003',
|
||||
engineer: 'iqbal',
|
||||
},
|
||||
{
|
||||
alarm_id: 4,
|
||||
tanggal: '2025-01-15 11:45:00',
|
||||
plant_sub_section: 'Plant C - Section 1',
|
||||
device: 'Device 004',
|
||||
tag: 'LEVEL-004',
|
||||
engineer: 'riski',
|
||||
},
|
||||
{
|
||||
alarm_id: 5,
|
||||
tanggal: '2025-01-15 13:20:00',
|
||||
plant_sub_section: 'Plant B - Section 3',
|
||||
device: 'Device 005',
|
||||
tag: 'TEMP-005',
|
||||
engineer: 'anton',
|
||||
},
|
||||
{
|
||||
alarm_id: 6,
|
||||
tanggal: '2025-01-15 14:00:00',
|
||||
plant_sub_section: 'Plant A - Section 2',
|
||||
device: 'Device 006',
|
||||
tag: 'PRESS-006',
|
||||
engineer: 'kurniawan',
|
||||
},
|
||||
{
|
||||
alarm_id: 7,
|
||||
tanggal: '2025-01-15 15:30:00',
|
||||
plant_sub_section: 'Plant C - Section 2',
|
||||
device: 'Device 007',
|
||||
tag: 'FLOW-007',
|
||||
engineer: 'wawan',
|
||||
},
|
||||
];
|
||||
|
||||
const ListEventAlarm = memo(function ListEventAlarm(props) {
|
||||
const columns = [
|
||||
{
|
||||
title: 'No',
|
||||
key: 'no',
|
||||
width: '5%',
|
||||
align: 'center',
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
title: 'Tanggal',
|
||||
dataIndex: 'tanggal',
|
||||
key: 'tanggal',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
title: 'Plant Sub Section',
|
||||
dataIndex: 'plant_sub_section',
|
||||
key: 'plant_sub_section',
|
||||
width: '25%',
|
||||
},
|
||||
{
|
||||
title: 'Device',
|
||||
dataIndex: 'device',
|
||||
key: 'device',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
title: 'Tag',
|
||||
dataIndex: 'tag',
|
||||
key: 'tag',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
title: 'Engineer',
|
||||
dataIndex: 'engineer',
|
||||
key: 'engineer',
|
||||
width: '15%',
|
||||
},
|
||||
];
|
||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||
const [alarmsData] = useState(initialAlarmsData);
|
||||
|
||||
const defaultFilter = { search: '' };
|
||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Dummy data function to simulate API call
|
||||
const getAllEventAlarm = async (params) => {
|
||||
// Simulate API delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
// Extract URLSearchParams
|
||||
const searchParam = params.get('search') || '';
|
||||
const page = parseInt(params.get('page')) || 1;
|
||||
const limit = parseInt(params.get('limit')) || 10;
|
||||
|
||||
console.log('getAllEventAlarm called with:', { searchParam, page, limit });
|
||||
|
||||
// Filter by search
|
||||
let filteredAlarms = alarmsData;
|
||||
if (searchParam) {
|
||||
const searchLower = searchParam.toLowerCase();
|
||||
filteredAlarms = alarmsData.filter(
|
||||
(alarm) =>
|
||||
alarm.tanggal.toLowerCase().includes(searchLower) ||
|
||||
alarm.plant_sub_section.toLowerCase().includes(searchLower) ||
|
||||
alarm.device.toLowerCase().includes(searchLower) ||
|
||||
alarm.tag.toLowerCase().includes(searchLower) ||
|
||||
alarm.engineer.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
// Pagination logic
|
||||
const totalData = filteredAlarms.length;
|
||||
const totalPages = Math.ceil(totalData / limit);
|
||||
const startIndex = (page - 1) * limit;
|
||||
const endIndex = startIndex + limit;
|
||||
const paginatedData = filteredAlarms.slice(startIndex, endIndex);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
statusCode: 200,
|
||||
data: {
|
||||
data: paginatedData,
|
||||
total: totalData,
|
||||
paging: {
|
||||
page: page,
|
||||
limit: limit,
|
||||
total: totalData,
|
||||
page_total: totalPages,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
if (props.actionMode == 'list') {
|
||||
setFormDataFilter(defaultFilter);
|
||||
doFilter();
|
||||
}
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, [props.actionMode, alarmsData]);
|
||||
|
||||
const doFilter = () => {
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
setFormDataFilter({ search: searchValue });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleSearchClear = () => {
|
||||
setSearchValue('');
|
||||
setFormDataFilter({ search: '' });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card>
|
||||
<Row>
|
||||
<Col xs={24}>
|
||||
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
||||
<Col xs={24} sm={24} md={12} lg={12}>
|
||||
<Input.Search
|
||||
placeholder="Search alarm by tanggal, plant, device, tag, engineer..."
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchValue(value);
|
||||
// Auto search when clearing by backspace/delete
|
||||
if (value === '') {
|
||||
setFormDataFilter({ search: '' });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
}
|
||||
}}
|
||||
onSearch={handleSearch}
|
||||
allowClear={{
|
||||
clearIcon: <span onClick={handleSearchClear}>✕</span>,
|
||||
}}
|
||||
enterButton={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
style={{
|
||||
backgroundColor: '#23A55A',
|
||||
borderColor: '#23A55A',
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
}
|
||||
size="large"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||
<TableList
|
||||
getData={getAllEventAlarm}
|
||||
queryParams={formDataFilter}
|
||||
columns={columns}
|
||||
triger={trigerFilter}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default ListEventAlarm;
|
||||
38
src/pages/history/alarm/IndexHistoryAlarm.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||
import { Typography } from 'antd';
|
||||
import ListHistoryAlarm from './component/ListHistoryAlarm';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const IndexHistoryAlarm = memo(function IndexHistoryAlarm() {
|
||||
const navigate = useNavigate();
|
||||
const { setBreadcrumbItems } = useBreadcrumb();
|
||||
const [selectedData, setSelectedData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
setBreadcrumbItems([
|
||||
{
|
||||
title: (
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
• History Event
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, [navigate, setBreadcrumbItems]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ListHistoryAlarm selectedData={selectedData} setSelectedData={setSelectedData} />
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default IndexHistoryAlarm;
|
||||
158
src/pages/history/alarm/component/ListHistoryAlarm.jsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { Button, Row, Col, Card, Input } from 'antd';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import TableList from '../../../../components/Global/TableList';
|
||||
import { getAllHistoryAlarm } from '../../../../api/history-value';
|
||||
|
||||
const ListHistoryAlarm = memo(function ListHistoryAlarm(props) {
|
||||
const columns = [
|
||||
{
|
||||
title: 'No',
|
||||
key: 'no',
|
||||
width: '5%',
|
||||
align: 'center',
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
title: 'Datetime',
|
||||
dataIndex: 'datetime',
|
||||
key: 'datetime',
|
||||
width: '15%',
|
||||
// render: (_, record) => toAppDateTimezoneFormatter(record.datetime),
|
||||
},
|
||||
{
|
||||
title: 'Tag Name',
|
||||
dataIndex: 'tag_name',
|
||||
key: 'tag_name',
|
||||
width: '40%',
|
||||
},
|
||||
{
|
||||
title: 'Value',
|
||||
dataIndex: 'new_val',
|
||||
key: 'new_val',
|
||||
width: '10%',
|
||||
render: (_, record) => Number(record.new_val).toFixed(4),
|
||||
},
|
||||
{
|
||||
title: 'Threshold',
|
||||
dataIndex: 'threshold',
|
||||
key: 'threshold',
|
||||
width: '10%',
|
||||
render: (_, record) => {
|
||||
switch (record.status) {
|
||||
case 1:
|
||||
return (
|
||||
<span>
|
||||
{record.lim_low} : {record.lim_high}
|
||||
</span>
|
||||
);
|
||||
case 2:
|
||||
return <span>{`< ${record.lim_low_crash}`}</span>;
|
||||
case 3:
|
||||
return (
|
||||
<span>
|
||||
{record.lim_low_crash} : {record.lim_low}
|
||||
</span>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<span>
|
||||
{record.lim_high} : {record.lim_high_crash}
|
||||
</span>
|
||||
);
|
||||
case 5:
|
||||
return <span>{`> ${record.lim_high_crash}`}</span>;
|
||||
default:
|
||||
return <span>Undefined</span>;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Condition',
|
||||
dataIndex: 'condition',
|
||||
key: 'condition',
|
||||
width: '20%',
|
||||
render: (_, record) => (
|
||||
<Button type="text" style={{ backgroundColor: record.status_color, width: '100%' }}>
|
||||
{record.condition}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Stat',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: '5%',
|
||||
},
|
||||
];
|
||||
|
||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||
|
||||
const defaultFilter = { criteria: '' };
|
||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const handleSearch = () => {
|
||||
setFormDataFilter({ criteria: searchValue });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleSearchClear = () => {
|
||||
setSearchValue('');
|
||||
setFormDataFilter({ criteria: '' });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card>
|
||||
<Row>
|
||||
<Col xs={24}>
|
||||
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
||||
<Col xs={24} sm={24} md={12} lg={12}>
|
||||
<Input.Search
|
||||
placeholder="Search ..."
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchValue(value);
|
||||
if (value === '') {
|
||||
handleSearchClear();
|
||||
}
|
||||
}}
|
||||
onSearch={handleSearch}
|
||||
allowClear={{
|
||||
clearIcon: <span onClick={handleSearchClear}>✕</span>,
|
||||
}}
|
||||
enterButton={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
style={{
|
||||
backgroundColor: '#23A55A',
|
||||
borderColor: '#23A55A',
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
}
|
||||
size="large"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||
<TableList
|
||||
getData={getAllHistoryAlarm}
|
||||
queryParams={formDataFilter}
|
||||
columns={columns}
|
||||
triger={trigerFilter}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default ListHistoryAlarm;
|
||||
38
src/pages/history/event/IndexHistoryEvent.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||
import { Typography } from 'antd';
|
||||
import ListHistoryEvent from './component/ListHistoryEvent';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const IndexHistoryEvent = memo(function IndexHistoryEvent() {
|
||||
const navigate = useNavigate();
|
||||
const { setBreadcrumbItems } = useBreadcrumb();
|
||||
const [selectedData, setSelectedData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
setBreadcrumbItems([
|
||||
{
|
||||
title: (
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
• History Event
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, [navigate, setBreadcrumbItems]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ListHistoryEvent selectedData={selectedData} setSelectedData={setSelectedData} />
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default IndexHistoryEvent;
|
||||
117
src/pages/history/event/component/ListHistoryEvent.jsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { Button, Row, Col, Card, Input } from 'antd';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import TableList from '../../../../components/Global/TableList';
|
||||
import { getAllHistoryEvent } from '../../../../api/history-value';
|
||||
|
||||
const ListHistoryEvent = memo(function ListHistoryEvent(props) {
|
||||
const columns = [
|
||||
{
|
||||
title: 'No',
|
||||
key: 'no',
|
||||
width: '5%',
|
||||
align: 'center',
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
title: 'Datetime',
|
||||
dataIndex: 'datetime',
|
||||
key: 'datetime',
|
||||
width: '15%',
|
||||
// render: (_, record) => toAppDateTimezoneFormatter(record.datetime),
|
||||
},
|
||||
{
|
||||
title: 'Tag Name',
|
||||
dataIndex: 'tagname',
|
||||
key: 'tagname',
|
||||
width: '40%',
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '20%',
|
||||
render: (_, record) => (
|
||||
<Button type="text" style={{ backgroundColor: record.status_color, width: '100%' }}>
|
||||
{record.description}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Stat',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: '5%',
|
||||
},
|
||||
];
|
||||
|
||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||
|
||||
const defaultFilter = { criteria: '' };
|
||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const handleSearch = () => {
|
||||
setFormDataFilter({ criteria: searchValue });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleSearchClear = () => {
|
||||
setSearchValue('');
|
||||
setFormDataFilter({ criteria: '' });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card>
|
||||
<Row>
|
||||
<Col xs={24}>
|
||||
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
||||
<Col xs={24} sm={24} md={12} lg={12}>
|
||||
<Input.Search
|
||||
placeholder="Search ..."
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchValue(value);
|
||||
if (value === '') {
|
||||
handleSearchClear();
|
||||
}
|
||||
}}
|
||||
onSearch={handleSearch}
|
||||
allowClear={{
|
||||
clearIcon: <span onClick={handleSearchClear}>✕</span>,
|
||||
}}
|
||||
enterButton={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
style={{
|
||||
backgroundColor: '#23A55A',
|
||||
borderColor: '#23A55A',
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
}
|
||||
size="large"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||
<TableList
|
||||
getData={getAllHistoryEvent}
|
||||
queryParams={formDataFilter}
|
||||
columns={columns}
|
||||
triger={trigerFilter}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default ListHistoryEvent;
|
||||
@@ -1,275 +0,0 @@
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||
import { Typography, Table, Card, Select, DatePicker, Button, Row, Col } from 'antd';
|
||||
import { FileTextOutlined } from '@ant-design/icons';
|
||||
import { decryptData } from '../../../components/Global/Formatter';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// New data structure for tag history
|
||||
const tagHistoryData = [
|
||||
{
|
||||
tag: 'TEMP_SENSOR_1',
|
||||
color: '#FF6B4A',
|
||||
history: [
|
||||
{ timestamp: '2025-10-09 08:00', value: 75 },
|
||||
{ timestamp: '2025-10-09 08:05', value: 76 },
|
||||
{ timestamp: '2025-10-09 08:10', value: 75 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tag: 'GAS_LEAK_SENSOR_1',
|
||||
color: '#4ECDC4',
|
||||
history: [
|
||||
{ timestamp: '2025-10-09 08:00', value: 10 },
|
||||
{ timestamp: '2025-10-09 08:05', value: 150 },
|
||||
{ timestamp: '2025-10-09 08:10', value: 12 },
|
||||
],
|
||||
},
|
||||
{
|
||||
tag: 'PRESSURE_SENSOR_1',
|
||||
color: '#FFE66D',
|
||||
history: [
|
||||
{ timestamp: '2025-10-09 08:00', value: 1.2 },
|
||||
{ timestamp: '2025-10-09 08:05', value: 1.3 },
|
||||
{ timestamp: '2025-10-09 08:10', value: 1.2 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const IndexReport = memo(function IndexReport() {
|
||||
const navigate = useNavigate();
|
||||
const { setBreadcrumbItems } = useBreadcrumb();
|
||||
|
||||
const [plantSubSection, setPlantSubSection] = useState('Semua Plant');
|
||||
const [startDate, setStartDate] = useState(dayjs('2025-09-30'));
|
||||
const [endDate, setEndDate] = useState(dayjs('2025-10-09'));
|
||||
const [periode, setPeriode] = useState('30 Menit');
|
||||
const [userRole, setUserRole] = useState(null);
|
||||
const [roleLevel, setRoleLevel] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
// Get user data and role
|
||||
let userData = null;
|
||||
const sessionData = localStorage.getItem('session');
|
||||
if (sessionData) {
|
||||
userData = decryptData(sessionData);
|
||||
} else {
|
||||
const userRaw = localStorage.getItem('user');
|
||||
if (userRaw) {
|
||||
try {
|
||||
userData = { user: JSON.parse(userRaw) };
|
||||
} catch (e) {
|
||||
console.error('Error parsing user data:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userData?.user) {
|
||||
setUserRole(userData.user.role_name);
|
||||
setRoleLevel(userData.user.role_level);
|
||||
}
|
||||
|
||||
setBreadcrumbItems([
|
||||
{
|
||||
title: (
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
• History
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
Report
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleReset = () => {
|
||||
setPlantSubSection('Semua Plant');
|
||||
setStartDate(dayjs('2025-09-30'));
|
||||
setEndDate(dayjs('2025-10-09'));
|
||||
setPeriode('30 Menit');
|
||||
};
|
||||
|
||||
// Check if user has permission to view data (all except guest)
|
||||
const canViewData = userRole && userRole !== 'guest';
|
||||
|
||||
// Convert tag history data to table format
|
||||
const convertToTableData = () => {
|
||||
const timestamps = {}; // Use an object to collect data per timestamp
|
||||
|
||||
tagHistoryData.forEach((tagData) => {
|
||||
tagData.history.forEach((point) => {
|
||||
if (!timestamps[point.timestamp]) {
|
||||
timestamps[point.timestamp] = {
|
||||
key: point.timestamp,
|
||||
'Date and Time': point.timestamp,
|
||||
};
|
||||
}
|
||||
timestamps[point.timestamp][tagData.tag] = point.value;
|
||||
});
|
||||
});
|
||||
|
||||
// Convert the object to an array
|
||||
return Object.values(timestamps);
|
||||
};
|
||||
|
||||
const tableData = convertToTableData();
|
||||
|
||||
// Create dynamic columns based on tags
|
||||
const tags = tagHistoryData.map((tagData) => tagData.tag);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Date and Time',
|
||||
dataIndex: 'Date and Time',
|
||||
key: 'Date and Time',
|
||||
fixed: 'left',
|
||||
width: 180,
|
||||
render: (text) => <Text strong>{text}</Text>,
|
||||
},
|
||||
...tags.map((tag) => ({
|
||||
title: tag,
|
||||
dataIndex: tag,
|
||||
key: tag,
|
||||
align: 'center',
|
||||
width: 150,
|
||||
render: (value) => <Text>{value !== undefined ? value : '-'}</Text>,
|
||||
})),
|
||||
];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div style={{ minHeight: 360 }}>
|
||||
{/* Filter Section */}
|
||||
<Card className="filter-card">
|
||||
<div className="filter-header">
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
☰ Filter Data
|
||||
</Text>
|
||||
</div>
|
||||
<Row gutter={16} style={{ marginTop: '16px' }}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<div className="filter-item">
|
||||
<Text style={{ fontSize: '12px', color: '#666' }}>
|
||||
Plant Sub Section
|
||||
</Text>
|
||||
<Select
|
||||
value={plantSubSection}
|
||||
onChange={setPlantSubSection}
|
||||
style={{ width: '100%', marginTop: '4px' }}
|
||||
options={[
|
||||
{ value: 'Semua Plant', label: 'Semua Plant' },
|
||||
{ value: 'Plant 1', label: 'Plant 1' },
|
||||
{ value: 'Plant 2', label: 'Plant 2' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<div className="filter-item">
|
||||
<Text style={{ fontSize: '12px', color: '#666' }}>
|
||||
Tanggal Mulai
|
||||
</Text>
|
||||
<DatePicker
|
||||
value={startDate}
|
||||
onChange={setStartDate}
|
||||
format="DD/MM/YYYY"
|
||||
style={{ width: '100%', marginTop: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<div className="filter-item">
|
||||
<Text style={{ fontSize: '12px', color: '#666' }}>
|
||||
Tanggal Akhir
|
||||
</Text>
|
||||
<DatePicker
|
||||
value={endDate}
|
||||
onChange={setEndDate}
|
||||
format="DD/MM/YYYY"
|
||||
style={{ width: '100%', marginTop: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<div className="filter-item">
|
||||
<Text style={{ fontSize: '12px', color: '#666' }}>Periode</Text>
|
||||
<Select
|
||||
value={periode}
|
||||
onChange={setPeriode}
|
||||
style={{ width: '100%', marginTop: '4px' }}
|
||||
options={[
|
||||
{ value: '5 Menit', label: '5 Menit' },
|
||||
{ value: '10 Menit', label: '10 Menit' },
|
||||
{ value: '30 Menit', label: '30 Menit' },
|
||||
{ value: '1 Jam', label: '1 Jam' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={8} style={{ marginTop: '16px' }}>
|
||||
<Col>
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
icon={<FileTextOutlined />}
|
||||
disabled={!canViewData}
|
||||
>
|
||||
Tampilkan
|
||||
</Button>
|
||||
</Col>
|
||||
<Col>
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
style={{ backgroundColor: '#6c757d', color: 'white' }}
|
||||
disabled={!canViewData}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
{/* Table Section */}
|
||||
{/* {!canViewData ? (
|
||||
<Card style={{ marginTop: '24px', textAlign: 'center', padding: '40px' }}>
|
||||
<Text style={{ fontSize: '16px', color: '#999' }}>
|
||||
Anda tidak memiliki akses untuk melihat data report.
|
||||
<br />
|
||||
Silakan hubungi administrator untuk mendapatkan akses.
|
||||
</Text>
|
||||
</Card>
|
||||
) : ( */}
|
||||
<Card style={{ marginTop: '24px' }}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Text strong style={{ fontSize: '16px' }}>
|
||||
☰ History Report
|
||||
</Text>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
scroll={{ x: 1000 }}
|
||||
size="middle"
|
||||
/>
|
||||
</Card>
|
||||
{/* )} */}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default IndexReport;
|
||||
@@ -1,297 +0,0 @@
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||
import { Typography, Select, DatePicker, Button, Row, Col, Card } from 'antd';
|
||||
import { ResponsiveLine } from '@nivo/line';
|
||||
import { FileTextOutlined } from '@ant-design/icons';
|
||||
import { decryptData } from '../../../components/Global/Formatter';
|
||||
import dayjs from 'dayjs';
|
||||
import './trending.css';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const IndexTrending = memo(function IndexTrending() {
|
||||
const navigate = useNavigate();
|
||||
const { setBreadcrumbItems } = useBreadcrumb();
|
||||
|
||||
const [plantSubSection, setPlantSubSection] = useState('Semua Plant');
|
||||
const [startDate, setStartDate] = useState(dayjs('2025-09-30'));
|
||||
const [endDate, setEndDate] = useState(dayjs('2025-10-09'));
|
||||
const [periode, setPeriode] = useState('10 Menit');
|
||||
const [userRole, setUserRole] = useState(null);
|
||||
const [roleLevel, setRoleLevel] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
// Get user data and role
|
||||
let userData = null;
|
||||
const sessionData = localStorage.getItem('session');
|
||||
if (sessionData) {
|
||||
userData = decryptData(sessionData);
|
||||
} else {
|
||||
const userRaw = localStorage.getItem('user');
|
||||
if (userRaw) {
|
||||
try {
|
||||
userData = { user: JSON.parse(userRaw) };
|
||||
} catch (e) {
|
||||
console.error('Error parsing user data:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userData?.user) {
|
||||
setUserRole(userData.user.role_name);
|
||||
setRoleLevel(userData.user.role_level);
|
||||
}
|
||||
|
||||
setBreadcrumbItems([
|
||||
{
|
||||
title: (
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
• History
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
Trending
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const tagTrendingData = [
|
||||
{
|
||||
id: 'TEMP_SENSOR_1',
|
||||
color: '#FF6B4A',
|
||||
data: [
|
||||
{ y: '08:00', x: 75 },
|
||||
{ y: '08:05', x: 76 },
|
||||
{ y: '08:10', x: 75 },
|
||||
{ y: '08:15', x: 77 },
|
||||
{ y: '08:20', x: 76 },
|
||||
{ y: '08:25', x: 78 },
|
||||
{ y: '08:30', x: 79 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'GAS_LEAK_SENSOR_1',
|
||||
color: '#4ECDC4',
|
||||
data: [
|
||||
{ y: '08:00', x: 10 },
|
||||
{ y: '08:05', x: 150 },
|
||||
{ y: '08:10', x: 40 },
|
||||
{ y: '08:15', x: 20 },
|
||||
{ y: '08:20', x: 15 },
|
||||
{ y: '08:25', x: 18 },
|
||||
{ y: '08:30', x: 25 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'PRESSURE_SENSOR_1',
|
||||
color: '#FFE66D',
|
||||
data: [
|
||||
{ y: '08:00', x: 1.2 },
|
||||
{ y: '08:05', x: 1.3 },
|
||||
{ y: '08:10', x: 1.2 },
|
||||
{ y: '08:15', x: 1.4 },
|
||||
{ y: '08:20', x: 1.5 },
|
||||
{ y: '08:25', x: 1.3 },
|
||||
{ y: '08:30', x: 1.2 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const handleReset = () => {
|
||||
setPlantSubSection('Semua Plant');
|
||||
setStartDate(dayjs('2025-09-30'));
|
||||
setEndDate(dayjs('2025-10-09'));
|
||||
setPeriode('10 Menit');
|
||||
};
|
||||
|
||||
// Check if user has permission to view data (all except guest)
|
||||
const canViewData = userRole && userRole !== 'guest';
|
||||
|
||||
// Check if user can export/filter (administrator, engineer)
|
||||
const canExportData = userRole && (userRole === 'administrator' || userRole === 'engineer');
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* Filter Section */}
|
||||
<Card className="filter-card">
|
||||
<div className="filter-header">
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
☰ Filter Data
|
||||
</Text>
|
||||
</div>
|
||||
<Row gutter={16} style={{ marginTop: '16px' }}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<div className="filter-item">
|
||||
<Text style={{ fontSize: '12px', color: '#666' }}>
|
||||
Plant Sub Section
|
||||
</Text>
|
||||
<Select
|
||||
value={plantSubSection}
|
||||
onChange={setPlantSubSection}
|
||||
style={{ width: '100%', marginTop: '4px' }}
|
||||
options={[
|
||||
{ value: 'Semua Plant', label: 'Semua Plant' },
|
||||
{ value: 'Plant 1', label: 'Plant 1' },
|
||||
{ value: 'Plant 2', label: 'Plant 2' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<div className="filter-item">
|
||||
<Text style={{ fontSize: '12px', color: '#666' }}>Tanggal Mulai</Text>
|
||||
<DatePicker
|
||||
value={startDate}
|
||||
onChange={setStartDate}
|
||||
format="DD/MM/YYYY"
|
||||
style={{ width: '100%', marginTop: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<div className="filter-item">
|
||||
<Text style={{ fontSize: '12px', color: '#666' }}>Tanggal Akhir</Text>
|
||||
<DatePicker
|
||||
value={endDate}
|
||||
onChange={setEndDate}
|
||||
format="DD/MM/YYYY"
|
||||
style={{ width: '100%', marginTop: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<div className="filter-item">
|
||||
<Text style={{ fontSize: '12px', color: '#666' }}>Periode</Text>
|
||||
<Select
|
||||
value={periode}
|
||||
onChange={setPeriode}
|
||||
style={{ width: '100%', marginTop: '4px' }}
|
||||
options={[
|
||||
{ value: '5 Menit', label: '5 Menit' },
|
||||
{ value: '10 Menit', label: '10 Menit' },
|
||||
{ value: '30 Menit', label: '30 Menit' },
|
||||
{ value: '1 Jam', label: '1 Jam' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={8} style={{ marginTop: '16px' }}>
|
||||
<Col>
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
icon={<FileTextOutlined />}
|
||||
disabled={!canViewData}
|
||||
>
|
||||
Tampilkan
|
||||
</Button>
|
||||
</Col>
|
||||
<Col>
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
style={{ backgroundColor: '#6c757d', color: 'white' }}
|
||||
disabled={!canViewData}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
{/* Charts Section */}
|
||||
{/* {!canViewData ? (
|
||||
<Card style={{ marginTop: '24px', textAlign: 'center', padding: '40px' }}>
|
||||
<Text style={{ fontSize: '16px', color: '#999' }}>
|
||||
Anda tidak memiliki akses untuk melihat data trending.
|
||||
<br />
|
||||
Silakan hubungi administrator untuk mendapatkan akses.
|
||||
</Text>
|
||||
</Card>
|
||||
) : ( */}
|
||||
<>
|
||||
<Row gutter={16} style={{ marginTop: '24px' }}>
|
||||
{/* Line Chart */}
|
||||
<Col xs={24}>
|
||||
<Card className="chart-card">
|
||||
<div className="chart-header">
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
☰ Tag Value Trending
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ height: '500px', marginTop: '16px' }}>
|
||||
<ResponsiveLine
|
||||
data={tagTrendingData}
|
||||
margin={{ top: 20, right: 20, bottom: 50, left: 60 }}
|
||||
xScale={{
|
||||
type: 'linear',
|
||||
min: 'auto',
|
||||
max: 'auto',
|
||||
stacked: false,
|
||||
reverse: false,
|
||||
}}
|
||||
yScale={{
|
||||
type: 'point',
|
||||
}}
|
||||
curve="natural"
|
||||
axisBottom={{
|
||||
tickSize: 5,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
legend: 'Value',
|
||||
legendOffset: 40,
|
||||
legendPosition: 'middle',
|
||||
}}
|
||||
axisLeft={{
|
||||
tickSize: 5,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
legend: 'Time',
|
||||
legendOffset: -45,
|
||||
legendPosition: 'middle',
|
||||
}}
|
||||
colors={{ datum: 'color' }}
|
||||
pointSize={6}
|
||||
pointColor={{ theme: 'background' }}
|
||||
pointBorderWidth={2}
|
||||
pointBorderColor={{ from: 'serieColor' }}
|
||||
pointLabelYOffset={-12}
|
||||
useMesh={true}
|
||||
legends={[
|
||||
{
|
||||
anchor: 'bottom-right',
|
||||
direction: 'column',
|
||||
justify: false,
|
||||
translateX: 100,
|
||||
translateY: 0,
|
||||
itemsSpacing: 2,
|
||||
itemDirection: 'left-to-right',
|
||||
itemWidth: 80,
|
||||
itemHeight: 20,
|
||||
itemOpacity: 0.75,
|
||||
symbolSize: 12,
|
||||
symbolShape: 'circle',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
{/* )} */}
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default IndexTrending;
|
||||
@@ -3,11 +3,12 @@ import { Card, Typography, Flex } from 'antd';
|
||||
import { setValSvg } from '../../components/Global/MqttConnection';
|
||||
import SvgTemplate from './SvgTemplate';
|
||||
import SvgViewer from './SvgViewer';
|
||||
import filePathSvg from '../../assets/svg/air_dryer_A_rev.svg';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const filePathSvg = '/svg/air_dryer_A_rev.svg';
|
||||
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||
// const filePathSvg = '/src/assets/svg/air_dryer_A_rev.svg';
|
||||
const topicMqtt = 'PIU_COD/AIR_DRYER/AIR_DRYER_A';
|
||||
|
||||
const SvgAirDryerA = () => {
|
||||
return (
|
||||
|
||||
@@ -3,11 +3,12 @@ import { Card, Typography, Flex } from 'antd';
|
||||
import { setValSvg } from '../../components/Global/MqttConnection';
|
||||
import SvgTemplate from './SvgTemplate';
|
||||
import SvgViewer from './SvgViewer';
|
||||
import filePathSvg from '../../assets/svg/air_dryer_B_rev.svg';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const filePathSvg = '/svg/air_dryer_B_rev.svg';
|
||||
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||
// const filePathSvg = '/src/assets/svg/air_dryer_B_rev.svg';
|
||||
const topicMqtt = 'PIU_COD/AIR_DRYER/AIR_DRYER_B';
|
||||
|
||||
const SvgAirDryerB = () => {
|
||||
return (
|
||||
|
||||
@@ -3,11 +3,12 @@ import { Card, Typography, Flex } from 'antd';
|
||||
import { setValSvg } from '../../components/Global/MqttConnection';
|
||||
import SvgTemplate from './SvgTemplate';
|
||||
import SvgViewer from './SvgViewer';
|
||||
import filePathSvg from '../../assets/svg/air_dryer_C_rev.svg';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const filePathSvg = '/svg/air_dryer_C_rev.svg';
|
||||
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||
// const filePathSvg = '/src/assets/svg/air_dryer_C_rev.svg';
|
||||
const topicMqtt = 'PIU_COD/AIR_DRYER/AIR_DRYER_C';
|
||||
|
||||
const SvgAirDryerC = () => {
|
||||
return (
|
||||
|
||||
@@ -3,11 +3,12 @@ import { Card, Typography, Flex } from 'antd';
|
||||
import { setValSvg } from '../../components/Global/MqttConnection';
|
||||
import SvgTemplate from './SvgTemplate';
|
||||
import SvgViewer from './SvgViewer';
|
||||
import filePathSvg from '../../assets/svg/compressorA_rev.svg';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const filePathSvg = '/svg/test-new.svg';
|
||||
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
||||
const topicMqtt = 'PIU_COD/COMPRESSOR/COMPRESSOR_A';
|
||||
|
||||
const SvgCompressorA = () => {
|
||||
return (
|
||||
|
||||
@@ -3,11 +3,10 @@ import { Card, Typography, Flex } from 'antd';
|
||||
import { setValSvg } from '../../components/Global/MqttConnection';
|
||||
import SvgTemplate from './SvgTemplate';
|
||||
import SvgViewer from './SvgViewer';
|
||||
import filePathSvg from '../../assets/svg/compressorB_rev.svg';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const filePathSvg = '/svg/test-new.svg';
|
||||
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||
const topicMqtt = 'PIU_COD/COMPRESSOR/COMPRESSOR_B';
|
||||
|
||||
const SvgCompressorB = () => {
|
||||
return (
|
||||
|
||||
@@ -3,11 +3,12 @@ import { Card, Typography, Flex } from 'antd';
|
||||
import { setValSvg } from '../../components/Global/MqttConnection';
|
||||
import SvgTemplate from './SvgTemplate';
|
||||
import SvgViewer from './SvgViewer';
|
||||
import filePathSvg from '../../assets/svg/compressorC_rev.svg';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const filePathSvg = '/svg/test-new.svg';
|
||||
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
||||
const topicMqtt = 'PIU_COD/COMPRESSOR/COMPRESSOR_C';
|
||||
|
||||
const SvgCompressorC = () => {
|
||||
return (
|
||||
|
||||
@@ -3,13 +3,14 @@ import { Card, Typography, Flex } from 'antd';
|
||||
import { setValSvg } from '../../components/Global/MqttConnection';
|
||||
import SvgTemplate from './SvgTemplate';
|
||||
import SvgViewer from './SvgViewer';
|
||||
import filePathSvg from '../../assets/svg/overview-airdryer.svg';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const filePathSvg = '/svg/test-new.svg';
|
||||
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
||||
const topicMqtt = 'PIU_COD/AIR_DRYER/OVERVIEW';
|
||||
|
||||
const SvgOverview = () => {
|
||||
const SvgOverviewAirDryer = () => {
|
||||
return (
|
||||
<SvgTemplate>
|
||||
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
|
||||
@@ -17,4 +18,4 @@ const SvgOverview = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SvgOverview;
|
||||
export default SvgOverviewAirDryer;
|
||||
21
src/pages/home/SvgOverviewCompressor.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Card, Typography, Flex } from 'antd';
|
||||
import { setValSvg } from '../../components/Global/MqttConnection';
|
||||
import SvgTemplate from './SvgTemplate';
|
||||
import SvgViewer from './SvgViewer';
|
||||
import filePathSvg from '../../assets/svg/overview-compressor.svg';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
||||
const topicMqtt = 'PIU_COD/COMPRESSOR/OVERVIEW';
|
||||
|
||||
const SvgOverviewCompressor = () => {
|
||||
return (
|
||||
<SvgTemplate>
|
||||
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
|
||||
</SvgTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default SvgOverviewCompressor;
|
||||
@@ -3,10 +3,11 @@ import { Card, Typography, Flex } from 'antd';
|
||||
// import { ReactSVG } from 'react-svg';
|
||||
import { setValSvg } from '../../components/Global/MqttConnection';
|
||||
import { ReactSVG } from 'react-svg';
|
||||
import filePathSvg from '../../assets/svg/test-new.svg';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const filePathSvg = '/svg/test-new.svg';
|
||||
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
||||
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||
|
||||
const SvgTest = () => {
|
||||
|
||||
@@ -1,64 +1,160 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Input,
|
||||
Typography,
|
||||
Button,
|
||||
ConfigProvider,
|
||||
Row,
|
||||
Col
|
||||
} from 'antd';
|
||||
import { Modal, Select, Typography, Button, ConfigProvider } from 'antd';
|
||||
import { NotifOk } from '../../../components/Global/ToastNotif';
|
||||
import { createJadwalShift, updateJadwalShift } from '../../../api/jadwal-shift';
|
||||
import { getAllUser } from '../../../api/user';
|
||||
import { getAllShift } from '../../../api/master-shift';
|
||||
import { validateRun } from '../../../Utils/validate';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
const DetailJadwalShift = (props) => {
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
const [employees, setEmployees] = useState([]);
|
||||
const [shifts, setShifts] = useState([]);
|
||||
const [loadingData, setLoadingData] = useState(false);
|
||||
|
||||
const defaultData = {
|
||||
id: '',
|
||||
nama_shift: '',
|
||||
jam_masuk: '',
|
||||
jam_pulang: '',
|
||||
username: '',
|
||||
nama_employee: '',
|
||||
whatsapp: ''
|
||||
user_id: null,
|
||||
shift_id: null,
|
||||
schedule_id: '',
|
||||
user_phone: null,
|
||||
};
|
||||
|
||||
const [FormData, setFormData] = useState(defaultData);
|
||||
const [formData, setFormData] = useState(defaultData);
|
||||
|
||||
const handleSelectChange = (name, value) => {
|
||||
const updates = { [name]: value };
|
||||
|
||||
if (name === 'user_id') {
|
||||
const selectedEmployee = employees.find((emp) => emp.user_id === value);
|
||||
updates.user_phone = selectedEmployee?.user_phone || '-';
|
||||
}
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
...updates,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
props.setSelectedData(null);
|
||||
props.setActionMode('list');
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoadingData(true);
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
const [usersResponse, shiftsResponse] = await Promise.all([
|
||||
getAllUser(params),
|
||||
getAllShift(params),
|
||||
]);
|
||||
|
||||
const userData = usersResponse?.data || usersResponse || [];
|
||||
const shiftData = shiftsResponse?.data || shiftsResponse || [];
|
||||
|
||||
setEmployees(Array.isArray(userData) ? userData : []);
|
||||
setShifts(Array.isArray(shiftData) ? shiftData : []);
|
||||
} catch (error) {
|
||||
NotifOk({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: 'Gagal memuat data karyawan atau shift.',
|
||||
});
|
||||
} finally {
|
||||
setLoadingData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setConfirmLoading(true);
|
||||
// This is a dummy save function for slicing purposes
|
||||
setTimeout(() => {
|
||||
|
||||
// Daftar aturan validasi
|
||||
const validationRules = [
|
||||
{ field: 'user_id', label: 'Nama Karyawan', required: true },
|
||||
{ field: 'shift_id', label: 'Shift', required: true },
|
||||
];
|
||||
|
||||
if (
|
||||
validateRun(formData, validationRules, (errorMessages) => {
|
||||
NotifOk({
|
||||
icon: 'warning',
|
||||
title: 'Peringatan',
|
||||
message: errorMessages,
|
||||
});
|
||||
setConfirmLoading(false);
|
||||
})
|
||||
)
|
||||
return;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
user_id: formData.user_id,
|
||||
shift_id: formData.shift_id,
|
||||
};
|
||||
|
||||
// Add schedule_id only if editing and it exists
|
||||
if (props.actionMode === 'edit' && formData.schedule_id) {
|
||||
payload.schedule_id = formData.schedule_id;
|
||||
}
|
||||
|
||||
const response =
|
||||
props.actionMode === 'edit'
|
||||
? await updateJadwalShift(formData.id, payload)
|
||||
: await createJadwalShift(payload);
|
||||
|
||||
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
||||
const action = props.actionMode === 'edit' ? 'diubah' : 'ditambahkan';
|
||||
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: 'Data dummy berhasil disimpan.',
|
||||
message: `Jadwal berhasil ${action}.`,
|
||||
});
|
||||
|
||||
props.setActionMode('list');
|
||||
}, 1000);
|
||||
} else {
|
||||
NotifOk({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: response?.message || 'Terjadi kesalahan saat menyimpan data.',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
NotifOk({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: error.message || 'Terjadi kesalahan pada server.',
|
||||
});
|
||||
} finally {
|
||||
setConfirmLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.showModal) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
if (props.selectedData) {
|
||||
setFormData(props.selectedData);
|
||||
setFormData({
|
||||
id: props.selectedData.id || '',
|
||||
user_id: props.selectedData.user_id || null,
|
||||
shift_id: props.selectedData.shift_id || null,
|
||||
schedule_id: props.selectedData.schedule_id || '',
|
||||
user_phone: props.selectedData.whatsapp || props.selectedData.user_phone || null,
|
||||
});
|
||||
} else {
|
||||
setFormData(defaultData);
|
||||
}
|
||||
}, [props.showModal, props.selectedData]);
|
||||
|
||||
// Dummy handler for slicing
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({ ...FormData, [name]: value });
|
||||
};
|
||||
}, [props.showModal, props.selectedData, props.actionMode]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -71,7 +167,6 @@ const DetailJadwalShift = (props) => {
|
||||
} Jadwal Shift`}
|
||||
open={props.showModal}
|
||||
onCancel={handleCancel}
|
||||
width={800}
|
||||
footer={[
|
||||
<React.Fragment key="modal-footer">
|
||||
<ConfigProvider
|
||||
@@ -107,64 +202,75 @@ const DetailJadwalShift = (props) => {
|
||||
</React.Fragment>,
|
||||
]}
|
||||
>
|
||||
{FormData && (
|
||||
{formData && (
|
||||
<div>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Nama Karyawan</Text>
|
||||
<Input
|
||||
name="nama_employee"
|
||||
value={FormData.nama_employee}
|
||||
onChange={handleInputChange}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text strong>Username</Text>
|
||||
<Input
|
||||
name="username"
|
||||
value={FormData.username}
|
||||
onChange={handleInputChange}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text strong>Nama Shift</Text>
|
||||
<Input
|
||||
name="nama_shift"
|
||||
value={FormData.nama_shift}
|
||||
onChange={handleInputChange}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text strong>Whatsapp</Text>
|
||||
<Input
|
||||
name="whatsapp"
|
||||
value={FormData.whatsapp}
|
||||
onChange={handleInputChange}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text strong>Jam Masuk</Text>
|
||||
<Input
|
||||
name="jam_masuk"
|
||||
value={FormData.jam_masuk}
|
||||
onChange={handleInputChange}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text strong>Jam Pulang</Text>
|
||||
<Input
|
||||
name="jam_pulang"
|
||||
value={FormData.jam_pulang}
|
||||
onChange={handleInputChange}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Select
|
||||
value={formData.user_id}
|
||||
onChange={(value) => handleSelectChange('user_id', value)}
|
||||
placeholder="Pilih karyawan"
|
||||
disabled={props.readOnly || loadingData}
|
||||
loading={loadingData}
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) =>
|
||||
option?.children?.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{employees
|
||||
.filter((emp) => emp.user_id != null)
|
||||
.map((emp) => (
|
||||
<Option key={`emp-${emp.user_id}`} value={emp.user_id}>
|
||||
{emp.user_fullname || emp.user_name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>No. Telepon</Text>
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '6px',
|
||||
|
||||
marginTop: '4px',
|
||||
color: formData.user_phone ? '#000' : '#999',
|
||||
}}
|
||||
>
|
||||
{formData.user_phone || 'Pilih karyawan terlebih dahulu'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Shift</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Select
|
||||
value={formData.shift_id}
|
||||
onChange={(value) => handleSelectChange('shift_id', value)}
|
||||
placeholder="Pilih shift"
|
||||
disabled={props.readOnly || loadingData}
|
||||
loading={loadingData}
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) =>
|
||||
option?.children?.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{shifts
|
||||
.filter((shift) => shift.shift_id != null)
|
||||
.map((shift) => (
|
||||
<Option key={`shift-${shift.shift_id}`} value={shift.shift_id}>
|
||||
{shift.shift_name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -1,115 +1,167 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { Space, Tag, ConfigProvider, Button, Row, Col, Card, Input } from 'antd';
|
||||
import {
|
||||
Space,
|
||||
ConfigProvider,
|
||||
Button,
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
Input,
|
||||
Typography,
|
||||
Divider,
|
||||
Checkbox,
|
||||
Select,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
EyeOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { NotifAlert, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
||||
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import TableList from '../../../components/Global/TableList';
|
||||
import { getAllJadwalShift, deleteJadwalShift } from '../../../api/jadwal-shift';
|
||||
import { getAllJadwalShift, deleteJadwalShift, updateJadwalShift } from '../../../api/jadwal-shift';
|
||||
import { getAllShift } from '../../../api/master-shift';
|
||||
|
||||
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
{
|
||||
title: 'Tanggal Jadwal',
|
||||
dataIndex: 'schedule_date',
|
||||
key: 'schedule_date',
|
||||
render: (date) => date ? new Date(date).toLocaleDateString('id-ID') : '-',
|
||||
},
|
||||
{
|
||||
title: 'Nama Shift',
|
||||
dataIndex: 'shift_name',
|
||||
key: 'shift_name',
|
||||
render: (text) => text || '-',
|
||||
},
|
||||
{
|
||||
title: 'Jam Masuk',
|
||||
dataIndex: 'start_time',
|
||||
key: 'start_time',
|
||||
render: (time) => time || '-',
|
||||
},
|
||||
{
|
||||
title: 'Jam Pulang',
|
||||
dataIndex: 'end_time',
|
||||
key: 'end_time',
|
||||
render: (time) => time || '-',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'is_active',
|
||||
key: 'is_active',
|
||||
render: (isActive) => (
|
||||
<Tag color={isActive ? 'green' : 'red'}>
|
||||
{isActive ? 'Aktif' : 'Tidak Aktif'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Aksi',
|
||||
key: 'aksi',
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EyeOutlined style={{ color: '#1890ff' }} />}
|
||||
onClick={() => showPreviewModal(record)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined style={{ color: '#faad14' }} />}
|
||||
onClick={() => showEditModal(record)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => showDeleteDialog(record)}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const ListJadwalShift = memo(function ListJadwalShift(props) {
|
||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||
const defaultFilter = { criteria: '' };
|
||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||
const [groupedSchedules, setGroupedSchedules] = useState({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [selectedSchedules, setSelectedSchedules] = useState([]);
|
||||
const [editingShift, setEditingShift] = useState(null);
|
||||
const [pendingChanges, setPendingChanges] = useState({});
|
||||
const [employeeOptions, setEmployeeOptions] = useState([]);
|
||||
const [shiftOptions, setShiftOptions] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const getData = async (queryParams) => {
|
||||
const formatRelativeTimestamp = (timestamp) => {
|
||||
const now = new Date();
|
||||
const date = new Date(timestamp);
|
||||
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const startOfYesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
||||
|
||||
let dayString;
|
||||
if (date >= startOfToday) {
|
||||
dayString = 'Hari ini';
|
||||
} else if (date >= startOfYesterday) {
|
||||
dayString = 'Kemarin';
|
||||
} else {
|
||||
dayString = date.toLocaleDateString('id-ID', { day: 'numeric', month: 'long' });
|
||||
}
|
||||
|
||||
const timeString = date
|
||||
.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', hour12: false })
|
||||
.replace('.', ':');
|
||||
return `${dayString}, ${timeString}`;
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: queryParams.page || 1,
|
||||
limit: queryParams.limit || 10,
|
||||
criteria: queryParams.criteria || ''
|
||||
const paging = {
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
};
|
||||
|
||||
const params = new URLSearchParams({ ...paging, ...formDataFilter });
|
||||
|
||||
// Fetch both schedules and shifts data
|
||||
const [schedulesResponse, shiftsResponse] = await Promise.all([
|
||||
getAllJadwalShift(params),
|
||||
getAllShift(params),
|
||||
]);
|
||||
|
||||
// Handle nested data structure from backend
|
||||
const rawData = schedulesResponse?.data || schedulesResponse || [];
|
||||
const shifts = shiftsResponse?.data || shiftsResponse || [];
|
||||
|
||||
setShiftOptions(shifts);
|
||||
|
||||
// Parse backend response structure: [{ shift: { shift_id, shift_name, users: [...] } }]
|
||||
const grouped = {};
|
||||
const allUsers = [];
|
||||
|
||||
rawData.forEach((item) => {
|
||||
if (item.shift && item.shift.shift_name) {
|
||||
const shift = item.shift;
|
||||
const shiftName = shift.shift_name.toUpperCase().trim();
|
||||
|
||||
// Initialize shift group
|
||||
if (!grouped[shiftName]) {
|
||||
grouped[shiftName] = {
|
||||
shift_id: shift.shift_id,
|
||||
users: [],
|
||||
lastUpdate: { user: 'N/A', timestamp: '1970-01-01T00:00:00Z' },
|
||||
};
|
||||
}
|
||||
|
||||
// Process users in this shift
|
||||
if (shift.users && Array.isArray(shift.users)) {
|
||||
shift.users.forEach((user) => {
|
||||
const normalizedUser = {
|
||||
id: user.user_schedule_id,
|
||||
user_schedule_id: user.user_schedule_id,
|
||||
user_id: user.user_id,
|
||||
shift_id: shift.shift_id,
|
||||
shift_name: shift.shift_name,
|
||||
nama_employee: user.user_fullname || user.user_name || 'Unknown',
|
||||
whatsapp: user.user_phone || '-',
|
||||
user_fullname: user.user_fullname,
|
||||
user_name: user.user_name,
|
||||
user_phone: user.user_phone,
|
||||
updated_at: user.updated_at,
|
||||
created_at: user.created_at,
|
||||
updated_by: user.updated_by,
|
||||
};
|
||||
|
||||
grouped[shiftName].users.push(normalizedUser);
|
||||
allUsers.push(normalizedUser);
|
||||
|
||||
// Update last update timestamp
|
||||
const currentUpdate = new Date(
|
||||
user.updated_at || user.created_at || new Date()
|
||||
);
|
||||
const lastUpdate = new Date(grouped[shiftName].lastUpdate.timestamp);
|
||||
if (currentUpdate > lastUpdate) {
|
||||
grouped[shiftName].lastUpdate = {
|
||||
user: user.updated_by || 'N/A',
|
||||
timestamp: currentUpdate.toISOString(),
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const response = await getAllJadwalShift(params);
|
||||
return response;
|
||||
setEmployeeOptions(allUsers);
|
||||
|
||||
// Add empty shifts that don't have users yet
|
||||
shifts.forEach((shift) => {
|
||||
const shiftName = shift.shift_name.toUpperCase().trim();
|
||||
if (!grouped[shiftName]) {
|
||||
grouped[shiftName] = {
|
||||
shift_id: shift.shift_id,
|
||||
users: [],
|
||||
lastUpdate: { user: 'N/A', timestamp: new Date().toISOString() },
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
setGroupedSchedules(grouped);
|
||||
} catch (error) {
|
||||
console.error('Error fetching jadwal shift:', error);
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'Gagal mengambil data jadwal shift.',
|
||||
title: 'Gagal Memuat Data',
|
||||
message: 'Terjadi kesalahan saat memuat data jadwal shift.',
|
||||
});
|
||||
return {
|
||||
status: 500,
|
||||
data: {
|
||||
data: [],
|
||||
paging: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
page_total: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -125,6 +177,12 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
||||
}
|
||||
}, [props.actionMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.actionMode === 'list') {
|
||||
fetchData();
|
||||
}
|
||||
}, [trigerFilter]);
|
||||
|
||||
const doFilter = () => {
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
@@ -156,52 +214,150 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
||||
};
|
||||
|
||||
const showDeleteDialog = (param) => {
|
||||
const dateStr = param.schedule_date ? new Date(param.schedule_date).toLocaleDateString('id-ID') : 'tanggal tidak diketahui';
|
||||
NotifConfirmDialog({
|
||||
icon: 'question',
|
||||
title: 'Konfirmasi Hapus',
|
||||
message: `Jadwal shift tanggal ${dateStr} akan dihapus?`,
|
||||
onConfirm: () => handleDelete(param.schedule_id),
|
||||
message: `Jadwal untuk karyawan "${param.nama_employee}" akan dihapus?`,
|
||||
onConfirm: () => handleDelete(param.id),
|
||||
onCancel: () => props.setSelectedData(null),
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
const response = await deleteJadwalShift(id);
|
||||
if (response.statusCode === 200) {
|
||||
NotifAlert({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: 'Data Jadwal Shift berhasil dihapus.',
|
||||
message: 'Jadwal berhasil dihapus.',
|
||||
});
|
||||
doFilter();
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: response.message || 'Gagal menghapus data jadwal shift.',
|
||||
title: 'Gagal',
|
||||
message: response?.message || 'Gagal menghapus jadwal.',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleShiftEditMode = (shiftName) => {
|
||||
setEditingShift(shiftName);
|
||||
setPendingChanges({}); // Clear pending changes
|
||||
setSelectedSchedules([]); // Clear selections when entering a new edit mode
|
||||
};
|
||||
|
||||
const cancelShiftEditMode = () => {
|
||||
setEditingShift(null);
|
||||
setPendingChanges({});
|
||||
setSelectedSchedules([]);
|
||||
};
|
||||
|
||||
const handleSelectSchedule = (id, isChecked) => {
|
||||
if (isChecked) {
|
||||
setSelectedSchedules((prev) => [...prev, id]);
|
||||
} else {
|
||||
setSelectedSchedules((prev) => prev.filter((scheduleId) => scheduleId !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkUpdateChange = (scheduleId, field, value) => {
|
||||
setPendingChanges((prev) => ({
|
||||
...prev,
|
||||
[scheduleId]: {
|
||||
...prev[scheduleId],
|
||||
[field]: value,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBulkSave = async () => {
|
||||
if (Object.keys(pendingChanges).length === 0) {
|
||||
NotifAlert({
|
||||
icon: 'info',
|
||||
title: 'Tidak Ada Perubahan',
|
||||
message: 'Tidak ada perubahan untuk disimpan.',
|
||||
});
|
||||
cancelShiftEditMode();
|
||||
return;
|
||||
}
|
||||
|
||||
const updatePromises = Object.keys(pendingChanges).map((id) => {
|
||||
const originalSchedule = groupedSchedules[editingShift].users.find(
|
||||
(u) => u.id.toString() === id
|
||||
);
|
||||
const changes = pendingChanges[id];
|
||||
|
||||
// Build payload according to backend schema
|
||||
const payload = {
|
||||
user_id: changes.user_id || originalSchedule.user_id,
|
||||
shift_id: changes.shift_id || originalSchedule.shift_id,
|
||||
};
|
||||
|
||||
if (originalSchedule.schedule_id) {
|
||||
payload.schedule_id = originalSchedule.schedule_id;
|
||||
}
|
||||
|
||||
return updateJadwalShift(id, payload);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(updatePromises);
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: 'Semua perubahan berhasil disimpan.',
|
||||
});
|
||||
doFilter();
|
||||
cancelShiftEditMode();
|
||||
} catch (error) {
|
||||
console.error('Error deleting jadwal shift:', error);
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'Terjadi kesalahan saat menghapus data.',
|
||||
title: 'Gagal',
|
||||
message: 'Gagal menyimpan beberapa perubahan.',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkDelete = () => {
|
||||
if (selectedSchedules.length === 0) {
|
||||
NotifAlert({
|
||||
icon: 'warning',
|
||||
title: 'Perhatian',
|
||||
message: 'Pilih setidaknya satu jadwal untuk dihapus.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
NotifConfirmDialog({
|
||||
icon: 'question',
|
||||
title: `Konfirmasi Hapus`,
|
||||
message: `Anda yakin ingin menghapus ${selectedSchedules.length} jadwal yang dipilih?`,
|
||||
onConfirm: async () => {
|
||||
await Promise.all(selectedSchedules.map((id) => deleteJadwalShift(id)));
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: `${selectedSchedules.length} jadwal berhasil dihapus.`,
|
||||
});
|
||||
doFilter();
|
||||
setEditingShift(null);
|
||||
setSelectedSchedules([]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card>
|
||||
<Row>
|
||||
<Title level={3}>Jadwal Shift</Title>
|
||||
<Divider />
|
||||
|
||||
{/* <Row>
|
||||
<Col xs={24}>
|
||||
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
||||
<Row justify="end" align="middle" gutter={[8, 8]}>
|
||||
<Col xs={24} sm={24} md={12} lg={12}>
|
||||
<Input.Search
|
||||
placeholder="Cari jadwal shift..."
|
||||
placeholder="Cari berdasarkan nama..."
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
@@ -211,9 +367,7 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
||||
}
|
||||
}}
|
||||
onSearch={handleSearch}
|
||||
allowClear={{
|
||||
clearIcon: <span onClick={handleSearchClear}>✕</span>,
|
||||
}}
|
||||
allowClear
|
||||
enterButton={
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -222,16 +376,91 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
||||
backgroundColor: '#23A55A',
|
||||
borderColor: '#23A55A',
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
/>
|
||||
}
|
||||
size="large"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row> */}
|
||||
|
||||
<div style={{ marginTop: '24px' }}>
|
||||
{loading ? (
|
||||
<Text>Memuat data...</Text>
|
||||
) : Object.keys(groupedSchedules).length === 0 ? (
|
||||
<Text>Tidak ada data jadwal untuk ditampilkan.</Text>
|
||||
) : (
|
||||
['SHIFT PAGI', 'SHIFT SORE', 'SHIFT MALAM']
|
||||
.filter((shiftName) => groupedSchedules[shiftName])
|
||||
.map((shiftName) => (
|
||||
<div key={shiftName} style={{ marginBottom: '32px' }}>
|
||||
{' '}
|
||||
{/* Container for each shift section */}
|
||||
<Row
|
||||
justify="space-between"
|
||||
align="middle"
|
||||
style={{
|
||||
paddingBottom: '12px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
<Col>
|
||||
<Space wrap size="small">
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
{shiftName} (
|
||||
{groupedSchedules[shiftName].users.length} Karyawan)
|
||||
</Title>
|
||||
</Col>
|
||||
{editingShift === shiftName ? (
|
||||
<Col>
|
||||
<Space wrap>
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={cancelShiftEditMode}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
key="delete"
|
||||
type="primary"
|
||||
danger
|
||||
onClick={handleBulkDelete}
|
||||
disabled={selectedSchedules.length === 0}
|
||||
>
|
||||
Hapus Dipilih ({selectedSchedules.length})
|
||||
</Button>
|
||||
<Button
|
||||
key="save"
|
||||
type="primary"
|
||||
onClick={handleBulkSave}
|
||||
style={{
|
||||
backgroundColor: '#23A55A',
|
||||
borderColor: '#23A55A',
|
||||
}}
|
||||
>
|
||||
Simpan Perubahan
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
) : (
|
||||
<Col>
|
||||
<Space wrap>
|
||||
<Button
|
||||
key="add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => showAddModal()}
|
||||
style={{
|
||||
backgroundColor: '#23A55A',
|
||||
borderColor: '#23A55A',
|
||||
}}
|
||||
disabled={editingShift !== null}
|
||||
>
|
||||
Tambah Jadwal Shift
|
||||
</Button>
|
||||
<ConfigProvider
|
||||
key="edit-config"
|
||||
theme={{
|
||||
components: {
|
||||
Button: {
|
||||
@@ -243,32 +472,348 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => showAddModal()}
|
||||
size="large"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleShiftEditMode(shiftName)}
|
||||
disabled={editingShift !== null}
|
||||
>
|
||||
Tambah Data
|
||||
Edit
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</Space>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Col>
|
||||
<Col xs={24} style={{ marginTop: '16px' }}>
|
||||
<TableList
|
||||
mobile
|
||||
cardColor={'#42AAFF'}
|
||||
header={'schedule_date'}
|
||||
showPreviewModal={showPreviewModal}
|
||||
showEditModal={showEditModal}
|
||||
showDeleteDialog={showDeleteDialog}
|
||||
getData={getData}
|
||||
queryParams={formDataFilter}
|
||||
columns={columns(showPreviewModal, showEditModal, showDeleteDialog)}
|
||||
triger={trigerFilter}
|
||||
{/* Horizontal scrollable container for employee cards */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
overflowX: 'auto',
|
||||
gap: '16px',
|
||||
paddingTop: '8px',
|
||||
paddingBottom: '10px',
|
||||
minWidth: `${4 * (320 + 16)}px`,
|
||||
}}
|
||||
>
|
||||
{groupedSchedules[shiftName].users.length > 0 ? (
|
||||
groupedSchedules[shiftName].users.map((user) => (
|
||||
<Card
|
||||
key={user.id}
|
||||
hoverable
|
||||
style={{
|
||||
width: 320,
|
||||
height: 240,
|
||||
flexShrink: 0,
|
||||
textAlign: 'left',
|
||||
border: '1px solid #42AAFF',
|
||||
opacity:
|
||||
editingShift !== null &&
|
||||
editingShift !== shiftName
|
||||
? 0.5
|
||||
: 1,
|
||||
pointerEvents:
|
||||
editingShift !== null &&
|
||||
editingShift !== shiftName
|
||||
? 'none'
|
||||
: 'auto',
|
||||
}}
|
||||
styles={{
|
||||
body: { padding: '16px', height: '100%' },
|
||||
}}
|
||||
>
|
||||
{editingShift === shiftName ? (
|
||||
// EDIT MODE VIEW
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
gap: '12px',
|
||||
padding: '16px 4px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
paddingTop: '8px',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedSchedules.includes(
|
||||
user.id
|
||||
)}
|
||||
onChange={(e) =>
|
||||
handleSelectSchedule(
|
||||
user.id,
|
||||
e.target.checked
|
||||
)
|
||||
}
|
||||
style={{
|
||||
transform: 'scale(1.4)',
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
gap: '14px',
|
||||
paddingRight: '4px',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Text
|
||||
strong
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: '#8c8c8c',
|
||||
display: 'block',
|
||||
marginBottom: '6px',
|
||||
}}
|
||||
>
|
||||
KARYAWAN
|
||||
</Text>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Pilih Karyawan"
|
||||
optionFilterProp="children"
|
||||
defaultValue={user.user_id}
|
||||
onChange={(value) =>
|
||||
handleBulkUpdateChange(
|
||||
user.id,
|
||||
'user_id',
|
||||
value
|
||||
)
|
||||
}
|
||||
size="large"
|
||||
>
|
||||
{employeeOptions.map(
|
||||
(emp) => (
|
||||
<Select.Option
|
||||
key={
|
||||
emp.user_id ||
|
||||
emp.id
|
||||
}
|
||||
value={
|
||||
emp.user_id ||
|
||||
emp.id
|
||||
}
|
||||
>
|
||||
{emp.user_fullname ||
|
||||
emp.user_name ||
|
||||
emp.nama_employee}
|
||||
</Select.Option>
|
||||
)
|
||||
)}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Text
|
||||
strong
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: '#8c8c8c',
|
||||
display: 'block',
|
||||
marginBottom: '6px',
|
||||
}}
|
||||
>
|
||||
NO. TELEPON
|
||||
</Text>
|
||||
<Input
|
||||
value={
|
||||
pendingChanges[user.id]
|
||||
?.user_id
|
||||
? employeeOptions.find(
|
||||
(emp) =>
|
||||
emp.user_id ===
|
||||
pendingChanges[
|
||||
user
|
||||
.id
|
||||
]?.user_id
|
||||
)?.user_phone ||
|
||||
user.whatsapp
|
||||
: user.whatsapp
|
||||
}
|
||||
readOnly
|
||||
style={{
|
||||
backgroundColor:
|
||||
'#f5f5f5',
|
||||
color: '#595959',
|
||||
cursor: 'not-allowed',
|
||||
}}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text
|
||||
strong
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: '#8c8c8c',
|
||||
display: 'block',
|
||||
marginBottom: '6px',
|
||||
}}
|
||||
>
|
||||
SHIFT
|
||||
</Text>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Pilih Shift"
|
||||
optionFilterProp="children"
|
||||
defaultValue={user.shift_id}
|
||||
onChange={(value) =>
|
||||
handleBulkUpdateChange(
|
||||
user.id,
|
||||
'shift_id',
|
||||
value
|
||||
)
|
||||
}
|
||||
size="large"
|
||||
>
|
||||
{shiftOptions.map(
|
||||
(shift) => (
|
||||
<Select.Option
|
||||
key={
|
||||
shift.shift_id
|
||||
}
|
||||
value={
|
||||
shift.shift_id
|
||||
}
|
||||
>
|
||||
{
|
||||
shift.shift_name
|
||||
}
|
||||
</Select.Option>
|
||||
)
|
||||
)}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// NORMAL VIEW
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#42AAFF',
|
||||
color: '#FFFFFF',
|
||||
padding: '9px 12px',
|
||||
borderRadius: '15px',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
strong
|
||||
style={{
|
||||
fontSize: '18px',
|
||||
color: '#FFFFFF',
|
||||
display: 'block',
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
>
|
||||
{user.nama_employee}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
color: '#FFFFFF',
|
||||
}}
|
||||
>
|
||||
{user.whatsapp}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
display: 'block',
|
||||
lineHeight: '1.4',
|
||||
}}
|
||||
>
|
||||
<Text strong>
|
||||
Terakhir diperbarui
|
||||
</Text>{' '}
|
||||
<br />
|
||||
{formatRelativeTimestamp(
|
||||
user.updated_at ||
|
||||
user.created_at ||
|
||||
new Date()
|
||||
)}{' '}
|
||||
<br />
|
||||
oleh {user.updated_by || 'N/A'}
|
||||
</Text>
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() =>
|
||||
showPreviewModal(user)
|
||||
}
|
||||
style={{
|
||||
color: '#1890ff',
|
||||
borderColor: '#1890ff',
|
||||
}}
|
||||
title="View"
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() =>
|
||||
showEditModal(user)
|
||||
}
|
||||
style={{
|
||||
color: '#faad14',
|
||||
borderColor: '#faad14',
|
||||
}}
|
||||
title="Edit"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() =>
|
||||
showDeleteDialog(user)
|
||||
}
|
||||
style={{
|
||||
borderColor: '#ff4d4f',
|
||||
}}
|
||||
title="Delete"
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<Text type="secondary" style={{ marginLeft: '16px' }}>
|
||||
Tidak ada karyawan yang dijadwalkan untuk shift ini.
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
1014
src/pages/master/brandDevice/EditBrandDevice.jsx
Normal file
766
src/pages/master/brandDevice/ViewBrandDevice.jsx
Normal file
@@ -0,0 +1,766 @@
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
Divider,
|
||||
Typography,
|
||||
Button,
|
||||
Steps,
|
||||
Form,
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
Spin,
|
||||
Tag,
|
||||
Space,
|
||||
ConfigProvider,
|
||||
Empty
|
||||
} from 'antd';
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||
import { NotifAlert } from '../../../components/Global/ToastNotif';
|
||||
import { getBrandById, getErrorCodesByBrandId } from '../../../api/master-brand';
|
||||
import { getFileUrl, getFolderFromFileType } from '../../../api/file-uploads';
|
||||
import { SendRequest } from '../../../components/Global/ApiRequest';
|
||||
import ListErrorCode from './component/ListErrorCode';
|
||||
import BrandForm from './component/BrandForm';
|
||||
import ErrorCodeForm from './component/ErrorCodeForm';
|
||||
import SolutionForm from './component/SolutionForm';
|
||||
import SparepartSelect from './component/SparepartSelect';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { Step } = Steps;
|
||||
|
||||
const ViewBrandDevice = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const location = useLocation();
|
||||
const { setBreadcrumbItems } = useBreadcrumb();
|
||||
const [brandForm] = Form.useForm();
|
||||
const [errorCodeForm] = Form.useForm();
|
||||
const [solutionForm] = Form.useForm();
|
||||
|
||||
const [brandData, setBrandData] = useState(null);
|
||||
const [errorCodes, setErrorCodes] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [selectedErrorCode, setSelectedErrorCode] = useState(null);
|
||||
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
|
||||
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
|
||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
|
||||
const [solutionFields, setSolutionFields] = useState([0]);
|
||||
const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' });
|
||||
const [solutionStatuses, setSolutionStatuses] = useState({ 0: true });
|
||||
const [currentSolutionData, setCurrentSolutionData] = useState([]);
|
||||
|
||||
|
||||
const [brandInfo, setBrandInfo] = useState({});
|
||||
|
||||
const resetSolutionFields = () => {
|
||||
if (solutionForm && solutionForm.resetFields) {
|
||||
solutionForm.resetFields();
|
||||
solutionForm.setFieldsValue({
|
||||
solution_items: {
|
||||
0: {
|
||||
name: '',
|
||||
type: 'text',
|
||||
text: '',
|
||||
status: true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setCurrentSolutionData([]);
|
||||
};
|
||||
|
||||
const getSolutionData = () => {
|
||||
if (!solutionForm) return [];
|
||||
try {
|
||||
const values = solutionForm.getFieldsValue(true);
|
||||
|
||||
let solutions = [];
|
||||
|
||||
if (values.solution_items) {
|
||||
if (Array.isArray(values.solution_items)) {
|
||||
solutions = values.solution_items.filter(Boolean);
|
||||
} else if (typeof values.solution_items === 'object') {
|
||||
solutions = Object.values(values.solution_items).filter(Boolean);
|
||||
}
|
||||
}
|
||||
|
||||
return solutions;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const savedPhase = location.state?.phase || localStorage.getItem(`brand_device_${id}_last_phase`);
|
||||
if (savedPhase) {
|
||||
setCurrentStep(parseInt(savedPhase));
|
||||
localStorage.removeItem(`brand_device_${id}_last_phase`);
|
||||
}
|
||||
}, [location.state, id]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumbItems([
|
||||
{
|
||||
title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}>• Master</span>
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<span
|
||||
style={{ fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}
|
||||
onClick={() => navigate('/master/brand-device')}
|
||||
>
|
||||
Brand Device
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<span style={{ fontSize: '14px', fontWeight: 'bold' }}>
|
||||
View Brand Device
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]);
|
||||
}, [setBreadcrumbItems, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBrandData = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
navigate('/signin');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getBrandById(id);
|
||||
|
||||
if (response && response.statusCode === 200) {
|
||||
const brandData = response.data;
|
||||
|
||||
const brandInfoData = {
|
||||
brand_code: brandData.brand_code,
|
||||
brand_name: brandData.brand_name,
|
||||
brand_type: brandData.brand_type || '',
|
||||
brand_manufacture: brandData.brand_manufacture || '',
|
||||
brand_model: brandData.brand_model || '',
|
||||
is_active: brandData.is_active
|
||||
};
|
||||
|
||||
setBrandInfo(brandInfoData);
|
||||
setBrandData(brandData);
|
||||
brandForm.setFieldsValue(brandInfoData);
|
||||
|
||||
if (brandData.brand_id) {
|
||||
try {
|
||||
const errorCodesResponse = await getErrorCodesByBrandId(id || brandData.brand_id);
|
||||
if (errorCodesResponse && errorCodesResponse.statusCode === 200) {
|
||||
const apiErrorData = errorCodesResponse.data || [];
|
||||
const existingCodes = apiErrorData.map(ec => ({
|
||||
...ec,
|
||||
tempId: `existing_${ec.error_code_id}`,
|
||||
status: 'existing',
|
||||
solution: ec.solution || [],
|
||||
spareparts: ec.spareparts || []
|
||||
}));
|
||||
setErrorCodes(existingCodes);
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: response?.message || 'Failed to fetch brand device data',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: error.message || 'Failed to fetch brand device data',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBrandData();
|
||||
}, [id, navigate, brandForm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentStep === 1 && id) {
|
||||
setTrigerFilter(prev => !prev);
|
||||
}
|
||||
}, [currentStep, id]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (currentStep === 1 && errorCodes.length > 0 && !selectedErrorCode) {
|
||||
handleErrorCodeSelect(errorCodes[0]);
|
||||
}
|
||||
}, [currentStep, errorCodes]);
|
||||
|
||||
const setSolutionsForExistingRecord = (solutions, targetForm) => {
|
||||
|
||||
if (!targetForm || !solutions || solutions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
targetForm.resetFields();
|
||||
|
||||
const solutionItems = {};
|
||||
const newSolutionFields = [];
|
||||
const newSolutionTypes = {};
|
||||
const newSolutionStatuses = {};
|
||||
|
||||
solutions.forEach((solution, index) => {
|
||||
const fieldKey = index;
|
||||
newSolutionFields.push(fieldKey);
|
||||
|
||||
const isFileType = solution.type_solution && solution.type_solution !== 'text';
|
||||
newSolutionTypes[fieldKey] = isFileType ? 'file' : 'text';
|
||||
newSolutionStatuses[fieldKey] = solution.is_active;
|
||||
|
||||
let fileObject = null;
|
||||
if (isFileType && (solution.path_solution || solution.path_document)) {
|
||||
fileObject = {
|
||||
uploadPath: solution.path_solution || solution.path_document,
|
||||
path_solution: solution.path_solution || solution.path_document,
|
||||
name: solution.file_upload_name || (solution.path_solution || solution.path_document).split('/').pop() || 'File',
|
||||
type_solution: solution.type_solution,
|
||||
isExisting: true,
|
||||
size: 0,
|
||||
url: solution.path_solution || solution.path_document
|
||||
};
|
||||
}
|
||||
|
||||
solutionItems[fieldKey] = {
|
||||
brand_code_solution_id: solution.brand_code_solution_id,
|
||||
name: solution.solution_name || '',
|
||||
type: isFileType ? 'file' : 'text',
|
||||
text: solution.text_solution || '',
|
||||
status: solution.is_active,
|
||||
file: fileObject,
|
||||
fileUpload: fileObject,
|
||||
path_solution: solution.path_solution || solution.path_document || null,
|
||||
fileName: solution.file_upload_name || null
|
||||
};
|
||||
});
|
||||
|
||||
setSolutionFields(newSolutionFields);
|
||||
setSolutionTypes(newSolutionTypes);
|
||||
setSolutionStatuses(newSolutionStatuses);
|
||||
|
||||
targetForm.resetFields();
|
||||
|
||||
setTimeout(() => {
|
||||
targetForm.setFieldsValue({
|
||||
solution_items: solutionItems
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Object.keys(solutionItems).forEach(key => {
|
||||
const solution = solutionItems[key];
|
||||
targetForm.setFieldValue(['solution_items', key, 'name'], solution.name);
|
||||
targetForm.setFieldValue(['solution_items', key, 'type'], solution.type);
|
||||
targetForm.setFieldValue(['solution_items', key, 'text'], solution.text);
|
||||
targetForm.setFieldValue(['solution_items', key, 'file'], solution.file);
|
||||
targetForm.setFieldValue(['solution_items', key, 'fileUpload'], solution.fileUpload);
|
||||
targetForm.setFieldValue(['solution_items', key, 'status'], solution.status);
|
||||
targetForm.setFieldValue(['solution_items', key, 'path_solution'], solution.path_solution);
|
||||
targetForm.setFieldValue(['solution_items', key, 'fileName'], solution.fileName);
|
||||
});
|
||||
|
||||
const finalValues = targetForm.getFieldsValue();
|
||||
}, 100);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleErrorCodeSelect = async (errorCode) => {
|
||||
|
||||
setSelectedErrorCode(errorCode);
|
||||
|
||||
try {
|
||||
|
||||
const directResponse = await SendRequest({
|
||||
method: 'get',
|
||||
prefix: `error-code/${errorCode.error_code_id}`,
|
||||
});
|
||||
|
||||
const apiResponse = directResponse.data;
|
||||
|
||||
if (apiResponse && apiResponse.statusCode === 200 && apiResponse.data) {
|
||||
const fullErrorCodeData = {
|
||||
...apiResponse.data,
|
||||
tempId: `existing_${apiResponse.data.error_code_id}`
|
||||
};
|
||||
|
||||
const formValues = {
|
||||
error_code: fullErrorCodeData.error_code,
|
||||
error_code_name: fullErrorCodeData.error_code_name,
|
||||
error_code_description: fullErrorCodeData.error_code_description || '',
|
||||
error_code_color: fullErrorCodeData.error_code_color && fullErrorCodeData.error_code_color !== '' ? fullErrorCodeData.error_code_color : '#000000',
|
||||
status: fullErrorCodeData.is_active,
|
||||
};
|
||||
|
||||
errorCodeForm.setFieldsValue(formValues);
|
||||
|
||||
if (fullErrorCodeData.path_icon && fullErrorCodeData.path_icon !== '') {
|
||||
const iconData = {
|
||||
name: fullErrorCodeData.path_icon.split('/').pop(),
|
||||
uploadPath: fullErrorCodeData.path_icon,
|
||||
};
|
||||
setErrorCodeIcon(iconData);
|
||||
} else {
|
||||
setErrorCodeIcon(null);
|
||||
}
|
||||
|
||||
if (apiResponse.data.solution && apiResponse.data.solution.length > 0) {
|
||||
setCurrentSolutionData(apiResponse.data.solution);
|
||||
setSolutionsForExistingRecord(apiResponse.data.solution, solutionForm);
|
||||
}
|
||||
|
||||
if (apiResponse.data.spareparts && apiResponse.data.spareparts.length > 0) {
|
||||
setSelectedSparepartIds(apiResponse.data.spareparts.map(sp => sp.sparepart_id));
|
||||
} else {
|
||||
setSelectedSparepartIds([]);
|
||||
}
|
||||
} else {
|
||||
const basicErrorCodeData = {
|
||||
...errorCode,
|
||||
tempId: `existing_${errorCode.error_code_id}`
|
||||
};
|
||||
|
||||
const formValues = {
|
||||
error_code: basicErrorCodeData.error_code,
|
||||
error_code_name: basicErrorCodeData.error_code_name,
|
||||
error_code_description: basicErrorCodeData.error_code_description || '',
|
||||
error_code_color: basicErrorCodeData.error_code_color && basicErrorCodeData.error_code_color !== '' ? basicErrorCodeData.error_code_color : '#000000',
|
||||
status: basicErrorCodeData.is_active,
|
||||
};
|
||||
|
||||
errorCodeForm.setFieldsValue(formValues);
|
||||
|
||||
if (basicErrorCodeData.path_icon && basicErrorCodeData.path_icon !== '') {
|
||||
const iconData = {
|
||||
name: basicErrorCodeData.path_icon.split('/').pop(),
|
||||
uploadPath: basicErrorCodeData.path_icon,
|
||||
};
|
||||
setErrorCodeIcon(iconData);
|
||||
} else {
|
||||
setErrorCodeIcon(null);
|
||||
}
|
||||
|
||||
resetSolutionFields();
|
||||
setSelectedSparepartIds([]);
|
||||
}
|
||||
} catch (error) {
|
||||
const basicErrorCodeData = {
|
||||
...errorCode,
|
||||
tempId: `existing_${errorCode.error_code_id}`
|
||||
};
|
||||
|
||||
const formValues = {
|
||||
error_code: basicErrorCodeData.error_code,
|
||||
error_code_name: basicErrorCodeData.error_code_name,
|
||||
error_code_description: basicErrorCodeData.error_code_description || '',
|
||||
error_code_color: basicErrorCodeData.error_code_color && basicErrorCodeData.error_code_color !== '' ? basicErrorCodeData.error_code_color : '#000000',
|
||||
status: basicErrorCodeData.is_active,
|
||||
};
|
||||
|
||||
errorCodeForm.setFieldsValue(formValues);
|
||||
|
||||
if (basicErrorCodeData.path_icon && basicErrorCodeData.path_icon !== '') {
|
||||
const iconData = {
|
||||
name: basicErrorCodeData.path_icon.split('/').pop(),
|
||||
uploadPath: basicErrorCodeData.path_icon,
|
||||
};
|
||||
setErrorCodeIcon(iconData);
|
||||
} else {
|
||||
setErrorCodeIcon(null);
|
||||
}
|
||||
|
||||
resetSolutionFields();
|
||||
setSelectedSparepartIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBrandFormValuesChange = useCallback((changedValues, allValues) => {
|
||||
setBrandInfo(allValues);
|
||||
}, [setBrandInfo]);
|
||||
|
||||
const handleSearch = () => {
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleSearchClear = () => {
|
||||
setSearchText('');
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleFileView = (fileName) => {
|
||||
try {
|
||||
let fileUrl = '';
|
||||
let actualFileName = '';
|
||||
|
||||
const filePath = fileName || '';
|
||||
if (filePath) {
|
||||
actualFileName = filePath.split('/').pop();
|
||||
}
|
||||
|
||||
if (actualFileName) {
|
||||
const fileExtension = actualFileName.split('.').pop()?.toLowerCase();
|
||||
const folder = getFolderFromFileType(fileExtension);
|
||||
fileUrl = getFileUrl(folder, actualFileName);
|
||||
}
|
||||
|
||||
if (!fileUrl && filePath) {
|
||||
fileUrl = filePath.startsWith('http') ? filePath : `${import.meta.env.VITE_API_SERVER}/${filePath}`;
|
||||
}
|
||||
|
||||
if (fileUrl && actualFileName) {
|
||||
const fileExtension = actualFileName.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const pdfExtensions = ['pdf'];
|
||||
|
||||
if (imageExtensions.includes(fileExtension) || pdfExtensions.includes(fileExtension)) {
|
||||
const viewerUrl = `/image-viewer/${encodeURIComponent(actualFileName)}`;
|
||||
window.open(viewerUrl, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
window.open(fileUrl, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'File URL not found'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'Failed to open file preview'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextStep = () => {
|
||||
setCurrentStep(1);
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
if (currentStep === 0) {
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
{loading && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 10,
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
)}
|
||||
<BrandForm
|
||||
form={brandForm}
|
||||
onValuesChange={handleBrandFormValuesChange}
|
||||
isEdit={false}
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentStep === 1) {
|
||||
return (
|
||||
<Row gutter={[16, 8]} style={{ minHeight: '70vh' }}>
|
||||
<Col xs={24} md={8} lg={8}>
|
||||
<ListErrorCode
|
||||
brandId={id}
|
||||
selectedErrorCode={selectedErrorCode}
|
||||
onErrorCodeSelect={handleErrorCodeSelect}
|
||||
tempErrorCodes={[]}
|
||||
trigerFilter={trigerFilter}
|
||||
searchText={searchText}
|
||||
onSearchChange={(value) => {
|
||||
setSearchText(value);
|
||||
if (value === '') {
|
||||
setTrigerFilter((prev) => !prev);
|
||||
}
|
||||
}}
|
||||
onSearch={handleSearch}
|
||||
onSearchClear={handleSearchClear}
|
||||
isReadOnly={true}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={16} lg={16}>
|
||||
<div style={{
|
||||
paddingLeft: '12px'
|
||||
}}>
|
||||
{selectedErrorCode ? (
|
||||
<Card
|
||||
title={
|
||||
<span style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
color: '#262626',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<span style={{
|
||||
width: '4px',
|
||||
height: '20px',
|
||||
backgroundColor: '#23A55A',
|
||||
borderRadius: '2px'
|
||||
}}></span>
|
||||
Error Code Form
|
||||
</span>
|
||||
}
|
||||
style={{
|
||||
width: '100%',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
|
||||
borderRadius: '12px'
|
||||
}}
|
||||
styles={{
|
||||
body: { padding: '16px 24px 12px 24px' },
|
||||
header: {
|
||||
padding: '16px 24px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
backgroundColor: '#fafafa'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: '10px',
|
||||
backgroundColor: '#ffffff',
|
||||
marginBottom: '0',
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.04)'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '12px',
|
||||
paddingBottom: '8px',
|
||||
borderBottom: '1px solid #f5f5f5'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '3px',
|
||||
height: '16px',
|
||||
backgroundColor: '#23A55A',
|
||||
borderRadius: '2px'
|
||||
}}></div>
|
||||
<h4 style={{ margin: 0, color: '#262626', fontSize: '14px', fontWeight: '600' }}>
|
||||
Error Code Details
|
||||
</h4>
|
||||
</div>
|
||||
<ErrorCodeForm
|
||||
errorCodeForm={errorCodeForm}
|
||||
isErrorCodeFormReadOnly={true}
|
||||
errorCodeIcon={errorCodeIcon}
|
||||
onErrorCodeIconUpload={() => { }}
|
||||
onErrorCodeIconRemove={() => { }}
|
||||
isEdit={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Row gutter={[20, 0]} style={{ marginTop: '0' }}>
|
||||
<Col xs={24} md={12} lg={12}>
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: '10px',
|
||||
backgroundColor: '#ffffff',
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.04)'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '12px',
|
||||
paddingBottom: '8px',
|
||||
borderBottom: '1px solid #f5f5f5'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '3px',
|
||||
height: '16px',
|
||||
backgroundColor: '#1890ff',
|
||||
borderRadius: '2px'
|
||||
}}></div>
|
||||
<h4 style={{ margin: 0, color: '#262626', fontSize: '14px', fontWeight: '600' }}>
|
||||
Solution
|
||||
</h4>
|
||||
</div>
|
||||
<SolutionForm
|
||||
solutionForm={solutionForm}
|
||||
solutionFields={solutionFields}
|
||||
solutionTypes={solutionTypes}
|
||||
solutionStatuses={solutionStatuses}
|
||||
onAddSolutionField={() => { }}
|
||||
onRemoveSolutionField={() => { }}
|
||||
onSolutionTypeChange={() => { }}
|
||||
onSolutionStatusChange={() => { }}
|
||||
onSolutionFileUpload={() => { }}
|
||||
onFileView={(fileData) => {
|
||||
if (fileData && (fileData.url || fileData.uploadPath)) {
|
||||
window.open(fileData.url || fileData.uploadPath, '_blank');
|
||||
}
|
||||
}}
|
||||
isReadOnly={true}
|
||||
solutionData={currentSolutionData}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} md={12} lg={12}>
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: '10px',
|
||||
backgroundColor: '#ffffff',
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.04)'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '12px',
|
||||
paddingBottom: '8px',
|
||||
borderBottom: '1px solid #f5f5f5'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '3px',
|
||||
height: '16px',
|
||||
backgroundColor: '#faad14',
|
||||
borderRadius: '2px'
|
||||
}}></div>
|
||||
<h4 style={{ margin: 0, color: '#262626', fontSize: '14px', fontWeight: '600' }}>
|
||||
Sparepart Selection
|
||||
</h4>
|
||||
</div>
|
||||
<div style={{
|
||||
maxHeight: '45vh',
|
||||
overflow: 'auto',
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#fafafa'
|
||||
}}>
|
||||
<SparepartSelect
|
||||
selectedSparepartIds={selectedSparepartIds}
|
||||
onSparepartChange={() => { }}
|
||||
isReadOnly={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<div style={{
|
||||
height: '100%', display: 'flex', flexDirection: 'column',
|
||||
justifyContent: 'center', alignItems: 'center',
|
||||
backgroundColor: '#ffffff', borderRadius: '12px',
|
||||
border: '1px dashed #d9d9d9', color: '#8c8c8c', padding: '48px'
|
||||
}}>
|
||||
<Empty description="Select an error code to view details" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Switch: {
|
||||
colorPrimary: '#23A55A',
|
||||
colorPrimaryHover: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Card>
|
||||
<Steps current={currentStep} style={{ marginBottom: 24 }}>
|
||||
<Step title="Brand Device Details" />
|
||||
<Step title="Error Codes & Solutions" />
|
||||
</Steps>
|
||||
{renderStepContent()}
|
||||
<Divider />
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
{currentStep === 1 && (
|
||||
<Button
|
||||
onClick={() => setCurrentStep(0)}
|
||||
>
|
||||
Back to Brand Info
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{currentStep === 0 && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleNextStep}
|
||||
style={{
|
||||
backgroundColor: '#23A55A',
|
||||
borderColor: '#23A55A',
|
||||
}}
|
||||
>
|
||||
Error Code
|
||||
</Button>
|
||||
)}
|
||||
{currentStep === 1 && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => navigate('/master/brand-device')}
|
||||
style={{
|
||||
backgroundColor: '#23A55A',
|
||||
borderColor: '#23A55A',
|
||||
}}
|
||||
>
|
||||
Selesai
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewBrandDevice;
|
||||
461
src/pages/master/brandDevice/ViewFilePage.jsx
Normal file
@@ -0,0 +1,461 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Card, Button, Typography, Spin, Alert, Space } from 'antd';
|
||||
import { NotifAlert } from '../../../components/Global/ToastNotif';
|
||||
import { ArrowLeftOutlined, FilePdfOutlined, FileImageOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||
import { getBrandById } from '../../../api/master-brand';
|
||||
import {
|
||||
downloadFile,
|
||||
getFile,
|
||||
getFileUrl,
|
||||
getFolderFromFileType,
|
||||
} from '../../../api/file-uploads';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const ViewFilePage = () => {
|
||||
const params = useParams();
|
||||
const { id, fileType, fileName } = params;
|
||||
const navigate = useNavigate();
|
||||
const { setBreadcrumbItems } = useBreadcrumb();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [brandData, setBrandData] = useState(null);
|
||||
const [actualFileName, setActualFileName] = useState('');
|
||||
const [pdfBlobUrl, setPdfBlobUrl] = useState(null);
|
||||
const [pdfLoading, setPdfLoading] = useState(false);
|
||||
|
||||
const isFromEdit = window.location.pathname.includes('/edit/');
|
||||
|
||||
let fallbackId = id;
|
||||
let fallbackFileType = fileType;
|
||||
let fallbackFileName = fileName;
|
||||
|
||||
if (!fileName || !fileType || !id) {
|
||||
|
||||
const urlParts = window.location.pathname.split('/');
|
||||
|
||||
const viewIndex = urlParts.indexOf('view');
|
||||
const editIndex = urlParts.indexOf('edit');
|
||||
const actionIndex = viewIndex !== -1 ? viewIndex : editIndex;
|
||||
|
||||
if (actionIndex !== -1 && urlParts.length > actionIndex + 4) {
|
||||
fallbackId = urlParts[actionIndex + 1];
|
||||
fallbackFileType = urlParts[actionIndex + 3];
|
||||
fallbackFileName = decodeURIComponent(urlParts[actionIndex + 4]);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setPdfBlobUrl(null);
|
||||
setPdfLoading(false);
|
||||
setError(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
navigate('/signin');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const actualId = fallbackId || id;
|
||||
const actualFileName = fallbackFileName || fileName;
|
||||
|
||||
const brandResponse = await getBrandById(actualId);
|
||||
if (brandResponse && brandResponse.statusCode === 200) {
|
||||
setBrandData(brandResponse.data);
|
||||
}
|
||||
|
||||
const decodedFileName = decodeURIComponent(actualFileName);
|
||||
setActualFileName(decodedFileName);
|
||||
|
||||
const fileExtension = decodedFileName.split('.').pop().toLowerCase();
|
||||
if (fileExtension === 'pdf') {
|
||||
setPdfLoading(true);
|
||||
const folder = getFolderFromFileType('pdf');
|
||||
try {
|
||||
const blobData = await getFile(folder, decodedFileName);
|
||||
const blobUrl = window.URL.createObjectURL(blobData);
|
||||
setPdfBlobUrl(blobUrl);
|
||||
} catch (pdfError) {
|
||||
setError('Failed to load PDF file: ' + (pdfError.message || pdfError));
|
||||
setPdfBlobUrl(null);
|
||||
} finally {
|
||||
setPdfLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setError('Failed to load data');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
if (pdfBlobUrl) {
|
||||
window.URL.revokeObjectURL(pdfBlobUrl);
|
||||
}
|
||||
};
|
||||
}, [id, fileName, fileType, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (brandData) {
|
||||
const breadcrumbItems = [
|
||||
{ title: <strong style={{ fontSize: '14px' }}>• Master</strong> },
|
||||
{
|
||||
title: <strong style={{ fontSize: '14px' }} onClick={() => navigate('/master/brand-device')}>Brand Device</strong>
|
||||
}
|
||||
];
|
||||
|
||||
if (isFromEdit) {
|
||||
breadcrumbItems.push({
|
||||
title: <strong style={{ fontSize: '14px' }} onClick={() => navigate(`/master/brand-device/edit/${fallbackId || id}`)}>Edit Brand Device</strong>
|
||||
});
|
||||
} else {
|
||||
breadcrumbItems.push({
|
||||
title: <strong style={{ fontSize: '14px' }} onClick={() => navigate(`/master/brand-device/view/${fallbackId || id}`)}>View Brand Device</strong>
|
||||
});
|
||||
}
|
||||
|
||||
breadcrumbItems.push({ title: <strong style={{ fontSize: '14px' }}>View Document</strong> });
|
||||
|
||||
setBreadcrumbItems(breadcrumbItems);
|
||||
}
|
||||
}, [brandData, id, isFromEdit, fallbackId, navigate, setBreadcrumbItems]);
|
||||
|
||||
const handleBack = () => {
|
||||
if (isFromEdit) {
|
||||
const savedPhase = localStorage.getItem(`brand_device_edit_${fallbackId || id}_last_phase`);
|
||||
|
||||
if (savedPhase) {
|
||||
localStorage.removeItem(`brand_device_edit_${fallbackId || id}_last_phase`);
|
||||
}
|
||||
|
||||
const targetPhase = savedPhase ? parseInt(savedPhase) : 1;
|
||||
|
||||
navigate(`/master/brand-device/edit/${fallbackId || id}`, {
|
||||
state: { phase: targetPhase, fromFileViewer: true },
|
||||
replace: true
|
||||
});
|
||||
} else {
|
||||
navigate(`/master/brand-device/view/${fallbackId || id}`, {
|
||||
state: { phase: 1 },
|
||||
replace: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<Alert
|
||||
message="Error Loading File"
|
||||
description={error}
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ margin: '20px 0' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const displayFileName = actualFileName || 'Loading...';
|
||||
const fileExtension = displayFileName.split('.').pop().toLowerCase();
|
||||
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
|
||||
const isPdf = fileExtension === 'pdf';
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
{isImage ? (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '300px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: '#999'
|
||||
}}>
|
||||
<div>
|
||||
<FileImageOutlined style={{ fontSize: '48px', marginBottom: '16px' }} />
|
||||
<div>Loading image...</div>
|
||||
</div>
|
||||
</div>
|
||||
) : isPdf ? (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '400px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: '#999'
|
||||
}}>
|
||||
<div>
|
||||
<FilePdfOutlined style={{ fontSize: '48px', marginBottom: '16px' }} />
|
||||
<div>Loading PDF...</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '200px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: '#999'
|
||||
}}>
|
||||
<div>
|
||||
<FilePdfOutlined style={{ fontSize: '48px', marginBottom: '16px' }} />
|
||||
<div>Loading file...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isImage) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
<img
|
||||
src={getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName)}
|
||||
alt={actualFileName}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '70vh',
|
||||
objectFit: 'contain',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
onError={() => setError('Failed to load image')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPdf) {
|
||||
const displayUrl = pdfBlobUrl || getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName);
|
||||
|
||||
return (
|
||||
<div style={{ height: '75vh', width: '100%', border: '1px solid #d9d9d9', borderRadius: '8px', overflow: 'hidden' }}>
|
||||
{pdfBlobUrl ? (
|
||||
<iframe
|
||||
src={pdfBlobUrl}
|
||||
title={actualFileName}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
onError={() => {
|
||||
setError('Failed to load PDF. Please try downloading the file.');
|
||||
}}
|
||||
/>
|
||||
) : pdfLoading ? (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
backgroundColor: '#f5f5f5'
|
||||
}}>
|
||||
<Spin size="large" />
|
||||
<div style={{ fontSize: '16px', color: '#666', textAlign: 'center' }}>
|
||||
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>Memuat PDF...</div>
|
||||
<div>Silakan tunggu sebentar</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
backgroundColor: '#f5f5f5'
|
||||
}}>
|
||||
<FilePdfOutlined style={{ fontSize: '48px', color: '#ff4d4f' }} />
|
||||
<div style={{ fontSize: '16px', color: '#666', textAlign: 'center' }}>
|
||||
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>PDF tidak dapat dimuat</div>
|
||||
<div>Silakan download file untuk melihat kontennya</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
const folder = getFolderFromFileType(fallbackFileType || fileType);
|
||||
downloadFile(folder, actualFileName);
|
||||
}}
|
||||
icon={<DownloadOutlined />}
|
||||
>
|
||||
Download PDF
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setPdfLoading(true);
|
||||
const folder = getFolderFromFileType('pdf');
|
||||
getFile(folder, actualFileName)
|
||||
.then(blobData => {
|
||||
const blobUrl = window.URL.createObjectURL(blobData);
|
||||
setPdfBlobUrl(blobUrl);
|
||||
})
|
||||
.catch(error => {
|
||||
setError('Failed to load PDF file: ' + (error.message || error));
|
||||
setPdfBlobUrl(null);
|
||||
})
|
||||
.finally(() => {
|
||||
setPdfLoading(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Coba Lagi
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<FilePdfOutlined style={{ fontSize: '48px', color: '#ff4d4f', marginBottom: '16px' }} />
|
||||
<div style={{ fontSize: '16px', marginBottom: '8px' }}>Preview tidak tersedia untuk jenis file ini</div>
|
||||
<div style={{ color: '#666', marginBottom: '16px' }}>{actualFileName}</div>
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<Button type="primary" href={getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName)} target="_blank" rel="noopener noreferrer">
|
||||
Buka di Tab Baru
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getFileIcon = () => {
|
||||
const displayFileName = actualFileName || 'Loading...';
|
||||
const fileExtension = displayFileName.split('.').pop().toLowerCase();
|
||||
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
|
||||
const isPdf = fileExtension === 'pdf';
|
||||
|
||||
if (isImage) return <FileImageOutlined style={{ color: '#1890ff', fontSize: '20px' }} />;
|
||||
if (isPdf) return <FilePdfOutlined style={{ color: '#ff4d4f', fontSize: '20px' }} />;
|
||||
return <FilePdfOutlined style={{ color: '#ff4d4f', fontSize: '20px' }} />;
|
||||
};
|
||||
|
||||
const getFileTypeColor = () => {
|
||||
const displayFileName = actualFileName || 'Loading...';
|
||||
const fileExtension = displayFileName.split('.').pop().toLowerCase();
|
||||
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
|
||||
const isPdf = fileExtension === 'pdf';
|
||||
|
||||
if (isImage) return '#1890ff';
|
||||
if (isPdf) return '#ff4d4f';
|
||||
return '#ff4d4f';
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', minHeight: '100vh', backgroundColor: '#f5f5f5' }}>
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
{getFileIcon()}
|
||||
<div>
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
{actualFileName || 'Loading...'}
|
||||
</Title>
|
||||
{brandData ? (
|
||||
<div style={{ color: '#666', fontSize: '14px' }}>
|
||||
Brand: {brandData.brand_name} | ID: {brandData.brand_id}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ color: '#666', fontSize: '14px' }}>
|
||||
Loading brand information...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={handleBack}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
const folder = getFolderFromFileType(fallbackFileType || fileType);
|
||||
downloadFile(folder, actualFileName);
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
Download File
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{
|
||||
display: 'inline-block',
|
||||
padding: '4px 12px',
|
||||
backgroundColor: getFileTypeColor() + '15',
|
||||
border: `1px solid ${getFileTypeColor()}30`,
|
||||
borderRadius: '16px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
color: getFileTypeColor()
|
||||
}}>
|
||||
{(fallbackFileType || fileType || 'FILE')?.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'relative' }}>
|
||||
|
||||
{loading && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.6)',
|
||||
backdropFilter: 'blur(0.8px)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 5,
|
||||
borderRadius: '8px'
|
||||
}}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ filter: loading ? 'blur(0.5px)' : 'none', transition: 'filter 0.3s ease' }}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewFilePage;
|
||||
99
src/pages/master/brandDevice/component/BrandForm.jsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import { Form, Input, Row, Col, Typography, Switch } from 'antd';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const BrandForm = ({
|
||||
form,
|
||||
onValuesChange,
|
||||
isEdit = false,
|
||||
brandInfo = null,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const isActive = Form.useWatch('is_active', form) ?? true;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (brandInfo && brandInfo.brand_code) {
|
||||
form.setFieldsValue({
|
||||
brand_code: brandInfo.brand_code
|
||||
});
|
||||
}
|
||||
}, [brandInfo, form]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
onValuesChange={onValuesChange}
|
||||
initialValues={{
|
||||
brand_name: '',
|
||||
brand_type: '',
|
||||
brand_model: '',
|
||||
brand_manufacture: '',
|
||||
is_active: true,
|
||||
}}
|
||||
>
|
||||
<Form.Item label="Status">
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Form.Item name="is_active" valuePropName="checked" noStyle>
|
||||
<Switch
|
||||
style={{ backgroundColor: isActive ? '#23A55A' : '#bfbfbf' }}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Text style={{ marginLeft: 8 }}>
|
||||
{isActive ? 'Running' : 'Offline'}
|
||||
</Text>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Brand Code" name="brand_code">
|
||||
<Input
|
||||
disabled={true}
|
||||
style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
cursor: 'not-allowed'
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Brand Name"
|
||||
name="brand_name"
|
||||
rules={[{ required: !readOnly, message: 'Brand Name wajib diisi!' }]}
|
||||
>
|
||||
<Input disabled={readOnly} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="Manufacturer"
|
||||
name="brand_manufacture"
|
||||
rules={[{ required: !readOnly, message: 'Manufacturer wajib diisi!' }]}
|
||||
>
|
||||
<Input placeholder="Enter Manufacturer" disabled={readOnly} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="Brand Type" name="brand_type">
|
||||
<Input placeholder="Enter Brand Type (Optional)" disabled={readOnly} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="Model" name="brand_model">
|
||||
<Input placeholder="Enter Model (Optional)" disabled={readOnly} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrandForm;
|
||||
397
src/pages/master/brandDevice/component/CustomSparepartCard.jsx
Normal file
@@ -0,0 +1,397 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Typography, Tag, Button, Modal, Row, Col, Space } from 'antd';
|
||||
import { EyeOutlined, DeleteOutlined, CheckOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const CustomSparepartCard = ({
|
||||
sparepart,
|
||||
isSelected = false,
|
||||
isReadOnly = false,
|
||||
showPreview = true,
|
||||
showDelete = false,
|
||||
onPreview,
|
||||
onDelete,
|
||||
onCardClick,
|
||||
loading = false,
|
||||
size = 'small',
|
||||
style = {},
|
||||
}) => {
|
||||
const [previewModalVisible, setPreviewModalVisible] = useState(false);
|
||||
|
||||
const getImageSrc = () => {
|
||||
if (sparepart.sparepart_foto) {
|
||||
if (sparepart.sparepart_foto.startsWith('http')) {
|
||||
return sparepart.sparepart_foto;
|
||||
} else {
|
||||
const fileName = sparepart.sparepart_foto.split('/').pop();
|
||||
if (fileName === 'defaultSparepartImg.jpg') {
|
||||
return `/assets/defaultSparepartImg.jpg`;
|
||||
} else {
|
||||
const token = localStorage.getItem('token');
|
||||
const baseURL = import.meta.env.VITE_API_SERVER || '';
|
||||
return `${baseURL}/file-uploads/images/${encodeURIComponent(fileName)}${token ? `?token=${encodeURIComponent(token)}` : ''}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'https://via.placeholder.com/150';
|
||||
};
|
||||
|
||||
const handlePreview = () => {
|
||||
if (onPreview) {
|
||||
onPreview(sparepart);
|
||||
} else {
|
||||
setPreviewModalVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const truncateText = (text, maxLength = 15) => {
|
||||
if (!text) return 'Unnamed';
|
||||
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
|
||||
};
|
||||
|
||||
const handleCardClick = () => {
|
||||
if (!isReadOnly && onCardClick) {
|
||||
onCardClick(sparepart);
|
||||
}
|
||||
};
|
||||
|
||||
const getCardActions = () => {
|
||||
const actions = [];
|
||||
|
||||
if (showPreview) {
|
||||
actions.push(
|
||||
<Button
|
||||
key="preview"
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
title="Lihat Detail"
|
||||
style={{ color: '#1890ff' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePreview();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showDelete && !isReadOnly) {
|
||||
actions.push(
|
||||
<Button
|
||||
key="delete"
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
title="Hapus"
|
||||
style={{ color: '#ff4d4f' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete?.(sparepart);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
const getCardStyle = () => {
|
||||
const baseStyle = {
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
border: isSelected ? '2px solid #1890ff' : '1px solid #E0E0E0',
|
||||
cursor: isReadOnly ? 'default' : 'pointer',
|
||||
position: 'relative',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
|
||||
transition: 'all 0.3s ease'
|
||||
};
|
||||
|
||||
switch (size) {
|
||||
case 'small':
|
||||
return {
|
||||
...baseStyle,
|
||||
height: '180px',
|
||||
minHeight: '180px'
|
||||
};
|
||||
case 'large':
|
||||
return {
|
||||
...baseStyle,
|
||||
height: '280px',
|
||||
minHeight: '280px'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...baseStyle,
|
||||
height: '220px',
|
||||
minHeight: '220px'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: '6px',
|
||||
padding: '12px 16px',
|
||||
marginBottom: '8px',
|
||||
backgroundColor: 'white',
|
||||
cursor: onCardClick && !isReadOnly ? 'pointer' : 'default',
|
||||
transition: 'all 0.2s ease',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '4px' }}>
|
||||
<Text
|
||||
strong
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
color: '#262626',
|
||||
marginRight: '12px'
|
||||
}}
|
||||
title={sparepart.sparepart_name || sparepart.name || 'Unnamed'}
|
||||
>
|
||||
{truncateText(sparepart.sparepart_name || sparepart.name || 'Unnamed')}
|
||||
</Text>
|
||||
<Tag
|
||||
color={sparepart.sparepart_stok === 'Available' ? 'green' : 'red'}
|
||||
style={{ fontSize: '11px', margin: 0 }}
|
||||
>
|
||||
{sparepart.sparepart_stok || 'Not Available'}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Text style={{ fontSize: '12px', color: '#666', marginRight: '4px' }}>
|
||||
qty:
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
color: '#262626'
|
||||
}}
|
||||
>
|
||||
{sparepart.sparepart_qty || 0}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Space size="small">
|
||||
{showPreview && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePreview();
|
||||
}}
|
||||
title="Preview"
|
||||
/>
|
||||
)}
|
||||
{showDelete && !isReadOnly && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
danger
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete?.(sparepart);
|
||||
}}
|
||||
title="Remove"
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
||||
<Modal
|
||||
title="Sparepart Details"
|
||||
open={previewModalVisible}
|
||||
onCancel={() => setPreviewModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setPreviewModalVisible(false)}>
|
||||
Close
|
||||
</Button>
|
||||
]}
|
||||
width={800}
|
||||
centered
|
||||
styles={{ body: { padding: '24px' } }}
|
||||
>
|
||||
<Row gutter={[24, 24]}>
|
||||
<Col span={10}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#f0f0f0',
|
||||
width: '220px',
|
||||
height: '220px',
|
||||
margin: '0 auto 16px',
|
||||
position: 'relative',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
border: '1px solid #E0E0E0',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={getImageSrc()}
|
||||
alt={sparepart.sparepart_name || 'Sparepart'}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
onError={(e) => {
|
||||
e.target.src = 'https://via.placeholder.com/220x220/d9d9d9/666666?text=No+Image';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{sparepart.sparepart_item_type && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<Tag color="blue" style={{ fontSize: '14px', padding: '4px 12px' }}>
|
||||
{sparepart.sparepart_item_type}
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div style={{
|
||||
textAlign: 'left',
|
||||
background: '#f8f9fa',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
marginTop: '25px'
|
||||
}}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Text strong style={{ fontSize: '14px', color: '#262626' }}>Stock Status:</Text>
|
||||
<Tag
|
||||
color={sparepart.sparepart_stok === 'Available' ? 'green' : 'red'}
|
||||
style={{ marginLeft: '8px', fontSize: '12px' }}
|
||||
>
|
||||
{sparepart.sparepart_stok || 'Not Available'}
|
||||
</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong style={{ fontSize: '14px', color: '#262626' }}>Quantity:</Text>
|
||||
<Text style={{ fontSize: '14px', marginLeft: '8px', fontWeight: 600 }}>
|
||||
{sparepart.sparepart_qty || 0} {sparepart.sparepart_unit || ''}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col span={14}>
|
||||
<div>
|
||||
|
||||
<Title level={3} style={{ marginBottom: '20px', color: '#262626' }}>
|
||||
{sparepart.sparepart_name || 'Unnamed'}
|
||||
</Title>
|
||||
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Row gutter={[16, 12]}>
|
||||
<Col span={24}>
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
backgroundColor: '#fafafa',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>Code</Text>
|
||||
<div style={{ fontSize: '15px', fontWeight: 500, marginTop: '2px' }}>
|
||||
{sparepart.sparepart_code || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>Brand</Text>
|
||||
<div style={{ fontSize: '15px', fontWeight: 500, marginTop: '2px' }}>
|
||||
{sparepart.sparepart_merk || '-'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>Unit</Text>
|
||||
<div style={{ fontSize: '15px', fontWeight: 500, marginTop: '2px' }}>
|
||||
{sparepart.sparepart_unit || '-'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
{sparepart.sparepart_model && (
|
||||
<Col span={24}>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>Model</Text>
|
||||
<div style={{ fontSize: '15px', fontWeight: 500, marginTop: '2px' }}>
|
||||
{sparepart.sparepart_model}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{sparepart.sparepart_description && (
|
||||
<Col span={24}>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>Description</Text>
|
||||
<div style={{ fontSize: '15px', marginTop: '2px', lineHeight: '1.5' }}>
|
||||
{sparepart.sparepart_description}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
|
||||
{sparepart.created_at && (
|
||||
<div>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>Created</Text>
|
||||
<div style={{ fontSize: '13px', marginTop: '2px' }}>
|
||||
{dayjs(sparepart.created_at).format('DD MMM YYYY, HH:mm')}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>Last Updated</Text>
|
||||
<div style={{ fontSize: '13px', marginTop: '2px' }}>
|
||||
{dayjs(sparepart.updated_at).format('DD MMM YYYY, HH:mm')}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomSparepartCard;
|
||||
288
src/pages/master/brandDevice/component/ErrorCodeForm.jsx
Normal file
@@ -0,0 +1,288 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Form, Input, Switch, Typography, ConfigProvider, Card, Button } from 'antd';
|
||||
import { FileOutlined, EyeOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import FileUploadHandler from './FileUploadHandler';
|
||||
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||
import { getFileUrl, getFolderFromFileType } from '../../../../api/file-uploads';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const ErrorCodeForm = ({
|
||||
errorCodeForm,
|
||||
isErrorCodeFormReadOnly = false,
|
||||
errorCodeIcon,
|
||||
onErrorCodeIconUpload,
|
||||
onErrorCodeIconRemove,
|
||||
isEdit = false,
|
||||
}) => {
|
||||
const [currentIcon, setCurrentIcon] = useState(null);
|
||||
const statusWatch = Form.useWatch('status', errorCodeForm) ?? true;
|
||||
|
||||
useEffect(() => {
|
||||
if (errorCodeIcon && typeof errorCodeIcon === 'object' && Object.keys(errorCodeIcon).length > 0) {
|
||||
setCurrentIcon(errorCodeIcon);
|
||||
} else {
|
||||
setCurrentIcon(null);
|
||||
}
|
||||
}, [errorCodeIcon]);
|
||||
|
||||
const handleIconRemove = () => {
|
||||
setCurrentIcon(null);
|
||||
onErrorCodeIconRemove();
|
||||
};
|
||||
|
||||
const renderIconUpload = () => {
|
||||
if (currentIcon) {
|
||||
const displayFileName = currentIcon.name || currentIcon.uploadPath?.split('/').pop() || currentIcon.url?.split('/').pop() || 'Icon File';
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
marginTop: 8,
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
border: '1px solid #e8e8e8'
|
||||
}}
|
||||
styles={{ body: { padding: '16px' } }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#f0f5ff',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
<FileOutlined style={{ fontSize: 24, color: '#1890ff' }} />
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#262626',
|
||||
marginBottom: 4,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{displayFileName}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#8c8c8c' }}>
|
||||
{currentIcon.size ? `${(currentIcon.size / 1024).toFixed(1)} KB` : 'Icon uploaded'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
icon={<EyeOutlined />}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4
|
||||
}}
|
||||
onClick={() => {
|
||||
try {
|
||||
let iconUrl = '';
|
||||
let actualFileName = '';
|
||||
|
||||
const filePath = currentIcon.uploadPath || currentIcon.url || currentIcon.path || '';
|
||||
const iconDisplayName = currentIcon.name || '';
|
||||
|
||||
if (iconDisplayName) {
|
||||
actualFileName = iconDisplayName;
|
||||
} else if (filePath) {
|
||||
actualFileName = filePath.split('/').pop();
|
||||
}
|
||||
|
||||
if (actualFileName) {
|
||||
const fileExtension = actualFileName.split('.').pop()?.toLowerCase();
|
||||
const folder = getFolderFromFileType(fileExtension);
|
||||
iconUrl = getFileUrl(folder, actualFileName);
|
||||
}
|
||||
|
||||
if (!iconUrl && filePath) {
|
||||
iconUrl = filePath.startsWith('http') ? filePath : `${import.meta.env.VITE_API_SERVER}/${filePath}`;
|
||||
}
|
||||
|
||||
if (iconUrl && actualFileName) {
|
||||
const fileExtension = actualFileName.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const pdfExtensions = ['pdf'];
|
||||
|
||||
if (imageExtensions.includes(fileExtension) || pdfExtensions.includes(fileExtension)) {
|
||||
const viewerUrl = `/image-viewer/${encodeURIComponent(actualFileName)}`;
|
||||
window.open(viewerUrl, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
window.open(iconUrl, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: `File URL not found. FileName: ${actualFileName}, FilePath: ${filePath}`
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: `Failed to open file preview: ${error.message}`
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
size="middle"
|
||||
icon={<DeleteOutlined />}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onClick={handleIconRemove}
|
||||
disabled={isErrorCodeFormReadOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FileUploadHandler
|
||||
type="error_code"
|
||||
existingFile={null}
|
||||
accept="image/*"
|
||||
onFileUpload={(fileData) => {
|
||||
setCurrentIcon(fileData);
|
||||
onErrorCodeIconUpload(fileData);
|
||||
}}
|
||||
onFileRemove={handleIconRemove}
|
||||
buttonText="Upload Icon"
|
||||
buttonStyle={{
|
||||
width: '100%',
|
||||
borderColor: '#23A55A',
|
||||
color: '#23A55A',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '8px'
|
||||
}}
|
||||
uploadText="Upload error code icon"
|
||||
disabled={isErrorCodeFormReadOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Switch: {
|
||||
colorPrimary: '#23A55A',
|
||||
colorPrimaryHover: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Form
|
||||
form={errorCodeForm}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
status: true,
|
||||
error_code_color: '#000000'
|
||||
}}
|
||||
>
|
||||
{/* Header bar with color picker, icon upload, and status toggle */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: '16px',
|
||||
gap: '16px'
|
||||
}}>
|
||||
{/* Color picker on left */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Form.Item
|
||||
name="error_code_color"
|
||||
noStyle
|
||||
getValueFromEvent={(e) => e.target.value}
|
||||
getValueProps={(value) => ({ value: value || '#000000' })}
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
style={{
|
||||
width: '120px',
|
||||
height: '40px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 4,
|
||||
cursor: isErrorCodeFormReadOnly ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
disabled={isErrorCodeFormReadOnly}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* Icon upload beside color picker */}
|
||||
<div style={{ flex: 1, maxWidth: '300px' }}>
|
||||
{renderIconUpload()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status toggle on right */}
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Form.Item name="status" valuePropName="checked" noStyle>
|
||||
<Switch
|
||||
disabled={isErrorCodeFormReadOnly}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Text style={{ marginLeft: 8 }}>
|
||||
{statusWatch ? 'Active' : 'Inactive'}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Code and Error Name in one row with 1/3 and 2/3 ratio */}
|
||||
<div style={{ display: 'flex', gap: '12px', marginBottom: '16px' }}>
|
||||
<Form.Item
|
||||
label="Error Code"
|
||||
name="error_code"
|
||||
rules={[{ required: true, message: 'Error code wajib diisi!' }]}
|
||||
style={{ flex: 1, marginBottom: 0, maxWidth: '33.33%' }}
|
||||
>
|
||||
<Input
|
||||
placeholder="Enter error code"
|
||||
disabled={isErrorCodeFormReadOnly}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Error Name"
|
||||
name="error_code_name"
|
||||
rules={[{ required: !isErrorCodeFormReadOnly, message: 'Error name wajib diisi!' }]}
|
||||
style={{ flex: 2, marginBottom: 0, maxWidth: '66.67%' }}
|
||||
>
|
||||
<Input placeholder="Enter error name" disabled={isErrorCodeFormReadOnly} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item label="Description" name="error_code_description">
|
||||
<Input.TextArea
|
||||
placeholder="Enter error description"
|
||||
rows={3}
|
||||
disabled={isErrorCodeFormReadOnly}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorCodeForm;
|
||||
421
src/pages/master/brandDevice/component/FileUploadHandler.jsx
Normal file
@@ -0,0 +1,421 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Upload, Modal, Button, Typography, Space, Image } from 'antd';
|
||||
import { UploadOutlined, EyeOutlined, DeleteOutlined, FileOutlined } from '@ant-design/icons';
|
||||
import { NotifOk, NotifAlert } from '../../../../components/Global/ToastNotif';
|
||||
import { uploadFile, getFolderFromFileType, getFileUrl, getFileType } from '../../../../api/file-uploads';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const FileUploadHandler = ({
|
||||
type = 'solution',
|
||||
maxCount = 1,
|
||||
accept = '.pdf,.jpg,.jpeg,.png,.gif',
|
||||
disabled = false,
|
||||
|
||||
fileList = [],
|
||||
onFileUpload,
|
||||
onFileRemove,
|
||||
|
||||
existingFile = null,
|
||||
clearSignal = null,
|
||||
debugProps = {},
|
||||
|
||||
uploadText = 'Click or drag file to this area to upload',
|
||||
uploadHint = 'Support for PDF and image files only',
|
||||
buttonText = 'Upload File',
|
||||
buttonType = 'default',
|
||||
|
||||
containerStyle = {},
|
||||
buttonStyle = {},
|
||||
showPreview = true
|
||||
}) => {
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewImage, setPreviewImage] = useState('');
|
||||
const [previewTitle, setPreviewTitle] = useState('');
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadedFile, setUploadedFile] = useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (clearSignal !== null && clearSignal > 0) {
|
||||
setUploadedFile(null);
|
||||
}
|
||||
}, [clearSignal, debugProps]);
|
||||
|
||||
const getBase64 = (file) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
|
||||
const handlePreview = async (file) => {
|
||||
if (!file.url && !file.preview) {
|
||||
file.preview = await getBase64(file.originFileObj);
|
||||
}
|
||||
setPreviewImage(file.url || file.preview);
|
||||
setPreviewOpen(true);
|
||||
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
|
||||
};
|
||||
|
||||
const validateFile = (file) => {
|
||||
const isAllowedType = [
|
||||
'application/pdf',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
].includes(file.type);
|
||||
|
||||
if (!isAllowedType) {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleFileUpload = async (file) => {
|
||||
if (isUploading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateFile(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsUploading(true);
|
||||
|
||||
const fileExtension = file.name.split('.').pop().toLowerCase();
|
||||
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension);
|
||||
const fileType = isImage ? 'image' : 'pdf';
|
||||
const folder = getFolderFromFileType(fileType);
|
||||
|
||||
const uploadResponse = await uploadFile(file, folder);
|
||||
|
||||
const isSuccess = uploadResponse && (
|
||||
uploadResponse.statusCode === 200 ||
|
||||
uploadResponse.statusCode === 201
|
||||
);
|
||||
|
||||
if (!isSuccess) {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: uploadResponse?.message || `Gagal mengupload ${file.name}`,
|
||||
});
|
||||
setIsUploading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
let actualPath = '';
|
||||
if (uploadResponse && typeof uploadResponse === 'object') {
|
||||
if (uploadResponse.data && uploadResponse.data.path_document) {
|
||||
actualPath = uploadResponse.data.path_document;
|
||||
}
|
||||
else if (uploadResponse.path_document) {
|
||||
actualPath = uploadResponse.path_document;
|
||||
}
|
||||
else if (uploadResponse.data && uploadResponse.data.path_solution) {
|
||||
actualPath = uploadResponse.data.path_solution;
|
||||
}
|
||||
else if (uploadResponse.data && typeof uploadResponse.data === 'object') {
|
||||
if (uploadResponse.data.file_url) {
|
||||
actualPath = uploadResponse.data.file_url;
|
||||
} else if (uploadResponse.data.url) {
|
||||
actualPath = uploadResponse.data.url;
|
||||
} else if (uploadResponse.data.path) {
|
||||
actualPath = uploadResponse.data.path;
|
||||
} else if (uploadResponse.data.location) {
|
||||
actualPath = uploadResponse.data.location;
|
||||
} else if (uploadResponse.data.filePath) {
|
||||
actualPath = uploadResponse.data.filePath;
|
||||
} else if (uploadResponse.data.file_path) {
|
||||
actualPath = uploadResponse.data.file_path;
|
||||
} else if (uploadResponse.data.publicUrl) {
|
||||
actualPath = uploadResponse.data.publicUrl;
|
||||
} else if (uploadResponse.data.public_url) {
|
||||
actualPath = uploadResponse.data.public_url;
|
||||
}
|
||||
}
|
||||
else if (uploadResponse && typeof uploadResponse === 'string') {
|
||||
actualPath = uploadResponse;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (actualPath) {
|
||||
let fileObject;
|
||||
|
||||
if (type === 'error_code') {
|
||||
fileObject = {
|
||||
name: file.name,
|
||||
path_icon: actualPath,
|
||||
uploadPath: actualPath,
|
||||
url: actualPath,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
fileExtension
|
||||
};
|
||||
} else {
|
||||
fileObject = {
|
||||
name: file.name,
|
||||
path_solution: actualPath,
|
||||
uploadPath: actualPath,
|
||||
type_solution: fileType,
|
||||
size: file.size,
|
||||
type: file.type
|
||||
};
|
||||
}
|
||||
|
||||
onFileUpload(fileObject);
|
||||
setUploadedFile(fileObject);
|
||||
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: `${file.name} berhasil diupload!`
|
||||
});
|
||||
|
||||
setIsUploading(false);
|
||||
return false;
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: `Gagal mengupload ${file.name}. Tidak dapat menemukan path file dalam response.`,
|
||||
});
|
||||
setIsUploading(false);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`,
|
||||
});
|
||||
setIsUploading(false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = ({ fileList }) => {
|
||||
if (fileList && fileList.length > 0 && fileList[0] && fileList[0].originFileObj) {
|
||||
handleFileUpload(fileList[0].originFileObj);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
if (existingFile && onFileRemove) {
|
||||
onFileRemove(existingFile);
|
||||
} else if (onFileRemove) {
|
||||
onFileRemove(null);
|
||||
}
|
||||
};
|
||||
|
||||
const renderExistingFile = () => {
|
||||
const fileToShow = existingFile || uploadedFile;
|
||||
if (!fileToShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filePath = fileToShow.uploadPath || fileToShow.url || fileToShow.path_icon || fileToShow.path_solution;
|
||||
const fileName = fileToShow.name || filePath?.split('/').pop() || 'Unknown file';
|
||||
const fileType = getFileType(fileName);
|
||||
const isImage = fileType === 'image';
|
||||
|
||||
const handlePreview = () => {
|
||||
if (!showPreview || !filePath) return;
|
||||
|
||||
if (isImage) {
|
||||
const folder = fileToShow.type_solution === 'pdf' ? 'pdf' : 'images';
|
||||
const filename = filePath.split('/').pop();
|
||||
const imageUrl = getFileUrl(folder, filename);
|
||||
|
||||
if (imageUrl) {
|
||||
setPreviewImage(imageUrl);
|
||||
setPreviewOpen(true);
|
||||
setPreviewTitle(fileName);
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'Cannot generate image preview URL',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const folder = fileToShow.type_solution === 'pdf' ? 'pdf' : 'images';
|
||||
const filename = filePath.split('/').pop();
|
||||
const fileUrl = getFileUrl(folder, filename);
|
||||
|
||||
if (fileUrl) {
|
||||
window.open(fileUrl, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'Cannot generate file preview URL',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getThumbnailUrl = () => {
|
||||
if (!isImage || !filePath) return null;
|
||||
|
||||
const folder = fileToShow.type_solution === 'pdf' ? 'pdf' : 'images';
|
||||
const filename = filePath.split('/').pop();
|
||||
return getFileUrl(folder, filename);
|
||||
};
|
||||
|
||||
const thumbnailUrl = getThumbnailUrl();
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '8px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#fafafa'
|
||||
}}>
|
||||
{isImage ? (
|
||||
<img
|
||||
src={thumbnailUrl || filePath}
|
||||
alt={fileName}
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
objectFit: 'cover',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 4,
|
||||
cursor: showPreview ? 'pointer' : 'default'
|
||||
}}
|
||||
onClick={handlePreview}
|
||||
onError={(e) => {
|
||||
e.target.src = filePath;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#f5f5f5',
|
||||
cursor: showPreview ? 'pointer' : 'default'
|
||||
}}
|
||||
onClick={handlePreview}
|
||||
>
|
||||
<FileOutlined style={{ fontSize: 24, color: '#666' }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text style={{ fontSize: 12, fontWeight: 500 }}>
|
||||
{fileName}
|
||||
</Text>
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: 10 }}>
|
||||
{fileType === 'image' ? 'Image' : fileType === 'pdf' ? 'PDF' : 'File'}
|
||||
{fileToShow.size && ` • ${(fileToShow.size / 1024).toFixed(1)} KB`}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{showPreview && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
size="small"
|
||||
onClick={handlePreview}
|
||||
title={isImage ? "Preview Image" : "Open File"}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
onClick={handleRemove}
|
||||
title="Remove File"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const uploadProps = {
|
||||
name: 'file',
|
||||
multiple: false,
|
||||
accept,
|
||||
disabled: disabled || isUploading,
|
||||
fileList: [],
|
||||
beforeUpload: () => false,
|
||||
onChange: handleFileChange,
|
||||
onPreview: handlePreview,
|
||||
maxCount,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ ...containerStyle }}>
|
||||
{!existingFile && (
|
||||
<Upload {...uploadProps}>
|
||||
{type === 'drag' ? (
|
||||
<Upload.Dragger>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<UploadOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">{uploadText}</p>
|
||||
<p className="ant-upload-hint">{uploadHint}</p>
|
||||
</Upload.Dragger>
|
||||
) : (
|
||||
<Button
|
||||
type={buttonType}
|
||||
icon={<UploadOutlined />}
|
||||
loading={isUploading}
|
||||
style={{ ...buttonStyle }}
|
||||
>
|
||||
{isUploading ? 'Uploading...' : buttonText}
|
||||
</Button>
|
||||
)}
|
||||
</Upload>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{showPreview && (
|
||||
<Modal
|
||||
open={previewOpen}
|
||||
title={previewTitle}
|
||||
footer={null}
|
||||
onCancel={() => setPreviewOpen(false)}
|
||||
width={600}
|
||||
style={{ top: 100 }}
|
||||
>
|
||||
{previewImage && (
|
||||
<img
|
||||
alt={previewTitle}
|
||||
style={{ width: '100%' }}
|
||||
src={previewImage}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileUploadHandler;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { Button, Col, Row, Space, Input, ConfigProvider, Card, Tag } from 'antd';
|
||||
import { Button, Col, Row, Space, Input, ConfigProvider, Card, Tag, Spin } from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
@@ -7,46 +7,10 @@ import {
|
||||
SearchOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
|
||||
import { NotifAlert, NotifConfirmDialog, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import TableList from '../../../../components/Global/TableList';
|
||||
import { getAllBrands } from '../../../../api/master-brand';
|
||||
|
||||
// Dummy data
|
||||
const initialBrandDeviceData = [
|
||||
{
|
||||
brand_id: 1,
|
||||
brandName: 'Siemens S7-1200',
|
||||
brandType: 'PLC',
|
||||
manufacturer: 'Siemens',
|
||||
model: 'S7-1200',
|
||||
status: 'Active',
|
||||
},
|
||||
{
|
||||
brand_id: 2,
|
||||
brandName: 'Allen Bradley CompactLogix',
|
||||
brandType: 'PLC',
|
||||
manufacturer: 'Rockwell Automation',
|
||||
model: 'CompactLogix 5370',
|
||||
status: 'Active',
|
||||
},
|
||||
{
|
||||
brand_id: 3,
|
||||
brandName: 'Schneider Modicon M580',
|
||||
brandType: 'PLC',
|
||||
manufacturer: 'Schneider Electric',
|
||||
model: 'M580',
|
||||
status: 'Active',
|
||||
},
|
||||
{
|
||||
brand_id: 4,
|
||||
brandName: 'Mitsubishi FX5U',
|
||||
brandType: 'PLC',
|
||||
manufacturer: 'Mitsubishi',
|
||||
model: 'FX5U',
|
||||
status: 'Inactive',
|
||||
},
|
||||
];
|
||||
import { getAllBrands, deleteBrand } from '../../../../api/master-brand';
|
||||
|
||||
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
{
|
||||
@@ -58,50 +22,38 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
},
|
||||
{
|
||||
title: 'Brand Device ',
|
||||
dataIndex: 'brandName',
|
||||
key: 'brandName',
|
||||
dataIndex: 'brand_name',
|
||||
key: 'brand_name',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: 'Type',
|
||||
dataIndex: 'brandType',
|
||||
key: 'brandType',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
title: 'Manufacturer',
|
||||
dataIndex: 'manufacturer',
|
||||
key: 'manufacturer',
|
||||
dataIndex: 'brand_manufacture',
|
||||
key: 'brand_manufacture',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: 'model',
|
||||
dataIndex: 'model',
|
||||
key: 'model',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
title: 'status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
title: 'Status',
|
||||
dataIndex: 'is_active',
|
||||
key: 'is_active',
|
||||
width: '10%',
|
||||
align: 'center',
|
||||
render: (_, { status }) => (
|
||||
render: (_, { is_active }) => (
|
||||
<>
|
||||
{status === 'Active' ? (
|
||||
{is_active === true ? (
|
||||
<Tag color={'green'} key={'status'}>
|
||||
Active
|
||||
Running
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color={'red'} key={'status'}>
|
||||
Inactive
|
||||
Offline
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Aksi',
|
||||
title: 'Action',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
@@ -138,77 +90,87 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
|
||||
const ListBrandDevice = memo(function ListBrandDevice(props) {
|
||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||
const [brandDeviceData, setBrandDeviceData] = useState(initialBrandDeviceData);
|
||||
|
||||
const defaultFilter = { search: '' };
|
||||
const defaultFilter = { criteria: '' };
|
||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
if (props.actionMode == 'list') {
|
||||
if (props.actionMode === 'list') {
|
||||
setFormDataFilter(defaultFilter);
|
||||
doFilter();
|
||||
}
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, [props.actionMode, brandDeviceData]);
|
||||
}, [props.actionMode, navigate]);
|
||||
|
||||
const doFilter = () => {
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
setFormDataFilter({ search: searchValue });
|
||||
setFormDataFilter({ criteria: searchText });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleSearchClear = () => {
|
||||
setSearchValue('');
|
||||
setFormDataFilter({ search: '' });
|
||||
setSearchText('');
|
||||
setFormDataFilter({ criteria: '' });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const showPreviewModal = (param) => {
|
||||
props.setSelectedData(param);
|
||||
props.setActionMode('preview');
|
||||
navigate(`/master/brand-device/view/${param.brand_id}`);
|
||||
};
|
||||
|
||||
const showEditModal = (param = null) => {
|
||||
props.setSelectedData(param);
|
||||
props.setActionMode('edit');
|
||||
if (param) {
|
||||
navigate(`/master/brand-device/edit/${param.brand_id}`);
|
||||
} else {
|
||||
navigate('/master/brand-device/add');
|
||||
}
|
||||
};
|
||||
|
||||
const showDeleteDialog = (param) => {
|
||||
NotifConfirmDialog({
|
||||
icon: 'question',
|
||||
title: 'Konfirmasi',
|
||||
message: 'Apakah anda yakin hapus data "' + param.brandName + '" ?',
|
||||
onConfirm: () => handleDelete(param.brand_id),
|
||||
onCancel: () => props.setSelectedData(null),
|
||||
message: 'Apakah anda yakin hapus data "' + param.brand_name + '" ?',
|
||||
onConfirm: () => handleDelete(param.brand_id, param.brand_name),
|
||||
onCancel: () => { },
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (brand_id) => {
|
||||
// Find brand name before deleting
|
||||
const brandToDelete = brandDeviceData.find((brand) => brand.brand_id === brand_id);
|
||||
const handleDelete = async (brand_id, brand_name) => {
|
||||
try {
|
||||
const response = await deleteBrand(brand_id);
|
||||
|
||||
// Simulate delete API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
// Remove from state
|
||||
const updatedBrands = brandDeviceData.filter((brand) => brand.brand_id !== brand_id);
|
||||
setBrandDeviceData(updatedBrands);
|
||||
|
||||
NotifAlert({
|
||||
if (response && response.statusCode === 200) {
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: `Data Brand Device "${brandToDelete?.brandName || ''}" berhasil dihapus.`,
|
||||
message: `Brand ${brand_name} deleted successfully.`,
|
||||
});
|
||||
doFilter();
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: response?.message || 'Gagal menghapus Data Brand Device',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: error.message || 'Gagal menghapus Data Brand Device',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -220,13 +182,12 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
||||
<Col xs={24} sm={24} md={12} lg={12}>
|
||||
<Input.Search
|
||||
placeholder="Search brand device..."
|
||||
value={searchValue}
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchValue(value);
|
||||
// Auto search when clearing by backspace/delete
|
||||
setSearchText(value);
|
||||
if (value === '') {
|
||||
setFormDataFilter({ search: '' });
|
||||
setFormDataFilter({ criteria: '' });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
}
|
||||
}}
|
||||
@@ -267,10 +228,12 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
||||
>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => navigate('/master/brand-device/add')}
|
||||
onClick={() => {
|
||||
navigate('/master/brand-device/add');
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
Tambah Brand Device
|
||||
Add data
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</Space>
|
||||
@@ -281,7 +244,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
||||
<TableList
|
||||
mobile
|
||||
cardColor={'#42AAFF'}
|
||||
header={'tag_name'}
|
||||
header={'brand_name'}
|
||||
showPreviewModal={showPreviewModal}
|
||||
showEditModal={showEditModal}
|
||||
showDeleteDialog={showDeleteDialog}
|
||||
|
||||
315
src/pages/master/brandDevice/component/ListErrorCode.jsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { Card, Input, Button, Row, Col, Empty } from 'antd';
|
||||
import { PlusOutlined, SearchOutlined, DeleteOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { getErrorCodesByBrandId, deleteErrorCode } from '../../../../api/master-brand';
|
||||
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
|
||||
|
||||
const ListErrorCode = ({
|
||||
brandId,
|
||||
selectedErrorCode,
|
||||
onErrorCodeSelect,
|
||||
onAddNew,
|
||||
tempErrorCodes = [],
|
||||
trigerFilter,
|
||||
searchText,
|
||||
onSearchChange,
|
||||
onSearch,
|
||||
onSearchClear,
|
||||
isReadOnly = false,
|
||||
errorCodes: propErrorCodes = null
|
||||
}) => {
|
||||
const [errorCodes, setErrorCodes] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pagination, setPagination] = useState({
|
||||
current_page: 1,
|
||||
current_limit: 15,
|
||||
total_limit: 0,
|
||||
total_page: 0,
|
||||
});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 15;
|
||||
|
||||
const queryParams = useMemo(() => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', currentPage.toString());
|
||||
params.set('limit', pageSize.toString());
|
||||
if (searchText) {
|
||||
params.set('criteria', searchText);
|
||||
}
|
||||
return params;
|
||||
}, [searchText, currentPage, pageSize]);
|
||||
|
||||
const fetchErrorCodes = async () => {
|
||||
if (!brandId) {
|
||||
setErrorCodes([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getErrorCodesByBrandId(brandId, queryParams);
|
||||
|
||||
if (response && response.statusCode === 200) {
|
||||
const apiErrorData = response.data || [];
|
||||
const allErrorCodes = [
|
||||
...apiErrorData.map(ec => ({
|
||||
...ec,
|
||||
tempId: `existing_${ec.error_code_id}`,
|
||||
status: 'existing'
|
||||
})),
|
||||
...tempErrorCodes.filter(ec => ec.status !== 'deleted')
|
||||
];
|
||||
|
||||
setErrorCodes(allErrorCodes);
|
||||
|
||||
if (response.paging) {
|
||||
setPagination({
|
||||
current_page: response.paging.current_page || 1,
|
||||
current_limit: response.paging.current_limit || 15,
|
||||
total_limit: response.paging.total_limit || 0,
|
||||
total_page: response.paging.total_page || 0,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setErrorCodes([]);
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorCodes([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isReadOnly && propErrorCodes) {
|
||||
|
||||
setErrorCodes(propErrorCodes);
|
||||
setLoading(false);
|
||||
} else {
|
||||
|
||||
fetchErrorCodes();
|
||||
}
|
||||
}, [brandId, queryParams, tempErrorCodes, trigerFilter, isReadOnly, propErrorCodes]);
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (pagination.current_page > 1) {
|
||||
setCurrentPage(pagination.current_page - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (pagination.current_page < pagination.total_page) {
|
||||
setCurrentPage(pagination.current_page + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1);
|
||||
if (onSearch) {
|
||||
onSearch();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchClear = () => {
|
||||
setCurrentPage(1);
|
||||
if (onSearchClear) {
|
||||
onSearchClear();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (item, e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (item.status === 'existing' && item.error_code_id) {
|
||||
NotifConfirmDialog({
|
||||
icon: 'warning',
|
||||
title: 'Hapus Error Code',
|
||||
message: `Apakah Anda yakin ingin menghapus error code ${item.error_code}?`,
|
||||
onConfirm: () => performDelete(item),
|
||||
onCancel: () => { },
|
||||
confirmButtonText: 'Hapus'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const performDelete = async (item) => {
|
||||
try {
|
||||
|
||||
if (!item.error_code_id || item.error_code_id === 'undefined') {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'Error code ID tidak valid'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.brand_id || item.brand_id === 'undefined') {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'Brand ID tidak valid'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await deleteErrorCode(item.brand_id, item.error_code_id);
|
||||
|
||||
if (response && response.statusCode === 200) {
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: 'Error code berhasil dihapus'
|
||||
});
|
||||
fetchErrorCodes();
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: 'Gagal menghapus error code'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'Terjadi kesalahan saat menghapus error code'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title="Daftar Error Code"
|
||||
style={{ width: '100%', minWidth: '472px' }}
|
||||
styles={{ body: { padding: '12px' } }}
|
||||
>
|
||||
<Input.Search
|
||||
placeholder="Cari error code..."
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (onSearchChange) {
|
||||
onSearchChange(value);
|
||||
}
|
||||
}}
|
||||
onSearch={handleSearch}
|
||||
allowClear
|
||||
enterButton={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
onClick={handleSearch}
|
||||
style={{
|
||||
backgroundColor: '#23A55A',
|
||||
borderColor: '#23A55A',
|
||||
height: '32px'
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
}
|
||||
size="default"
|
||||
style={{
|
||||
marginBottom: 12,
|
||||
height: '32px',
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{
|
||||
height: '90vh',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '6px',
|
||||
overflow: 'auto',
|
||||
marginBottom: 12,
|
||||
backgroundColor: '#fafafa'
|
||||
}}>
|
||||
{errorCodes.length === 0 ? (
|
||||
<Empty
|
||||
description="Belum ada error code"
|
||||
style={{ marginTop: 50 }}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ padding: '8px' }}>
|
||||
{errorCodes.map((item) => (
|
||||
<div
|
||||
key={item.tempId || item.error_code_id}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
marginBottom: '4px',
|
||||
border: selectedErrorCode?.tempId === item.tempId ? '2px solid #23A55A' : '1px solid #d9d9d9',
|
||||
backgroundColor: selectedErrorCode?.tempId === item.tempId ? '#f6ffed' : '#fff',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
onClick={() => onErrorCodeSelect(item)}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 'bold', fontSize: '12px' }}>
|
||||
{item.error_code}
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: '#666' }}>
|
||||
{item.error_code_name}
|
||||
</div>
|
||||
</div>
|
||||
{item.status === 'existing' && (
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => handleDelete(item, e)}
|
||||
style={{
|
||||
padding: '2px 6px',
|
||||
height: '24px',
|
||||
fontSize: '11px',
|
||||
border: '1px solid #ff4d4f'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{pagination.total_limit > 0 && (
|
||||
<Row justify="space-between" align="middle" gutter={16}>
|
||||
<Col flex="auto">
|
||||
<span style={{ fontSize: '12px', color: '#666' }}>
|
||||
Menampilkan {pagination.current_limit} data halaman{' '}
|
||||
{pagination.current_page} dari total {pagination.total_limit} data
|
||||
</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<Button
|
||||
icon={<LeftOutlined />}
|
||||
onClick={handlePrevious}
|
||||
disabled={pagination.current_page <= 1}
|
||||
size="small"
|
||||
>
|
||||
</Button>
|
||||
<span style={{ fontSize: '12px', color: '#666', minWidth: '60px', textAlign: 'center' }}>
|
||||
{pagination.current_page} / {pagination.total_page}
|
||||
</span>
|
||||
<Button
|
||||
icon={<RightOutlined />}
|
||||
onClick={handleNext}
|
||||
disabled={pagination.current_page >= pagination.total_page}
|
||||
size="small"
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListErrorCode;
|
||||
@@ -1,33 +0,0 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Row, Col } from 'antd';
|
||||
|
||||
const ListErrorMaster = memo(function ListErrorMaster(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Row>
|
||||
<Col xs={24}>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '100px 20px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
color: '#595959',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
Cooming soon
|
||||
</h2>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default ListErrorMaster;
|
||||
496
src/pages/master/brandDevice/component/SolutionField.jsx
Normal file
@@ -0,0 +1,496 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Form, Input, Button, Switch, Radio, Typography, Space, Card, ConfigProvider } from 'antd';
|
||||
import { DeleteOutlined, EyeOutlined, FileOutlined } from '@ant-design/icons';
|
||||
import FileUploadHandler from './FileUploadHandler';
|
||||
import { NotifAlert } from '../../../../components/Global/ToastNotif';
|
||||
import { getFileUrl, getFolderFromFileType } from '../../../../api/file-uploads';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
const SolutionFieldNew = ({
|
||||
fieldKey,
|
||||
fieldName,
|
||||
index,
|
||||
solutionType,
|
||||
solutionStatus,
|
||||
isReadOnly = false,
|
||||
canRemove = true,
|
||||
onTypeChange,
|
||||
onStatusChange,
|
||||
onRemove,
|
||||
onFileUpload,
|
||||
onFileView,
|
||||
fileList = [],
|
||||
originalSolutionData = null
|
||||
}) => {
|
||||
const form = Form.useFormInstance();
|
||||
const [currentFile, setCurrentFile] = useState(null);
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
|
||||
const fileUpload = Form.useWatch(['solution_items', fieldKey, 'fileUpload'], form);
|
||||
const file = Form.useWatch(['solution_items', fieldKey, 'file'], form);
|
||||
const nameValue = Form.useWatch(['solution_items', fieldKey, 'name'], form);
|
||||
const fileNameValue = Form.useWatch(['solution_items', fieldKey, 'fileName'], form);
|
||||
const statusValue = Form.useWatch(['solution_items', fieldKey, 'status'], form) ?? true;
|
||||
|
||||
const pathSolution = Form.useWatch(['solution_items', fieldKey, 'path_solution'], form);
|
||||
|
||||
const [deleteCounter, setDeleteCounter] = useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!nameValue || nameValue === '') {
|
||||
setCurrentFile(null);
|
||||
setIsDeleted(false);
|
||||
setDeleteCounter(prev => prev + 1);
|
||||
}
|
||||
}, [nameValue]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const getFileFromFormValues = () => {
|
||||
const hasValidFileUpload = fileUpload && typeof fileUpload === 'object' && Object.keys(fileUpload).length > 0;
|
||||
const hasValidFile = file && typeof file === 'object' && Object.keys(file).length > 0;
|
||||
const hasValidPath = pathSolution && pathSolution.trim() !== '';
|
||||
|
||||
const wasExplicitlyDeleted =
|
||||
(fileUpload === null || file === null || pathSolution === null) &&
|
||||
!hasValidFileUpload &&
|
||||
!hasValidFile &&
|
||||
!hasValidPath;
|
||||
|
||||
if (wasExplicitlyDeleted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (solutionType === 'text') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasValidFileUpload) {
|
||||
return fileUpload;
|
||||
}
|
||||
if (hasValidFile) {
|
||||
return file;
|
||||
}
|
||||
if (hasValidPath) {
|
||||
return {
|
||||
name: fileNameValue || pathSolution.split('/').pop() || 'File',
|
||||
uploadPath: pathSolution,
|
||||
url: pathSolution,
|
||||
path: pathSolution
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const fileFromForm = getFileFromFormValues();
|
||||
|
||||
if (JSON.stringify(currentFile) !== JSON.stringify(fileFromForm)) {
|
||||
setCurrentFile(fileFromForm);
|
||||
}
|
||||
}, [fileUpload, file, pathSolution, solutionType, deleteCounter, fileNameValue, fieldKey]);
|
||||
|
||||
|
||||
const renderSolutionContent = () => {
|
||||
if (solutionType === 'text') {
|
||||
return (
|
||||
<Form.Item
|
||||
name={['solution_items', fieldKey, 'text']}
|
||||
rules={[{ required: true, message: 'Text solution wajib diisi!' }]}
|
||||
>
|
||||
<TextArea
|
||||
placeholder="Enter solution text"
|
||||
rows={3}
|
||||
disabled={isReadOnly}
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
if (solutionType === 'file') {
|
||||
const hasOriginalFile = originalSolutionData && (
|
||||
originalSolutionData.path_solution ||
|
||||
originalSolutionData.path_document
|
||||
);
|
||||
|
||||
let displayFile = null;
|
||||
|
||||
if (currentFile && Object.keys(currentFile).length > 0) {
|
||||
displayFile = currentFile;
|
||||
}
|
||||
else if (hasOriginalFile && !isDeleted) {
|
||||
displayFile = {
|
||||
name: originalSolutionData.file_upload_name ||
|
||||
(originalSolutionData.path_solution || originalSolutionData.path_document)?.split('/').pop() ||
|
||||
'File',
|
||||
uploadPath: originalSolutionData.path_solution || originalSolutionData.path_document,
|
||||
url: originalSolutionData.path_solution || originalSolutionData.path_document,
|
||||
path: originalSolutionData.path_solution || originalSolutionData.path_document,
|
||||
isExisting: true
|
||||
};
|
||||
}
|
||||
else if (fileUpload && typeof fileUpload === 'object' && Object.keys(fileUpload).length > 0) {
|
||||
displayFile = fileUpload;
|
||||
}
|
||||
else if (file && typeof file === 'object' && Object.keys(file).length > 0) {
|
||||
displayFile = file;
|
||||
}
|
||||
else if (pathSolution && pathSolution.trim() !== '') {
|
||||
displayFile = {
|
||||
name: pathSolution.split('/').pop() || 'File',
|
||||
uploadPath: pathSolution,
|
||||
url: pathSolution,
|
||||
path: pathSolution
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (displayFile) {
|
||||
const getFileNameFromPath = () => {
|
||||
const filePath = displayFile.uploadPath || displayFile.url || displayFile.path || '';
|
||||
if (filePath) {
|
||||
const fileName = filePath.split('/').pop();
|
||||
return fileName || 'Uploaded File';
|
||||
}
|
||||
return displayFile.name || 'Uploaded File';
|
||||
};
|
||||
|
||||
const displayFileName = getFileNameFromPath();
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
marginBottom: 8,
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
border: '1px solid #e8e8e8'
|
||||
}}
|
||||
styles={{ body: { padding: '16px' } }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#f0f5ff',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
<FileOutlined style={{ fontSize: 24, color: '#1890ff' }} />
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#262626',
|
||||
marginBottom: 4,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{displayFileName}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#8c8c8c' }}>
|
||||
{displayFile.size ? `${(displayFile.size / 1024).toFixed(1)} KB` : 'File uploaded'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
icon={<EyeOutlined />}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4
|
||||
}}
|
||||
onClick={() => {
|
||||
try {
|
||||
let fileUrl = '';
|
||||
let actualFileName = '';
|
||||
|
||||
const filePath = displayFile.uploadPath || displayFile.url || displayFile.path || '';
|
||||
|
||||
if (filePath) {
|
||||
actualFileName = filePath.split('/').pop();
|
||||
|
||||
if (actualFileName) {
|
||||
const fileExtension = actualFileName.split('.').pop()?.toLowerCase();
|
||||
const folder = getFolderFromFileType(fileExtension);
|
||||
|
||||
fileUrl = getFileUrl(folder, actualFileName);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileUrl && filePath) {
|
||||
fileUrl = filePath.startsWith('http') ? filePath : `${import.meta.env.VITE_API_SERVER}/${filePath}`;
|
||||
}
|
||||
|
||||
if (fileUrl && actualFileName) {
|
||||
const fileExtension = actualFileName.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
|
||||
if (imageExtensions.includes(fileExtension)) {
|
||||
const viewerUrl = `/image-viewer/${encodeURIComponent(actualFileName)}`;
|
||||
window.open(viewerUrl, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
window.open(fileUrl, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'File URL not found'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: 'Failed to open file preview'
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
size="middle"
|
||||
icon={<DeleteOutlined />}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsDeleted(true);
|
||||
|
||||
form.setFieldValue(['solution_items', fieldKey, 'fileUpload'], null);
|
||||
form.setFieldValue(['solution_items', fieldKey, 'file'], null);
|
||||
form.setFieldValue(['solution_items', fieldKey, 'path_solution'], null);
|
||||
form.setFieldValue(['solution_items', fieldKey, 'fileName'], null);
|
||||
|
||||
setCurrentFile(null);
|
||||
|
||||
if (onFileUpload && typeof onFileUpload === 'function') {
|
||||
onFileUpload(null);
|
||||
}
|
||||
|
||||
setDeleteCounter(prev => prev + 1);
|
||||
|
||||
setTimeout(() => {
|
||||
form.validateFields(['solution_items', fieldKey]);
|
||||
}, 50);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FileUploadHandler
|
||||
type="solution"
|
||||
existingFile={null}
|
||||
clearSignal={deleteCounter}
|
||||
debugProps={{
|
||||
currentFile: !!currentFile,
|
||||
deleteCounter,
|
||||
shouldClear: !currentFile && deleteCounter > 0
|
||||
}}
|
||||
onFileUpload={(fileObject) => {
|
||||
setIsDeleted(false);
|
||||
|
||||
const filePath = fileObject.path_solution || fileObject.uploadPath || fileObject.path || fileObject.url;
|
||||
|
||||
const fileWithKey = {
|
||||
...fileObject,
|
||||
solutionId: fieldKey,
|
||||
path_solution: filePath,
|
||||
uploadPath: filePath
|
||||
};
|
||||
|
||||
if (onFileUpload && typeof onFileUpload === 'function') {
|
||||
onFileUpload(fileWithKey);
|
||||
}
|
||||
|
||||
form.setFieldValue(['solution_items', fieldKey, 'fileUpload'], fileWithKey);
|
||||
form.setFieldValue(['solution_items', fieldKey, 'file'], fileWithKey);
|
||||
form.setFieldValue(['solution_items', fieldKey, 'type'], 'file');
|
||||
form.setFieldValue(['solution_items', fieldKey, 'path_solution'], filePath);
|
||||
form.setFieldValue(['solution_items', fieldKey, 'fileName'], fileObject.name);
|
||||
|
||||
setTimeout(() => {
|
||||
const values = form.getFieldValue(['solution_items', fieldKey]);
|
||||
const pathSolutionValue = form.getFieldValue(['solution_items', fieldKey, 'path_solution']);
|
||||
}, 100);
|
||||
|
||||
setCurrentFile(fileWithKey);
|
||||
}}
|
||||
onFileRemove={() => {
|
||||
form.setFieldValue(['solution_items', fieldKey, 'fileUpload'], null);
|
||||
form.setFieldValue(['solution_items', fieldKey, 'file'], null);
|
||||
form.setFieldValue(['solution_items', fieldKey, 'path_solution'], null);
|
||||
|
||||
setCurrentFile(null);
|
||||
|
||||
if (onFileUpload && typeof onFileUpload === 'function') {
|
||||
onFileUpload(null);
|
||||
}
|
||||
|
||||
setDeleteCounter(prev => prev + 1);
|
||||
}}
|
||||
disabled={isReadOnly}
|
||||
buttonText="Upload File"
|
||||
buttonStyle={{ width: '100%', fontSize: 12 }}
|
||||
uploadText="Upload solution file (includes images, PDF, documents)"
|
||||
acceptFileTypes="*"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Switch: {
|
||||
colorPrimary: '#23A55A',
|
||||
colorPrimaryHover: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 6,
|
||||
padding: 12,
|
||||
marginBottom: 12,
|
||||
backgroundColor: isReadOnly ? '#f5f5f5' : 'white'
|
||||
}}>
|
||||
<div style={{
|
||||
marginBottom: 8,
|
||||
gap: 8
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<Text strong style={{
|
||||
fontSize: 12,
|
||||
color: '#262626',
|
||||
display: 'block'
|
||||
}}>
|
||||
Solution #{index + 1}
|
||||
</Text>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Form.Item name={['solution_items', fieldKey, 'status']} valuePropName="checked" noStyle>
|
||||
<Switch
|
||||
size="small"
|
||||
disabled={isReadOnly}
|
||||
onChange={(checked) => {
|
||||
onStatusChange(fieldKey, checked);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Text style={{
|
||||
fontSize: 11,
|
||||
color: '#666',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{statusValue ? 'Active' : 'Inactive'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{canRemove && !isReadOnly && (
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={onRemove}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
padding: '2px 4px',
|
||||
height: '24px'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name={['solution_items', fieldKey, 'name']}
|
||||
rules={[{ required: true, message: 'Solution name wajib diisi!' }]}
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
<Input
|
||||
placeholder="Solution name"
|
||||
disabled={isReadOnly}
|
||||
size="default"
|
||||
style={{ fontSize: 13 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name={['solution_items', fieldKey, 'type']}
|
||||
rules={[{ required: true, message: 'Solution type wajib diisi!' }]}
|
||||
style={{ marginBottom: 8 }}
|
||||
initialValue={solutionType || 'text'}
|
||||
>
|
||||
<Radio.Group
|
||||
onChange={(e) => {
|
||||
const newType = e.target.value;
|
||||
|
||||
if (newType === 'text') {
|
||||
form.setFieldValue(['solution_items', fieldKey, 'fileUpload'], null);
|
||||
form.setFieldValue(['solution_items', fieldKey, 'file'], null);
|
||||
form.setFieldValue(['solution_items', fieldKey, 'path_solution'], null);
|
||||
form.setFieldValue(['solution_items', fieldKey, 'fileName'], null);
|
||||
setCurrentFile(null);
|
||||
setIsDeleted(true);
|
||||
|
||||
if (onFileUpload && typeof onFileUpload === 'function') {
|
||||
onFileUpload(null);
|
||||
}
|
||||
} else if (newType === 'file') {
|
||||
form.setFieldValue(['solution_items', fieldKey, 'text'], null);
|
||||
setIsDeleted(false);
|
||||
}
|
||||
|
||||
onTypeChange(fieldKey, newType);
|
||||
}}
|
||||
disabled={isReadOnly}
|
||||
size="small"
|
||||
>
|
||||
<Radio value="text" style={{ fontSize: 12 }}>Text</Radio>
|
||||
<Radio value="file" style={{ fontSize: 12 }}>File</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={['solution_items', fieldKey, 'status']}
|
||||
initialValue={solutionStatus !== false ? true : false}
|
||||
noStyle
|
||||
>
|
||||
<input type="hidden" />
|
||||
</Form.Item>
|
||||
|
||||
{renderSolutionContent()}
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SolutionFieldNew;
|
||||
77
src/pages/master/brandDevice/component/SolutionForm.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { Typography, Divider, Button, Form } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import SolutionFieldNew from './SolutionField';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const SolutionForm = ({
|
||||
solutionForm,
|
||||
solutionFields,
|
||||
solutionTypes,
|
||||
solutionStatuses,
|
||||
onAddSolutionField,
|
||||
onRemoveSolutionField,
|
||||
onSolutionTypeChange,
|
||||
onSolutionStatusChange,
|
||||
onSolutionFileUpload,
|
||||
onFileView,
|
||||
fileList,
|
||||
isReadOnly = false,
|
||||
solutionData = [],
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 0 }}>
|
||||
|
||||
<Form form={solutionForm} layout="vertical">
|
||||
<div style={{
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
paddingRight: '8px'
|
||||
}}>
|
||||
{solutionFields.map((field, displayIndex) => (
|
||||
<SolutionFieldNew
|
||||
key={field}
|
||||
fieldKey={field}
|
||||
fieldName={['solution_items', field]}
|
||||
index={displayIndex}
|
||||
solutionType={solutionTypes[field]}
|
||||
solutionStatus={solutionStatuses[field]}
|
||||
onTypeChange={onSolutionTypeChange}
|
||||
onStatusChange={onSolutionStatusChange}
|
||||
onRemove={() => onRemoveSolutionField(field)}
|
||||
onFileUpload={onSolutionFileUpload}
|
||||
onFileView={onFileView}
|
||||
fileList={fileList}
|
||||
isReadOnly={isReadOnly}
|
||||
canRemove={solutionFields.length > 1 && displayIndex > 0}
|
||||
originalSolutionData={solutionData[displayIndex]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!isReadOnly && (
|
||||
<div style={{ marginBottom: 8, marginTop: 12 }}>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={onAddSolutionField}
|
||||
icon={<PlusOutlined />}
|
||||
style={{
|
||||
width: '100%',
|
||||
borderColor: '#23A55A',
|
||||
color: '#23A55A',
|
||||
height: '32px',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Add sollution
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SolutionForm;
|
||||
178
src/pages/master/brandDevice/component/SparepartSelect.jsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Select, Typography, Tag, Spin, Empty, Button } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined, CheckOutlined, EyeOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { getAllSparepart } from '../../../../api/sparepart';
|
||||
import CustomSparepartCard from './CustomSparepartCard';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
const SparepartSelect = ({
|
||||
selectedSparepartIds = [],
|
||||
onSparepartChange,
|
||||
isReadOnly = false
|
||||
}) => {
|
||||
const [spareparts, setSpareparts] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedSpareparts, setSelectedSpareparts] = useState([]);
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSpareparts();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSparepartIds && selectedSparepartIds.length > 0) {
|
||||
const fullSelectedSpareparts = spareparts.filter(sp =>
|
||||
selectedSparepartIds.includes(sp.sparepart_id)
|
||||
);
|
||||
setSelectedSpareparts(fullSelectedSpareparts);
|
||||
} else {
|
||||
setSelectedSpareparts([]);
|
||||
}
|
||||
}, [selectedSparepartIds, spareparts]);
|
||||
|
||||
const fetchSpareparts = async (searchQuery = '') => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.set('limit', '10');
|
||||
|
||||
if (searchQuery && searchQuery.trim() !== '') {
|
||||
params.set('criteria', searchQuery.trim());
|
||||
}
|
||||
|
||||
const response = await getAllSparepart(params);
|
||||
if (response && (response.statusCode === 200 || response.data)) {
|
||||
const sparepartData = response.data?.data || response.data || [];
|
||||
setSpareparts(sparepartData);
|
||||
} else {
|
||||
setSpareparts([]);
|
||||
}
|
||||
} catch (error) {
|
||||
setSpareparts([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSparepartSelect = (sparepartId) => {
|
||||
const selectedSparepart = spareparts.find(sp => sp.sparepart_id === sparepartId);
|
||||
|
||||
if (selectedSparepart) {
|
||||
const isAlreadySelected = selectedSpareparts.some(sp => sp.sparepart_id === sparepartId);
|
||||
|
||||
if (!isAlreadySelected) {
|
||||
const newSelectedSpareparts = [...selectedSpareparts, selectedSparepart];
|
||||
setSelectedSpareparts(newSelectedSpareparts);
|
||||
|
||||
const newSelectedIds = newSelectedSpareparts.map(sp => sp.sparepart_id);
|
||||
onSparepartChange(newSelectedIds);
|
||||
}
|
||||
}
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
|
||||
const handleSearch = (value) => {
|
||||
fetchSpareparts(value);
|
||||
};
|
||||
|
||||
const onDropdownOpenChange = (open) => {
|
||||
setDropdownOpen(open);
|
||||
if (open) {
|
||||
fetchSpareparts();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSparepart = (sparepartId) => {
|
||||
const newSelectedSpareparts = selectedSpareparts.filter(sp => sp.sparepart_id !== sparepartId);
|
||||
setSelectedSpareparts(newSelectedSpareparts);
|
||||
|
||||
const newSelectedIds = newSelectedSpareparts.map(sp => sp.sparepart_id);
|
||||
onSparepartChange(newSelectedIds);
|
||||
};
|
||||
|
||||
const renderSparepartCard = (sparepart, isSelected = false) => {
|
||||
const isAlreadySelected = selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id);
|
||||
|
||||
return (
|
||||
<CustomSparepartCard
|
||||
key={sparepart.sparepart_id}
|
||||
sparepart={sparepart}
|
||||
isSelected={isSelected}
|
||||
isReadOnly={isReadOnly}
|
||||
showPreview={true}
|
||||
showDelete={isAlreadySelected && !isReadOnly}
|
||||
onCardClick={!isAlreadySelected && !isReadOnly ? () => handleSparepartSelect(sparepart.sparepart_id) : undefined}
|
||||
onDelete={() => handleRemoveSparepart(sparepart.sparepart_id)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{!isReadOnly && (
|
||||
<div style={{
|
||||
marginBottom: 16,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'white',
|
||||
padding: '8px 0',
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<Select
|
||||
placeholder="search and select sparepart"
|
||||
style={{ width: '100%' }}
|
||||
loading={loading}
|
||||
onSelect={handleSparepartSelect}
|
||||
value={null}
|
||||
showSearch
|
||||
onSearch={handleSearch}
|
||||
filterOption={false}
|
||||
open={dropdownOpen}
|
||||
onOpenChange={onDropdownOpenChange}
|
||||
suffixIcon={<PlusOutlined />}
|
||||
>
|
||||
{spareparts
|
||||
.filter(sparepart => !selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id))
|
||||
.slice(0, 10)
|
||||
.map((sparepart) => (
|
||||
<Option key={sparepart.sparepart_id} value={sparepart.sparepart_id}>
|
||||
<div>
|
||||
<Text strong>{sparepart.sparepart_name || sparepart.name || 'Unnamed'}</Text>
|
||||
<Text type="secondary" style={{ marginLeft: 8 }}>
|
||||
({sparepart.sparepart_code || 'No code'})
|
||||
</Text>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div>
|
||||
{selectedSpareparts.length > 0 ? (
|
||||
<div>
|
||||
<Title level={5} style={{ marginBottom: 16 }}>
|
||||
Selected Spareparts ({selectedSpareparts.length})
|
||||
</Title>
|
||||
<div>
|
||||
{selectedSpareparts.map(sparepart => renderSparepartCard(sparepart, true))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="No spareparts selected"
|
||||
style={{ margin: '20px 0' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SparepartSelect;
|
||||
@@ -1,84 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Card, Button, Row, Col, Typography, Space, Tag } from 'antd';
|
||||
import { EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const CardDevice = ({ data, showPreviewModal, showEditModal, showDeleteDialog }) => {
|
||||
const getCardStyle = () => {
|
||||
const color = '#FF8C42'; // Orange color
|
||||
return {
|
||||
border: `2px solid ${color}`,
|
||||
borderRadius: '8px',
|
||||
textAlign: 'center' // Center text
|
||||
};
|
||||
};
|
||||
|
||||
const getTitleStyle = () => {
|
||||
const backgroundColor = '#FF8C42'; // Orange color
|
||||
return {
|
||||
backgroundColor,
|
||||
color: '#fff',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
display: 'inline-block',
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]} style={{ marginTop: '16px', justifyContent: 'center' }}>
|
||||
{data.map((item) => (
|
||||
<Col xs={24} sm={12} md={8} lg={6} key={item.device_id}>
|
||||
<Card
|
||||
title={
|
||||
<span style={getTitleStyle()}>
|
||||
{item.device_name}
|
||||
</span>
|
||||
}
|
||||
style={getCardStyle()}
|
||||
actions={[
|
||||
<Space size="middle" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ color: '#1890ff' }}
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => showPreviewModal(item)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ color: '#faad14' }}
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => showEditModal(item)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => showDeleteDialog(item)}
|
||||
/>
|
||||
</Space>,
|
||||
]}
|
||||
>
|
||||
<p>
|
||||
<Text strong>Code:</Text> {item.device_code}
|
||||
</p>
|
||||
<p>
|
||||
<Text strong>Location:</Text> {item.device_location}
|
||||
</p>
|
||||
<p>
|
||||
<Text strong>IP Address:</Text> {item.ip_address}
|
||||
</p>
|
||||
<p>
|
||||
<Text strong>Status:</Text>{' '}
|
||||
<Tag color={item.device_status ? 'green' : 'red'}>
|
||||
{item.device_status ? 'Running' : 'Offline'}
|
||||
</Tag>
|
||||
</p>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardDevice;
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider } from 'antd';
|
||||
import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, Select } from 'antd';
|
||||
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||
import { createDevice, updateDevice } from '../../../../api/master-device';
|
||||
import { getAllBrands } from '../../../../api/master-brand';
|
||||
import { validateRun } from '../../../../Utils/validate';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -9,15 +10,20 @@ const { TextArea } = Input;
|
||||
|
||||
const DetailDevice = (props) => {
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
const [brands, setBrands] = useState([]);
|
||||
const [loadingBrands, setLoadingBrands] = useState(false);
|
||||
|
||||
const defaultData = {
|
||||
device_id: '',
|
||||
device_code: '',
|
||||
device_name: '',
|
||||
brand_id: '',
|
||||
brand_code: '',
|
||||
is_active: true,
|
||||
device_location: '',
|
||||
device_description: '',
|
||||
ip_address: '',
|
||||
listen_channel: '',
|
||||
};
|
||||
|
||||
const [formData, setFormData] = useState(defaultData);
|
||||
@@ -34,6 +40,7 @@ const DetailDevice = (props) => {
|
||||
const validationRules = [
|
||||
{ field: 'device_name', label: 'Device Name', required: true },
|
||||
{ field: 'ip_address', label: 'Ip Address', required: true, ip: true },
|
||||
{ field: 'brand_id', label: 'Brand Device', required: true },
|
||||
];
|
||||
|
||||
if (
|
||||
@@ -53,8 +60,13 @@ const DetailDevice = (props) => {
|
||||
device_name: formData.device_name,
|
||||
is_active: formData.is_active,
|
||||
device_location: formData.device_location,
|
||||
device_description: formData.device_description,
|
||||
device_description:
|
||||
formData.device_description && formData.device_description.trim() !== ''
|
||||
? formData.device_description
|
||||
: ' ',
|
||||
ip_address: formData.ip_address,
|
||||
brand_id: formData.brand_id,
|
||||
listen_channel: formData.listen_channel,
|
||||
};
|
||||
|
||||
const response = formData.device_id
|
||||
@@ -101,6 +113,13 @@ const DetailDevice = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectChange = (name, value) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleStatusToggle = (event) => {
|
||||
const isChecked = event;
|
||||
setFormData({
|
||||
@@ -109,6 +128,32 @@ const DetailDevice = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Fungsi untuk mengambil daftar brand
|
||||
const fetchBrands = async () => {
|
||||
setLoadingBrands(true);
|
||||
try {
|
||||
const response = await getAllBrands(new URLSearchParams());
|
||||
if (response && response.data) {
|
||||
setBrands(response.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching brands:', error);
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: error.message || 'Gagal mengambil data brand',
|
||||
});
|
||||
} finally {
|
||||
setLoadingBrands(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.showModal && (props.actionMode === 'add' || props.actionMode === 'edit')) {
|
||||
fetchBrands();
|
||||
}
|
||||
}, [props.showModal, props.actionMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.selectedData) {
|
||||
setFormData(props.selectedData);
|
||||
@@ -142,7 +187,6 @@ const DetailDevice = (props) => {
|
||||
defaultBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
defaultHoverBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -228,6 +272,7 @@ const DetailDevice = (props) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Device Name</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
@@ -239,6 +284,30 @@ const DetailDevice = (props) => {
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Brand Device</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Select
|
||||
name="brand_id"
|
||||
value={formData.brand_id}
|
||||
onChange={(value) => handleSelectChange('brand_id', value)}
|
||||
placeholder="Select Brand Device"
|
||||
disabled={props.readOnly}
|
||||
loading={loadingBrands}
|
||||
style={{ width: '100%' }}
|
||||
allowClear
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
>
|
||||
{brands.map((brand) => (
|
||||
<Select.Option key={brand.brand_id} value={brand.brand_id}>
|
||||
{`${brand.brand_code} - ${brand.brand_name} `}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Device Location</Text>
|
||||
<Input
|
||||
@@ -261,6 +330,16 @@ const DetailDevice = (props) => {
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Listen Channel</Text>
|
||||
<Input
|
||||
name="listen_channel"
|
||||
value={formData.listen_channel}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter Listen Channel"
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Device Description</Text>
|
||||
<TextArea
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Button, ConfigProvider } from 'antd';
|
||||
import { jsPDF } from 'jspdf';
|
||||
import logoPiEnergi from '../../../../assets/images/logo/pi-energi.png';
|
||||
@@ -22,12 +22,12 @@ const GeneratePdf = (props) => {
|
||||
};
|
||||
|
||||
const generatePdf = async () => {
|
||||
const {images, title} = await kopReportPdf(logoPiEnergi, 'COLD WORK PERMIT');
|
||||
const { images, title } = await kopReportPdf(logoPiEnergi, 'COLD WORK PERMIT');
|
||||
|
||||
const doc = new jsPDF({
|
||||
orientation: "portrait",
|
||||
unit: "mm",
|
||||
format: "a4"
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a4',
|
||||
});
|
||||
|
||||
const width = 45;
|
||||
@@ -50,27 +50,27 @@ const GeneratePdf = (props) => {
|
||||
doc.setLineWidth(0.6);
|
||||
doc.line(10, 32.8, 200, 32.8);
|
||||
|
||||
doc.text("Tanggal Pengajuan", 10, 42);
|
||||
doc.text(":", 59, 42);
|
||||
doc.text('Tanggal Pengajuan', 10, 42);
|
||||
doc.text(':', 59, 42);
|
||||
|
||||
doc.text("Deskripsi Pekerjaan", 10, 48);
|
||||
doc.text(":", 59, 48);
|
||||
doc.text('Deskripsi Pekerjaan', 10, 48);
|
||||
doc.text(':', 59, 48);
|
||||
|
||||
doc.text("No. Permit", 10, 54);
|
||||
doc.text(":", 59, 54);
|
||||
doc.text("Spesifik Lokasi", 120, 54);
|
||||
doc.text(":", 160, 54);
|
||||
doc.text('No. Permit', 10, 54);
|
||||
doc.text(':', 59, 54);
|
||||
doc.text('Spesifik Lokasi', 120, 54);
|
||||
doc.text(':', 160, 54);
|
||||
|
||||
doc.text("No. Order", 10, 60);
|
||||
doc.text(":", 59, 60);
|
||||
doc.text("Jum. Personil Terlihat", 120, 60);
|
||||
doc.text(":", 160, 60);
|
||||
doc.text('No. Order', 10, 60);
|
||||
doc.text(':', 59, 60);
|
||||
doc.text('Jum. Personil Terlihat', 120, 60);
|
||||
doc.text(':', 160, 60);
|
||||
|
||||
doc.text("Peralatan yang digunakan", 10, 66);
|
||||
doc.text(":", 59, 66);
|
||||
doc.text('Peralatan yang digunakan', 10, 66);
|
||||
doc.text(':', 59, 66);
|
||||
|
||||
doc.text("Jenis APD yang digunakan", 10, 72);
|
||||
doc.text(":", 59, 72);
|
||||
doc.text('Jenis APD yang digunakan', 10, 72);
|
||||
doc.text(':', 59, 72);
|
||||
|
||||
const blob = doc.output('blob');
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -84,7 +84,7 @@ const GeneratePdf = (props) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
width='60%'
|
||||
width="60%"
|
||||
title="Preview PDF"
|
||||
open={props.showPdf}
|
||||
// open={true}
|
||||
@@ -101,7 +101,6 @@ const GeneratePdf = (props) => {
|
||||
defaultBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
defaultHoverBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -13,8 +13,16 @@ import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../../components/
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { deleteDevice, getAllDevice } from '../../../../api/master-device';
|
||||
import TableList from '../../../../components/Global/TableList';
|
||||
import { getAllBrands } from '../../../../api/master-brand';
|
||||
|
||||
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
{
|
||||
title: 'No',
|
||||
key: 'no',
|
||||
width: '5%',
|
||||
align: 'center',
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'device_id',
|
||||
@@ -27,6 +35,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
dataIndex: 'device_code',
|
||||
key: 'device_code',
|
||||
width: '10%',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
title: 'Device Name',
|
||||
@@ -34,6 +43,13 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
key: 'device_name',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: 'Brand Device',
|
||||
dataIndex: 'brand_name',
|
||||
key: 'brand_name',
|
||||
width: '20%',
|
||||
render: (brand_name) => brand_name || '-'
|
||||
},
|
||||
{
|
||||
title: 'Location',
|
||||
dataIndex: 'device_location',
|
||||
@@ -46,6 +62,13 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
key: 'ip_address',
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: 'Listen Channel',
|
||||
dataIndex: 'listen_channel',
|
||||
key: 'listen_channel',
|
||||
width: '10%',
|
||||
render: (listen_channel) => listen_channel || '-'
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'is_active',
|
||||
@@ -67,7 +90,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Aksi',
|
||||
title: 'Action',
|
||||
key: 'aksi',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
@@ -235,7 +258,7 @@ const ListDevice = memo(function ListDevice(props) {
|
||||
onClick={() => showAddModal()}
|
||||
size="large"
|
||||
>
|
||||
Tambah Data
|
||||
Add data
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</Space>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ListPlantSection from './component/ListPlantSection';
|
||||
import DetailPlantSection from './component/DetailPlantSection';
|
||||
import ListPlantSection from './component/ListPlantSubSection';
|
||||
import DetailPlantSection from './component/DetailPlantSubSection';
|
||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||
import { Typography } from 'antd';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const IndexPlantSection = memo(function IndexPlantSection() {
|
||||
const IndexPlantSubSection = memo(function IndexPlantSubSection() {
|
||||
const navigate = useNavigate();
|
||||
const { setBreadcrumbItems } = useBreadcrumb();
|
||||
|
||||
@@ -71,4 +71,4 @@ const IndexPlantSection = memo(function IndexPlantSection() {
|
||||
);
|
||||
});
|
||||
|
||||
export default IndexPlantSection;
|
||||
export default IndexPlantSubSection;
|
||||
@@ -3,27 +3,49 @@ import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider } fro
|
||||
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||
import { createPlantSection, updatePlantSection } from '../../../../api/master-plant-section';
|
||||
import { validateRun } from '../../../../Utils/validate';
|
||||
import TextArea from 'antd/es/input/TextArea';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const DetailPlantSection = (props) => {
|
||||
const DetailPlantSubSection = (props) => {
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
|
||||
const defaultData = {
|
||||
sub_section_id: '',
|
||||
sub_section_code: '',
|
||||
sub_section_name: '',
|
||||
plant_sub_section_id: '',
|
||||
plant_sub_section_code: '',
|
||||
plant_sub_section_name: '',
|
||||
table_name_value: '', // Fix field name
|
||||
plant_sub_section_description: '',
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
const [formData, setFormData] = useState(defaultData);
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
// Handle different input types
|
||||
let name, value;
|
||||
|
||||
if (e && e.target) {
|
||||
// Standard input
|
||||
name = e.target.name;
|
||||
value = e.target.value;
|
||||
} else if (e && e.type === 'change') {
|
||||
// Switch or other components
|
||||
name = e.name || e.target?.name;
|
||||
value = e.value !== undefined ? e.value : e.checked;
|
||||
} else {
|
||||
// Fallback
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(`📝 Input change: ${name} = ${value}`);
|
||||
|
||||
if (name) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
});
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
@@ -36,7 +58,7 @@ const DetailPlantSection = (props) => {
|
||||
|
||||
// Daftar aturan validasi
|
||||
const validationRules = [
|
||||
{ field: 'sub_section_name', label: 'Plant Sub Section Name', required: true },
|
||||
{ field: 'plant_sub_section_name', label: 'Plant Sub Section Name', required: true },
|
||||
];
|
||||
|
||||
if (
|
||||
@@ -52,14 +74,24 @@ const DetailPlantSection = (props) => {
|
||||
return;
|
||||
|
||||
try {
|
||||
// console.log('💾 Current formData before save:', formData);
|
||||
|
||||
const payload = {
|
||||
plant_sub_section_name: formData.plant_sub_section_name,
|
||||
plant_sub_section_description:
|
||||
formData.plant_sub_section_description &&
|
||||
formData.plant_sub_section_description.trim() !== ''
|
||||
? formData.plant_sub_section_description
|
||||
: ' ',
|
||||
table_name_value: formData.table_name_value, // Fix field name
|
||||
is_active: formData.is_active,
|
||||
sub_section_name: formData.sub_section_name,
|
||||
};
|
||||
|
||||
// console.log('📤 Payload to be sent:', payload);
|
||||
|
||||
const response =
|
||||
props.actionMode === 'edit'
|
||||
? await updatePlantSection(formData.sub_section_id, payload)
|
||||
? await updatePlantSection(formData.plant_sub_section_id, payload)
|
||||
: await createPlantSection(payload);
|
||||
|
||||
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
||||
@@ -98,9 +130,17 @@ const DetailPlantSection = (props) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('🔄 Modal state changed:', {
|
||||
// showModal: props.showModal,
|
||||
// actionMode: props.actionMode,
|
||||
// selectedData: props.selectedData,
|
||||
// });
|
||||
|
||||
if (props.selectedData) {
|
||||
// console.log('📋 Setting form data from selectedData:', props.selectedData);
|
||||
setFormData(props.selectedData);
|
||||
} else {
|
||||
// console.log('📋 Resetting to default data');
|
||||
setFormData(defaultData);
|
||||
}
|
||||
}, [props.showModal, props.selectedData, props.actionMode]);
|
||||
@@ -177,7 +217,7 @@ const DetailPlantSection = (props) => {
|
||||
|
||||
{/* Plant Section Code - Auto Increment & Read Only */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Plant Section Code</Text>
|
||||
<Text strong>Plant Sub Section Code</Text>
|
||||
<Input
|
||||
name="sub_section_code"
|
||||
value={formData.sub_section_code || ''}
|
||||
@@ -195,17 +235,38 @@ const DetailPlantSection = (props) => {
|
||||
<Text strong>Plant Sub Section Name</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Input
|
||||
name="sub_section_name"
|
||||
value={formData.sub_section_name}
|
||||
name="plant_sub_section_name"
|
||||
value={formData.plant_sub_section_name}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter Plant Sub Section Name"
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Table Name Value</Text>
|
||||
<Input
|
||||
name="table_name_value"
|
||||
value={formData.table_name_value}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter Table Name Value (Optional)"
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Description</Text>
|
||||
<TextArea
|
||||
name="plant_sub_section_description"
|
||||
value={formData.plant_sub_section_description}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter Description (Optional)"
|
||||
readOnly={props.readOnly}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailPlantSection;
|
||||
export default DetailPlantSubSection;
|
||||
@@ -14,31 +14,55 @@ import TableList from '../../../../components/Global/TableList';
|
||||
|
||||
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
{
|
||||
title: 'Section Code',
|
||||
dataIndex: 'sub_section_code',
|
||||
key: 'sub_section_code',
|
||||
width: '20%',
|
||||
title: 'No',
|
||||
key: 'no',
|
||||
width: '5%',
|
||||
align: 'center',
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
title: 'Plant Sub Section Code',
|
||||
dataIndex: 'plant_sub_section_code',
|
||||
key: 'plant_sub_section_code',
|
||||
width: '10%',
|
||||
align: 'center',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
title: 'Plant Sub Section Name',
|
||||
dataIndex: 'sub_section_name',
|
||||
key: 'sub_section_name',
|
||||
width: '40%',
|
||||
dataIndex: 'plant_sub_section_name',
|
||||
key: 'plant_sub_section_name',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'plant_sub_section_description',
|
||||
key: 'plant_sub_section_description',
|
||||
width: '30%',
|
||||
render: (text) => text || '-',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'is_active',
|
||||
key: 'is_active',
|
||||
width: '15%',
|
||||
width: '10%',
|
||||
align: 'center',
|
||||
render: (status) => (
|
||||
<Tag color={status ? 'green' : 'red'}>
|
||||
{status ? 'Active' : 'Inactive'}
|
||||
render: (_, { is_active }) => (
|
||||
<>
|
||||
{is_active === true ? (
|
||||
<Tag color={'green'} key={'status'}>
|
||||
Running
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color={'red'} key={'status'}>
|
||||
Offline
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Aksi',
|
||||
title: 'Action',
|
||||
key: 'aksi',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
@@ -46,29 +70,32 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ borderColor: '#1890ff' }}
|
||||
icon={<EyeOutlined style={{ color: '#1890ff' }} />}
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => showPreviewModal(record)}
|
||||
style={{ color: '#1890ff', borderColor: '#1890ff' }}
|
||||
title="View"
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ borderColor: '#faad14' }}
|
||||
icon={<EditOutlined style={{ color: '#faad14' }} />}
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => showEditModal(record)}
|
||||
style={{ color: '#faad14', borderColor: '#faad14' }}
|
||||
title="Edit"
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
style={{ borderColor: 'red' }}
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => showDeleteDialog(record)}
|
||||
style={{ borderColor: '#ff4d4f' }}
|
||||
title="Delete"
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const ListPlantSection = memo(function ListPlantSection(props) {
|
||||
const ListPlantSubSection = memo(function ListPlantSubSection(props) {
|
||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||
const defaultFilter = { criteria: '' };
|
||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||
@@ -121,8 +148,8 @@ const ListPlantSection = memo(function ListPlantSection(props) {
|
||||
NotifConfirmDialog({
|
||||
icon: 'question',
|
||||
title: 'Konfirmasi Hapus',
|
||||
message: 'Plant Section "' + param.sub_section_name + '" akan dihapus?',
|
||||
onConfirm: () => handleDelete(param.sub_section_id),
|
||||
message: `Plant Sub Section "${param.plant_sub_section_name}" akan dihapus?`,
|
||||
onConfirm: () => handleDelete(param.plant_sub_section_id),
|
||||
onCancel: () => props.setSelectedData(null),
|
||||
});
|
||||
};
|
||||
@@ -199,7 +226,7 @@ const ListPlantSection = memo(function ListPlantSection(props) {
|
||||
onClick={() => showAddModal()}
|
||||
size="large"
|
||||
>
|
||||
Tambah Data
|
||||
Add data
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</Space>
|
||||
@@ -210,7 +237,7 @@ const ListPlantSection = memo(function ListPlantSection(props) {
|
||||
<TableList
|
||||
mobile
|
||||
cardColor={'#42AAFF'}
|
||||
header={'sub_section_name'}
|
||||
header={'plant_sub_section_name'}
|
||||
showPreviewModal={showPreviewModal}
|
||||
showEditModal={showEditModal}
|
||||
showDeleteDialog={showDeleteDialog}
|
||||
@@ -226,4 +253,4 @@ const ListPlantSection = memo(function ListPlantSection(props) {
|
||||
);
|
||||
});
|
||||
|
||||
export default ListPlantSection;
|
||||
export default ListPlantSubSection;
|
||||
@@ -1,13 +1,25 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider } from 'antd';
|
||||
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Input,
|
||||
Typography,
|
||||
Switch,
|
||||
Button,
|
||||
ConfigProvider,
|
||||
Divider,
|
||||
TimePicker,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import { NotifOk } from '../../../../components/Global/ToastNotif';
|
||||
import { createShift, updateShift } from '../../../../api/master-shift';
|
||||
import { validateRun } from '../../../../Utils/validate';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
const { Text } = Typography;
|
||||
const timeFormat = 'HH:mm';
|
||||
|
||||
const DetailShift = (props) => {
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
@@ -20,7 +32,15 @@ const DetailShift = (props) => {
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
const [FormData, setFormData] = useState(defaultData);
|
||||
const [formData, setFormData] = useState(defaultData);
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
props.setSelectedData(null);
|
||||
@@ -30,214 +50,150 @@ const DetailShift = (props) => {
|
||||
const handleSave = async () => {
|
||||
setConfirmLoading(true);
|
||||
|
||||
// Validasi required fields
|
||||
if (!FormData.shift_name || FormData.shift_name.trim() === '') {
|
||||
// Daftar aturan validasi
|
||||
const validationRules = [
|
||||
{ field: 'shift_name', label: 'Shift Name', required: true },
|
||||
{ field: 'start_time', label: 'Start Time', required: true },
|
||||
{ field: 'end_time', label: 'End Time', required: true },
|
||||
];
|
||||
|
||||
if (
|
||||
validateRun(formData, validationRules, (errorMessages) => {
|
||||
NotifOk({
|
||||
icon: 'warning',
|
||||
title: 'Peringatan',
|
||||
message: 'Kolom Nama Shift Tidak Boleh Kosong',
|
||||
message: errorMessages,
|
||||
});
|
||||
setConfirmLoading(false);
|
||||
})
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!FormData.start_time || FormData.start_time.trim() === '') {
|
||||
// Validasi format waktu
|
||||
if (!formData.start_time || !formData.end_time) {
|
||||
NotifOk({
|
||||
icon: 'warning',
|
||||
title: 'Peringatan',
|
||||
message: 'Kolom Jam Mulai Tidak Boleh Kosong',
|
||||
});
|
||||
setConfirmLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!FormData.end_time || FormData.end_time.trim() === '') {
|
||||
NotifOk({
|
||||
icon: 'warning',
|
||||
title: 'Peringatan',
|
||||
message: 'Kolom Jam Selesai Tidak Boleh Kosong',
|
||||
});
|
||||
setConfirmLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate time format
|
||||
const timePattern = /^([01]\d|2[0-3]):([0-5]\d)(:[0-5]\d)?$/;
|
||||
if (!timePattern.test(FormData.start_time)) {
|
||||
NotifOk({
|
||||
icon: 'warning',
|
||||
title: 'Peringatan',
|
||||
message:
|
||||
'Format Jam Mulai tidak valid. Gunakan format HH:mm atau HH:mm:ss (contoh: 08:00)',
|
||||
});
|
||||
setConfirmLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!timePattern.test(FormData.end_time)) {
|
||||
NotifOk({
|
||||
icon: 'warning',
|
||||
title: 'Peringatan',
|
||||
message:
|
||||
'Format Jam Selesai tidak valid. Gunakan format HH:mm atau HH:mm:ss (contoh: 17:00)',
|
||||
message: 'Waktu Mulai dan Waktu Selesai wajib diisi.',
|
||||
});
|
||||
setConfirmLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (FormData.shift_id) {
|
||||
// Update existing shift
|
||||
const payload = {
|
||||
shift_name: FormData.shift_name,
|
||||
start_time: FormData.start_time,
|
||||
end_time: FormData.end_time,
|
||||
is_active: FormData.is_active,
|
||||
// Pastikan format waktu HH:mm sesuai validasi BE (regex: /^([01]\d|2[0-3]):([0-5]\d)(:[0-5]\d)?$/)
|
||||
const formatTimeForAPI = (timeValue) => {
|
||||
if (!timeValue) return '';
|
||||
|
||||
// Jika sudah dalam format HH:mm, return langsung
|
||||
if (typeof timeValue === 'string' && timeValue.match(/^\d{2}:\d{2}$/)) {
|
||||
return timeValue;
|
||||
}
|
||||
|
||||
// Parse dengan dayjs dan format ke HH:mm (string murni, bukan Date object)
|
||||
const time = dayjs(timeValue, 'HH:mm', true); // strict mode
|
||||
if (time.isValid()) {
|
||||
return time.format('HH:mm'); // Return string "08:00" bukan Date object
|
||||
}
|
||||
|
||||
// Fallback: coba parse sebagai ISO date dan ambil jam/menitnya (gunakan UTC)
|
||||
const isoTime = dayjs.utc(timeValue);
|
||||
if (isoTime.isValid()) {
|
||||
return isoTime.format('HH:mm');
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const response = await updateShift(FormData.shift_id, payload);
|
||||
console.log('updateShift response:', response);
|
||||
const payload = {
|
||||
shift_name: formData.shift_name,
|
||||
start_time: formatTimeForAPI(formData.start_time),
|
||||
end_time: formatTimeForAPI(formData.end_time),
|
||||
is_active: formData.is_active,
|
||||
};
|
||||
|
||||
// console.log('Payload yang dikirim:', payload);
|
||||
// console.log('Type start_time:', typeof payload.start_time, payload.start_time);
|
||||
// console.log('Type end_time:', typeof payload.end_time, payload.end_time);
|
||||
|
||||
const response =
|
||||
props.actionMode === 'edit'
|
||||
? await updateShift(formData.shift_id, payload)
|
||||
: await createShift(payload);
|
||||
|
||||
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
||||
const action = props.actionMode === 'edit' ? 'diubah' : 'ditambahkan';
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: `Data Shift "${FormData.shift_name}" berhasil diubah.`,
|
||||
message: `Data Shift berhasil ${action}.`,
|
||||
});
|
||||
|
||||
props.setActionMode('list');
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: response.message || 'Gagal mengubah data Shift.',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Create new shift
|
||||
const payload = {
|
||||
shift_name: FormData.shift_name,
|
||||
start_time: FormData.start_time,
|
||||
end_time: FormData.end_time,
|
||||
is_active: FormData.is_active,
|
||||
};
|
||||
|
||||
const response = await createShift(payload);
|
||||
console.log('createShift response:', response);
|
||||
|
||||
if (response.statusCode === 200 || response.statusCode === 201) {
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: `Data Shift "${FormData.shift_name}" berhasil ditambahkan.`,
|
||||
});
|
||||
props.setActionMode('list');
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: response.message || 'Gagal menambahkan data Shift.',
|
||||
message: response?.message || 'Terjadi kesalahan saat menyimpan data.',
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save Shift Error:', error);
|
||||
NotifAlert({
|
||||
NotifOk({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: error.message || 'Terjadi kesalahan saat menyimpan data.',
|
||||
message: error.message || 'Terjadi kesalahan pada server.',
|
||||
});
|
||||
}
|
||||
|
||||
} finally {
|
||||
setConfirmLoading(false);
|
||||
};
|
||||
|
||||
// Helper function to format time input
|
||||
const formatTimeInput = (value) => {
|
||||
if (!value) return value;
|
||||
|
||||
// Remove any whitespace
|
||||
value = value.trim();
|
||||
|
||||
// If user inputs single digit hour like "8:00", convert to "08:00"
|
||||
const timeRegex = /^(\d{1,2}):(\d{2})(:\d{2})?$/;
|
||||
const match = value.match(timeRegex);
|
||||
|
||||
if (match) {
|
||||
const hours = match[1].padStart(2, '0');
|
||||
const minutes = match[2];
|
||||
const seconds = match[3] || '';
|
||||
return `${hours}:${minutes}${seconds}`;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
// Just set the value without formatting during typing
|
||||
const handleTimeChange = (time, _, field) => {
|
||||
// Pastikan format HH:mm yang konsisten sesuai validasi BE
|
||||
const formattedTime = time && time.isValid() ? time.format('HH:mm') : '';
|
||||
setFormData({
|
||||
...FormData,
|
||||
[name]: value,
|
||||
...formData,
|
||||
[field]: formattedTime,
|
||||
});
|
||||
};
|
||||
|
||||
// Format time when user leaves the input field (onBlur)
|
||||
const handleTimeBlur = (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
if (name === 'start_time' || name === 'end_time') {
|
||||
const formattedValue = formatTimeInput(value);
|
||||
const handleStatusToggle = (checked) => {
|
||||
setFormData({
|
||||
...FormData,
|
||||
[name]: formattedValue,
|
||||
...formData,
|
||||
is_active: checked,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusToggle = (isChecked) => {
|
||||
setFormData({
|
||||
...FormData,
|
||||
is_active: isChecked,
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to extract time from ISO timestamp using dayjs
|
||||
const extractTime = (timeString) => {
|
||||
if (!timeString) return '';
|
||||
|
||||
// If it's ISO timestamp like "1970-01-01T08:00:00.000Z"
|
||||
if (timeString.includes('T')) {
|
||||
return dayjs.utc(timeString).format('HH:mm');
|
||||
}
|
||||
|
||||
// If it's already in HH:mm:ss format, remove seconds
|
||||
if (timeString.includes(':')) {
|
||||
const parts = timeString.split(':');
|
||||
return `${parts[0]}:${parts[1]}`;
|
||||
}
|
||||
|
||||
return timeString;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
if (props.selectedData != null) {
|
||||
// Only set fields that are in defaultData
|
||||
const filteredData = {
|
||||
shift_id: props.selectedData.shift_id || '',
|
||||
shift_name: props.selectedData.shift_name || '',
|
||||
start_time: extractTime(props.selectedData.start_time) || '',
|
||||
end_time: extractTime(props.selectedData.end_time) || '',
|
||||
is_active: props.selectedData.is_active ?? true,
|
||||
if (props.selectedData) {
|
||||
// Konversi waktu dari berbagai format ke HH:mm menggunakan dayjs
|
||||
const convertTimeToString = (timeValue) => {
|
||||
if (!timeValue) return '';
|
||||
|
||||
// Jika sudah dalam format HH:mm, return langsung
|
||||
if (typeof timeValue === 'string' && timeValue.match(/^\d{2}:\d{2}$/)) {
|
||||
return timeValue;
|
||||
}
|
||||
|
||||
// Jika dalam format ISO (1970-01-01T08:00:00.000Z), extract jam:menit dalam UTC
|
||||
const time = dayjs.utc(timeValue);
|
||||
if (time.isValid()) {
|
||||
return time.format('HH:mm');
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
setFormData(filteredData);
|
||||
|
||||
setFormData({
|
||||
...props.selectedData,
|
||||
start_time: convertTimeToString(props.selectedData.start_time),
|
||||
end_time: convertTimeToString(props.selectedData.end_time),
|
||||
});
|
||||
} else {
|
||||
setFormData(defaultData);
|
||||
}
|
||||
}
|
||||
}, [props.showModal]);
|
||||
}, [props.showModal, props.selectedData, props.actionMode]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -251,34 +207,27 @@ const DetailShift = (props) => {
|
||||
open={props.showModal}
|
||||
onCancel={handleCancel}
|
||||
footer={[
|
||||
<>
|
||||
<React.Fragment key="modal-footer">
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: { colorBgContainer: '#E9F6EF' },
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: 'white',
|
||||
defaultColor: '#23A55A',
|
||||
defaultBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button onClick={handleCancel}>Batal</Button>
|
||||
<Button onClick={handleCancel}>{props.readOnly ? 'Tutup' : 'Batal'}</Button>
|
||||
</ConfigProvider>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorBgContainer: '#209652',
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: '#23a55a',
|
||||
defaultColor: '#FFFFFF',
|
||||
defaultBorderColor: '#23a55a',
|
||||
defaultHoverColor: '#FFFFFF',
|
||||
defaultHoverBorderColor: '#23a55a',
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -289,85 +238,71 @@ const DetailShift = (props) => {
|
||||
</Button>
|
||||
)}
|
||||
</ConfigProvider>
|
||||
</>,
|
||||
</React.Fragment>,
|
||||
]}
|
||||
>
|
||||
{FormData && (
|
||||
{formData && (
|
||||
<div>
|
||||
<div>
|
||||
{/* Status Toggle */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div>
|
||||
<Text strong>Status</Text>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: '8px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginTop: '8px' }}>
|
||||
<div style={{ marginRight: '8px' }}>
|
||||
<Switch
|
||||
disabled={props.readOnly}
|
||||
style={{
|
||||
backgroundColor:
|
||||
FormData.is_active === true ? '#23A55A' : '#bfbfbf',
|
||||
backgroundColor: formData.is_active ? '#23A55A' : '#bfbfbf',
|
||||
}}
|
||||
checked={FormData.is_active === true}
|
||||
checked={formData.is_active}
|
||||
onChange={handleStatusToggle}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text>{FormData.is_active === true ? 'Active' : 'Inactive'}</Text>
|
||||
<Text>{formData.is_active ? 'Active' : 'Inactive'}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Nama Shift</Text>
|
||||
<Text strong>Shift Name</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Input
|
||||
name="shift_name"
|
||||
value={FormData.shift_name}
|
||||
value={formData.shift_name}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Masukkan Nama Shift"
|
||||
placeholder="Contoh: Pagi, Sore, Malam"
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Jam Mulai</Text>
|
||||
<Text strong>Shift Time</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Input
|
||||
name="start_time"
|
||||
value={FormData.start_time}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Masukkan Jam Mulai"
|
||||
readOnly={props.readOnly}
|
||||
maxLength={8}
|
||||
<Space.Compact block style={{ marginTop: '4px' }}>
|
||||
<TimePicker
|
||||
format={timeFormat}
|
||||
onChange={(time, timeString) =>
|
||||
handleTimeChange(time, timeString, 'start_time')
|
||||
}
|
||||
style={{ width: '50%' }}
|
||||
placeholder="Start Time "
|
||||
disabled={props.readOnly}
|
||||
/>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{ fontSize: '12px', display: 'block', marginTop: '4px' }}
|
||||
>
|
||||
Contoh: 08:00 atau 08:00:00
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Jam Selesai</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Input
|
||||
name="end_time"
|
||||
value={FormData.end_time}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Masukkan Jam Selesai"
|
||||
readOnly={props.readOnly}
|
||||
maxLength={8}
|
||||
<TimePicker
|
||||
value={
|
||||
formData.end_time ? dayjs(formData.end_time, timeFormat) : null
|
||||
}
|
||||
format={timeFormat}
|
||||
onChange={(time, timeString) =>
|
||||
handleTimeChange(time, timeString, 'end_time')
|
||||
}
|
||||
style={{ width: '50%' }}
|
||||
placeholder="End Time "
|
||||
disabled={props.readOnly}
|
||||
/>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{ fontSize: '12px', display: 'block', marginTop: '4px' }}
|
||||
>
|
||||
Contoh: 17:00 atau 17:00:00
|
||||
</Text>
|
||||
</Space.Compact>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,28 +9,22 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { deleteShift, getAllShift } from '../../../../api/master-shift';
|
||||
import TableList from '../../../../components/Global/TableList';
|
||||
import { getAllShift, deleteShift } from '../../../../api/master-shift';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
|
||||
// Helper function to extract time from ISO timestamp
|
||||
const extractTime = (timeString) => {
|
||||
if (!timeString) return '-';
|
||||
dayjs.extend(utc);
|
||||
|
||||
// If it's ISO timestamp like "1970-01-01T08:00:00.000Z"
|
||||
if (timeString.includes('T')) {
|
||||
const date = new Date(timeString);
|
||||
const hours = String(date.getUTCHours()).padStart(2, '0');
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
// Helper function untuk convert ISO time ke HH:mm
|
||||
const formatTime = (timeValue) => {
|
||||
if (!timeValue) return '-';
|
||||
if (typeof timeValue === 'string' && timeValue.match(/^\d{2}:\d{2}$/)) {
|
||||
return timeValue;
|
||||
}
|
||||
|
||||
// If it's already in HH:mm or HH:mm:ss format
|
||||
if (timeString.includes(':')) {
|
||||
const parts = timeString.split(':');
|
||||
return `${parts[0]}:${parts[1]}`; // Return HH:mm only
|
||||
}
|
||||
|
||||
return timeString;
|
||||
// UTC untuk menghindari timezone conversion
|
||||
const time = dayjs.utc(timeValue);
|
||||
return time.isValid() ? time.format('HH:mm') : '-';
|
||||
};
|
||||
|
||||
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
@@ -42,74 +36,72 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
title: 'Nama Shift',
|
||||
title: 'Shift Name',
|
||||
dataIndex: 'shift_name',
|
||||
key: 'shift_name',
|
||||
width: '20%',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: 'Jam Mulai',
|
||||
title: 'Start Time',
|
||||
dataIndex: 'start_time',
|
||||
key: 'start_time',
|
||||
width: '15%',
|
||||
render: (time) => extractTime(time),
|
||||
align: 'center',
|
||||
render: (time) => formatTime(time),
|
||||
},
|
||||
{
|
||||
title: 'Jam Selesai',
|
||||
title: 'End Time',
|
||||
dataIndex: 'end_time',
|
||||
key: 'end_time',
|
||||
width: '15%',
|
||||
render: (time) => extractTime(time),
|
||||
align: 'center',
|
||||
render: (time) => formatTime(time),
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'is_active',
|
||||
key: 'is_active',
|
||||
width: '10%',
|
||||
width: '15%',
|
||||
align: 'center',
|
||||
render: (_, { is_active }) => {
|
||||
const color = is_active ? 'green' : 'red';
|
||||
const text = is_active ? 'Active' : 'Inactive';
|
||||
return (
|
||||
<Tag color={color} key={'status'}>
|
||||
{text}
|
||||
render: (_, { is_active }) => (
|
||||
<>
|
||||
{is_active === true ? (
|
||||
<Tag color={'green'} key={'status'}>
|
||||
Running
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
) : (
|
||||
<Tag color={'red'} key={'status'}>
|
||||
Offline
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Aksi',
|
||||
title: 'Action',
|
||||
key: 'aksi',
|
||||
align: 'center',
|
||||
width: '20%',
|
||||
width: '25%',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
style={{ borderColor: '#1890ff' }}
|
||||
icon={<EyeOutlined style={{ color: '#1890ff' }} />}
|
||||
onClick={() => showPreviewModal(record)}
|
||||
style={{
|
||||
color: '#1890ff',
|
||||
borderColor: '#1890ff',
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
style={{ borderColor: '#faad14' }}
|
||||
icon={<EditOutlined style={{ color: '#faad14' }} />}
|
||||
onClick={() => showEditModal(record)}
|
||||
style={{
|
||||
color: '#faad14',
|
||||
borderColor: '#faad14',
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="text"
|
||||
danger
|
||||
style={{ borderColor: 'red' }}
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => showDeleteDialog(record)}
|
||||
style={{
|
||||
borderColor: '#ff4d4f',
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
@@ -118,19 +110,16 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
|
||||
const ListShift = memo(function ListShift(props) {
|
||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||
|
||||
const defaultFilter = {
|
||||
criteria: '',
|
||||
};
|
||||
const defaultFilter = { criteria: '' };
|
||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
if (props.actionMode == 'list') {
|
||||
if (props.actionMode === 'list') {
|
||||
setFormDataFilter(defaultFilter);
|
||||
doFilter();
|
||||
}
|
||||
} else {
|
||||
@@ -143,14 +132,14 @@ const ListShift = memo(function ListShift(props) {
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
setFormDataFilter((prev) => ({ ...prev, criteria: searchValue }));
|
||||
doFilter();
|
||||
setFormDataFilter({ criteria: searchValue });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleSearchClear = () => {
|
||||
setSearchValue('');
|
||||
setFormDataFilter((prev) => ({ ...prev, criteria: '' }));
|
||||
doFilter();
|
||||
setFormDataFilter({ criteria: '' });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const showPreviewModal = (param) => {
|
||||
@@ -171,53 +160,31 @@ const ListShift = memo(function ListShift(props) {
|
||||
const showDeleteDialog = (param) => {
|
||||
NotifConfirmDialog({
|
||||
icon: 'question',
|
||||
title: 'Konfirmasi',
|
||||
message: `Apakah anda yakin hapus data "${param.shift_name}" ?`,
|
||||
onConfirm: () => handleDelete(param),
|
||||
title: 'Konfirmasi Hapus',
|
||||
message: 'Shift "' + param.shift_name + '" akan dihapus?',
|
||||
onConfirm: () => handleDelete(param.shift_id),
|
||||
onCancel: () => props.setSelectedData(null),
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (param) => {
|
||||
try {
|
||||
const response = await deleteShift(param.shift_id);
|
||||
console.log('deleteShift response:', response);
|
||||
|
||||
const handleDelete = async (shift_id) => {
|
||||
const response = await deleteShift(shift_id);
|
||||
if (response.statusCode === 200) {
|
||||
NotifAlert({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: `Data Shift "${param.shift_name}" berhasil dihapus.`,
|
||||
message: 'Data Shift berhasil dihapus.',
|
||||
});
|
||||
// Refresh table
|
||||
doFilter();
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: response.message || 'Gagal menghapus data Shift.',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete Shift Error:', error);
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: error.message || 'Terjadi kesalahan saat menghapus data.',
|
||||
message: response?.message || 'Gagal Menghapus Data Shift',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Function untuk dipanggil dari DetailShift setelah create/update
|
||||
const refreshData = () => {
|
||||
doFilter();
|
||||
};
|
||||
|
||||
// Pass refresh function to props
|
||||
if (props.setRefreshData) {
|
||||
props.setRefreshData(refreshData);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card>
|
||||
@@ -226,19 +193,19 @@ const ListShift = memo(function ListShift(props) {
|
||||
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
||||
<Col xs={24} sm={24} md={12} lg={12}>
|
||||
<Input.Search
|
||||
placeholder="Search shift by name..."
|
||||
placeholder="Cari berdasarkan nama shift..."
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchValue(value);
|
||||
// Auto search when clearing by backspace/delete
|
||||
if (value === '') {
|
||||
handleSearchClear();
|
||||
}
|
||||
}}
|
||||
onSearch={handleSearch}
|
||||
allowClear
|
||||
onClear={handleSearchClear}
|
||||
allowClear={{
|
||||
clearIcon: <span onClick={handleSearchClear}>✕</span>,
|
||||
}}
|
||||
enterButton={
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -258,14 +225,11 @@ const ListShift = memo(function ListShift(props) {
|
||||
<Space wrap size="small">
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: { colorBgContainer: '#E9F6EF' },
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: 'white',
|
||||
defaultColor: '#23A55A',
|
||||
defaultBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
defaultHoverBorderColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -275,14 +239,14 @@ const ListShift = memo(function ListShift(props) {
|
||||
onClick={() => showAddModal()}
|
||||
size="large"
|
||||
>
|
||||
Tambah Data
|
||||
Add data
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||
<Col xs={24} style={{ marginTop: '16px' }}>
|
||||
<TableList
|
||||
mobile
|
||||
cardColor={'#42AAFF'}
|
||||
|
||||
75
src/pages/master/sparepart/IndexSparepart.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||
import { Typography } from 'antd';
|
||||
import ListSparepart from './component/ListSparepart';
|
||||
import DetailSparepart from './component/DetailSparepart';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const IndexSparepart = memo(function IndexSparepart() {
|
||||
const navigate = useNavigate();
|
||||
const { setBreadcrumbItems } = useBreadcrumb();
|
||||
|
||||
const [actionMode, setActionMode] = useState('list');
|
||||
const [selectedData, setSelectedData] = useState(null);
|
||||
const [readOnly, setReadOnly] = useState(false);
|
||||
const [showModal, setShowmodal] = useState(false);
|
||||
|
||||
const setMode = (param) => {
|
||||
setShowmodal(true);
|
||||
switch (param) {
|
||||
case 'add':
|
||||
setReadOnly(false);
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
setReadOnly(false);
|
||||
break;
|
||||
|
||||
case 'preview':
|
||||
setReadOnly(true);
|
||||
break;
|
||||
|
||||
default:
|
||||
setShowmodal(false);
|
||||
break;
|
||||
}
|
||||
setActionMode(param);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
setBreadcrumbItems([
|
||||
{ title: <Text strong style={{ fontSize: '14px' }}>• Master</Text> },
|
||||
{ title: <Text strong style={{ fontSize: '14px' }}>Sparepart</Text> }
|
||||
]);
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ListSparepart
|
||||
actionMode={actionMode}
|
||||
setActionMode={setMode}
|
||||
selectedData={selectedData}
|
||||
setSelectedData={setSelectedData}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
<DetailSparepart
|
||||
setActionMode={setMode}
|
||||
selectedData={selectedData}
|
||||
setSelectedData={setSelectedData}
|
||||
readOnly={readOnly}
|
||||
showModal={showModal}
|
||||
permitDefault={false}
|
||||
actionMode={actionMode}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default IndexSparepart;
|
||||
591
src/pages/master/sparepart/component/DetailSparepart.jsx
Normal file
@@ -0,0 +1,591 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Input,
|
||||
Select,
|
||||
Divider,
|
||||
Typography,
|
||||
Button,
|
||||
ConfigProvider,
|
||||
Upload,
|
||||
Row,
|
||||
Col,
|
||||
Image,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, EyeOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||
import { createSparepart, updateSparepart } from '../../../../api/sparepart';
|
||||
import { uploadFile } from '../../../../api/file-uploads';
|
||||
import { validateRun } from '../../../../Utils/validate';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
const getBase64 = (file) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
|
||||
const DetailSparepart = (props) => {
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
const [fileList, setFileList] = useState([]);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewImage, setPreviewImage] = useState('');
|
||||
const [previewTitle, setPreviewTitle] = useState('');
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
||||
const defaultData = {
|
||||
sparepart_id: '',
|
||||
sparepart_name: '',
|
||||
sparepart_description: '',
|
||||
sparepart_model: '',
|
||||
sparepart_item_type: null,
|
||||
sparepart_qty: 0,
|
||||
sparepart_unit: '',
|
||||
sparepart_merk: '',
|
||||
sparepart_stok: 'Not Available',
|
||||
sparepart_foto: '',
|
||||
};
|
||||
|
||||
const [formData, setFormData] = useState(defaultData);
|
||||
|
||||
const handleCancel = () => {
|
||||
props.setSelectedData(null);
|
||||
props.setActionMode('list');
|
||||
setFileList([]);
|
||||
};
|
||||
|
||||
const handlePreviewCancel = () => setPreviewOpen(false);
|
||||
|
||||
const handlePreview = async (file) => {
|
||||
if (!file.url && !file.preview) {
|
||||
file.preview = await getBase64(file.originFileObj);
|
||||
}
|
||||
setPreviewImage(file.url || file.preview);
|
||||
setPreviewOpen(true);
|
||||
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
|
||||
};
|
||||
|
||||
const handleChange = ({ fileList: newFileList }) => setFileList(newFileList);
|
||||
|
||||
const handleRemove = () => {
|
||||
setFileList([]);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setConfirmLoading(true);
|
||||
|
||||
const validationRules = [
|
||||
{ field: 'sparepart_name', label: 'Sparepart Name', required: true },
|
||||
];
|
||||
|
||||
if (
|
||||
validateRun(formData, validationRules, (errorMessages) => {
|
||||
NotifOk({ icon: 'warning', title: 'Peringatan', message: errorMessages });
|
||||
setConfirmLoading(false);
|
||||
})
|
||||
)
|
||||
return;
|
||||
|
||||
try {
|
||||
let imageUrl = formData.sparepart_foto;
|
||||
const newFile = fileList.length > 0 ? fileList[0] : null;
|
||||
|
||||
if (newFile && newFile.originFileObj) {
|
||||
// console.log('Uploading file:', newFile.originFileObj);
|
||||
const uploadResponse = await uploadFile(newFile.originFileObj, 'images');
|
||||
|
||||
// Log untuk debugging
|
||||
// console.log('Upload response:', uploadResponse);
|
||||
|
||||
// Cek berbagai kemungkinan struktur respons dari API
|
||||
let uploadedUrl = null;
|
||||
|
||||
// Cek berbagai kemungkinan struktur respons dari API
|
||||
// Cek langsung properti file_url atau url
|
||||
if (uploadResponse && typeof uploadResponse === 'object') {
|
||||
// Cek jika uploadResponse langsung memiliki file_url
|
||||
if (uploadResponse.file_url) {
|
||||
uploadedUrl = uploadResponse.file_url;
|
||||
}
|
||||
// Cek jika uploadResponse memiliki data yang berisi file_url
|
||||
else if (uploadResponse.data && uploadResponse.data.file_url) {
|
||||
uploadedUrl = uploadResponse.data.file_url;
|
||||
}
|
||||
// Cek jika uploadResponse memiliki data yang berisi url
|
||||
else if (uploadResponse.data && uploadResponse.data.url) {
|
||||
uploadedUrl = uploadResponse.data.url;
|
||||
}
|
||||
// Cek jika uploadResponse langsung memiliki url
|
||||
else if (uploadResponse.url) {
|
||||
uploadedUrl = uploadResponse.url;
|
||||
}
|
||||
// Cek jika uploadResponse.data adalah string URL
|
||||
else if (uploadResponse.data && typeof uploadResponse.data === 'string') {
|
||||
uploadedUrl = uploadResponse.data;
|
||||
}
|
||||
// Cek jika uploadResponse.data adalah objek yang berisi file URL dalam format berbeda
|
||||
else if (uploadResponse.data && typeof uploadResponse.data === 'object') {
|
||||
// Cek kemungkinan nama field lain
|
||||
if (uploadResponse.data.file) {
|
||||
uploadedUrl = uploadResponse.data.file;
|
||||
} else if (uploadResponse.data.filename) {
|
||||
// Jika hanya nama file dikembalikan, bangun URL
|
||||
const baseUrl = import.meta.env.VITE_API_SERVER || '';
|
||||
uploadedUrl = `${baseUrl}/uploads/images/${uploadResponse.data.filename}`;
|
||||
} else if (uploadResponse.data.path) {
|
||||
uploadedUrl = uploadResponse.data.path;
|
||||
} else if (uploadResponse.data.location) {
|
||||
uploadedUrl = uploadResponse.data.location;
|
||||
}
|
||||
// Tambahkan kemungkinan lain berdasarkan struktur respons umum
|
||||
else if (uploadResponse.data.filePath) {
|
||||
uploadedUrl = uploadResponse.data.filePath;
|
||||
} else if (uploadResponse.data.file_path) {
|
||||
uploadedUrl = uploadResponse.data.file_path;
|
||||
} else if (uploadResponse.data.publicUrl) {
|
||||
uploadedUrl = uploadResponse.data.publicUrl;
|
||||
} else if (uploadResponse.data.public_url) {
|
||||
uploadedUrl = uploadResponse.data.public_url;
|
||||
}
|
||||
// Berdasarkan log yang ditampilkan, API mengembalikan path_document atau path_solution
|
||||
else if (uploadResponse.data.path_document) {
|
||||
uploadedUrl = uploadResponse.data.path_document;
|
||||
} else if (uploadResponse.data.path_solution) {
|
||||
uploadedUrl = uploadResponse.data.path_solution;
|
||||
} else if (uploadResponse.data.file_upload_name) {
|
||||
// Jika hanya nama file dikembalikan, bangun URL
|
||||
const baseUrl = import.meta.env.VITE_API_SERVER || '';
|
||||
uploadedUrl = `${baseUrl}/uploads/images/${uploadResponse.data.file_upload_name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Jika respons adalah string, mungkin itu adalah URL
|
||||
else if (uploadResponse && typeof uploadResponse === 'string') {
|
||||
uploadedUrl = uploadResponse;
|
||||
}
|
||||
|
||||
if (uploadedUrl) {
|
||||
// console.log('Successfully extracted image URL:', uploadedUrl);
|
||||
imageUrl = uploadedUrl;
|
||||
} else {
|
||||
console.error('Upload response structure:', uploadResponse);
|
||||
console.error('Available properties:', Object.keys(uploadResponse || {}));
|
||||
console.error('Response type:', typeof uploadResponse);
|
||||
console.error(
|
||||
'Is response an object?',
|
||||
uploadResponse && typeof uploadResponse === 'object'
|
||||
);
|
||||
if (uploadResponse && typeof uploadResponse === 'object') {
|
||||
console.error('Response keys:', Object.keys(uploadResponse));
|
||||
console.error(
|
||||
'Response data keys:',
|
||||
uploadResponse.data
|
||||
? Object.keys(uploadResponse.data)
|
||||
: 'No data property'
|
||||
);
|
||||
}
|
||||
|
||||
// Tampilkan notifikasi bahwa upload gagal tapi lanjutkan penyimpanan
|
||||
NotifOk({
|
||||
icon: 'warning',
|
||||
title: 'Peringatan',
|
||||
message: 'Upload gambar gagal. Data akan disimpan tanpa gambar.',
|
||||
});
|
||||
|
||||
// Gunakan URL gambar yang sebelumnya jika ada, atau kosongkan
|
||||
imageUrl = formData.sparepart_foto || '';
|
||||
}
|
||||
} else if (fileList.length === 0) {
|
||||
// Jika tidak ada file di fileList (termasuk saat user menghapus file), gunakan gambar default
|
||||
imageUrl = '/assets/defaultSparepartImg.jpg';
|
||||
}
|
||||
|
||||
// Payload hanya berisi field yang tidak kosong untuk menghindari error validasi
|
||||
const payload = {
|
||||
sparepart_name: formData.sparepart_name, // Wajib
|
||||
};
|
||||
|
||||
payload.sparepart_description =
|
||||
formData.sparepart_description && formData.sparepart_description.trim() !== ''
|
||||
? formData.sparepart_description
|
||||
: ' ';
|
||||
if (formData.sparepart_model && formData.sparepart_model.trim() !== '') {
|
||||
payload.sparepart_model = formData.sparepart_model;
|
||||
}
|
||||
if (formData.sparepart_item_type && formData.sparepart_item_type.trim() !== '') {
|
||||
payload.sparepart_item_type = formData.sparepart_item_type;
|
||||
}
|
||||
if (formData.sparepart_unit && formData.sparepart_unit.trim() !== '') {
|
||||
payload.sparepart_unit = formData.sparepart_unit;
|
||||
}
|
||||
if (formData.sparepart_merk && formData.sparepart_merk.trim() !== '') {
|
||||
payload.sparepart_merk = formData.sparepart_merk;
|
||||
}
|
||||
// sparepart_qty disimpan sebagai angka kuantitas
|
||||
const qty = parseInt(formData.sparepart_qty) || 0;
|
||||
payload.sparepart_qty = qty;
|
||||
|
||||
// sparepart_stok ditentukan otomatis berdasarkan qty sebenarnya
|
||||
payload.sparepart_stok = qty > 0 ? 'Available' : 'Not Available';
|
||||
// Sertakan sparepart_foto hanya jika nilainya tidak kosong, agar tidak memicu validasi
|
||||
if (imageUrl && imageUrl.trim() !== '') {
|
||||
payload.sparepart_foto = imageUrl;
|
||||
}
|
||||
|
||||
// console.log('Sending payload:', payload);
|
||||
|
||||
const response = formData.sparepart_id
|
||||
? await updateSparepart(formData.sparepart_id, payload)
|
||||
: await createSparepart(payload);
|
||||
|
||||
// console.log('API response:', response);
|
||||
|
||||
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: `Data Sparepart berhasil ${
|
||||
formData.sparepart_id ? 'diubah' : 'ditambahkan'
|
||||
}.`,
|
||||
});
|
||||
props.setActionMode('list');
|
||||
setFileList([]);
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: response?.message || 'Terjadi kesalahan saat menyimpan data.',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save Sparepart Error:', error);
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: error.message || 'Terjadi kesalahan pada server. Coba lagi nanti.',
|
||||
});
|
||||
}
|
||||
|
||||
setConfirmLoading(false);
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({ ...formData, [name]: value });
|
||||
};
|
||||
|
||||
const handleSelectChange = (name, value) => {
|
||||
setFormData({ ...formData, [name]: value });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.selectedData) {
|
||||
setFormData(props.selectedData);
|
||||
if (props.selectedData.sparepart_foto) {
|
||||
let displayUrl = props.selectedData.sparepart_foto;
|
||||
|
||||
// Jika URL bukan full URL (tidak mengandung http/https), bangun URL lokal
|
||||
if (!props.selectedData.sparepart_foto.startsWith('http')) {
|
||||
const fileName = props.selectedData.sparepart_foto.split('/').pop();
|
||||
|
||||
// Cek apakah ini file default
|
||||
if (fileName === 'defaultSparepartImg.jpg') {
|
||||
displayUrl = '/assets/defaultSparepartImg.jpg';
|
||||
} else {
|
||||
// Gunakan format file URL seperti di brandDevice
|
||||
const token = localStorage.getItem('token');
|
||||
const baseURL = import.meta.env.VITE_API_SERVER || '';
|
||||
displayUrl = `${baseURL}/file-uploads/images/${encodeURIComponent(
|
||||
fileName
|
||||
)}${token ? `?token=${encodeURIComponent(token)}` : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
const fileName = props.selectedData.sparepart_foto.split('/').pop();
|
||||
|
||||
setFileList([
|
||||
{
|
||||
uid: '-1',
|
||||
name: fileName,
|
||||
status: 'done',
|
||||
url: displayUrl,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setFileList([]);
|
||||
}
|
||||
} else {
|
||||
setFormData(defaultData);
|
||||
setFileList([]);
|
||||
}
|
||||
}, [props.showModal, props.selectedData, props.actionMode]);
|
||||
|
||||
const uploadButton = (
|
||||
<div>
|
||||
<PlusOutlined />
|
||||
<div style={{ marginTop: 8 }}>Upload</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${
|
||||
props.actionMode === 'add'
|
||||
? 'Tambah'
|
||||
: props.actionMode === 'preview'
|
||||
? 'Preview'
|
||||
: 'Edit'
|
||||
} Sparepart`}
|
||||
open={props.showModal}
|
||||
onCancel={handleCancel}
|
||||
footer={[
|
||||
<React.Fragment key="modal-footer">
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: { colorBgContainer: '#E9F6EF' },
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: 'white',
|
||||
defaultColor: '#23A55A',
|
||||
defaultBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
defaultHoverBorderColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button onClick={handleCancel}>Batal</Button>
|
||||
</ConfigProvider>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: { colorBgContainer: '#209652' },
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: '#23a55a',
|
||||
defaultColor: '#FFFFFF',
|
||||
defaultBorderColor: '#23a55a',
|
||||
defaultHoverColor: '#FFFFFF',
|
||||
defaultHoverBorderColor: '#23a55a',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!props.readOnly && (
|
||||
<Button loading={confirmLoading} onClick={handleSave}>
|
||||
Simpan
|
||||
</Button>
|
||||
)}
|
||||
</ConfigProvider>
|
||||
</React.Fragment>,
|
||||
]}
|
||||
>
|
||||
{formData && (
|
||||
<div>
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* Kolom untuk foto */}
|
||||
<Col span={10} style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Text strong>Foto</Text>
|
||||
<div
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{fileList.length > 0 ? (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '180px', // Fixed width for square
|
||||
height: '180px', // Fixed height
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={fileList[0].url || fileList[0].thumbUrl}
|
||||
alt="preview"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
preview={false} // Disable default preview
|
||||
/>
|
||||
{isHovering && !props.readOnly && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: 'white',
|
||||
gap: '16px',
|
||||
fontSize: '20px',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<EyeOutlined
|
||||
onClick={() => handlePreview(fileList[0])}
|
||||
/>
|
||||
<DeleteOutlined onClick={handleRemove} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Upload
|
||||
name="file"
|
||||
multiple={false}
|
||||
fileList={fileList}
|
||||
onChange={handleChange}
|
||||
beforeUpload={() => false}
|
||||
maxCount={1}
|
||||
disabled={props.readOnly}
|
||||
showUploadList={false}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '180px', // Fixed width for square
|
||||
height: '180px',
|
||||
border: '1px dashed #d9d9d9',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
<div>Upload</div>
|
||||
</div>
|
||||
</Upload>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
{/* Kolom untuk field lainnya */}
|
||||
<Col span={14}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Text strong>Sparepart Name</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Input
|
||||
name="sparepart_name"
|
||||
value={formData.sparepart_name}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter Sparepart Name"
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Text strong>Item Type</Text>
|
||||
<Select
|
||||
name="sparepart_item_type"
|
||||
value={formData.sparepart_item_type}
|
||||
onChange={(value) =>
|
||||
handleSelectChange('sparepart_item_type', value)
|
||||
}
|
||||
placeholder="Enter Item Type"
|
||||
disabled={props.readOnly}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Select.Option value="Air Dryer">Air Dryer</Select.Option>
|
||||
<Select.Option value="Compressor">Compressor</Select.Option>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text strong>Qty</Text>
|
||||
<Input
|
||||
name="sparepart_qty"
|
||||
value={formData.sparepart_qty}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter quantity"
|
||||
readOnly={props.readOnly}
|
||||
type="number"
|
||||
min="0"
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text strong>Unit</Text>
|
||||
<Input
|
||||
name="sparepart_unit"
|
||||
value={formData.sparepart_unit}
|
||||
onChange={handleInputChange}
|
||||
placeholder="e.g., pcs"
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||
<Col span={12}>
|
||||
<Text strong>Brand</Text>
|
||||
<Input
|
||||
name="sparepart_merk"
|
||||
value={formData.sparepart_merk}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter Brand (Optional)"
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text strong>Model</Text>
|
||||
<Input
|
||||
name="sparepart_model"
|
||||
value={formData.sparepart_model}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter Model (Optional)"
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||
<Col span={24}>
|
||||
<Text strong>Description</Text>
|
||||
<TextArea
|
||||
name="sparepart_description"
|
||||
value={formData.sparepart_description}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter Description (Optional)"
|
||||
readOnly={props.readOnly}
|
||||
rows={3}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
open={previewOpen}
|
||||
title={previewTitle}
|
||||
footer={null}
|
||||
onCancel={handlePreviewCancel}
|
||||
>
|
||||
<img alt="preview" style={{ width: '100%' }} src={previewImage} />
|
||||
</Modal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailSparepart;
|
||||
292
src/pages/master/sparepart/component/ListSparepart.jsx
Normal file
@@ -0,0 +1,292 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { Space, Tag, ConfigProvider, Button, Row, Col, Card, Input, Segmented } from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
SearchOutlined,
|
||||
AppstoreOutlined,
|
||||
TableOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { deleteSparepart, getAllSparepart } from '../../../../api/sparepart';
|
||||
import TableList from '../../../../components/Global/TableList';
|
||||
import SparepartCardList from './SparepartCardList'; // Import the new custom card component
|
||||
|
||||
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
{
|
||||
title: 'No',
|
||||
key: 'no',
|
||||
width: '5%',
|
||||
align: 'center',
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'sparepart_id',
|
||||
key: 'sparepart_id',
|
||||
width: '5%',
|
||||
hidden: 'true',
|
||||
},
|
||||
{
|
||||
title: 'Sparepart Name',
|
||||
dataIndex: 'sparepart_name',
|
||||
key: 'sparepart_name',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'sparepart_description',
|
||||
key: 'sparepart_description',
|
||||
width: '20%',
|
||||
render: (sparepart_description) => sparepart_description || '-'
|
||||
},
|
||||
{
|
||||
title: 'Model',
|
||||
dataIndex: 'sparepart_model',
|
||||
key: 'sparepart_model',
|
||||
width: '10%',
|
||||
render: (sparepart_model) => sparepart_model || '-'
|
||||
},
|
||||
{
|
||||
title: 'Item Type',
|
||||
dataIndex: 'sparepart_item_type',
|
||||
key: 'sparepart_item_type',
|
||||
width: '10%',
|
||||
render: (sparepart_item_type) => sparepart_item_type || '-'
|
||||
},
|
||||
{
|
||||
title: 'Unit',
|
||||
dataIndex: 'sparepart_unit',
|
||||
key: 'sparepart_unit',
|
||||
width: '8%',
|
||||
render: (sparepart_unit) => sparepart_unit || '-'
|
||||
},
|
||||
{
|
||||
title: 'Merk',
|
||||
dataIndex: 'sparepart_merk',
|
||||
key: 'sparepart_merk',
|
||||
width: '12%',
|
||||
render: (sparepart_merk) => sparepart_merk || '-'
|
||||
},
|
||||
{
|
||||
title: 'Qty',
|
||||
dataIndex: 'sparepart_qty',
|
||||
key: 'sparepart_qty',
|
||||
width: '8%',
|
||||
render: (sparepart_qty) => sparepart_qty || '0'
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'sparepart_stok',
|
||||
key: 'sparepart_stok',
|
||||
width: '8%',
|
||||
render: (sparepart_stok) => sparepart_stok || 'Not Available'
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'aksi',
|
||||
align: 'center',
|
||||
width: '12%',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ borderColor: '#1890ff' }}
|
||||
icon={<EyeOutlined style={{ color: '#1890ff' }} />}
|
||||
onClick={() => showPreviewModal(record)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ borderColor: '#faad14' }}
|
||||
icon={<EditOutlined style={{ color: '#faad14' }} />}
|
||||
onClick={() => showEditModal(record)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
style={{ borderColor: 'red' }}
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => showDeleteDialog(record)}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const ListSparepart = memo(function ListSparepart(props) {
|
||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||
|
||||
const defaultFilter = { criteria: '' };
|
||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
if (props.actionMode === 'list') {
|
||||
setFormDataFilter(defaultFilter);
|
||||
doFilter();
|
||||
}
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, [props.actionMode]);
|
||||
|
||||
const doFilter = () => {
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
setFormDataFilter({ criteria: searchValue });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleSearchClear = () => {
|
||||
setSearchValue('');
|
||||
setFormDataFilter({ criteria: '' });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const showPreviewModal = (param) => {
|
||||
props.setSelectedData(param);
|
||||
props.setActionMode('preview');
|
||||
};
|
||||
|
||||
const showEditModal = (param = null) => {
|
||||
props.setSelectedData(param);
|
||||
props.setActionMode('edit');
|
||||
};
|
||||
|
||||
const showAddModal = (param = null) => {
|
||||
props.setSelectedData(param);
|
||||
props.setActionMode('add');
|
||||
};
|
||||
|
||||
const showDeleteDialog = (param) => {
|
||||
NotifConfirmDialog({
|
||||
icon: 'question',
|
||||
title: 'Konfirmasi Hapus',
|
||||
message: 'Sparepart "' + param.sparepart_name + '" akan dihapus?',
|
||||
onConfirm: () => handleDelete(param.sparepart_id),
|
||||
onCancel: () => props.setSelectedData(null),
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (sparepart_id) => {
|
||||
const response = await deleteSparepart(sparepart_id);
|
||||
|
||||
if (response.statusCode === 200 && response.data === true) {
|
||||
NotifAlert({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: response.message || 'Data Sparepart berhasil dihapus.',
|
||||
});
|
||||
doFilter();
|
||||
} else {
|
||||
NotifOk({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: response?.message || 'Gagal Menghapus Data Sparepart',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card>
|
||||
<Row>
|
||||
<Col xs={24}>
|
||||
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
||||
<Col xs={24} sm={24} md={12} lg={12}>
|
||||
<Input.Search
|
||||
placeholder="Search sparepart by name, model, or merk..."
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchValue(value);
|
||||
if (value === '') {
|
||||
setFormDataFilter({ criteria: '' });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
}
|
||||
}}
|
||||
onSearch={handleSearch}
|
||||
allowClear={{
|
||||
clearIcon: <span onClick={handleSearchClear}>✕</span>,
|
||||
}}
|
||||
enterButton={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
style={{
|
||||
backgroundColor: '#23A55A',
|
||||
borderColor: '#23A55A',
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
}
|
||||
size="large"
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Space wrap size="small">
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: { colorBgContainer: '#E9F6EF' },
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: 'white',
|
||||
defaultColor: '#23A55A',
|
||||
defaultBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
defaultHoverBorderColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => showAddModal()}
|
||||
size="large"
|
||||
>
|
||||
Add data
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||
<TableList
|
||||
mobile
|
||||
cardColor={'#42AAFF'}
|
||||
header={'sparepart_name'}
|
||||
showPreviewModal={showPreviewModal}
|
||||
showEditModal={showEditModal}
|
||||
showDeleteDialog={showDeleteDialog}
|
||||
getData={getAllSparepart}
|
||||
queryParams={formDataFilter}
|
||||
columns={columns(showPreviewModal, showEditModal, showDeleteDialog)}
|
||||
triger={trigerFilter}
|
||||
cardComponent={SparepartCardList} // Pass the custom component here
|
||||
onStockUpdate={doFilter}
|
||||
onGetData={(data) => {
|
||||
if(data && data.length > 0) {
|
||||
console.log('Sample sparepart data from API:', data[0]);
|
||||
console.log('Available fields:', Object.keys(data[0] || {}));
|
||||
}
|
||||
}} // Log untuk debugging field-field yang tersedia
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default ListSparepart;
|
||||
395
src/pages/master/sparepart/component/SparepartCardList.jsx
Normal file
@@ -0,0 +1,395 @@
|
||||
import React, { useState } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { Card, Button, Row, Col, Typography, Divider, Tag, Space, InputNumber, Input } from 'antd';
|
||||
import { EditOutlined, DeleteOutlined, PlusOutlined, MinusOutlined } from '@ant-design/icons';
|
||||
import { updateSparepart } from '../../../../api/sparepart';
|
||||
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
const SparepartCardList = ({
|
||||
data,
|
||||
header,
|
||||
showPreviewModal,
|
||||
showEditModal,
|
||||
showDeleteDialog,
|
||||
fieldColor,
|
||||
cardColor,
|
||||
onStockUpdate, // Prop to refresh the list
|
||||
}) => {
|
||||
const [updateQuantities, setUpdateQuantities] = useState({});
|
||||
const [loadingQuantities, setLoadingQuantities] = useState({});
|
||||
|
||||
const handleQuantityChange = (id, value) => {
|
||||
// Prevent the adjustment from going below the negative value of the original quantity
|
||||
// This ensures the final quantity (original + adjustment) never goes below 0
|
||||
const originalQty = data.find((item) => item.sparepart_id === id)?.sparepart_qty || 0;
|
||||
const maxNegativeAdjustment = -originalQty;
|
||||
|
||||
const clampedValue = Math.max(value, maxNegativeAdjustment);
|
||||
|
||||
const newQuantities = { ...updateQuantities };
|
||||
newQuantities[id] = clampedValue;
|
||||
setUpdateQuantities(newQuantities);
|
||||
};
|
||||
|
||||
const handleUpdateStock = async (item) => {
|
||||
const quantityToAdd = updateQuantities[item.sparepart_id] || 0;
|
||||
if (quantityToAdd === 0) {
|
||||
NotifAlert({
|
||||
icon: 'info',
|
||||
title: 'Info',
|
||||
message: 'Please change the quantity first.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const currentQty = Number(item.sparepart_qty) || 0;
|
||||
const newQty = currentQty + quantityToAdd;
|
||||
if (newQty < 0) {
|
||||
NotifAlert({ icon: 'error', title: 'Error', message: 'Quantity cannot be negative.' });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingQuantities((prev) => ({ ...prev, [item.sparepart_id]: true }));
|
||||
|
||||
// sparepart_qty disimpan sebagai angka kuantitas (update boleh 0 sesuai validasi update schema)
|
||||
const payload = {
|
||||
sparepart_qty: newQty,
|
||||
sparepart_stok: newQty > 0 ? 'Available' : 'Not Available', // Otomatis tentukan status
|
||||
};
|
||||
|
||||
// Hanya tambahkan field jika nilainya tidak kosong untuk menghindari validasi error
|
||||
if (item.sparepart_unit && item.sparepart_unit.trim() !== '') {
|
||||
payload.sparepart_unit = item.sparepart_unit;
|
||||
}
|
||||
if (item.sparepart_merk && item.sparepart_merk.trim() !== '') {
|
||||
payload.sparepart_merk = item.sparepart_merk;
|
||||
}
|
||||
if (item.sparepart_model && item.sparepart_model.trim() !== '') {
|
||||
payload.sparepart_model = item.sparepart_model;
|
||||
}
|
||||
if (item.sparepart_description && item.sparepart_description.trim() !== '') {
|
||||
payload.sparepart_description = item.sparepart_description;
|
||||
}
|
||||
if (item.sparepart_item_type && item.sparepart_item_type !== null) {
|
||||
payload.sparepart_item_type = item.sparepart_item_type;
|
||||
}
|
||||
if (item.sparepart_foto && item.sparepart_foto.trim() !== '') {
|
||||
payload.sparepart_foto = item.sparepart_foto;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await updateSparepart(item.sparepart_id, payload);
|
||||
|
||||
// Periksa apakah response valid sebelum mengakses propertinya
|
||||
if (response && response.statusCode === 200) {
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Success',
|
||||
message: 'Stock updated successfully.',
|
||||
});
|
||||
|
||||
// Cek apakah qty baru kurang dari 1, tampilkan alert
|
||||
if (newQty < 1) {
|
||||
NotifAlert({
|
||||
icon: 'warning',
|
||||
title: 'Low Stock',
|
||||
message: `Warning: Sparepart "${item.sparepart_name}" is out of stock. Please restock immediately.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (onStockUpdate) {
|
||||
onStockUpdate();
|
||||
}
|
||||
handleQuantityChange(item.sparepart_id, 0); // Reset quantity
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Failed',
|
||||
message: response?.message || 'Failed to update stock.',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
message: error.message || 'An error occurred.',
|
||||
});
|
||||
} finally {
|
||||
setLoadingQuantities((prev) => ({ ...prev, [item.sparepart_id]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]} style={{ marginTop: '16px' }}>
|
||||
{data.map((item) => {
|
||||
const quantity = updateQuantities[item.sparepart_id] || 0;
|
||||
const isLoading = loadingQuantities[item.sparepart_id] || false;
|
||||
|
||||
return (
|
||||
<Col xs={24} sm={12} md={8} lg={6} key={item.sparepart_id || item.key}>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
border: `1px solid ${
|
||||
fieldColor ? item[fieldColor] : cardColor || '#E0E0E0'
|
||||
}`,
|
||||
}}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<Row>
|
||||
<Col span={8}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
padding: '16px 8px',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
{item.sparepart_item_type && (
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
{item.sparepart_item_type}
|
||||
</Tag>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#f0f0f0',
|
||||
width: '100%',
|
||||
paddingTop:
|
||||
'100%' /* Ini membuat tinggi sama dengan lebar (aspect ratio 1:1) */,
|
||||
position: 'relative',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
// Debug: log the image path construction
|
||||
let imgSrc;
|
||||
if (item.sparepart_foto) {
|
||||
if (item.sparepart_foto.startsWith('http')) {
|
||||
imgSrc = item.sparepart_foto;
|
||||
} else {
|
||||
// Gunakan format file URL seperti di brandDevice
|
||||
const fileName = item.sparepart_foto
|
||||
.split('/')
|
||||
.pop();
|
||||
|
||||
// Jika filename adalah default file, gunakan dari public assets
|
||||
if (
|
||||
fileName === 'defaultSparepartImg.jpg'
|
||||
) {
|
||||
imgSrc = `/assets/defaultSparepartImg.jpg`;
|
||||
} else {
|
||||
// Gunakan API getFileUrl untuk mendapatkan URL yang benar untuk file upload
|
||||
const token =
|
||||
localStorage.getItem('token');
|
||||
const baseURL =
|
||||
import.meta.env.VITE_API_SERVER ||
|
||||
'';
|
||||
imgSrc = `${baseURL}/file-uploads/images/${encodeURIComponent(
|
||||
fileName
|
||||
)}${
|
||||
token
|
||||
? `?token=${encodeURIComponent(
|
||||
token
|
||||
)}`
|
||||
: ''
|
||||
}`;
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
'Image path being constructed:',
|
||||
imgSrc
|
||||
);
|
||||
} else {
|
||||
imgSrc = 'https://via.placeholder.com/150';
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={imgSrc}
|
||||
alt={item[header]}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover', // Mengisi container dan crop sisi berlebih
|
||||
}}
|
||||
onError={(e) => {
|
||||
console.error(
|
||||
'Image failed to load:',
|
||||
imgSrc
|
||||
);
|
||||
e.target.src =
|
||||
'https://via.placeholder.com/150';
|
||||
}}
|
||||
onLoad={() =>
|
||||
console.log(
|
||||
'Image loaded successfully:',
|
||||
imgSrc
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<div
|
||||
style={{
|
||||
padding: '16px',
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
{showEditModal && (
|
||||
<Button
|
||||
style={{
|
||||
color: '#faad14',
|
||||
borderColor: '#faad14',
|
||||
}}
|
||||
icon={<EditOutlined />}
|
||||
key="edit"
|
||||
onClick={() => showEditModal(item)}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
{showDeleteDialog && (
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
key="delete"
|
||||
onClick={() => showDeleteDialog(item)}
|
||||
size="small"
|
||||
danger
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Title
|
||||
level={5}
|
||||
style={{
|
||||
margin: 0,
|
||||
marginBottom: '8px',
|
||||
paddingRight: '60px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{item[header]}
|
||||
</Title>
|
||||
<Text type="secondary" style={{ display: 'block' }}>
|
||||
Stok: {item.sparepart_stok || 'Not Available'}
|
||||
</Text>
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
|
||||
<Space
|
||||
align="center"
|
||||
style={{
|
||||
marginBottom: '8px',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<Text type="secondary">Qty</Text>
|
||||
<Button
|
||||
icon={<MinusOutlined />}
|
||||
onClick={() =>
|
||||
handleQuantityChange(
|
||||
item.sparepart_id,
|
||||
quantity - 1
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
isLoading || item.sparepart_qty + quantity <= 0
|
||||
}
|
||||
style={{ width: 28, height: 28 }}
|
||||
/>
|
||||
<Text
|
||||
strong
|
||||
style={{ padding: '0 8px', fontSize: '16px' }}
|
||||
>
|
||||
{item.sparepart_qty + (quantity || 0)}
|
||||
</Text>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() =>
|
||||
handleQuantityChange(
|
||||
item.sparepart_id,
|
||||
quantity + 1
|
||||
)
|
||||
}
|
||||
disabled={isLoading}
|
||||
style={{ width: 28, height: 28 }}
|
||||
/>
|
||||
<Text type="secondary">
|
||||
{item.sparepart_unit
|
||||
? ` / ${item.sparepart_unit}`
|
||||
: ' / pcs'}
|
||||
</Text>
|
||||
</Space>
|
||||
|
||||
{quantity !== 0 && (
|
||||
<Button
|
||||
type={quantity === 0 ? 'default' : 'primary'}
|
||||
size="small"
|
||||
style={{ width: '100%' }}
|
||||
onClick={() => handleUpdateStock(item)}
|
||||
loading={isLoading}
|
||||
>
|
||||
Update Stock
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<br />
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
marginTop: '8px',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
>
|
||||
Last updated:{' '}
|
||||
{item.updated_at
|
||||
? dayjs(item.updated_at).format('DD MMM YYYY')
|
||||
: 'N/A'}
|
||||
</Text>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default SparepartCardList;
|
||||
@@ -13,46 +13,73 @@ const IndexStatus = memo(function IndexStatus() {
|
||||
|
||||
const [actionMode, setActionMode] = useState('list');
|
||||
const [selectedData, setSelectedData] = useState(null);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [readOnly, setReadOnly] = useState(false);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const setMode = (param) => {
|
||||
setShowModal(true);
|
||||
switch (param) {
|
||||
case 'add':
|
||||
setReadOnly(false);
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
setReadOnly(false);
|
||||
break;
|
||||
|
||||
case 'preview':
|
||||
setReadOnly(true);
|
||||
break;
|
||||
|
||||
default:
|
||||
setShowModal(false);
|
||||
break;
|
||||
}
|
||||
setActionMode(param);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
setBreadcrumbItems([
|
||||
{ title: <Text strong>• Master</Text> },
|
||||
{ title: <Text strong>Status</Text> }
|
||||
{
|
||||
title: (
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
• Master
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
Status
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, [navigate, setBreadcrumbItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (actionMode === 'add' || actionMode === 'edit' || actionMode === 'preview') {
|
||||
setIsModalVisible(true);
|
||||
setReadOnly(actionMode === 'preview');
|
||||
} else {
|
||||
setIsModalVisible(false);
|
||||
}
|
||||
}, [actionMode]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{actionMode === 'list' &&
|
||||
<ListStatus
|
||||
setActionMode={setActionMode}
|
||||
setSelectedData={setSelectedData}
|
||||
actionMode={actionMode}
|
||||
/>
|
||||
}
|
||||
<DetailStatus
|
||||
showModal={isModalVisible}
|
||||
setActionMode={setActionMode}
|
||||
setActionMode={setMode}
|
||||
selectedData={selectedData}
|
||||
setSelectedData={setSelectedData}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
<DetailStatus
|
||||
setActionMode={setMode}
|
||||
selectedData={selectedData}
|
||||
setSelectedData={setSelectedData}
|
||||
readOnly={readOnly}
|
||||
showModal={showModal}
|
||||
actionMode={actionMode}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Input, Divider, Typography, Button, ConfigProvider, InputNumber, Switch } from 'antd';
|
||||
import {
|
||||
Modal,
|
||||
Input,
|
||||
Divider,
|
||||
Typography,
|
||||
Button,
|
||||
InputNumber,
|
||||
Switch,
|
||||
Row,
|
||||
Col,
|
||||
ColorPicker,
|
||||
} from 'antd';
|
||||
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||
import { validateRun } from '../../../../Utils/validate';
|
||||
import { createStatus, updateStatus } from '../../../../api/master-status';
|
||||
@@ -34,6 +45,10 @@ const DetailStatus = (props) => {
|
||||
setFormData({ ...formData, is_active: checked });
|
||||
};
|
||||
|
||||
const handleColorChange = (color, hex) => {
|
||||
setFormData({ ...formData, status_color: hex });
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
props.setSelectedData(null);
|
||||
props.setActionMode('list');
|
||||
@@ -46,7 +61,6 @@ const DetailStatus = (props) => {
|
||||
{ field: 'status_number', label: 'Status Number', required: true },
|
||||
{ field: 'status_name', label: 'Status Name', required: true },
|
||||
{ field: 'status_color', label: 'Status Color', required: true },
|
||||
{ field: 'status_description', label: 'Description', required: true },
|
||||
];
|
||||
|
||||
if (
|
||||
@@ -67,7 +81,7 @@ const DetailStatus = (props) => {
|
||||
status_number: formData.status_number,
|
||||
status_name: formData.status_name,
|
||||
status_color: formData.status_color,
|
||||
status_description: formData.status_description,
|
||||
status_description: (formData.status_description && formData.status_description.trim() !== '') ? formData.status_description : ' ',
|
||||
is_active: formData.is_active,
|
||||
};
|
||||
|
||||
@@ -114,7 +128,7 @@ const DetailStatus = (props) => {
|
||||
title={
|
||||
<Text style={{ fontSize: '18px' }}>
|
||||
{props.actionMode === 'add'
|
||||
? 'Tambah Data'
|
||||
? 'Add data'
|
||||
: props.actionMode === 'preview'
|
||||
? 'Preview Status'
|
||||
: 'Edit Status'}
|
||||
@@ -124,7 +138,14 @@ const DetailStatus = (props) => {
|
||||
onCancel={handleCancel}
|
||||
footer={
|
||||
!props.readOnly && (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', paddingTop: '15px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '10px',
|
||||
paddingTop: '15px',
|
||||
}}
|
||||
>
|
||||
<Button onClick={handleCancel}>Batal</Button>
|
||||
<Button type="primary" loading={confirmLoading} onClick={handleSave}>
|
||||
Simpan
|
||||
@@ -142,9 +163,13 @@ const DetailStatus = (props) => {
|
||||
checked={formData.is_active}
|
||||
onChange={handleStatusToggle}
|
||||
/>
|
||||
<Text style={{ marginLeft: 8 }}>{formData.is_active ? 'Active' : 'Inactive'}</Text>
|
||||
<Text style={{ marginLeft: 8 }}>
|
||||
{formData.is_active ? 'Active' : 'Inactive'}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Status Number</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
@@ -157,6 +182,8 @@ const DetailStatus = (props) => {
|
||||
onChange={handleInputNumberChange}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Status Name</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
@@ -168,20 +195,47 @@ const DetailStatus = (props) => {
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Status Color</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<Input
|
||||
name="status_color"
|
||||
value={formData.status_color}
|
||||
placeholder="Masukan warna status (e.g., hijau, #00ff00)"
|
||||
readOnly={props.readOnly}
|
||||
onChange={handleInputChange}
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<ColorPicker
|
||||
value={formData.status_color || '#000000'}
|
||||
onChange={handleColorChange}
|
||||
disabled={props.readOnly}
|
||||
showText={(color) => `color hex: ${color.toHexString()}`}
|
||||
allowClear={false}
|
||||
format="hex"
|
||||
style={{ width: '100%' }}
|
||||
presets={[
|
||||
{
|
||||
label: 'Recommended',
|
||||
colors: [
|
||||
'#EF4444', // Merah
|
||||
'#3B82F6', // Biru
|
||||
'#10B981', // Hijau
|
||||
'#F59E0B', // Kuning
|
||||
'#8B5CF6', // Ungu
|
||||
'#EC4899', // Pink
|
||||
'#F97316', // Orange
|
||||
'#14B8A6', // Teal
|
||||
'#6B7280', // Gray
|
||||
'#000000', // Black
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Description</Text>
|
||||
<Text style={{ color: 'red' }}> *</Text>
|
||||
<TextArea
|
||||
name="status_description"
|
||||
value={formData.status_description}
|
||||
|
||||
@@ -1,68 +1,110 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { Space, ConfigProvider, Button, Row, Col, Card, Input, Segmented, Table, Pagination } from 'antd';
|
||||
import { Space, ConfigProvider, Button, Row, Col, Card, Input } from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
SearchOutlined,
|
||||
AppstoreOutlined,
|
||||
TableOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { deleteStatus, getAllStatuss } from '../../../../api/master-status';
|
||||
import TableList from '../../../../components/Global/TableList';
|
||||
|
||||
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
{
|
||||
title: 'No',
|
||||
key: 'no',
|
||||
width: '5%',
|
||||
align: 'center',
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{ title: 'Status Number', dataIndex: 'status_number', key: 'status_number', width: '15%' },
|
||||
{ title: 'Name', dataIndex: 'status_name', key: 'status_name', width: '25%' },
|
||||
{
|
||||
title: 'Color',
|
||||
dataIndex: 'status_color',
|
||||
key: 'status_color',
|
||||
align: 'center',
|
||||
width: '10%',
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="text"
|
||||
style={{ backgroundColor: record.status_color }}
|
||||
onClick={() => showPreviewModal(record)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'status_description',
|
||||
key: 'status_description',
|
||||
width: '40%',
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'aksi',
|
||||
align: 'center',
|
||||
width: '20%',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ borderColor: '#1890ff' }}
|
||||
icon={<EyeOutlined style={{ color: '#1890ff' }} />}
|
||||
onClick={() => showPreviewModal(record)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ borderColor: '#faad14' }}
|
||||
icon={<EditOutlined style={{ color: '#faad14' }} />}
|
||||
onClick={() => showEditModal(record)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
style={{ borderColor: 'red' }}
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => showDeleteDialog(record)}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const ListStatus = memo(function ListStatus(props) {
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [viewMode, setViewMode] = useState('card');
|
||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||
const defaultFilter = { criteria: '' };
|
||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetchData = async (page = 1, pageSize = 10) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', page);
|
||||
params.append('limit', pageSize);
|
||||
if (searchValue) {
|
||||
params.append('search', searchValue);
|
||||
}
|
||||
const response = await getAllStatuss(params);
|
||||
setData(response.data || []);
|
||||
setPagination(prev => ({ ...prev, total: response.paging?.total || 0, current: page, pageSize: pageSize }));
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch status data:", error);
|
||||
setData([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
navigate('/signin');
|
||||
return;
|
||||
if (token) {
|
||||
if (props.actionMode === 'list') {
|
||||
setFormDataFilter(defaultFilter);
|
||||
doFilter();
|
||||
}
|
||||
fetchData(pagination.current, pagination.pageSize);
|
||||
}, [props.actionMode, trigerFilter, navigate]);
|
||||
} else {
|
||||
navigate('/signin');
|
||||
}
|
||||
}, [props.actionMode]);
|
||||
|
||||
const doFilter = () => {
|
||||
setTrigerFilter(prev => !prev);
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleSearch = (value) => {
|
||||
setSearchValue(value);
|
||||
setPagination(prev => ({ ...prev, current: 1 })); // Reset to first page on search
|
||||
doFilter();
|
||||
const handleSearch = () => {
|
||||
setFormDataFilter({ criteria: searchValue });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const handlePaginationChange = (page, pageSize) => {
|
||||
fetchData(page, pageSize);
|
||||
const handleSearchClear = () => {
|
||||
setSearchValue('');
|
||||
setFormDataFilter({ criteria: '' });
|
||||
setTrigerFilter((prev) => !prev);
|
||||
};
|
||||
|
||||
const showPreviewModal = (record) => {
|
||||
@@ -93,40 +135,24 @@ const ListStatus = memo(function ListStatus(props) {
|
||||
try {
|
||||
const response = await deleteStatus(status_id);
|
||||
if (response.statusCode === 200) {
|
||||
NotifAlert({ icon: 'success', title: 'Berhasil', message: 'Data Status berhasil dihapus.' });
|
||||
NotifAlert({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: 'Data Status berhasil dihapus.',
|
||||
});
|
||||
doFilter();
|
||||
} else {
|
||||
NotifAlert({ icon: 'error', title: 'Gagal', message: response?.message || 'Gagal Menghapus Data' });
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
message: response?.message || 'Gagal Menghapus Data',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
NotifAlert({ icon: 'error', title: 'Error', message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: 'Number', dataIndex: 'status_number', key: 'status_number', width: '15%' },
|
||||
{ title: 'Name', dataIndex: 'status_name', key: 'status_name', width: '25%' },
|
||||
{ title: 'Description', dataIndex: 'status_description', key: 'status_description', width: '40%' },
|
||||
{
|
||||
title: 'Aksi', key: 'aksi', align: 'center', width: '20%',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button type="text" icon={<EyeOutlined />} onClick={() => showPreviewModal(record)} />
|
||||
<Button type="text" icon={<EditOutlined />} onClick={() => showEditModal(record)} />
|
||||
<Button danger type="text" icon={<DeleteOutlined />} onClick={() => showDeleteDialog(record)} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const getCardStyle = (color) => {
|
||||
return { border: `2px solid ${color || '#d9d9d9'}`, height: '100%' };
|
||||
};
|
||||
|
||||
const getTitleStyle = (color) => {
|
||||
return { backgroundColor: color || 'transparent', color: '#fff', padding: '2px 8px', borderRadius: '4px', display: 'inline-block' };
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Row justify="space-between" align="middle" gutter={[16, 16]}>
|
||||
@@ -134,7 +160,16 @@ const ListStatus = memo(function ListStatus(props) {
|
||||
<Input.Search
|
||||
placeholder="Search by status name..."
|
||||
onSearch={handleSearch}
|
||||
allowClear
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchValue(value);
|
||||
if (value === '') {
|
||||
handleSearchClear();
|
||||
}
|
||||
}}
|
||||
allowClear={{
|
||||
clearIcon: <span onClick={handleSearchClear}>✕</span>,
|
||||
}}
|
||||
enterButton={
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -163,61 +198,27 @@ const ListStatus = memo(function ListStatus(props) {
|
||||
}}
|
||||
>
|
||||
<Button icon={<PlusOutlined />} onClick={showAddModal} size="large">
|
||||
Tambah Data
|
||||
Add data
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ marginTop: 16 }}>
|
||||
<Col>
|
||||
<Segmented
|
||||
options={[{ value: 'card', icon: <AppstoreOutlined /> }, { value: 'table', icon: <TableOutlined /> }]}
|
||||
value={viewMode}
|
||||
onChange={setViewMode}
|
||||
<Col xs={24} style={{ marginTop: '16px' }}>
|
||||
<TableList
|
||||
mobile
|
||||
cardColor={'#42AAFF'}
|
||||
fieldColor={'status_color'}
|
||||
header={'status_name'}
|
||||
showPreviewModal={showPreviewModal}
|
||||
showEditModal={showEditModal}
|
||||
showDeleteDialog={showDeleteDialog}
|
||||
getData={getAllStatuss}
|
||||
queryParams={formDataFilter}
|
||||
columns={columns(showPreviewModal, showEditModal, showDeleteDialog)}
|
||||
triger={trigerFilter}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ marginTop: 24 }}>
|
||||
{viewMode === 'card' ? (
|
||||
<Row gutter={[16, 16]}>
|
||||
{data.map(item => (
|
||||
<Col xs={24} sm={12} md={8} lg={6} key={item.status_id}>
|
||||
<Card
|
||||
title={<span style={getTitleStyle(item.status_color)}>{item.status_name}</span>}
|
||||
style={getCardStyle(item.status_color)}
|
||||
actions={[
|
||||
<EyeOutlined key="preview" onClick={() => showPreviewModal(item)} />,
|
||||
<EditOutlined key="edit" onClick={() => showEditModal(item)} />,
|
||||
<DeleteOutlined key="delete" onClick={() => showDeleteDialog(item)} />,
|
||||
]}
|
||||
>
|
||||
<p><b>Number:</b> {item.status_number}</p>
|
||||
<p><b>Description:</b> {item.status_description}</p>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : (
|
||||
<>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data.map(item => ({ ...item, key: item.status_id }))}
|
||||
pagination={false}
|
||||
loading={loading}
|
||||
/>
|
||||
<Pagination
|
||||
style={{ marginTop: 16, textAlign: 'right' }}
|
||||
current={pagination.current}
|
||||
pageSize={pagination.pageSize}
|
||||
total={pagination.total}
|
||||
onChange={handlePaginationChange}
|
||||
showSizeChanger
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,13 @@ import TableList from '../../../../components/Global/TableList';
|
||||
import { getAllTag, deleteTag } from '../../../../api/master-tag';
|
||||
|
||||
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
{
|
||||
title: 'No',
|
||||
key: 'no',
|
||||
width: '5%',
|
||||
align: 'center',
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'tag_id',
|
||||
@@ -25,12 +32,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
dataIndex: 'tag_code',
|
||||
key: 'tag_code',
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: 'Tag Name',
|
||||
dataIndex: 'tag_name',
|
||||
key: 'tag_name',
|
||||
width: '15%',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
title: 'Tag Number',
|
||||
@@ -40,22 +42,31 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'Data Type',
|
||||
title: 'Tag Name',
|
||||
dataIndex: 'tag_name',
|
||||
key: 'tag_name',
|
||||
width: '20%',
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Type',
|
||||
dataIndex: 'data_type',
|
||||
key: 'data_type',
|
||||
width: '10%',
|
||||
width: '8%',
|
||||
render: (text) => text || '-',
|
||||
},
|
||||
{
|
||||
title: 'Unit',
|
||||
dataIndex: 'unit',
|
||||
key: 'unit',
|
||||
width: '8%',
|
||||
render: (text) => text || '-',
|
||||
},
|
||||
{
|
||||
title: 'Sub Section',
|
||||
dataIndex: 'sub_section_name',
|
||||
key: 'sub_section_name',
|
||||
width: '12%',
|
||||
title: 'Plant Sub Section',
|
||||
dataIndex: 'plant_sub_section_name',
|
||||
key: 'plant_sub_section_name',
|
||||
width: '10%',
|
||||
render: (text) => text || '-',
|
||||
},
|
||||
{
|
||||
@@ -63,25 +74,31 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
dataIndex: 'device_name',
|
||||
key: 'device_name',
|
||||
width: '12%',
|
||||
render: (text) => text || '-',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'is_active',
|
||||
key: 'is_active',
|
||||
width: '8%',
|
||||
width: '5%',
|
||||
align: 'center',
|
||||
render: (_, { is_active }) => {
|
||||
const color = is_active ? 'green' : 'red';
|
||||
const text = is_active ? 'Active' : 'Inactive';
|
||||
return (
|
||||
<Tag color={color} key={'status'}>
|
||||
{text}
|
||||
render: (_, { is_active }) => (
|
||||
<>
|
||||
{is_active === true ? (
|
||||
<Tag color={'green'} key={'status'}>
|
||||
Running
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
) : (
|
||||
<Tag color={'red'} key={'status'}>
|
||||
Offline
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Aksi',
|
||||
title: 'Action',
|
||||
key: 'aksi',
|
||||
align: 'center',
|
||||
width: '15%',
|
||||
@@ -272,7 +289,7 @@ const ListTag = memo(function ListTag(props) {
|
||||
onClick={() => showAddModal()}
|
||||
size="large"
|
||||
>
|
||||
Tambah Data
|
||||
Add data
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</Space>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider } from 'antd';
|
||||
import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider, Select } from 'antd';
|
||||
import { NotifOk } from '../../../../components/Global/ToastNotif';
|
||||
import { createUnit, updateUnit } from '../../../../api/master-unit';
|
||||
import { validateRun } from '../../../../Utils/validate';
|
||||
@@ -13,6 +13,7 @@ const DetailUnit = (props) => {
|
||||
unit_id: '',
|
||||
unit_code: '',
|
||||
unit_name: '',
|
||||
unit_description: '',
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
@@ -26,10 +27,7 @@ const DetailUnit = (props) => {
|
||||
const handleSave = async () => {
|
||||
setConfirmLoading(true);
|
||||
|
||||
// Daftar aturan validasi
|
||||
const validationRules = [
|
||||
{ field: 'unit_name', label: 'Unit Name', required: true },
|
||||
];
|
||||
const validationRules = [{ field: 'unit_name', label: 'Unit Name', required: true }];
|
||||
|
||||
if (
|
||||
validateRun(formData, validationRules, (errorMessages) => {
|
||||
@@ -40,13 +38,15 @@ const DetailUnit = (props) => {
|
||||
});
|
||||
setConfirmLoading(false);
|
||||
})
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
is_active: formData.is_active,
|
||||
unit_name: formData.unit_name,
|
||||
unit_description: formData.unit_description,
|
||||
is_active: formData.is_active,
|
||||
};
|
||||
|
||||
const response =
|
||||
@@ -116,6 +116,7 @@ const DetailUnit = (props) => {
|
||||
} Unit`}
|
||||
open={props.showModal}
|
||||
onCancel={handleCancel}
|
||||
width={600}
|
||||
footer={[
|
||||
<React.Fragment key="modal-footer">
|
||||
<ConfigProvider
|
||||
@@ -175,13 +176,12 @@ const DetailUnit = (props) => {
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
{/* Unit Code - Auto Increment & Read Only */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Unit Code</Text>
|
||||
<Input
|
||||
name="unit_code"
|
||||
value={formData.unit_code || ''}
|
||||
placeholder={'Unit Code Auto Fill'}
|
||||
placeholder="Unit Code Auto Fill"
|
||||
disabled
|
||||
style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
@@ -202,6 +202,19 @@ const DetailUnit = (props) => {
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>Description</Text>
|
||||
<Input.TextArea
|
||||
name="unit_description"
|
||||
value={formData.unit_description}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter Description (Optional)"
|
||||
readOnly={props.readOnly}
|
||||
rows={4}
|
||||
maxLength={255}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -24,13 +24,21 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
title: 'Unit Code',
|
||||
dataIndex: 'unit_code',
|
||||
key: 'unit_code',
|
||||
width: '20%',
|
||||
width: '10%',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'unit_name',
|
||||
key: 'unit_name',
|
||||
width: '20%',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'unit_description',
|
||||
key: 'unit_description',
|
||||
width: '30%',
|
||||
render: (text) => text || '-',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
@@ -38,18 +46,22 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
||||
key: 'is_active',
|
||||
width: '10%',
|
||||
align: 'center',
|
||||
render: (_, { is_active }) => {
|
||||
const color = is_active ? 'green' : 'red';
|
||||
const text = is_active ? 'Active' : 'Inactive';
|
||||
return (
|
||||
<Tag color={color} key={'status'}>
|
||||
{text}
|
||||
render: (_, { is_active }) => (
|
||||
<>
|
||||
{is_active === true ? (
|
||||
<Tag color={'green'} key={'status'}>
|
||||
Running
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
) : (
|
||||
<Tag color={'red'} key={'status'}>
|
||||
Offline
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Aksi',
|
||||
title: 'Action',
|
||||
key: 'aksi',
|
||||
align: 'center',
|
||||
width: '20%',
|
||||
@@ -152,7 +164,7 @@ const ListUnit = memo(function ListUnit(props) {
|
||||
const handleDelete = async (param) => {
|
||||
try {
|
||||
const response = await deleteUnit(param.unit_id);
|
||||
console.log('deleteUnit response:', response);
|
||||
// console.log('deleteUnit response:', response);
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
NotifAlert({
|
||||
@@ -246,7 +258,7 @@ const ListUnit = memo(function ListUnit(props) {
|
||||
onClick={() => showAddModal()}
|
||||
size="large"
|
||||
>
|
||||
Tambah Data
|
||||
Add Data
|
||||
</Button>
|
||||
</ConfigProvider>
|
||||
</Space>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
|
||||
import { Form, Typography } from 'antd';
|
||||
import { Typography, Row, Col } from 'antd';
|
||||
import ListNotification from './component/ListNotification';
|
||||
import DetailNotification from './component/DetailNotification';
|
||||
|
||||
@@ -10,11 +10,7 @@ const { Text } = Typography;
|
||||
const IndexNotification = memo(function IndexNotification() {
|
||||
const navigate = useNavigate();
|
||||
const { setBreadcrumbItems } = useBreadcrumb();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [actionMode, setActionMode] = useState('list');
|
||||
const [selectedData, setSelectedData] = useState(null);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
@@ -23,7 +19,7 @@ const IndexNotification = memo(function IndexNotification() {
|
||||
{
|
||||
title: (
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
• Notifikasi
|
||||
• Notification
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
@@ -33,39 +29,34 @@ const IndexNotification = memo(function IndexNotification() {
|
||||
}
|
||||
}, [navigate, setBreadcrumbItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (actionMode === 'preview') {
|
||||
setIsModalVisible(true);
|
||||
if (selectedData) {
|
||||
form.setFieldsValue(selectedData);
|
||||
}
|
||||
} else {
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
}
|
||||
}, [actionMode, selectedData, form]);
|
||||
|
||||
const handleCancel = () => {
|
||||
setActionMode('list');
|
||||
const handleCloseDetail = () => {
|
||||
setSelectedData(null);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
// This handler will be passed to ListNotification to update the selected item
|
||||
const handleSelectNotification = (data) => {
|
||||
setSelectedData(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Row gutter={16}>
|
||||
<Col span={selectedData ? 16 : 24}>
|
||||
<ListNotification
|
||||
actionMode={actionMode}
|
||||
setActionMode={setActionMode}
|
||||
selectedData={selectedData}
|
||||
setSelectedData={setSelectedData}
|
||||
// The setActionMode is likely not needed anymore,
|
||||
// but we pass the selection handler
|
||||
setActionMode={() => {}} // Keep prop for safety, but can be empty
|
||||
setSelectedData={handleSelectNotification}
|
||||
/>
|
||||
</Col>
|
||||
{selectedData && (
|
||||
<Col span={8}>
|
||||
<DetailNotification
|
||||
visible={isModalVisible}
|
||||
onCancel={handleCancel}
|
||||
form={form}
|
||||
selectedData={selectedData}
|
||||
onClose={handleCloseDetail}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Modal, Row, Col, Tag, Divider } from 'antd';
|
||||
import { CloseCircleFilled, WarningFilled, CheckCircleFilled, InfoCircleFilled } from '@ant-design/icons';
|
||||
import { Row, Col, Tag, Card, Button } from 'antd';
|
||||
import {
|
||||
CloseCircleFilled,
|
||||
WarningFilled,
|
||||
CheckCircleFilled,
|
||||
InfoCircleFilled,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const DetailNotification = memo(function DetailNotification({ selectedData, onClose }) {
|
||||
if (!selectedData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get error code data from the nested structure
|
||||
const errorCodeData = selectedData.error_code;
|
||||
// Get active solution (is_active: true) or first solution
|
||||
const activeSolution = errorCodeData?.solution?.find(sol => sol.is_active) || errorCodeData?.solution?.[0] || {};
|
||||
const sparepartsData = selectedData.spareparts || errorCodeData?.spareparts || [];
|
||||
|
||||
// Determine notification type based on is_read status
|
||||
const getTypeFromStatus = () => {
|
||||
if (selectedData.is_read === false) return 'critical'; // Not read yet
|
||||
if (selectedData.is_delivered === false) return 'warning'; // Not delivered
|
||||
return 'resolved'; // Read and delivered
|
||||
};
|
||||
|
||||
const DetailNotification = memo(function DetailNotification({ visible, onCancel, form, selectedData }) {
|
||||
const getIconAndColor = (type) => {
|
||||
switch (type) {
|
||||
case 'critical':
|
||||
@@ -36,133 +58,194 @@ const DetailNotification = memo(function DetailNotification({ visible, onCancel,
|
||||
}
|
||||
};
|
||||
|
||||
const { IconComponent, color, bgColor, tagColor } = selectedData ? getIconAndColor(selectedData.type) : {};
|
||||
const notificationType = getTypeFromStatus();
|
||||
const { IconComponent, color, bgColor, tagColor } = getIconAndColor(notificationType);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Card
|
||||
title="Detail Notifikasi"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={onCancel}
|
||||
okText="Tutup"
|
||||
cancelButtonProps={{ style: { display: 'none' } }}
|
||||
width={700}
|
||||
extra={<Button onClick={onClose}>Tutup</Button>}
|
||||
style={{ height: '100%' }}
|
||||
bodyStyle={{ padding: '0 24px' }}
|
||||
>
|
||||
{selectedData && (
|
||||
<div>
|
||||
{/* Header with Icon and Status */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
marginBottom: '24px',
|
||||
padding: '16px',
|
||||
gap: '8px',
|
||||
marginBottom: '0',
|
||||
padding: '2px 0',
|
||||
backgroundColor: '#fafafa',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '64px',
|
||||
height: '64px',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: bgColor,
|
||||
color: color,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '32px',
|
||||
fontSize: '18px',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{IconComponent && <IconComponent style={{ fontSize: '32px' }} />}
|
||||
{IconComponent && <IconComponent style={{ fontSize: '18px' }} />}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Tag color={tagColor} style={{ marginBottom: '8px', fontSize: '12px' }}>
|
||||
{selectedData.type.toUpperCase()}
|
||||
<Tag color={tagColor} style={{ marginBottom: '2px', fontSize: '11px' }}>
|
||||
{notificationType.toUpperCase()}
|
||||
</Tag>
|
||||
<div style={{ fontSize: '16px', fontWeight: 600, color: '#262626' }}>
|
||||
{selectedData.title}
|
||||
<div style={{ fontSize: '14px', fontWeight: 600, color: '#262626' }}>
|
||||
{errorCodeData?.error_code_name || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
|
||||
{/* Information Grid */}
|
||||
<Row gutter={[16, 16]}>
|
||||
<Row gutter={[16, 0]}>
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
|
||||
PLC
|
||||
<div style={{ marginBottom: '2px' }}>
|
||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
|
||||
Kode Error
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
|
||||
{selectedData.plc}
|
||||
<div style={{ fontSize: '13px', color: '#262626', fontWeight: 500 }}>
|
||||
{errorCodeData?.error_code || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>Tag</div>
|
||||
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
|
||||
{selectedData.tag}
|
||||
<div style={{ marginBottom: '2px' }}>
|
||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
|
||||
ID Notifikasi
|
||||
</div>
|
||||
<div style={{ fontSize: '13px', color: '#262626', fontWeight: 500 }}>
|
||||
{selectedData.notification_error_id || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Row gutter={[16, 0]}>
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
|
||||
Engineer
|
||||
<div style={{ marginBottom: '2px' }}>
|
||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
|
||||
Solusi
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
|
||||
{selectedData.engineer}
|
||||
<div style={{ fontSize: '13px', color: '#262626', fontWeight: 500 }}>
|
||||
{activeSolution?.solution_name || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
|
||||
Waktu
|
||||
<div style={{ marginBottom: '2px' }}>
|
||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
|
||||
Waktu Dibuat
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
|
||||
{selectedData.time}
|
||||
<div style={{ fontSize: '13px', color: '#262626', fontWeight: 500 }}>
|
||||
{selectedData.created_at
|
||||
? new Date(selectedData.created_at).toLocaleString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}) + ' WIB'
|
||||
: 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
|
||||
{/* Status */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '8px' }}>Status</div>
|
||||
<Tag color={selectedData.isRead ? 'default' : 'blue'}>
|
||||
{selectedData.isRead ? 'Sudah Dibaca' : 'Belum Dibaca'}
|
||||
{/* Status Information */}
|
||||
<Row gutter={[16, 0]}>
|
||||
<Col span={8}>
|
||||
<div style={{ marginBottom: '2px' }}>
|
||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
|
||||
Status Kirim
|
||||
</div>
|
||||
<Tag color={selectedData.is_send ? 'success' : 'error'}>
|
||||
{selectedData.is_send ? 'Terkirim' : 'Belum Terkirim'}
|
||||
</Tag>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<div style={{ marginBottom: '2px' }}>
|
||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
|
||||
Status Terkirim
|
||||
</div>
|
||||
<Tag color={selectedData.is_delivered ? 'success' : 'warning'}>
|
||||
{selectedData.is_delivered ? 'Terkirim' : 'Belum Terkirim'}
|
||||
</Tag>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<div style={{ marginBottom: '2px' }}>
|
||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
|
||||
Status Baca
|
||||
</div>
|
||||
<Tag color={selectedData.is_read ? 'success' : 'processing'}>
|
||||
{selectedData.is_read ? 'Dibaca' : 'Belum Dibaca'}
|
||||
</Tag>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Additional Info */}
|
||||
{/* Description */}
|
||||
<div style={{ marginTop: '16px', marginBottom: '8px' }}>
|
||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '4px' }}>
|
||||
Deskripsi Error
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: '16px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#f6f9ff',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #d6e4ff',
|
||||
fontSize: '13px',
|
||||
color: '#262626',
|
||||
fontWeight: 500,
|
||||
padding: '8px',
|
||||
backgroundColor: '#fafafa',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '12px', color: '#595959' }}>
|
||||
<strong>Catatan:</strong> Notifikasi ini telah dikirim ke engineer yang bersangkutan
|
||||
untuk ditindaklanjuti sesuai dengan prosedur yang berlaku.
|
||||
{selectedData.message_error_issue || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Spareparts Information */}
|
||||
{sparepartsData.length > 0 && (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '4px' }}>
|
||||
Spareparts Terkait
|
||||
</div>
|
||||
{sparepartsData.map((sparepart, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
padding: '8px',
|
||||
marginBottom: '4px',
|
||||
backgroundColor: '#fafafa',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600, marginBottom: '4px' }}>
|
||||
{sparepart.sparepart_name}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px' }}>
|
||||
Kode: {sparepart.sparepart_code} | Stok:{' '}
|
||||
{sparepart.sparepart_stok}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
95
src/pages/notification/component/LogHistoryCard.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import { Card, Table, Tag, Typography } from 'antd';
|
||||
import { ClockCircleOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const getDummyLogHistory = (notification) => {
|
||||
if (!notification) return [];
|
||||
return [
|
||||
{
|
||||
key: '1',
|
||||
timestamp: dayjs().subtract(2, 'hour').format('DD-MM-YYYY HH:mm:ss'),
|
||||
activity: 'Notification Created',
|
||||
details: `System generated a ${notification.type} notification for: ${notification.issue}`,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
timestamp: dayjs().subtract(1, 'hour').format('DD-MM-YYYY HH:mm:ss'),
|
||||
activity: 'Notification Sent',
|
||||
details: 'Sent to 2 engineers',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
timestamp: dayjs().subtract(30, 'minute').format('DD-MM-YYYY HH:mm:ss'),
|
||||
activity: 'Notification Read',
|
||||
details: 'Read by Engineer A',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
timestamp: dayjs().subtract(5, 'minute').format('DD-MM-YYYY HH:mm:ss'),
|
||||
activity: 'Resend Triggered',
|
||||
details: 'Notification resent by Admin',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Timestamp',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
render: (text) => (
|
||||
<span>
|
||||
<ClockCircleOutlined style={{ marginRight: 8 }} />
|
||||
{text}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Activity',
|
||||
dataIndex: 'activity',
|
||||
key: 'activity',
|
||||
render: (text) => {
|
||||
let color = 'blue';
|
||||
if (text.includes('Created')) {
|
||||
color = 'geekblue';
|
||||
} else if (text.includes('Sent')) {
|
||||
color = 'purple';
|
||||
} else if (text.includes('Read')) {
|
||||
color = 'green';
|
||||
} else if (text.includes('Triggered')) {
|
||||
color = 'orange';
|
||||
}
|
||||
return <Tag color={color}>{text.toUpperCase()}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Details',
|
||||
dataIndex: 'details',
|
||||
key: 'details',
|
||||
},
|
||||
];
|
||||
|
||||
const LogHistoryCard = ({ notificationData }) => {
|
||||
const logHistoryData = getDummyLogHistory(notificationData);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title="Log History"
|
||||
size="small"
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={logHistoryData}
|
||||
pagination={false} // Remove pagination entirely
|
||||
size="small"
|
||||
scroll={{ y: 200 }} // Use scroll for overflow, adjust height as needed
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogHistoryCard;
|
||||
102
src/pages/notification/component/LogHistoryModal.jsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { Modal, Table, Tag, Typography } from 'antd';
|
||||
import { ClockCircleOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// Dummy data untuk log history
|
||||
const getDummyLogHistory = (notification) => {
|
||||
if (!notification) return [];
|
||||
return [
|
||||
{
|
||||
key: '1',
|
||||
timestamp: dayjs().subtract(2, 'hour').format('DD-MM-YYYY HH:mm:ss'),
|
||||
activity: 'Notification Created',
|
||||
details: `System generated a ${notification.type} notification for: ${notification.issue}`,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
timestamp: dayjs().subtract(1, 'hour').format('DD-MM-YYYY HH:mm:ss'),
|
||||
activity: 'Notification Sent',
|
||||
details: 'Sent to 2 engineers',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
timestamp: dayjs().subtract(30, 'minute').format('DD-MM-YYYY HH:mm:ss'),
|
||||
activity: 'Notification Read',
|
||||
details: 'Read by Engineer A',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
timestamp: dayjs().subtract(5, 'minute').format('DD-MM-YYYY HH:mm:ss'),
|
||||
activity: 'Resend Triggered',
|
||||
details: 'Notification resent by Admin',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Timestamp',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
render: (text) => (
|
||||
<span>
|
||||
<ClockCircleOutlined style={{ marginRight: 8 }} />
|
||||
{text}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Activity',
|
||||
dataIndex: 'activity',
|
||||
key: 'activity',
|
||||
render: (text) => {
|
||||
let color = 'blue';
|
||||
if (text.includes('Created')) {
|
||||
color = 'geekblue';
|
||||
} else if (text.includes('Sent')) {
|
||||
color = 'purple';
|
||||
} else if (text.includes('Read')) {
|
||||
color = 'green';
|
||||
} else if (text.includes('Triggered')) {
|
||||
color = 'orange';
|
||||
}
|
||||
return <Tag color={color}>{text.toUpperCase()}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Details',
|
||||
dataIndex: 'details',
|
||||
key: 'details',
|
||||
},
|
||||
];
|
||||
|
||||
const LogHistoryModal = ({ visible, onCancel, notificationData }) => {
|
||||
const logHistoryData = getDummyLogHistory(notificationData);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Text strong>
|
||||
Log History: <Text type="secondary">{notificationData?.title}</Text>
|
||||
</Text>
|
||||
}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={800}
|
||||
destroyOnClose
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={logHistoryData}
|
||||
pagination={{ pageSize: 5 }}
|
||||
style={{ marginTop: 24 }}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogHistoryModal;
|
||||
132
src/pages/notification/component/UserHistoryModal.jsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React from 'react';
|
||||
import { Modal, Typography, Card, Row, Col, Avatar, Tag, Button, Space } from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
PhoneOutlined,
|
||||
CheckCircleOutlined,
|
||||
SyncOutlined,
|
||||
SendOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// Dummy data baru untuk user history
|
||||
const getDummyUsers = (notification) => {
|
||||
if (!notification) return [];
|
||||
return [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Budi Santoso',
|
||||
phone: '081234567890',
|
||||
status: 'delivered',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Citra Lestari',
|
||||
phone: '082345678901',
|
||||
status: 'sent',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Agus Wijaya',
|
||||
phone: '083456789012',
|
||||
status: 'failed',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Dewi Anggraini',
|
||||
phone: '084567890123',
|
||||
status: 'delivered',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const UserHistoryModal = ({ visible, onCancel, notificationData }) => {
|
||||
const userData = getDummyUsers(notificationData);
|
||||
|
||||
const getStatusTag = (status) => {
|
||||
switch (status) {
|
||||
case 'delivered':
|
||||
return (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
Delivered
|
||||
</Tag>
|
||||
);
|
||||
case 'sent':
|
||||
return (
|
||||
<Tag icon={<SyncOutlined spin />} color="processing">
|
||||
Sent
|
||||
</Tag>
|
||||
);
|
||||
case 'failed':
|
||||
return <Tag color="error">Failed</Tag>;
|
||||
default:
|
||||
return <Tag color="default">{status}</Tag>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Text strong style={{ fontSize: '18px' }}>
|
||||
History User Notification
|
||||
</Text>
|
||||
}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="back" onClick={onCancel}>
|
||||
Close
|
||||
</Button>,
|
||||
]}
|
||||
width={600}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ maxHeight: '60vh', overflowY: 'auto', padding: '8px' }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{userData.map((user) => (
|
||||
<Card key={user.id} size="small" style={{ width: '100%' }}>
|
||||
<Row align="middle" justify="space-between">
|
||||
<Col>
|
||||
<Space align="center">
|
||||
<Avatar size="large" icon={<UserOutlined />} />
|
||||
<div>
|
||||
<Text strong>{user.name}</Text>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<PhoneOutlined style={{ color: '#8c8c8c' }} />
|
||||
<Text type="secondary">{user.phone}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col>
|
||||
<Space align="center" size="large">
|
||||
{getStatusTag(user.status)}
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log(`Resend to ${user.name}`);
|
||||
}}
|
||||
>
|
||||
Resend
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserHistoryModal;
|
||||
92
src/pages/notification/detail/UserHistory.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { Button, Row, Col, Card, Badge, Typography, Space, Divider } from 'antd';
|
||||
import {
|
||||
SendOutlined,
|
||||
MobileOutlined,
|
||||
CheckCircleFilled,
|
||||
ArrowLeftOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// Dummy data for user history
|
||||
const userHistoryData = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
phone: '081234567890',
|
||||
status: 'Delivered',
|
||||
timestamp: '04-11-2025 11:40 WIB',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Jane Smith',
|
||||
phone: '087654321098',
|
||||
status: 'Delivered',
|
||||
timestamp: '04-11-2025 11:41 WIB',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Peter Jones',
|
||||
phone: '082345678901',
|
||||
status: 'Delivered',
|
||||
timestamp: '04-11-2025 11:42 WIB',
|
||||
},
|
||||
];
|
||||
|
||||
const UserHistory = ({ notification, onBack }) => {
|
||||
return (
|
||||
<Card>
|
||||
<Row justify="space-between" align="middle" style={{ marginBottom: '20px' }}>
|
||||
<Col>
|
||||
<Space align="center">
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={onBack} />
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
History User Notification
|
||||
</Typography.Title>
|
||||
</Space>
|
||||
<Text type="secondary" style={{ marginLeft: '40px' }}>
|
||||
{notification.title} - {notification.issue}
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
||||
{userHistoryData.map((user) => (
|
||||
<Card
|
||||
key={user.id}
|
||||
style={{ backgroundColor: '#e6f7ff', borderColor: '#91d5ff' }}
|
||||
>
|
||||
<Row align="middle" justify="space-between">
|
||||
<Col>
|
||||
<Space align="center">
|
||||
<Text strong>{user.name}</Text>
|
||||
<Text>|</Text>
|
||||
<Text>
|
||||
<MobileOutlined /> {user.phone}
|
||||
</Text>
|
||||
<Text>|</Text>
|
||||
<Badge status="success" text={user.status} />
|
||||
</Space>
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<Space align="center">
|
||||
<CheckCircleFilled style={{ color: '#52c41a' }} />
|
||||
<Text type="secondary">
|
||||
Success Delivered at {user.timestamp}
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col>
|
||||
<Button type="primary" ghost icon={<SendOutlined />}>
|
||||
Resend
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserHistory;
|
||||
955
src/pages/notificationDetail/IndexNotificationDetail.jsx
Normal file
@@ -0,0 +1,955 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Layout,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Typography,
|
||||
Space,
|
||||
Button,
|
||||
Spin,
|
||||
Result,
|
||||
Input,
|
||||
message,
|
||||
Avatar,
|
||||
Tag,
|
||||
Badge,
|
||||
Divider,
|
||||
} from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
CloseCircleFilled,
|
||||
WarningFilled,
|
||||
CheckCircleFilled,
|
||||
InfoCircleFilled,
|
||||
CloseOutlined,
|
||||
BookOutlined,
|
||||
ToolOutlined,
|
||||
HistoryOutlined,
|
||||
FilePdfOutlined,
|
||||
PlusOutlined,
|
||||
UserOutlined,
|
||||
LoadingOutlined,
|
||||
PhoneOutlined,
|
||||
CheckCircleOutlined,
|
||||
SyncOutlined,
|
||||
SendOutlined,
|
||||
MobileOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
getNotificationDetail,
|
||||
createNotificationLog,
|
||||
getNotificationLogByNotificationId,
|
||||
updateIsRead,
|
||||
resendNotificationToUser,
|
||||
resendChatByUser,
|
||||
} from '../../api/notification';
|
||||
|
||||
const { Content } = Layout;
|
||||
const { Text, Paragraph, Link } = Typography;
|
||||
|
||||
// Transform API response to component format
|
||||
const transformNotificationData = (apiData) => {
|
||||
// Extract nested data
|
||||
const errorCodeData = apiData.error_code;
|
||||
// Get active solution (is_active: true)
|
||||
const activeSolution =
|
||||
errorCodeData?.solution?.find((sol) => sol.is_active) || errorCodeData?.solution?.[0] || {};
|
||||
|
||||
return {
|
||||
id: `notification-${apiData.notification_error_id}-0`,
|
||||
type: apiData.is_read ? 'resolved' : apiData.is_delivered ? 'warning' : 'critical',
|
||||
title: errorCodeData?.error_code_name || 'Unknown Error',
|
||||
issue: errorCodeData?.error_code || 'Unknown Error',
|
||||
description: apiData.message_error_issue || 'No details available',
|
||||
timestamp: apiData.created_at
|
||||
? new Date(apiData.created_at).toLocaleString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}) + ' WIB'
|
||||
: 'N/A',
|
||||
location: apiData.plant_sub_section_name || 'Location not specified',
|
||||
details: apiData.message_error_issue || 'No details available',
|
||||
isRead: apiData.is_read || false,
|
||||
isDelivered: apiData.is_delivered || false,
|
||||
isSend: apiData.is_send || false,
|
||||
status: apiData.is_read ? 'Resolved' : apiData.is_delivered ? 'Delivered' : 'Pending',
|
||||
tag: errorCodeData?.error_code,
|
||||
plc: 'N/A', // PLC not available in API response
|
||||
notification_error_id: apiData.notification_error_id,
|
||||
error_code_id: apiData.error_code_id,
|
||||
error_chanel: apiData.error_chanel,
|
||||
spareparts: errorCodeData?.spareparts || [],
|
||||
solution: {
|
||||
...activeSolution,
|
||||
path_document: activeSolution.path_document
|
||||
? activeSolution.path_document.replace(
|
||||
'/detail-notification/pdf/',
|
||||
'/notification-detail/pdf/'
|
||||
)
|
||||
: activeSolution.path_document,
|
||||
}, // Include the active solution data with fixed URL
|
||||
error_code: errorCodeData,
|
||||
device_info: {
|
||||
device_code: apiData.device_code,
|
||||
device_name: apiData.device_name,
|
||||
device_location: apiData.device_location,
|
||||
brand_name: apiData.brand_name,
|
||||
},
|
||||
users: apiData.users || [],
|
||||
};
|
||||
};
|
||||
|
||||
// Function to get actual users from notification data
|
||||
const getUsersFromNotification = (notification) => {
|
||||
if (!notification || !notification.users) return [];
|
||||
|
||||
return notification.users.map((user) => ({
|
||||
id: user.notification_error_user_id.toString(),
|
||||
name: user.contact_name,
|
||||
phone: user.contact_phone,
|
||||
status: user.is_send ? 'Delivered' : 'Pending',
|
||||
loading: user.loading || false,
|
||||
timestamp: user.updated_at
|
||||
? new Date(user.updated_at)
|
||||
.toLocaleString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
.replace('.', ':') + ' WIB'
|
||||
: 'N/A',
|
||||
}));
|
||||
};
|
||||
|
||||
const getStatusTag = (status) => {
|
||||
switch (status) {
|
||||
case 'delivered':
|
||||
return (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
Delivered
|
||||
</Tag>
|
||||
);
|
||||
case 'sent':
|
||||
return (
|
||||
<Tag icon={<SyncOutlined spin />} color="processing">
|
||||
Sent
|
||||
</Tag>
|
||||
);
|
||||
case 'failed':
|
||||
return <Tag color="error">Failed</Tag>;
|
||||
default:
|
||||
return <Tag color="default">{status}</Tag>;
|
||||
}
|
||||
};
|
||||
|
||||
const getIconAndColor = (type) => {
|
||||
switch (type) {
|
||||
case 'critical':
|
||||
return { IconComponent: CloseCircleFilled, color: '#ff4d4f', bgColor: '#fff1f0' };
|
||||
case 'warning':
|
||||
return { IconComponent: WarningFilled, color: '#faad14', bgColor: '#fffbe6' };
|
||||
case 'resolved':
|
||||
return { IconComponent: CheckCircleFilled, color: '#52c41a', bgColor: '#f6ffed' };
|
||||
default:
|
||||
return { IconComponent: InfoCircleFilled, color: '#1890ff', bgColor: '#e6f7ff' };
|
||||
}
|
||||
};
|
||||
|
||||
const NotificationDetailTab = (props) => {
|
||||
const params = useParams(); // Mungkin perlu disesuaikan jika route berbeda
|
||||
const notificationId = props.id ?? params.notificationId;
|
||||
const navigate = useNavigate();
|
||||
const [notification, setNotification] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [isAddingLog, setIsAddingLog] = useState(false);
|
||||
|
||||
// Log history states
|
||||
const [logHistoryData, setLogHistoryData] = useState([]);
|
||||
const [logLoading, setLogLoading] = useState(false);
|
||||
const [newLogDescription, setNewLogDescription] = useState('');
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
|
||||
// Fetch log history from API
|
||||
const fetchLogHistory = async (notifId) => {
|
||||
try {
|
||||
setLogLoading(true);
|
||||
const response = await getNotificationLogByNotificationId(notifId);
|
||||
if (response && response.data) {
|
||||
// Transform API data to component format
|
||||
const transformedLogs = response.data.map((log) => ({
|
||||
id: log.notification_error_log_id,
|
||||
timestamp: log.created_at
|
||||
? new Date(log.created_at).toLocaleString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}) + ' WIB'
|
||||
: 'N/A',
|
||||
addedBy: {
|
||||
name: log.contact_name || 'Unknown',
|
||||
phone: log.contact_phone || '',
|
||||
},
|
||||
description: log.notification_error_log_description || '',
|
||||
}));
|
||||
setLogHistoryData(transformedLogs);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching log history:', err);
|
||||
} finally {
|
||||
setLogLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle submit new log
|
||||
const handleSubmitLog = async () => {
|
||||
if (!newLogDescription.trim()) {
|
||||
message.warning('Mohon isi deskripsi log terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitLoading(true);
|
||||
const payload = {
|
||||
notification_error_id: parseInt(notificationId),
|
||||
notification_error_log_description: newLogDescription.trim(),
|
||||
};
|
||||
|
||||
const response = await createNotificationLog(payload);
|
||||
|
||||
if (response && response.statusCode === 200) {
|
||||
message.success('Log berhasil ditambahkan');
|
||||
setNewLogDescription('');
|
||||
setIsAddingLog(false);
|
||||
// Refresh log history
|
||||
fetchLogHistory(notificationId);
|
||||
} else {
|
||||
throw new Error(response?.message || 'Gagal menambahkan log');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error submitting log:', err);
|
||||
message.error(err.message || 'Gagal menambahkan log');
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDetail = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Fetch using the actual API
|
||||
const response = await getNotificationDetail(notificationId);
|
||||
|
||||
if (response && response.data) {
|
||||
const transformedData = transformNotificationData(response.data);
|
||||
setNotification(transformedData);
|
||||
|
||||
// Fetch log history
|
||||
fetchLogHistory(notificationId);
|
||||
|
||||
// Fetch using the actual API
|
||||
const resUpdate = await updateIsRead(notificationId);
|
||||
} else {
|
||||
throw new Error('Notification not found');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
console.error('Error fetching notification detail:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDetail();
|
||||
}, [notificationId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Spin size="large" />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !notification) {
|
||||
return (
|
||||
<Layout
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Result
|
||||
status="404"
|
||||
title="404"
|
||||
subTitle="Sorry, the notification you visited does not exist."
|
||||
extra={
|
||||
<Button type="primary" onClick={() => navigate('/notification')}>
|
||||
Back to List
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
const { color } = getIconAndColor(notification.type);
|
||||
|
||||
return (
|
||||
<Layout style={{ padding: '24px', backgroundColor: '#f0f2f5' }}>
|
||||
<Content>
|
||||
<Card>
|
||||
<div
|
||||
style={{
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
paddingBottom: '16px',
|
||||
marginBottom: '24px',
|
||||
}}
|
||||
>
|
||||
{!props.id && (
|
||||
<Row justify="space-between" align="middle">
|
||||
<Col>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/notification')}
|
||||
style={{ paddingLeft: 0 }}
|
||||
>
|
||||
Back to notification list
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
borderRadius: '4px',
|
||||
padding: '8px 16px',
|
||||
textAlign: 'center',
|
||||
marginTop: '16px',
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={4} style={{ margin: 0, color: '#262626' }}>
|
||||
Error Notification Detail
|
||||
</Typography.Title>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Row gutter={[8, 8]}>
|
||||
{/* Kolom Kiri: Data Kompresor */}
|
||||
<Col xs={24} lg={8}>
|
||||
<Card
|
||||
size="small"
|
||||
style={{ height: '100%', borderColor: '#d4380d' }}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="large"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Row gutter={16} align="middle">
|
||||
<Col>
|
||||
<div
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#d4380d',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#ffffff',
|
||||
fontSize: '18px',
|
||||
}}
|
||||
>
|
||||
<CloseOutlined />
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<Text>{notification.title}</Text>
|
||||
<div style={{ marginTop: '2px' }}>
|
||||
<Text strong style={{ fontSize: '16px' }}>
|
||||
Error Code {notification.issue}
|
||||
</Text>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<div>
|
||||
<Text strong>Plant Subsection</Text>
|
||||
<div>{notification.location}</div>
|
||||
<Text
|
||||
strong
|
||||
style={{ display: 'block', marginTop: '8px' }}
|
||||
>
|
||||
Date & Time
|
||||
</Text>
|
||||
<div>{notification.timestamp}</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Kolom Tengah: Informasi Teknis */}
|
||||
<Col xs={24} lg={8}>
|
||||
<Card
|
||||
title="Device Information"
|
||||
size="small"
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="middle"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<div>
|
||||
<Text strong>Error Channel</Text>
|
||||
<div>{notification.error_chanel || 'N/A'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Device Code</Text>
|
||||
<div>
|
||||
{notification.device_info?.device_code || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Device Name</Text>
|
||||
<div>
|
||||
{notification.device_info?.device_name || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Device Location</Text>
|
||||
<div>
|
||||
{notification.device_info?.device_location || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Brand</Text>
|
||||
<div>
|
||||
{notification.device_info?.brand_name || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Kolom Kanan: User History */}
|
||||
<Col xs={24} lg={8}>
|
||||
<Card title="User History" size="small" style={{ height: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
padding: '2px',
|
||||
}}
|
||||
>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size={2}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{getUsersFromNotification(notification).map((user) => (
|
||||
<Card
|
||||
key={user.id}
|
||||
size="small"
|
||||
style={{ width: '100%', margin: 0 }}
|
||||
>
|
||||
<Row align="middle" justify="space-between">
|
||||
<Col>
|
||||
<Space align="center">
|
||||
<Text strong>{user.name}</Text>
|
||||
<Text>|</Text>
|
||||
<Text>
|
||||
<MobileOutlined /> {user.phone}
|
||||
</Text>
|
||||
<Text>|</Text>
|
||||
<Badge
|
||||
status={
|
||||
user.status === 'Delivered'
|
||||
? 'success'
|
||||
: 'default'
|
||||
}
|
||||
text={user.status}
|
||||
/>
|
||||
</Space>
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<Space align="center">
|
||||
{user.status === 'Delivered' ? (
|
||||
<CheckCircleFilled
|
||||
style={{ color: '#52c41a' }}
|
||||
/>
|
||||
) : (
|
||||
<ClockCircleOutlined
|
||||
style={{ color: '#faad14' }}
|
||||
/>
|
||||
)}
|
||||
<Text type="secondary">
|
||||
{user.status === 'Delivered'
|
||||
? 'Success Delivered at'
|
||||
: 'Status '}{' '}
|
||||
{user.timestamp}
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col>
|
||||
<Col>
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<SendOutlined />}
|
||||
onClick={async () => {
|
||||
await resendChatByUser(
|
||||
user.id,
|
||||
user.phone
|
||||
);
|
||||
}}
|
||||
>
|
||||
Resend
|
||||
</Button>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col xs={24} md={8}>
|
||||
<div>
|
||||
<Card
|
||||
hoverable
|
||||
bodyStyle={{ padding: '12px'}}
|
||||
>
|
||||
<Space>
|
||||
<BookOutlined
|
||||
style={{ fontSize: '16px', color: '#1890ff' }}
|
||||
/>
|
||||
<Text
|
||||
strong
|
||||
style={{ fontSize: '16px', color: '#262626' }}
|
||||
>
|
||||
Handling Guideline
|
||||
</Text>
|
||||
</Space>
|
||||
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="small"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{notification.error_code?.solution &&
|
||||
notification.error_code.solution.length > 0 ? (
|
||||
<>
|
||||
{notification.error_code.solution
|
||||
.filter((sol) => sol.is_active) // Hanya tampilkan solusi yang aktif
|
||||
.map((sol, index) => (
|
||||
<div
|
||||
key={
|
||||
sol.brand_code_solution_id ||
|
||||
index
|
||||
}
|
||||
>
|
||||
{sol.path_document ? (
|
||||
<Card
|
||||
size="small"
|
||||
bodyStyle={{
|
||||
padding: '8px 12px',
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
hoverable
|
||||
extra={
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize:
|
||||
'10px',
|
||||
}}
|
||||
>
|
||||
PDF
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent:
|
||||
'space-between',
|
||||
alignItems:
|
||||
'center',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Text
|
||||
style={{
|
||||
fontSize:
|
||||
'12px',
|
||||
color: '#262626',
|
||||
}}
|
||||
>
|
||||
<FilePdfOutlined
|
||||
style={{
|
||||
marginRight:
|
||||
'8px',
|
||||
}}
|
||||
/>{' '}
|
||||
{sol.file_upload_name ||
|
||||
'Solution Document.pdf'}
|
||||
</Text>
|
||||
<Link
|
||||
href={sol.path_document.replace(
|
||||
'/detail-notification/pdf/',
|
||||
'/notification-detail/pdf/'
|
||||
)}
|
||||
target="_blank"
|
||||
style={{
|
||||
fontSize:
|
||||
'12px',
|
||||
display:
|
||||
'block',
|
||||
}}
|
||||
>
|
||||
lihat disini
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
) : null}
|
||||
{sol.type_solution === 'text' &&
|
||||
sol.text_solution ? (
|
||||
<Card
|
||||
size="small"
|
||||
title={
|
||||
<Text strong>
|
||||
{sol.solution_name}:
|
||||
</Text>
|
||||
}
|
||||
bodyStyle={{
|
||||
padding: '8px 12px',
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
extra={
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize:
|
||||
'10px',
|
||||
}}
|
||||
>
|
||||
{sol.type_solution.toUpperCase()}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
marginTop:
|
||||
'4px',
|
||||
}}
|
||||
>
|
||||
{sol.text_solution}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
color: '#8c8c8c',
|
||||
}}
|
||||
>
|
||||
Tidak ada dokumen solusi tersedia
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<div>
|
||||
<Card
|
||||
hoverable
|
||||
bodyStyle={{ padding: '12px'}}
|
||||
>
|
||||
<Space>
|
||||
<ToolOutlined
|
||||
style={{ fontSize: '16px', color: '#1890ff' }}
|
||||
/>
|
||||
<Text
|
||||
strong
|
||||
style={{ fontSize: '16px', color: '#262626' }}
|
||||
>
|
||||
Spare Part
|
||||
</Text>
|
||||
</Space>
|
||||
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="small"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{notification.spareparts &&
|
||||
notification.spareparts.length > 0 ? (
|
||||
notification.spareparts.map((sparepart, index) => (
|
||||
<Card
|
||||
size="small"
|
||||
key={index}
|
||||
bodyStyle={{ padding: '12px' }}
|
||||
hoverable
|
||||
>
|
||||
<Row gutter={16} align="top">
|
||||
<Col
|
||||
span={7}
|
||||
style={{ textAlign: 'center' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '60px',
|
||||
backgroundColor: '#f0f0f0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
<ToolOutlined
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
color: '#bfbfbf',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color:
|
||||
sparepart.sparepart_stok ===
|
||||
'Available' ||
|
||||
sparepart.sparepart_stok ===
|
||||
'available'
|
||||
? '#52c41a'
|
||||
: '#ff4d4f',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{sparepart.sparepart_stok}
|
||||
</Text>
|
||||
</Col>
|
||||
<Col span={17}>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size={4}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Text strong>
|
||||
{sparepart.sparepart_name}
|
||||
</Text>
|
||||
<Paragraph
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
margin: 0,
|
||||
color: '#595959',
|
||||
}}
|
||||
>
|
||||
{sparepart.sparepart_description ||
|
||||
'Deskripsi tidak tersedia'}
|
||||
</Paragraph>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
padding: '4px 8px',
|
||||
fontSize: '11px',
|
||||
color: '#8c8c8c',
|
||||
marginTop: '8px',
|
||||
}}
|
||||
>
|
||||
Kode:{' '}
|
||||
{sparepart.sparepart_code} |
|
||||
Qty:{' '}
|
||||
{sparepart.sparepart_qty} |
|
||||
Unit:{' '}
|
||||
{sparepart.sparepart_unit}
|
||||
</div>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
color: '#8c8c8c',
|
||||
}}
|
||||
>
|
||||
Tidak ada spare parts terkait
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<div>
|
||||
<Card bodyStyle={{ padding: '12px'}}>
|
||||
<Space>
|
||||
<HistoryOutlined
|
||||
style={{ fontSize: '16px', color: '#1890ff' }}
|
||||
/>
|
||||
<Text
|
||||
strong
|
||||
style={{ fontSize: '16px', color: '#262626' }}
|
||||
>
|
||||
Log Activity
|
||||
</Text>
|
||||
</Space>
|
||||
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="small"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Card
|
||||
size="small"
|
||||
bodyStyle={{
|
||||
padding: '8px 12px',
|
||||
backgroundColor: isAddingLog
|
||||
? '#fafafa'
|
||||
: '#fff',
|
||||
}}
|
||||
>
|
||||
<Space
|
||||
direction="vertical"
|
||||
style={{ width: '100%' }}
|
||||
size="small"
|
||||
>
|
||||
{isAddingLog && (
|
||||
<>
|
||||
<Text
|
||||
strong
|
||||
style={{ fontSize: '12px' }}
|
||||
>
|
||||
Add New Log / Update Progress
|
||||
</Text>
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder="Tuliskan update penanganan di sini..."
|
||||
value={newLogDescription}
|
||||
onChange={(e) =>
|
||||
setNewLogDescription(
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
disabled={submitLoading}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
type={isAddingLog ? 'primary' : 'dashed'}
|
||||
size="small"
|
||||
block
|
||||
icon={
|
||||
submitLoading ? (
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
!isAddingLog && <PlusOutlined />
|
||||
)
|
||||
}
|
||||
onClick={
|
||||
isAddingLog
|
||||
? handleSubmitLog
|
||||
: () => setIsAddingLog(true)
|
||||
}
|
||||
loading={submitLoading}
|
||||
disabled={submitLoading}
|
||||
>
|
||||
{isAddingLog ? 'Submit Log' : 'Add Log'}
|
||||
</Button>
|
||||
{isAddingLog && (
|
||||
<Button
|
||||
size="small"
|
||||
block
|
||||
onClick={() => {
|
||||
setIsAddingLog(false);
|
||||
setNewLogDescription('');
|
||||
}}
|
||||
disabled={submitLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
{logHistoryData.map((log) => (
|
||||
<Card
|
||||
key={log.id}
|
||||
size="small"
|
||||
bodyStyle={{
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{ fontSize: '12px', margin: 0 }}
|
||||
// ellipsis={{ rows: 2 }}
|
||||
>
|
||||
<Text strong>{log.addedBy.name}:</Text>{' '}
|
||||
{log.description}
|
||||
</Paragraph>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{ fontSize: '11px' }}
|
||||
>
|
||||
{log.timestamp}
|
||||
</Text>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
</Card>
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationDetailTab;
|
||||