Compare commits
29 Commits
lavoce
...
4226a24e79
| Author | SHA1 | Date | |
|---|---|---|---|
| 4226a24e79 | |||
| 038009433f | |||
| 64ba51b17c | |||
| 47d0638a42 | |||
| d8c5f3ed44 | |||
| affd9146bb | |||
| 4022b3f8f4 | |||
| 446a4e2b95 | |||
| 83a475c708 | |||
| ab1c510a77 | |||
| 59859c6d18 | |||
| 2bd27937dc | |||
| 1058c660d6 | |||
| 35b2167791 | |||
| ec676983d0 | |||
| c07c5f8235 | |||
| b32ad97034 | |||
| 76244f6f6e | |||
| 0a128cbb3c | |||
| bd4ab26680 | |||
| 3e728a1ff5 | |||
| 9db143972e | |||
| 029ea269a7 | |||
| 4cdaa042da | |||
| 56af2a16c0 | |||
| deadf2ffb4 | |||
| 4da80c7089 | |||
| 56e3ce78a6 | |||
| 7c2a019dd2 |
1
.gitignore
vendored
@@ -6,6 +6,7 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
*.config
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
|||||||
@@ -22,8 +22,7 @@
|
|||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"jspdf": "^3.0.4",
|
"jspdf": "^3.0.1",
|
||||||
"jspdf-autotable": "^5.0.2",
|
|
||||||
"mqtt": "^5.14.0",
|
"mqtt": "^5.14.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -31,7 +30,6 @@
|
|||||||
"react-icons": "^4.11.0",
|
"react-icons": "^4.11.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"react-svg": "^16.3.0",
|
"react-svg": "^16.3.0",
|
||||||
"recharts": "^3.6.0",
|
|
||||||
"sweetalert2": "^11.17.2"
|
"sweetalert2": "^11.17.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 78 KiB |
@@ -3,7 +3,7 @@
|
|||||||
<system.webServer>
|
<system.webServer>
|
||||||
<rewrite>
|
<rewrite>
|
||||||
<rules>
|
<rules>
|
||||||
<rule name="reactViteSypiu">
|
<rule name="CallOfDuty">
|
||||||
<match url=".*" />
|
<match url=".*" />
|
||||||
<conditions logicalGrouping="MatchAll">
|
<conditions logicalGrouping="MatchAll">
|
||||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
||||||
|
|||||||
21
src/App.jsx
@@ -36,7 +36,7 @@ import IndexNotification from './pages/notification/IndexNotification';
|
|||||||
import IndexRole from './pages/role/IndexRole';
|
import IndexRole from './pages/role/IndexRole';
|
||||||
import IndexUser from './pages/user/IndexUser';
|
import IndexUser from './pages/user/IndexUser';
|
||||||
import IndexContact from './pages/contact/IndexContact';
|
import IndexContact from './pages/contact/IndexContact';
|
||||||
import DetailNotificationTab from './pages/notificationDetail/IndexNotificationDetail';
|
import DetailNotificationTab from './pages/detailNotification/IndexDetailNotification';
|
||||||
import IndexVerificationSparepart from './pages/verificationSparepart/IndexVerificationSparepart';
|
import IndexVerificationSparepart from './pages/verificationSparepart/IndexVerificationSparepart';
|
||||||
|
|
||||||
import SvgTest from './pages/home/SvgTest';
|
import SvgTest from './pages/home/SvgTest';
|
||||||
@@ -51,10 +51,6 @@ import SvgAirDryerC from './pages/home/SvgAirDryerC';
|
|||||||
import IndexHistoryAlarm from './pages/history/alarm/IndexHistoryAlarm';
|
import IndexHistoryAlarm from './pages/history/alarm/IndexHistoryAlarm';
|
||||||
import IndexHistoryEvent from './pages/history/event/IndexHistoryEvent';
|
import IndexHistoryEvent from './pages/history/event/IndexHistoryEvent';
|
||||||
|
|
||||||
// Image Viewer
|
|
||||||
import ImageViewer from './Utils/ImageViewer';
|
|
||||||
import RedirectWa from './pages/blank/RedirectWa';
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
@@ -65,7 +61,7 @@ const App = () => {
|
|||||||
<Route path="/signup" element={<SignUp />} />
|
<Route path="/signup" element={<SignUp />} />
|
||||||
<Route path="/svg" element={<SvgTest />} />
|
<Route path="/svg" element={<SvgTest />} />
|
||||||
<Route
|
<Route
|
||||||
path="/notification-detail/:notificationId"
|
path="/detail-notification/:notificationId"
|
||||||
element={<DetailNotificationTab />}
|
element={<DetailNotificationTab />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
@@ -73,16 +69,12 @@ const App = () => {
|
|||||||
element={<IndexVerificationSparepart />}
|
element={<IndexVerificationSparepart />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path="/redirect" element={<RedirectWa />} />
|
|
||||||
|
|
||||||
{/* Protected Routes */}
|
{/* Protected Routes */}
|
||||||
<Route path="/dashboard" element={<ProtectedRoute />}>
|
<Route path="/dashboard" element={<ProtectedRoute />}>
|
||||||
<Route path="home" element={<Home />} />
|
<Route path="home" element={<Home />} />
|
||||||
<Route path="blank" element={<Blank />} />
|
<Route path="blank" element={<Blank />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/image-viewer/:fileName" element={<ImageViewer />} />
|
|
||||||
|
|
||||||
<Route path="/dashboard-svg" element={<ProtectedRoute />}>
|
<Route path="/dashboard-svg" element={<ProtectedRoute />}>
|
||||||
<Route path="overview-compressor" element={<SvgOverviewCompressor />} />
|
<Route path="overview-compressor" element={<SvgOverviewCompressor />} />
|
||||||
<Route path="compressor-a" element={<SvgCompressorA />} />
|
<Route path="compressor-a" element={<SvgCompressorA />} />
|
||||||
@@ -99,11 +91,6 @@ const App = () => {
|
|||||||
<Route path="tag" element={<IndexTag />} />
|
<Route path="tag" element={<IndexTag />} />
|
||||||
<Route path="unit" element={<IndexUnit />} />
|
<Route path="unit" element={<IndexUnit />} />
|
||||||
<Route path="sparepart" element={<IndexSparepart />} />
|
<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" element={<IndexBrandDevice />} />
|
||||||
<Route path="brand-device/add" element={<AddBrandDevice />} />
|
<Route path="brand-device/add" element={<AddBrandDevice />} />
|
||||||
<Route path="brand-device/edit/:id" element={<EditBrandDevice />} />
|
<Route path="brand-device/edit/:id" element={<EditBrandDevice />} />
|
||||||
@@ -120,6 +107,9 @@ const App = () => {
|
|||||||
path="brand-device/view/temp/files/:fileName"
|
path="brand-device/view/temp/files/:fileName"
|
||||||
element={<ViewFilePage />}
|
element={<ViewFilePage />}
|
||||||
/>
|
/>
|
||||||
|
<Route path="plant-sub-section" element={<IndexPlantSubSection />} />
|
||||||
|
<Route path="shift" element={<IndexShift />} />
|
||||||
|
<Route path="status" element={<IndexStatus />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/report" element={<ProtectedRoute />}>
|
<Route path="/report" element={<ProtectedRoute />}>
|
||||||
@@ -152,6 +142,7 @@ const App = () => {
|
|||||||
<Route index element={<IndexJadwalShift />} />
|
<Route index element={<IndexJadwalShift />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
{/* Catch-all */}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -1,248 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -47,63 +47,4 @@ const deleteBrand = async (id) => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getErrorCodesByBrandId = async (brandId, queryParams) => {
|
export { getAllBrands, getBrandById, createBrand, updateBrand, deleteBrand };
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -18,88 +18,4 @@ const getNotificationById = async (id) => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNotificationDetail = async (id) => {
|
export { getAllNotification, getNotificationById };
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -26,29 +26,27 @@
|
|||||||
<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"/>
|
<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>
|
</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"/>
|
<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>
|
<g transform="matrix(0.826913, 0, 0, 0.698383, 0.443817, 3.138935)">
|
||||||
<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);"/>
|
<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);"/>
|
||||||
<g>
|
<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="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="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);"/>
|
||||||
<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: 1; fill: rgb(243, 243, 243);" cx="773.446" cy="384.7" rx="11.751" ry="11.009"/>
|
||||||
<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>
|
</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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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)">°C</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; font-weight: bold;" x="748" y="347.676" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER</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: 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"/>
|
<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"/>
|
||||||
@@ -110,12 +108,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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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: 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>
|
<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;">
|
<g transform="matrix(-0.387768, 0, 0, -0.200385, 743.634644, -199.991287)" style="transform-origin: 72.2405px 412.5px;">
|
||||||
@@ -179,17 +177,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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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);"/>
|
<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>
|
<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>
|
||||||
@@ -207,15 +205,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>
|
<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);"/>
|
<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="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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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="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="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;"/>
|
<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>
|
<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);"/>
|
<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);"/>
|
||||||
@@ -229,33 +227,12 @@
|
|||||||
<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);"/>
|
<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"/>
|
<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>
|
<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="336.418" cy="237.483" rx="13.582" ry="12.517"/>
|
<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"/>
|
||||||
<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>
|
<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(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"/>
|
||||||
<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>
|
<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(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.772" cy="233.876" rx="13.582" ry="12.517"/>
|
<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"/>
|
||||||
<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>
|
<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"/>
|
<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: 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>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
@@ -3,7 +3,7 @@
|
|||||||
<defs>
|
<defs>
|
||||||
<bx:grid x="0" y="0" width="25" height="25"/>
|
<bx:grid x="0" y="0" width="25" height="25"/>
|
||||||
</defs>
|
</defs>
|
||||||
<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"/>
|
<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"/>
|
||||||
<g transform="matrix(0.826913, 0, 0, 0.698383, 74.03907, 53.375034)">
|
<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); 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"/>
|
<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,26 +26,27 @@
|
|||||||
<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"/>
|
<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>
|
</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"/>
|
<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>
|
<g transform="matrix(0.826913, 0, 0, 0.698383, 0.443817, 3.138935)">
|
||||||
<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);"/>
|
<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);"/>
|
||||||
<g>
|
<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="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="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);"/>
|
||||||
<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: 1; fill: rgb(243, 243, 243);" cx="773.446" cy="384.7" rx="11.751" ry="11.009"/>
|
||||||
<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>
|
</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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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="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;" 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; font-weight: bold;" x="748" y="347.676" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER</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: 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"/>
|
<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"/>
|
||||||
@@ -107,10 +108,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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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: 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>
|
<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;">
|
<g transform="matrix(-0.387768, 0, 0, -0.200385, 743.634644, -199.991287)" style="transform-origin: 72.2405px 412.5px;">
|
||||||
@@ -174,14 +177,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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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);"/>
|
<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>
|
<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>
|
||||||
@@ -199,12 +205,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>
|
<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);"/>
|
<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="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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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="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="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;"/>
|
<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>
|
<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);"/>
|
<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);"/>
|
||||||
@@ -218,33 +227,12 @@
|
|||||||
<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);"/>
|
<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"/>
|
<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>
|
<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="336.418" cy="237.483" rx="13.582" ry="12.517"/>
|
<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"/>
|
||||||
<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>
|
<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(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"/>
|
||||||
<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>
|
<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(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.772" cy="233.876" rx="13.582" ry="12.517"/>
|
<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"/>
|
||||||
<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>
|
<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"/>
|
<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: 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>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
@@ -3,7 +3,7 @@
|
|||||||
<defs>
|
<defs>
|
||||||
<bx:grid x="0" y="0" width="25" height="25"/>
|
<bx:grid x="0" y="0" width="25" height="25"/>
|
||||||
</defs>
|
</defs>
|
||||||
<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"/>
|
<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"/>
|
||||||
<g transform="matrix(0.826913, 0, 0, 0.698383, 74.03907, 53.375034)">
|
<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); 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"/>
|
<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,26 +26,27 @@
|
|||||||
<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"/>
|
<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>
|
</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"/>
|
<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>
|
<g transform="matrix(0.826913, 0, 0, 0.698383, 0.443817, 3.138935)">
|
||||||
<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);"/>
|
<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);"/>
|
||||||
<g>
|
<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="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="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);"/>
|
||||||
<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: 1; fill: rgb(243, 243, 243);" cx="773.446" cy="384.7" rx="11.751" ry="11.009"/>
|
||||||
<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>
|
</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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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="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;" 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; font-weight: bold;" x="748" y="347.676" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER</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: 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"/>
|
<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"/>
|
||||||
@@ -107,10 +108,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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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: 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>
|
<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;">
|
<g transform="matrix(-0.387768, 0, 0, -0.200385, 743.634644, -199.991287)" style="transform-origin: 72.2405px 412.5px;">
|
||||||
@@ -174,14 +177,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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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>
|
<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);"/>
|
<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>
|
<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>
|
||||||
@@ -199,12 +205,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>
|
<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);"/>
|
<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="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>
|
<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;"/>
|
<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>
|
<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);"/>
|
<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="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="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;"/>
|
<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>
|
<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);"/>
|
<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);"/>
|
||||||
@@ -218,34 +227,12 @@
|
|||||||
<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);"/>
|
<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"/>
|
<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>
|
<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="336.418" cy="237.483" rx="13.582" ry="12.517"/>
|
<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"/>
|
||||||
<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>
|
<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(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"/>
|
||||||
|
|
||||||
<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>
|
<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(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.772" cy="233.876" rx="13.582" ry="12.517"/>
|
<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"/>
|
||||||
<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>
|
<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"/>
|
<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: 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>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
@@ -1971,12 +1971,12 @@
|
|||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="718.035" y="174.17">MPa</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="718.035" y="174.17">MPa</text>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="229.981" y="200.126">####</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="229.981" y="200.126">####</text>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="229.217" y="233.522">####</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="229.217" y="233.522">####</text>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="229.599" y="265.154" id="c_1003">####</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="229.599" y="265.154">####</text>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="189.546" y="326.378">####</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="189.546" y="326.378">####</text>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="361.232" y="371.05" id="c_1004">####</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="361.232" y="371.05">####</text>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="622.21" y="304.496">####</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="622.21" y="304.496">####</text>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="684.474" y="141.612" id="c_1001">####</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="684.474" y="141.612">####</text>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="686.145" y="174.534" id="c_1002">####</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="686.145" y="174.534">####</text>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="893.7" y="201.982">####</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="893.7" y="201.982">####</text>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="893.661" y="239.324">####</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 9px; stroke-width: 1;" x="893.661" y="239.324">####</text>
|
||||||
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 9px; white-space: pre; font-weight: bolder;" transform="matrix(0.705508, 0, 0, 0.49184, 796.826824, 48.14839)" x="38.471" y="128.844">Plant Air Reciever</text>
|
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 9px; white-space: pre; font-weight: bolder;" transform="matrix(0.705508, 0, 0, 0.49184, 796.826824, 48.14839)" x="38.471" y="128.844">Plant Air Reciever</text>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 177 KiB |
@@ -30,18 +30,18 @@ instance.interceptors.response.use(
|
|||||||
originalRequest._retry = true;
|
originalRequest._retry = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// console.log('🔄 Refresh token dipanggil...');
|
console.log('🔄 Refresh token dipanggil...');
|
||||||
const refreshRes = await refreshApi.post('/auth/refresh-token');
|
const refreshRes = await refreshApi.post('/auth/refresh-token');
|
||||||
|
|
||||||
const newAccessToken = refreshRes.data.data.accessToken;
|
const newAccessToken = refreshRes.data.data.accessToken;
|
||||||
localStorage.setItem('token', newAccessToken);
|
localStorage.setItem('token', newAccessToken);
|
||||||
// console.log('✅ Token refreshed successfully');
|
console.log('✅ Token refreshed successfully');
|
||||||
|
|
||||||
// update token di header
|
// update token di header
|
||||||
instance.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;
|
instance.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;
|
||||||
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
|
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
|
||||||
|
|
||||||
// console.log('🔁 Retrying original request...');
|
console.log('🔁 Retrying original request...');
|
||||||
return instance(originalRequest);
|
return instance(originalRequest);
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -70,35 +70,24 @@ async function ApiRequest({ method = 'GET', params = {}, prefix = '/', token = t
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenRedirect = sessionStorage.getItem('token_redirect');
|
const rawToken = localStorage.getItem('token');
|
||||||
|
|
||||||
let rawToken = '';
|
|
||||||
|
|
||||||
if (tokenRedirect !== null) {
|
|
||||||
rawToken = tokenRedirect;
|
|
||||||
// console.log(`sessionStorage: ${tokenRedirect}`);
|
|
||||||
} else {
|
|
||||||
rawToken = localStorage.getItem('token');
|
|
||||||
// console.log(`localStorage: ${rawToken}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token && rawToken) {
|
if (token && rawToken) {
|
||||||
const cleanToken = rawToken.replace(/"/g, '');
|
const cleanToken = rawToken.replace(/"/g, '');
|
||||||
request.headers['Authorization'] = `Bearer ${cleanToken}`;
|
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 {
|
} else {
|
||||||
console.warn('⚠️ No token found in localStorage');
|
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 {
|
try {
|
||||||
const response = await instance(request);
|
const response = await instance(request);
|
||||||
// console.log('✅ API Response:', {
|
console.log('✅ API Response:', {
|
||||||
// url: prefix,
|
url: prefix,
|
||||||
// status: response.status,
|
status: response.status,
|
||||||
// statusCode: response.data?.statusCode,
|
statusCode: response.data?.statusCode,
|
||||||
// });
|
});
|
||||||
return { ...response, error: false };
|
return { ...response, error: false };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const status = error?.response?.status || 500;
|
const status = error?.response?.status || 500;
|
||||||
@@ -143,10 +132,17 @@ async function cekError(status, message = '') {
|
|||||||
const SendRequest = async (queryParams) => {
|
const SendRequest = async (queryParams) => {
|
||||||
try {
|
try {
|
||||||
const response = await ApiRequest(queryParams);
|
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 ApiRequest returned error flag, return error structure
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
const errorMsg = response.data?.message || response.statusText || 'Request failed';
|
const errorMsg = response.data?.message || response.statusText || 'Request failed';
|
||||||
|
console.error('❌ SendRequest error response:', errorMsg);
|
||||||
|
|
||||||
// Return consistent error structure instead of empty array
|
// Return consistent error structure instead of empty array
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,16 +2,7 @@
|
|||||||
import mqtt from 'mqtt';
|
import mqtt from 'mqtt';
|
||||||
|
|
||||||
const mqttUrl = `${import.meta.env.VITE_MQTT_SERVER ?? 'ws://localhost:1884'}`;
|
const mqttUrl = `${import.meta.env.VITE_MQTT_SERVER ?? 'ws://localhost:1884'}`;
|
||||||
const topics = [
|
const topics = ['PIU_GGCP/Devices/PB'];
|
||||||
'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 = {
|
const options = {
|
||||||
keepalive: 30,
|
keepalive: 30,
|
||||||
clientId: 'react_mqtt_' + Math.random().toString(16).substr(2, 8),
|
clientId: 'react_mqtt_' + Math.random().toString(16).substr(2, 8),
|
||||||
@@ -75,8 +66,7 @@ const listenMessage = (callback) => {
|
|||||||
|
|
||||||
const setValSvg = (listenTopic, svg) => {
|
const setValSvg = (listenTopic, svg) => {
|
||||||
client.on('message', (topic, message) => {
|
client.on('message', (topic, message) => {
|
||||||
// console.log(topic ,' = ', listenTopic);
|
if (topic == listenTopic) {
|
||||||
if (topic === listenTopic) {
|
|
||||||
const objChanel = JSON.parse(message);
|
const objChanel = JSON.parse(message);
|
||||||
|
|
||||||
Object.entries(objChanel).forEach(([key, value]) => {
|
Object.entries(objChanel).forEach(([key, value]) => {
|
||||||
@@ -88,7 +78,7 @@ const setValSvg = (listenTopic, svg) => {
|
|||||||
} else if (value === false) {
|
} else if (value === false) {
|
||||||
el.style.display = 'none';
|
el.style.display = 'none';
|
||||||
} else if (!isNaN(value)) {
|
} else if (!isNaN(value)) {
|
||||||
el.textContent = Number(value ?? 0.0).toFixed(2);
|
el.textContent = Number(value ?? 0.0);
|
||||||
} else {
|
} else {
|
||||||
el.textContent = value;
|
el.textContent = value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,65 +20,36 @@ html body {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom green Sidebar Menu Styles */
|
/* Custom Orange Sidebar Menu Styles */
|
||||||
.custom-green-menu.ant-menu-dark .ant-menu-item-selected {
|
.custom-orange-menu.ant-menu-dark .ant-menu-item-selected {
|
||||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-green-menu.ant-menu-dark .ant-menu-item-selected::after {
|
.custom-orange-menu.ant-menu-dark .ant-menu-item-selected::after {
|
||||||
border-right-color: white !important;
|
border-right-color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-green-menu.ant-menu-dark .ant-menu-item:hover,
|
.custom-orange-menu.ant-menu-dark .ant-menu-item:hover,
|
||||||
.custom-green-menu.ant-menu-dark .ant-menu-submenu-title:hover {
|
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-title:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.15) !important;
|
background-color: rgba(255, 255, 255, 0.15) !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-green-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title {
|
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-green-menu.ant-menu-dark.ant-menu-inline .ant-menu-sub {
|
.custom-orange-menu.ant-menu-dark.ant-menu-inline .ant-menu-sub {
|
||||||
background: rgba(0, 0, 0, 0.2) !important;
|
background: rgba(0, 0, 0, 0.2) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-green-menu.ant-menu-dark .ant-menu-item,
|
.custom-orange-menu.ant-menu-dark .ant-menu-item,
|
||||||
.custom-green-menu.ant-menu-dark .ant-menu-submenu-title {
|
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-title {
|
||||||
color: rgba(255, 255, 255, 0.9) !important;
|
color: rgba(255, 255, 255, 0.9) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-green-menu.ant-menu-dark .ant-menu-item-active,
|
.custom-orange-menu.ant-menu-dark .ant-menu-item-active,
|
||||||
.custom-green-menu.ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title {
|
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title {
|
||||||
color: white !important;
|
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 */
|
|
||||||
@@ -146,7 +146,7 @@ const allItems = [
|
|||||||
{
|
{
|
||||||
key: 'master-sparepart',
|
key: 'master-sparepart',
|
||||||
icon: <ToolOutlined style={{ fontSize: '19px' }} />,
|
icon: <ToolOutlined style={{ fontSize: '19px' }} />,
|
||||||
label: <Link to="/master/sparepart">Sparepart</Link>,
|
label: <Link to="/master/sparepart">sparepart</Link>,
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// key: 'master-shift',
|
// key: 'master-shift',
|
||||||
@@ -422,7 +422,7 @@ const LayoutMenu = () => {
|
|||||||
border: 'none',
|
border: 'none',
|
||||||
}}
|
}}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
className="custom-green-menu"
|
className="custom-orange-menu"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,24 +30,8 @@ const LayoutSidebar = () => {
|
|||||||
zIndex: 9999
|
zIndex: 9999
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{
|
<LayoutLogo />
|
||||||
display: 'flex',
|
<LayoutMenu />
|
||||||
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>
|
</Sider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
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} />;
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,7 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
name: '',
|
name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
is_active: true,
|
is_active: true,
|
||||||
|
contact_type: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const [formData, setFormData] = useState(defaultData);
|
const [formData, setFormData] = useState(defaultData);
|
||||||
@@ -36,7 +37,13 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleContactTypeChange = (value) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
contact_type: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const handleStatusToggle = (checked) => {
|
const handleStatusToggle = (checked) => {
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
@@ -51,6 +58,7 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
const validationRules = [
|
const validationRules = [
|
||||||
{ field: 'name', label: 'Contact Name', required: true },
|
{ field: 'name', label: 'Contact Name', required: true },
|
||||||
{ field: 'phone', label: 'Phone', required: true },
|
{ field: 'phone', label: 'Phone', required: true },
|
||||||
|
{ field: 'contact_type', label: 'Contact Type', required: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -89,6 +97,7 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
contact_name: formData.name,
|
contact_name: formData.name,
|
||||||
contact_phone: formData.phone.replace(/[\s\-\(\)]/g, ''), // Clean phone number
|
contact_phone: formData.phone.replace(/[\s\-\(\)]/g, ''), // Clean phone number
|
||||||
is_active: formData.is_active,
|
is_active: formData.is_active,
|
||||||
|
contact_type: formData.contact_type,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
@@ -136,16 +145,18 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
phone: props.selectedData.contact_phone || props.selectedData.phone,
|
phone: props.selectedData.contact_phone || props.selectedData.phone,
|
||||||
is_active:
|
is_active:
|
||||||
props.selectedData.is_active || props.selectedData.status === 'active',
|
props.selectedData.is_active || props.selectedData.status === 'active',
|
||||||
|
contact_type: props.selectedData.contact_type || props.contactType || '',
|
||||||
});
|
});
|
||||||
} else if (props.actionMode === 'add') {
|
} else if (props.actionMode === 'add') {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
is_active: true,
|
is_active: true,
|
||||||
|
contact_type: props.contactType === 'all' ? '' : props.contactType || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [props.showModal, props.actionMode, props.selectedData]);
|
}, [props.showModal, props.actionMode, props.selectedData, props.contactType]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -194,36 +205,27 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div style={{ padding: '8px 0' }}>
|
<div style={{ padding: '8px 0' }}>
|
||||||
{/* Status field only show in add mode*/}
|
<div>
|
||||||
{props.actionMode === 'add' && (
|
<div>
|
||||||
<>
|
<Text strong>Status</Text>
|
||||||
<div>
|
</div>
|
||||||
<div>
|
<div style={{ display: 'flex', alignItems: 'center', marginTop: '8px' }}>
|
||||||
<Text strong>Status</Text>
|
<div style={{ marginRight: '8px' }}>
|
||||||
</div>
|
<Switch
|
||||||
<div
|
disabled={props.readOnly}
|
||||||
style={{ display: 'flex', alignItems: 'center', marginTop: '8px' }}
|
style={{
|
||||||
>
|
backgroundColor: formData.is_active ? '#23A55A' : '#bfbfbf',
|
||||||
<div style={{ marginRight: '8px' }}>
|
}}
|
||||||
<Switch
|
checked={formData.is_active}
|
||||||
disabled={props.readOnly}
|
onChange={handleStatusToggle}
|
||||||
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>
|
</div>
|
||||||
<Divider style={{ margin: '12px 0' }} />
|
<div>
|
||||||
</>
|
<Text>{formData.is_active ? 'Active' : 'Inactive'}</Text>
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider style={{ margin: '12px 0' }} />
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Text strong>Name</Text>
|
<Text strong>Name</Text>
|
||||||
@@ -249,8 +251,7 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
style={{ color: formData.is_active ? '#000000' : '#ff4d4f' }}
|
style={{ color: formData.is_active ? '#000000' : '#ff4d4f' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Contact Type */}
|
<div style={{ marginBottom: 12 }}>
|
||||||
{/* <div style={{ marginBottom: 12 }}>
|
|
||||||
<Text strong>Contact Type</Text>
|
<Text strong>Contact Type</Text>
|
||||||
<Text style={{ color: 'red' }}> *</Text>
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
<Select
|
<Select
|
||||||
@@ -263,7 +264,7 @@ const DetailContact = memo(function DetailContact(props) {
|
|||||||
<Select.Option value="operator">Operator</Select.Option>
|
<Select.Option value="operator">Operator</Select.Option>
|
||||||
<Select.Option value="gudang">Gudang</Select.Option>
|
<Select.Option value="gudang">Gudang</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
</div> */}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { memo, useState, useEffect } from 'react';
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
import { Button, Row, Col, Input, Tabs, Space, ConfigProvider, Card, Tag, Switch } from 'antd';
|
import { Button, Row, Col, Input, Tabs, Space, ConfigProvider, Card, Tag } from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
@@ -10,43 +10,9 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { NotifAlert, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
||||||
import { getAllContact, deleteContact, updateContact } from '../../../api/contact';
|
import { getAllContact, deleteContact } 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',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const ContactCard = memo(function ContactCard({ contact, showEditModal, showDeleteModal }) {
|
||||||
return (
|
return (
|
||||||
<Col xs={24} sm={12} md={8} lg={6}>
|
<Col xs={24} sm={12} md={8} lg={6}>
|
||||||
<div
|
<div
|
||||||
@@ -78,7 +44,7 @@ const ContactCard = memo(function ContactCard({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Type Badge - Top Left */}
|
{/* Type Badge - Top Left */}
|
||||||
{/* <div style={{ position: 'absolute', top: 0, left: 0, zIndex: 1 }}>
|
<div style={{ position: 'absolute', top: 0, left: 0, zIndex: 1 }}>
|
||||||
<Tag
|
<Tag
|
||||||
color={
|
color={
|
||||||
contact.contact_type === 'operator'
|
contact.contact_type === 'operator'
|
||||||
@@ -91,37 +57,19 @@ const ContactCard = memo(function ContactCard({
|
|||||||
>
|
>
|
||||||
{contact.contact_type === 'operator' ? 'Operator' : contact.contact_type === 'gudang' ? 'Gudang' : 'Unknown'}
|
{contact.contact_type === 'operator' ? 'Operator' : contact.contact_type === 'gudang' ? 'Gudang' : 'Unknown'}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
{/* Status Slider - Top Right */}
|
{/* Status Badge - Top Right */}
|
||||||
<div
|
<div style={{ position: 'absolute', top: 0, right: 0, zIndex: 1 }}>
|
||||||
style={{
|
{contact.status === 'active' ? (
|
||||||
position: 'absolute',
|
<Tag color={'green'} style={{ fontSize: '11px' }}>
|
||||||
top: 0,
|
Active
|
||||||
right: 0,
|
</Tag>
|
||||||
zIndex: 1,
|
) : (
|
||||||
padding: '4px 8px',
|
<Tag color={'red'} style={{ fontSize: '11px' }}>
|
||||||
}}
|
InActive
|
||||||
>
|
</Tag>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
@@ -267,6 +215,9 @@ const ListContact = memo(function ListContact(props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backend doesn't support is_active filter or order parameter
|
||||||
|
// Contact hanya supports: criteria, name, code, limit, page
|
||||||
|
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
Object.entries(searchParams).forEach(([key, value]) => {
|
Object.entries(searchParams).forEach(([key, value]) => {
|
||||||
if (value !== '' && value !== null && value !== undefined) {
|
if (value !== '' && value !== null && value !== undefined) {
|
||||||
@@ -306,10 +257,11 @@ const ListContact = memo(function ListContact(props) {
|
|||||||
// Listen for saved contact data
|
// Listen for saved contact data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.lastSavedContact) {
|
if (props.lastSavedContact) {
|
||||||
fetchContacts();
|
fetchContacts(); // Refetch all contacts when data is saved
|
||||||
}
|
}
|
||||||
}, [props.lastSavedContact]);
|
}, [props.lastSavedContact]);
|
||||||
|
|
||||||
|
// Get contacts (already filtered by backend)
|
||||||
const getFilteredContacts = () => {
|
const getFilteredContacts = () => {
|
||||||
return filteredContacts;
|
return filteredContacts;
|
||||||
};
|
};
|
||||||
@@ -322,7 +274,7 @@ const ListContact = memo(function ListContact(props) {
|
|||||||
const showAddModal = () => {
|
const showAddModal = () => {
|
||||||
props.setSelectedData(null);
|
props.setSelectedData(null);
|
||||||
props.setActionMode('add');
|
props.setActionMode('add');
|
||||||
|
// Pass the current active tab to determine contact type
|
||||||
props.setContactType?.(activeTab);
|
props.setContactType?.(activeTab);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -364,7 +316,7 @@ const ListContact = memo(function ListContact(props) {
|
|||||||
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
||||||
<Col xs={24} sm={24} md={12} lg={12}>
|
<Col xs={24} sm={24} md={12} lg={12}>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder="Search by name..."
|
placeholder="Search by name or type..."
|
||||||
value={formDataFilter.criteria}
|
value={formDataFilter.criteria}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
@@ -430,8 +382,7 @@ const ListContact = memo(function ListContact(props) {
|
|||||||
marginBottom: '16px',
|
marginBottom: '16px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Tabs */}
|
<Tabs
|
||||||
{/* <Tabs
|
|
||||||
activeKey={activeTab}
|
activeKey={activeTab}
|
||||||
onChange={setActiveTab}
|
onChange={setActiveTab}
|
||||||
size="large"
|
size="large"
|
||||||
@@ -449,7 +400,7 @@ const ListContact = memo(function ListContact(props) {
|
|||||||
label: 'Gudang',
|
label: 'Gudang',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/> */}
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{getFilteredContacts().length === 0 ? (
|
{getFilteredContacts().length === 0 ? (
|
||||||
@@ -472,7 +423,6 @@ const ListContact = memo(function ListContact(props) {
|
|||||||
}}
|
}}
|
||||||
showEditModal={showEditModal}
|
showEditModal={showEditModal}
|
||||||
showDeleteModal={showDeleteModal}
|
showDeleteModal={showDeleteModal}
|
||||||
onStatusToggle={fetchContacts}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
357
src/pages/detailNotification/IndexDetailNotification.jsx
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Card,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Typography,
|
||||||
|
Space,
|
||||||
|
Button,
|
||||||
|
Spin,
|
||||||
|
Result,
|
||||||
|
Input,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
ArrowLeftOutlined,
|
||||||
|
CloseCircleFilled,
|
||||||
|
WarningFilled,
|
||||||
|
CheckCircleFilled,
|
||||||
|
InfoCircleFilled,
|
||||||
|
CloseOutlined,
|
||||||
|
BookOutlined,
|
||||||
|
ToolOutlined,
|
||||||
|
HistoryOutlined,
|
||||||
|
FilePdfOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
// Path disesuaikan karena lokasi file berubah
|
||||||
|
// import { getNotificationById } from '../../api/notification'; // Dihapus karena belum ada di file API
|
||||||
|
import UserHistoryModal from '../notification/component/UserHistoryModal';
|
||||||
|
import LogHistoryModal from '../notification/component/LogHistoryModal';
|
||||||
|
|
||||||
|
const { Content } = Layout;
|
||||||
|
const { Text, Paragraph, Link } = Typography;
|
||||||
|
|
||||||
|
// Menggunakan kembali fungsi transform dari ListNotification untuk konsistensi data
|
||||||
|
const transformNotificationData = (item) => ({
|
||||||
|
id: `notification-${item.notification_error_id || 'dummy'}-0`,
|
||||||
|
type: item.is_read ? 'resolved' : item.is_delivered ? 'warning' : 'critical',
|
||||||
|
title: item.device_name || 'Unknown Device',
|
||||||
|
issue: item.error_code_name || 'Unknown Error',
|
||||||
|
description: `${item.error_code} - ${item.error_code_name}`,
|
||||||
|
timestamp: new Date(item.created_at || Date.now()).toLocaleString('id-ID'),
|
||||||
|
location: item.device_location || 'Location not specified',
|
||||||
|
details: item.message_error_issue || 'No details available',
|
||||||
|
link: '#',
|
||||||
|
subsection: item.solution_name || 'N/A',
|
||||||
|
isRead: item.is_read || false,
|
||||||
|
status: item.is_read ? 'Resolved' : item.is_delivered ? 'Delivered' : 'Pending',
|
||||||
|
tag: item.error_code,
|
||||||
|
plc: item.plc || 'N/A',
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDummyNotificationById = (id) => {
|
||||||
|
console.log("Fetching dummy data for ID:", id);
|
||||||
|
// Data mentah dummy, seolah-olah dari API
|
||||||
|
const rawDummyData = { device_name: 'Compressor C-101', error_code_name: 'High Temperature', error_code: 'TEMP-H-303', device_location: 'Gudang Produksi A', message_error_issue: 'Suhu kompresor terdeteksi melebihi ambang batas aman.', is_delivered: true, plc: 'PLC-UTL-01' };
|
||||||
|
// Mengolah data mentah dummy menggunakan transform function
|
||||||
|
return transformNotificationData(rawDummyData);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 DetailNotificationTab = () => {
|
||||||
|
const { notificationId } = useParams(); // Mungkin perlu disesuaikan jika route berbeda
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [notification, setNotification] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [modalContent, setModalContent] = useState(null); // 'user', 'log', atau null
|
||||||
|
const [isAddingLog, setIsAddingLog] = useState(false);
|
||||||
|
|
||||||
|
const logHistoryData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
timestamp: '04-11-2025 11:55 WIB',
|
||||||
|
addedBy: {
|
||||||
|
name: 'Budi Santoso',
|
||||||
|
phone: '081122334455',
|
||||||
|
},
|
||||||
|
description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
timestamp: '04-11-2025 11:45 WIB',
|
||||||
|
addedBy: {
|
||||||
|
name: 'John Doe',
|
||||||
|
phone: '081234567890',
|
||||||
|
},
|
||||||
|
description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
timestamp: '04-11-2025 11:40 WIB',
|
||||||
|
addedBy: {
|
||||||
|
name: 'Jane Smith',
|
||||||
|
phone: '087654321098',
|
||||||
|
},
|
||||||
|
description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDetail = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
// Ganti dengan fungsi API asli Anda
|
||||||
|
// const response = await getNotificationById(notificationId);
|
||||||
|
// setNotification(response.data);
|
||||||
|
|
||||||
|
// Menggunakan data dummy untuk sekarang
|
||||||
|
const dummyData = getDummyNotificationById(notificationId);
|
||||||
|
if (dummyData) {
|
||||||
|
setNotification(dummyData);
|
||||||
|
} else {
|
||||||
|
throw new Error('Notification not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} 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' }}>
|
||||||
|
<Row justify="space-between" align="middle">
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
onClick={() => navigate('/notification')}
|
||||||
|
style={{ paddingLeft: 0 }}
|
||||||
|
>
|
||||||
|
Back to notification list
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
icon={<UserOutlined />}
|
||||||
|
onClick={() => setModalContent('user')}
|
||||||
|
>
|
||||||
|
User History
|
||||||
|
</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="large" style={{ width: '100%' }}>
|
||||||
|
<Row gutter={[24, 24]}>
|
||||||
|
{/* Kolom Kiri: Data Kompresor */}
|
||||||
|
<Col xs={24} lg={12}>
|
||||||
|
<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' }}>{notification.issue}</Text></div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div>
|
||||||
|
<Text strong>Plant Subsection</Text>
|
||||||
|
<div>{notification.subsection}</div>
|
||||||
|
<Text strong style={{ display: 'block', marginTop: '8px' }}>Time</Text>
|
||||||
|
<div>{notification.timestamp}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ border: '1px solid #d4380d', borderRadius: '4px', padding: '8px', background: 'linear-gradient(to right, #ffe7e6, #ffffff)' }}>
|
||||||
|
<Row justify="space-around" align="middle">
|
||||||
|
<Col><Text style={{ fontSize: '12px', color: color }}>Value</Text><div style={{ fontWeight: 'bold', fontSize: '16px', color: color }}>N/A</div></Col>
|
||||||
|
<Col><Text type="secondary" style={{ fontSize: '12px' }}>Treshold</Text><div style={{ fontWeight: 500 }}>N/A</div></Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Kolom Kanan: Informasi Teknis */}
|
||||||
|
<Col xs={24} lg={12}>
|
||||||
|
<Card title="Informasi Teknis" size="small" style={{ height: '100%' }}>
|
||||||
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
|
<div><Text strong>PLC</Text><div>{notification.plc || 'N/A'}</div></div>
|
||||||
|
<div><Text strong>Status</Text><div style={{ color: '#faad14', fontWeight: 500 }}>{notification.status}</div></div>
|
||||||
|
<div><Text strong>Tag</Text><div style={{ fontFamily: 'monospace', backgroundColor: '#f0f0f0', padding: '2px 6px', borderRadius: '4px', display: 'inline-block' }}>{notification.tag}</div></div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col xs={24} md={8}><Card hoverable bodyStyle={{ padding: '12px', textAlign: 'center' }}><Space><BookOutlined style={{ fontSize: '16px', color: '#1890ff' }} /><Text strong style={{ fontSize: '16px', color: '#262626' }}>Handling Guideline</Text></Space></Card></Col>
|
||||||
|
<Col xs={24} md={8}><Card hoverable bodyStyle={{ padding: '12px', textAlign: 'center' }}><Space><ToolOutlined style={{ fontSize: '16px', color: '#1890ff' }} /><Text strong style={{ fontSize: '16px', color: '#262626' }}>Spare Part</Text></Space></Card></Col>
|
||||||
|
<Col xs={24} md={8} onClick={() => setModalContent('log')} style={{ cursor: 'pointer' }}><Card hoverable bodyStyle={{ padding: '12px', textAlign: 'center' }}><Space><HistoryOutlined style={{ fontSize: '16px', color: '#1890ff' }} /><Text strong style={{ fontSize: '16px', color: '#262626' }}>Log Activity</Text></Space></Card></Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col xs={24} md={8}>
|
||||||
|
<Card size="small" title="Guideline Documents" style={{ height: '100%' }}>
|
||||||
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
|
<Card size="small" hoverable>
|
||||||
|
<Text><FilePdfOutlined style={{ marginRight: '8px' }} /> Error 303.pdf</Text>
|
||||||
|
<Link href="#" target="_blank" style={{ fontSize: '12px', display: 'block', marginLeft: '24px' }}>lihat disini</Link>
|
||||||
|
</Card>
|
||||||
|
<Card size="small" hoverable>
|
||||||
|
<Text><FilePdfOutlined style={{ marginRight: '8px' }} /> SOP Kompresor.pdf</Text>
|
||||||
|
<Link href="#" target="_blank" style={{ fontSize: '12px', display: 'block', marginLeft: '24px' }}>lihat disini</Link>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={8}>
|
||||||
|
<Card size="small" title="Required Spare Parts" style={{ height: '100%' }}>
|
||||||
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
|
<Card size="small">
|
||||||
|
<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: '#52c41a', fontWeight: 500 }}>Available</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={17}>
|
||||||
|
<Text strong>Air Filter</Text>
|
||||||
|
<Paragraph style={{ fontSize: '12px', margin: 0, color: '#595959' }}>Filters incoming air to remove dust.</Paragraph>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card size="small" style={{ height: '100%' }}>
|
||||||
|
<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..."
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type={isAddingLog ? 'primary' : 'dashed'}
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
icon={!isAddingLog && <PlusOutlined />}
|
||||||
|
onClick={() => setIsAddingLog(!isAddingLog)}
|
||||||
|
>
|
||||||
|
{isAddingLog ? 'Submit Log' : 'Add Log'}
|
||||||
|
</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>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Content>
|
||||||
|
|
||||||
|
<UserHistoryModal
|
||||||
|
visible={modalContent === 'user'}
|
||||||
|
onCancel={() => setModalContent(null)}
|
||||||
|
notificationData={notification}
|
||||||
|
/>
|
||||||
|
<LogHistoryModal
|
||||||
|
visible={modalContent === 'log'}
|
||||||
|
onCancel={() => setModalContent(null)}
|
||||||
|
notificationData={notification}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailNotificationTab;
|
||||||
@@ -8,7 +8,7 @@ import filePathSvg from '../../assets/svg/air_dryer_A_rev.svg';
|
|||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
// const filePathSvg = '/src/assets/svg/air_dryer_A_rev.svg';
|
// const filePathSvg = '/src/assets/svg/air_dryer_A_rev.svg';
|
||||||
const topicMqtt = 'PIU_COD/AIR_DRYER/AIR_DRYER_A';
|
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||||
|
|
||||||
const SvgAirDryerA = () => {
|
const SvgAirDryerA = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import filePathSvg from '../../assets/svg/air_dryer_B_rev.svg';
|
|||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
// const filePathSvg = '/src/assets/svg/air_dryer_B_rev.svg';
|
// const filePathSvg = '/src/assets/svg/air_dryer_B_rev.svg';
|
||||||
const topicMqtt = 'PIU_COD/AIR_DRYER/AIR_DRYER_B';
|
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||||
|
|
||||||
const SvgAirDryerB = () => {
|
const SvgAirDryerB = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import filePathSvg from '../../assets/svg/air_dryer_C_rev.svg';
|
|||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
// const filePathSvg = '/src/assets/svg/air_dryer_C_rev.svg';
|
// const filePathSvg = '/src/assets/svg/air_dryer_C_rev.svg';
|
||||||
const topicMqtt = 'PIU_COD/AIR_DRYER/AIR_DRYER_C';
|
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||||
|
|
||||||
const SvgAirDryerC = () => {
|
const SvgAirDryerC = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import filePathSvg from '../../assets/svg/compressorA_rev.svg';
|
|||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
||||||
const topicMqtt = 'PIU_COD/COMPRESSOR/COMPRESSOR_A';
|
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||||
|
|
||||||
const SvgCompressorA = () => {
|
const SvgCompressorA = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import SvgViewer from './SvgViewer';
|
|||||||
import filePathSvg from '../../assets/svg/compressorB_rev.svg';
|
import filePathSvg from '../../assets/svg/compressorB_rev.svg';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const topicMqtt = 'PIU_COD/COMPRESSOR/COMPRESSOR_B';
|
|
||||||
|
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
||||||
|
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||||
|
|
||||||
const SvgCompressorB = () => {
|
const SvgCompressorB = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import filePathSvg from '../../assets/svg/compressorC_rev.svg';
|
|||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
||||||
const topicMqtt = 'PIU_COD/COMPRESSOR/COMPRESSOR_C';
|
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||||
|
|
||||||
const SvgCompressorC = () => {
|
const SvgCompressorC = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import filePathSvg from '../../assets/svg/overview-airdryer.svg';
|
|||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
||||||
const topicMqtt = 'PIU_COD/AIR_DRYER/OVERVIEW';
|
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||||
|
|
||||||
const SvgOverviewAirDryer = () => {
|
const SvgOverviewAirDryer = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import filePathSvg from '../../assets/svg/overview-compressor.svg';
|
|||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
// const filePathSvg = '/src/assets/svg/test-new.svg';
|
||||||
const topicMqtt = 'PIU_COD/COMPRESSOR/OVERVIEW';
|
const topicMqtt = 'PIU_GGCP/Devices/PB';
|
||||||
|
|
||||||
const SvgOverviewCompressor = () => {
|
const SvgOverviewCompressor = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -352,7 +352,7 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
<Title level={3}>Jadwal Shift</Title>
|
<Title level={3}>Jadwal Shift</Title>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{/* <Row>
|
<Row>
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
<Row justify="end" align="middle" gutter={[8, 8]}>
|
<Row justify="end" align="middle" gutter={[8, 8]}>
|
||||||
<Col xs={24} sm={24} md={12} lg={12}>
|
<Col xs={24} sm={24} md={12} lg={12}>
|
||||||
@@ -383,7 +383,7 @@ const ListJadwalShift = memo(function ListJadwalShift(props) {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row> */}
|
</Row>
|
||||||
|
|
||||||
<div style={{ marginTop: '24px' }}>
|
<div style={{ marginTop: '24px' }}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import { ArrowLeftOutlined, FilePdfOutlined, FileImageOutlined, DownloadOutlined
|
|||||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||||
import { getBrandById } from '../../../api/master-brand';
|
import { getBrandById } from '../../../api/master-brand';
|
||||||
import {
|
import {
|
||||||
downloadFile,
|
downloadFile,
|
||||||
getFile,
|
getFile,
|
||||||
getFileUrl,
|
getFileUrl,
|
||||||
getFolderFromFileType,
|
getFolderFromFileType,
|
||||||
} from '../../../api/file-uploads';
|
} from '../../../api/file-uploads';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
@@ -26,7 +26,17 @@ const ViewFilePage = () => {
|
|||||||
const [pdfBlobUrl, setPdfBlobUrl] = useState(null);
|
const [pdfBlobUrl, setPdfBlobUrl] = useState(null);
|
||||||
const [pdfLoading, setPdfLoading] = useState(false);
|
const [pdfLoading, setPdfLoading] = useState(false);
|
||||||
|
|
||||||
|
// Debug: Log URL parameters and location
|
||||||
const isFromEdit = window.location.pathname.includes('/edit/');
|
const isFromEdit = window.location.pathname.includes('/edit/');
|
||||||
|
console.log('ViewFilePage URL Parameters:', {
|
||||||
|
id,
|
||||||
|
fileType,
|
||||||
|
fileName,
|
||||||
|
allParams: params,
|
||||||
|
windowLocation: window.location.pathname,
|
||||||
|
urlParts: window.location.pathname.split('/'),
|
||||||
|
isFromEdit
|
||||||
|
});
|
||||||
|
|
||||||
let fallbackId = id;
|
let fallbackId = id;
|
||||||
let fallbackFileType = fileType;
|
let fallbackFileType = fileType;
|
||||||
@@ -35,6 +45,7 @@ const ViewFilePage = () => {
|
|||||||
if (!fileName || !fileType || !id) {
|
if (!fileName || !fileType || !id) {
|
||||||
|
|
||||||
const urlParts = window.location.pathname.split('/');
|
const urlParts = window.location.pathname.split('/');
|
||||||
|
// console.log('URL Parts from pathname:', urlParts);
|
||||||
|
|
||||||
const viewIndex = urlParts.indexOf('view');
|
const viewIndex = urlParts.indexOf('view');
|
||||||
const editIndex = urlParts.indexOf('edit');
|
const editIndex = urlParts.indexOf('edit');
|
||||||
@@ -44,6 +55,13 @@ const ViewFilePage = () => {
|
|||||||
fallbackId = urlParts[actionIndex + 1];
|
fallbackId = urlParts[actionIndex + 1];
|
||||||
fallbackFileType = urlParts[actionIndex + 3];
|
fallbackFileType = urlParts[actionIndex + 3];
|
||||||
fallbackFileName = decodeURIComponent(urlParts[actionIndex + 4]);
|
fallbackFileName = decodeURIComponent(urlParts[actionIndex + 4]);
|
||||||
|
|
||||||
|
console.log('Fallback extraction:', {
|
||||||
|
fallbackId,
|
||||||
|
fallbackFileType,
|
||||||
|
fallbackFileName,
|
||||||
|
actionType: viewIndex !== -1 ? 'view' : 'edit'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,9 +95,12 @@ const ViewFilePage = () => {
|
|||||||
const folder = getFolderFromFileType('pdf');
|
const folder = getFolderFromFileType('pdf');
|
||||||
try {
|
try {
|
||||||
const blobData = await getFile(folder, decodedFileName);
|
const blobData = await getFile(folder, decodedFileName);
|
||||||
|
console.log('PDF blob data received:', blobData);
|
||||||
const blobUrl = window.URL.createObjectURL(blobData);
|
const blobUrl = window.URL.createObjectURL(blobData);
|
||||||
setPdfBlobUrl(blobUrl);
|
setPdfBlobUrl(blobUrl);
|
||||||
|
console.log('PDF blob URL created successfully:', blobUrl);
|
||||||
} catch (pdfError) {
|
} catch (pdfError) {
|
||||||
|
console.error('Error loading PDF:', pdfError);
|
||||||
setError('Failed to load PDF file: ' + (pdfError.message || pdfError));
|
setError('Failed to load PDF file: ' + (pdfError.message || pdfError));
|
||||||
setPdfBlobUrl(null);
|
setPdfBlobUrl(null);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -89,6 +110,7 @@ const ViewFilePage = () => {
|
|||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
setError('Failed to load data');
|
setError('Failed to load data');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -138,6 +160,12 @@ const ViewFilePage = () => {
|
|||||||
|
|
||||||
const targetPhase = savedPhase ? parseInt(savedPhase) : 1;
|
const targetPhase = savedPhase ? parseInt(savedPhase) : 1;
|
||||||
|
|
||||||
|
console.log('ViewFilePage handleBack - Edit mode:', {
|
||||||
|
savedPhase,
|
||||||
|
targetPhase,
|
||||||
|
id: fallbackId || id
|
||||||
|
});
|
||||||
|
|
||||||
navigate(`/master/brand-device/edit/${fallbackId || id}`, {
|
navigate(`/master/brand-device/edit/${fallbackId || id}`, {
|
||||||
state: { phase: targetPhase, fromFileViewer: true },
|
state: { phase: targetPhase, fromFileViewer: true },
|
||||||
replace: true
|
replace: true
|
||||||
@@ -168,7 +196,9 @@ const ViewFilePage = () => {
|
|||||||
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
|
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
|
||||||
const isPdf = fileExtension === 'pdf';
|
const isPdf = fileExtension === 'pdf';
|
||||||
|
|
||||||
|
// const fileUrl = loading ? null : getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName);
|
||||||
|
|
||||||
|
// Show placeholder when loading
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||||
@@ -310,14 +340,17 @@ const ViewFilePage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
// Retry loading PDF
|
||||||
setPdfLoading(true);
|
setPdfLoading(true);
|
||||||
const folder = getFolderFromFileType('pdf');
|
const folder = getFolderFromFileType('pdf');
|
||||||
getFile(folder, actualFileName)
|
getFile(folder, actualFileName)
|
||||||
.then(blobData => {
|
.then(blobData => {
|
||||||
|
console.log('Retry PDF blob data:', blobData);
|
||||||
const blobUrl = window.URL.createObjectURL(blobData);
|
const blobUrl = window.URL.createObjectURL(blobData);
|
||||||
setPdfBlobUrl(blobUrl);
|
setPdfBlobUrl(blobUrl);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
console.error('Error retrying PDF load:', error);
|
||||||
setError('Failed to load PDF file: ' + (error.message || error));
|
setError('Failed to load PDF file: ' + (error.message || error));
|
||||||
setPdfBlobUrl(null);
|
setPdfBlobUrl(null);
|
||||||
})
|
})
|
||||||
@@ -412,7 +445,7 @@ const ViewFilePage = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* File type indicator */}
|
||||||
<div style={{ marginBottom: '16px' }}>
|
<div style={{ marginBottom: '16px' }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
@@ -429,7 +462,7 @@ const ViewFilePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
|
{/* Overlay with blur effect during loading */}
|
||||||
{loading && (
|
{loading && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@@ -3,96 +3,74 @@ import { Form, Input, Row, Col, Typography, Switch } from 'antd';
|
|||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const BrandForm = ({
|
const BrandForm = ({ form, formData, onValuesChange, isEdit = false }) => {
|
||||||
form,
|
const isActive = Form.useWatch('is_active', form) ?? formData.is_active ?? true;
|
||||||
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 (
|
return (
|
||||||
<div>
|
<Form
|
||||||
<Form
|
layout="vertical"
|
||||||
layout="vertical"
|
form={form}
|
||||||
form={form}
|
onValuesChange={onValuesChange}
|
||||||
onValuesChange={onValuesChange}
|
initialValues={formData}
|
||||||
initialValues={{
|
>
|
||||||
brand_name: '',
|
<Form.Item label="Status">
|
||||||
brand_type: '',
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
brand_model: '',
|
<Form.Item name="is_active" valuePropName="checked" noStyle>
|
||||||
brand_manufacture: '',
|
<Switch
|
||||||
is_active: true,
|
style={{ backgroundColor: isActive ? '#23A55A' : '#bfbfbf' }}
|
||||||
}}
|
/>
|
||||||
>
|
</Form.Item>
|
||||||
<Form.Item label="Status">
|
<Text style={{ marginLeft: 8 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
{isActive ? 'Running' : 'Offline'}
|
||||||
<Form.Item name="is_active" valuePropName="checked" noStyle>
|
</Text>
|
||||||
<Switch
|
</div>
|
||||||
style={{ backgroundColor: isActive ? '#23A55A' : '#bfbfbf' }}
|
</Form.Item>
|
||||||
disabled={readOnly}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Text style={{ marginLeft: 8 }}>
|
|
||||||
{isActive ? 'Running' : 'Offline'}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item label="Brand Code" name="brand_code">
|
<Form.Item label="Brand Code" name="brand_code">
|
||||||
<Input
|
<Input
|
||||||
disabled={true}
|
placeholder={'Auto Fill Brand Code'}
|
||||||
style={{
|
disabled={true}
|
||||||
backgroundColor: '#f5f5f5',
|
style={{
|
||||||
cursor: 'not-allowed'
|
backgroundColor: '#f5f5f5',
|
||||||
}}
|
cursor: 'not-allowed'
|
||||||
/>
|
}}
|
||||||
</Form.Item>
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Brand Name"
|
label="Brand Name"
|
||||||
name="brand_name"
|
name="brand_name"
|
||||||
rules={[{ required: !readOnly, message: 'Brand Name wajib diisi!' }]}
|
rules={[{ required: true, message: 'Brand Name wajib diisi!' }]}
|
||||||
>
|
>
|
||||||
<Input disabled={readOnly} />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Manufacturer"
|
label="Manufacturer"
|
||||||
name="brand_manufacture"
|
name="brand_manufacture"
|
||||||
rules={[{ required: !readOnly, message: 'Manufacturer wajib diisi!' }]}
|
rules={[{ required: true, message: 'Manufacturer wajib diisi!' }]}
|
||||||
>
|
>
|
||||||
<Input placeholder="Enter Manufacturer" disabled={readOnly} />
|
<Input placeholder="Enter Manufacturer" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item label="Brand Type" name="brand_type">
|
<Form.Item label="Brand Type" name="brand_type">
|
||||||
<Input placeholder="Enter Brand Type (Optional)" disabled={readOnly} />
|
<Input placeholder="Enter Brand Type (Optional)" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item label="Model" name="brand_model">
|
<Form.Item label="Model" name="brand_model">
|
||||||
<Input placeholder="Enter Model (Optional)" disabled={readOnly} />
|
<Input placeholder="Enter Model (Optional)" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,397 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
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;
|
|
||||||
200
src/pages/master/brandDevice/component/ErrorCodeListModal.jsx
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Modal, Table, Button, Space, message, Tag, ConfigProvider } from 'antd';
|
||||||
|
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
|
||||||
|
import { NotifConfirmDialog, NotifOk, NotifAlert } from '../../../../components/Global/ToastNotif';
|
||||||
|
|
||||||
|
const ErrorCodeListModal = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
errorCodes,
|
||||||
|
loading,
|
||||||
|
onPreview,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onAddNew,
|
||||||
|
}) => {
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'No',
|
||||||
|
key: 'no',
|
||||||
|
width: '5%',
|
||||||
|
align: 'center',
|
||||||
|
render: (_, __, index) => index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Error Code',
|
||||||
|
dataIndex: 'error_code',
|
||||||
|
key: 'error_code',
|
||||||
|
width: '15%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Error Name',
|
||||||
|
dataIndex: 'error_code_name',
|
||||||
|
key: 'error_code_name',
|
||||||
|
width: '30%',
|
||||||
|
render: (text) => text || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Description',
|
||||||
|
dataIndex: 'error_code_description',
|
||||||
|
key: 'error_code_description',
|
||||||
|
width: '25%',
|
||||||
|
render: (text) => text || '-',
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Solutions',
|
||||||
|
key: 'solutions',
|
||||||
|
width: '10%',
|
||||||
|
align: 'center',
|
||||||
|
render: (_, record) => {
|
||||||
|
const solutionCount = record.solution ? record.solution.length : 0;
|
||||||
|
return <Tag color={solutionCount > 0 ? 'green' : 'red'}>{solutionCount} Sol</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Status',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: '10%',
|
||||||
|
align: 'center',
|
||||||
|
render: (_, { status }) => (
|
||||||
|
<Tag color={status ? 'green' : 'red'}>{status ? 'Active' : 'Inactive'}</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Action',
|
||||||
|
key: 'action',
|
||||||
|
align: 'center',
|
||||||
|
width: '15%',
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => onPreview(record)}
|
||||||
|
style={{
|
||||||
|
color: '#23A55A',
|
||||||
|
borderColor: '#23A55A',
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => onEdit(record)}
|
||||||
|
style={{
|
||||||
|
color: '#faad14',
|
||||||
|
borderColor: '#faad14',
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => handleDelete(record)}
|
||||||
|
style={{
|
||||||
|
borderColor: '#ff4d4f',
|
||||||
|
color: '#ff4d4f',
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleDelete = (record) => {
|
||||||
|
if (errorCodes.length <= 1) {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Perhatian',
|
||||||
|
message: 'Setiap brand harus memiliki minimal 1 error code!',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifConfirmDialog({
|
||||||
|
icon: 'question',
|
||||||
|
title: 'Konfirmasi',
|
||||||
|
message: `Apakah anda yakin hapus error code "${
|
||||||
|
record.error_code_name || record.error_code
|
||||||
|
}" ?`,
|
||||||
|
onConfirm: () => {
|
||||||
|
setConfirmLoading(true);
|
||||||
|
onDelete(record.key);
|
||||||
|
setConfirmLoading(false);
|
||||||
|
},
|
||||||
|
onCancel: () => {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Daftar Error Codes</span>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#23a55ade' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#23a55a',
|
||||||
|
defaultColor: '#FFFFFF',
|
||||||
|
defaultBorderColor: '#23a55a',
|
||||||
|
defaultHoverBg: '#209652',
|
||||||
|
defaultHoverColor: '#FFFFFF',
|
||||||
|
defaultHoverBorderColor: '#23a55a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={onAddNew}
|
||||||
|
>
|
||||||
|
Add New Error Code
|
||||||
|
</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
open={visible}
|
||||||
|
onCancel={onClose}
|
||||||
|
closable={false}
|
||||||
|
maskClosable={false}
|
||||||
|
width={1200}
|
||||||
|
footer={[
|
||||||
|
<Button key="close" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={errorCodes}
|
||||||
|
loading={loading || confirmLoading}
|
||||||
|
rowKey="key"
|
||||||
|
pagination={{
|
||||||
|
pageSize: 10,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} items`,
|
||||||
|
}}
|
||||||
|
scroll={{ x: 1000 }}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorCodeListModal;
|
||||||
217
src/pages/master/brandDevice/component/ErrorCodeSimpleForm.jsx
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import { Form, Input, Switch, Upload, Button, Typography, message, ConfigProvider } from 'antd';
|
||||||
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
|
import { uploadFile } from '../../../../api/file-uploads';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const ErrorCodeSimpleForm = ({
|
||||||
|
errorCodeForm,
|
||||||
|
isErrorCodeFormReadOnly = false,
|
||||||
|
errorCodeIcon,
|
||||||
|
onErrorCodeIconUpload,
|
||||||
|
onErrorCodeIconRemove,
|
||||||
|
onAddErrorCode,
|
||||||
|
}) => {
|
||||||
|
const statusValue = Form.useWatch('status', errorCodeForm);
|
||||||
|
|
||||||
|
const handleIconUpload = async (file) => {
|
||||||
|
// Check if file is an image
|
||||||
|
const isImage = file.type.startsWith('image/');
|
||||||
|
if (!isImage) {
|
||||||
|
message.error('You can only upload image files!');
|
||||||
|
return Upload.LIST_IGNORE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file size (max 2MB)
|
||||||
|
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||||
|
if (!isLt2M) {
|
||||||
|
message.error('Image must be smaller than 2MB!');
|
||||||
|
return Upload.LIST_IGNORE;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileExtension = file.name.split('.').pop().toLowerCase();
|
||||||
|
const isImageFile = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(
|
||||||
|
fileExtension
|
||||||
|
);
|
||||||
|
const fileType = isImageFile ? 'image' : 'pdf';
|
||||||
|
const folder = 'images';
|
||||||
|
|
||||||
|
const uploadResponse = await uploadFile(file, folder);
|
||||||
|
const iconPath =
|
||||||
|
uploadResponse.data?.path_icon || uploadResponse.data?.path_solution || '';
|
||||||
|
|
||||||
|
if (iconPath) {
|
||||||
|
onErrorCodeIconUpload({
|
||||||
|
name: file.name,
|
||||||
|
uploadPath: iconPath,
|
||||||
|
fileExtension,
|
||||||
|
isImage: isImageFile,
|
||||||
|
size: file.size,
|
||||||
|
});
|
||||||
|
message.success(`${file.name} uploaded successfully!`);
|
||||||
|
} else {
|
||||||
|
message.error(`Failed to upload ${file.name}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading icon:', error);
|
||||||
|
message.error(`Failed to upload ${file.name}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIconRemove = () => {
|
||||||
|
onErrorCodeIconRemove();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Status Switch */}
|
||||||
|
<Form.Item label="Status" name="status">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Form.Item name="status" valuePropName="checked" noStyle>
|
||||||
|
<Switch
|
||||||
|
disabled={isErrorCodeFormReadOnly}
|
||||||
|
style={{ backgroundColor: statusValue ? '#23A55A' : '#bfbfbf' }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Text style={{ marginLeft: 8 }}>{statusValue ? 'Active' : 'Inactive'}</Text>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* Error Code */}
|
||||||
|
<Form.Item
|
||||||
|
label="Error Code"
|
||||||
|
name="error_code"
|
||||||
|
rules={[{ required: true, message: 'Error code wajib diisi!' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Enter error code" disabled={isErrorCodeFormReadOnly} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* Error Name */}
|
||||||
|
<Form.Item
|
||||||
|
label="Error Name"
|
||||||
|
name="error_code_name"
|
||||||
|
rules={[{ required: true, message: 'Error name wajib diisi!' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Enter error name" disabled={isErrorCodeFormReadOnly} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* Error Description */}
|
||||||
|
<Form.Item label="Description" name="error_code_description">
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="Enter error description"
|
||||||
|
rows={3}
|
||||||
|
disabled={isErrorCodeFormReadOnly}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* Color and Icon in same row */}
|
||||||
|
<Form.Item label="Color & Icon">
|
||||||
|
<Input.Group compact>
|
||||||
|
<Form.Item name="error_code_color" noStyle>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
disabled={isErrorCodeFormReadOnly}
|
||||||
|
style={{
|
||||||
|
width: '30%',
|
||||||
|
height: '40px',
|
||||||
|
border: '1px solid #d9d9d9',
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
defaultValue="#000000"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item noStyle style={{ width: '70%', paddingLeft: 8 }}>
|
||||||
|
{!isErrorCodeFormReadOnly ? (
|
||||||
|
<Upload
|
||||||
|
beforeUpload={handleIconUpload}
|
||||||
|
showUploadList={false}
|
||||||
|
accept="image/*"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Button icon={<UploadOutlined />} style={{ width: '100%' }}>
|
||||||
|
Upload Icon
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
border: '1px solid #d9d9d9',
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text type="secondary">No upload allowed</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
</Input.Group>
|
||||||
|
|
||||||
|
{errorCodeIcon && (
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<img
|
||||||
|
src={errorCodeIcon.url || errorCodeIcon.uploadPath}
|
||||||
|
alt="Error Code Icon"
|
||||||
|
style={{
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
objectFit: 'cover',
|
||||||
|
border: '1px solid #d9d9d9',
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Text style={{ fontSize: 12 }}>{errorCodeIcon.name}</Text>
|
||||||
|
<br />
|
||||||
|
<Text type="secondary" style={{ fontSize: 10 }}>
|
||||||
|
Size: {(errorCodeIcon.size / 1024).toFixed(1)} KB
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{!isErrorCodeFormReadOnly && (
|
||||||
|
<Button type="text" danger size="small" onClick={handleIconRemove}>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* Add Error Code Button */}
|
||||||
|
{!isErrorCodeFormReadOnly && (
|
||||||
|
<Form.Item>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#23a55ade' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#23a55a',
|
||||||
|
defaultColor: '#FFFFFF',
|
||||||
|
defaultBorderColor: '#23a55a',
|
||||||
|
defaultHoverBg: '#209652',
|
||||||
|
defaultHoverColor: '#FFFFFF',
|
||||||
|
defaultHoverBorderColor: '#23a55a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
htmlType="button"
|
||||||
|
onClick={() => {
|
||||||
|
// Call parent function to add error code
|
||||||
|
onAddErrorCode();
|
||||||
|
}}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
Simpan Error Code
|
||||||
|
</Button>
|
||||||
|
</ConfigProvider>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorCodeSimpleForm;
|
||||||
@@ -1,45 +1,18 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Upload, Modal, Button, Typography, Space, Image } from 'antd';
|
import { Upload, Modal } from 'antd';
|
||||||
import { UploadOutlined, EyeOutlined, DeleteOutlined, FileOutlined } from '@ant-design/icons';
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
import { NotifOk, NotifAlert } from '../../../../components/Global/ToastNotif';
|
import { NotifOk, NotifAlert } from '../../../../components/Global/ToastNotif';
|
||||||
import { uploadFile, getFolderFromFileType, getFileUrl, getFileType } from '../../../../api/file-uploads';
|
import { uploadFile, getFolderFromFileType } from '../../../../api/file-uploads';
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
const FileUploadHandler = ({
|
const FileUploadHandler = ({
|
||||||
type = 'solution',
|
solutionFields,
|
||||||
maxCount = 1,
|
fileList,
|
||||||
accept = '.pdf,.jpg,.jpeg,.png,.gif',
|
|
||||||
disabled = false,
|
|
||||||
|
|
||||||
fileList = [],
|
|
||||||
onFileUpload,
|
onFileUpload,
|
||||||
onFileRemove,
|
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 [previewOpen, setPreviewOpen] = useState(false);
|
||||||
const [previewImage, setPreviewImage] = useState('');
|
const [previewImage, setPreviewImage] = useState('');
|
||||||
const [previewTitle, setPreviewTitle] = 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) =>
|
const getBase64 = (file) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
@@ -49,372 +22,99 @@ const FileUploadHandler = ({
|
|||||||
reader.onerror = (error) => reject(error);
|
reader.onerror = (error) => reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handlePreview = async (file) => {
|
const handleUploadPreview = async (file) => {
|
||||||
if (!file.url && !file.preview) {
|
const preview = await getBase64(file);
|
||||||
file.preview = await getBase64(file.originFileObj);
|
setPreviewImage(preview);
|
||||||
}
|
|
||||||
setPreviewImage(file.url || file.preview);
|
|
||||||
setPreviewOpen(true);
|
|
||||||
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
|
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
|
||||||
|
setPreviewOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateFile = (file) => {
|
const handleFileUpload = async (file) => {
|
||||||
const isAllowedType = [
|
const isAllowedType = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'].includes(file.type);
|
||||||
'application/pdf',
|
|
||||||
'image/jpeg',
|
|
||||||
'image/png',
|
|
||||||
'image/gif',
|
|
||||||
].includes(file.type);
|
|
||||||
|
|
||||||
if (!isAllowedType) {
|
if (!isAllowedType) {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`,
|
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`
|
||||||
});
|
});
|
||||||
return false;
|
return Upload.LIST_IGNORE;
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileUpload = async (file) => {
|
|
||||||
if (isUploading) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validateFile(file)) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsUploading(true);
|
|
||||||
|
|
||||||
const fileExtension = file.name.split('.').pop().toLowerCase();
|
const fileExtension = file.name.split('.').pop().toLowerCase();
|
||||||
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension);
|
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension);
|
||||||
const fileType = isImage ? 'image' : 'pdf';
|
const fileType = isImage ? 'image' : 'pdf';
|
||||||
const folder = getFolderFromFileType(fileType);
|
const folder = getFolderFromFileType(fileType);
|
||||||
|
|
||||||
const uploadResponse = await uploadFile(file, folder);
|
const uploadResponse = await uploadFile(file, folder);
|
||||||
|
const actualPath = uploadResponse.data?.path_solution || '';
|
||||||
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) {
|
if (actualPath) {
|
||||||
let fileObject;
|
file.uploadPath = actualPath;
|
||||||
|
file.solution_name = file.name;
|
||||||
if (type === 'error_code') {
|
file.solutionId = solutionFields[0];
|
||||||
fileObject = {
|
file.type_solution = fileType;
|
||||||
name: file.name,
|
onFileUpload(file);
|
||||||
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({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: `${file.name} berhasil diupload!`
|
message: `${file.name} berhasil diupload!`
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsUploading(false);
|
|
||||||
return false;
|
|
||||||
} else {
|
} else {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Gagal',
|
title: 'Gagal',
|
||||||
message: `Gagal mengupload ${file.name}. Tidak dapat menemukan path file dalam response.`,
|
message: `Gagal mengupload ${file.name}`
|
||||||
});
|
});
|
||||||
setIsUploading(false);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error uploading file:', error);
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`,
|
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;
|
return false;
|
||||||
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 = {
|
const uploadProps = {
|
||||||
name: 'file',
|
multiple: true,
|
||||||
multiple: false,
|
accept: '.pdf,.jpg,.jpeg,.png,.gif',
|
||||||
accept,
|
onRemove: onFileRemove,
|
||||||
disabled: disabled || isUploading,
|
beforeUpload: handleFileUpload,
|
||||||
fileList: [],
|
fileList,
|
||||||
beforeUpload: () => false,
|
onPreview: handleUploadPreview,
|
||||||
onChange: handleFileChange,
|
|
||||||
onPreview: handlePreview,
|
|
||||||
maxCount,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ ...containerStyle }}>
|
<>
|
||||||
{!existingFile && (
|
<Upload.Dragger {...uploadProps}>
|
||||||
<Upload {...uploadProps}>
|
<p className="ant-upload-drag-icon">
|
||||||
{type === 'drag' ? (
|
<UploadOutlined />
|
||||||
<Upload.Dragger>
|
</p>
|
||||||
<p className="ant-upload-drag-icon">
|
<p className="ant-upload-text">Click or drag file to this area to upload</p>
|
||||||
<UploadOutlined />
|
<p className="ant-upload-hint">Support for PDF and image files only</p>
|
||||||
</p>
|
</Upload.Dragger>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={previewOpen}
|
||||||
{showPreview && (
|
title={previewTitle}
|
||||||
<Modal
|
footer={null}
|
||||||
open={previewOpen}
|
onCancel={() => setPreviewOpen(false)}
|
||||||
title={previewTitle}
|
width="80%"
|
||||||
footer={null}
|
style={{ top: 20 }}
|
||||||
onCancel={() => setPreviewOpen(false)}
|
>
|
||||||
width={600}
|
{previewImage && (
|
||||||
style={{ top: 100 }}
|
<img
|
||||||
>
|
alt={previewTitle}
|
||||||
{previewImage && (
|
style={{ width: '100%' }}
|
||||||
<img
|
src={previewImage}
|
||||||
alt={previewTitle}
|
/>
|
||||||
style={{ width: '100%' }}
|
)}
|
||||||
src={previewImage}
|
</Modal>
|
||||||
/>
|
</>
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
70
src/pages/master/brandDevice/component/FormActions.jsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, ConfigProvider } from 'antd';
|
||||||
|
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
const FormActions = ({
|
||||||
|
currentStep,
|
||||||
|
onPreviousStep,
|
||||||
|
onNextStep,
|
||||||
|
onSave,
|
||||||
|
onCancel,
|
||||||
|
confirmLoading,
|
||||||
|
isEditMode = false,
|
||||||
|
showCancelButton = true
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#E9F6EF' },
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: 'white',
|
||||||
|
defaultColor: '#23A55A',
|
||||||
|
defaultBorderColor: '#23A55A',
|
||||||
|
defaultHoverColor: '#23A55A',
|
||||||
|
defaultHoverBorderColor: '#23A55A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showCancelButton && (
|
||||||
|
<Button onClick={onCancel}>Batal</Button>
|
||||||
|
)}
|
||||||
|
{currentStep > 0 && (
|
||||||
|
<Button onClick={onPreviousStep} style={{ marginRight: 8 }}>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ConfigProvider>
|
||||||
|
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#23a55a',
|
||||||
|
defaultColor: '#FFFFFF',
|
||||||
|
defaultBorderColor: '#23a55a',
|
||||||
|
defaultHoverBg: '#209652',
|
||||||
|
defaultHoverColor: '#FFFFFF',
|
||||||
|
defaultHoverBorderColor: '#23a55a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentStep < 1 && (
|
||||||
|
<Button loading={confirmLoading} onClick={onNextStep}>
|
||||||
|
Lanjut
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{currentStep === 1 && (
|
||||||
|
<Button loading={confirmLoading} onClick={onSave}>
|
||||||
|
{isEditMode ? 'Update' : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ConfigProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormActions;
|
||||||
@@ -26,12 +26,26 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
|||||||
key: 'brand_name',
|
key: 'brand_name',
|
||||||
width: '20%',
|
width: '20%',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Type',
|
||||||
|
dataIndex: 'brand_type',
|
||||||
|
key: 'brand_type',
|
||||||
|
width: '15%',
|
||||||
|
render: (text) => text || '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Manufacturer',
|
title: 'Manufacturer',
|
||||||
dataIndex: 'brand_manufacture',
|
dataIndex: 'brand_manufacture',
|
||||||
key: 'brand_manufacture',
|
key: 'brand_manufacture',
|
||||||
width: '20%',
|
width: '20%',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Model',
|
||||||
|
dataIndex: 'brand_model',
|
||||||
|
key: 'brand_model',
|
||||||
|
width: '15%',
|
||||||
|
render: (text) => text || '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Status',
|
title: 'Status',
|
||||||
dataIndex: 'is_active',
|
dataIndex: 'is_active',
|
||||||
@@ -91,9 +105,9 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
|||||||
const ListBrandDevice = memo(function ListBrandDevice(props) {
|
const ListBrandDevice = memo(function ListBrandDevice(props) {
|
||||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||||
|
|
||||||
const defaultFilter = { criteria: '' };
|
const defaultFilter = { search: '' };
|
||||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -114,21 +128,23 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
setFormDataFilter({ criteria: searchText });
|
setFormDataFilter({ search: searchValue });
|
||||||
setTrigerFilter((prev) => !prev);
|
setTrigerFilter((prev) => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchClear = () => {
|
const handleSearchClear = () => {
|
||||||
setSearchText('');
|
setSearchValue('');
|
||||||
setFormDataFilter({ criteria: '' });
|
setFormDataFilter({ search: '' });
|
||||||
setTrigerFilter((prev) => !prev);
|
setTrigerFilter((prev) => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showPreviewModal = (param) => {
|
const showPreviewModal = (param) => {
|
||||||
|
// Direct navigation without loading, page will handle its own loading
|
||||||
navigate(`/master/brand-device/view/${param.brand_id}`);
|
navigate(`/master/brand-device/view/${param.brand_id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEditModal = (param = null) => {
|
const showEditModal = (param = null) => {
|
||||||
|
// Direct navigation without loading, page will handle its own loading
|
||||||
if (param) {
|
if (param) {
|
||||||
navigate(`/master/brand-device/edit/${param.brand_id}`);
|
navigate(`/master/brand-device/edit/${param.brand_id}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -142,7 +158,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
|||||||
title: 'Konfirmasi',
|
title: 'Konfirmasi',
|
||||||
message: 'Apakah anda yakin hapus data "' + param.brand_name + '" ?',
|
message: 'Apakah anda yakin hapus data "' + param.brand_name + '" ?',
|
||||||
onConfirm: () => handleDelete(param.brand_id, param.brand_name),
|
onConfirm: () => handleDelete(param.brand_id, param.brand_name),
|
||||||
onCancel: () => { },
|
onCancel: () => {},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,7 +172,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
|||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: `Brand ${brand_name} deleted successfully.`,
|
message: `Brand ${brand_name} deleted successfully.`,
|
||||||
});
|
});
|
||||||
doFilter();
|
doFilter(); // Refresh data
|
||||||
} else {
|
} else {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
@@ -165,6 +181,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Delete Brand Device Error:', error);
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
@@ -182,12 +199,13 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
|||||||
<Col xs={24} sm={24} md={12} lg={12}>
|
<Col xs={24} sm={24} md={12} lg={12}>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder="Search brand device..."
|
placeholder="Search brand device..."
|
||||||
value={searchText}
|
value={searchValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
setSearchText(value);
|
setSearchValue(value);
|
||||||
|
// Auto search when clearing by backspace/delete
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
setFormDataFilter({ criteria: '' });
|
setFormDataFilter({ search: '' });
|
||||||
setTrigerFilter((prev) => !prev);
|
setTrigerFilter((prev) => !prev);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -233,7 +251,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
|||||||
}}
|
}}
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
Add data
|
Add Brand Device
|
||||||
</Button>
|
</Button>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -1,315 +1,84 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React from 'react';
|
||||||
import { Card, Input, Button, Row, Col, Empty } from 'antd';
|
import { Table, Button, Space } from 'antd';
|
||||||
import { PlusOutlined, SearchOutlined, DeleteOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
|
import { EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import { getErrorCodesByBrandId, deleteErrorCode } from '../../../../api/master-brand';
|
|
||||||
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
|
|
||||||
|
|
||||||
const ListErrorCode = ({
|
const ErrorCodeTable = ({
|
||||||
brandId,
|
errorCodes,
|
||||||
selectedErrorCode,
|
loading,
|
||||||
onErrorCodeSelect,
|
onPreview,
|
||||||
onAddNew,
|
onEdit,
|
||||||
tempErrorCodes = [],
|
onDelete,
|
||||||
trigerFilter,
|
onFileView
|
||||||
searchText,
|
|
||||||
onSearchChange,
|
|
||||||
onSearch,
|
|
||||||
onSearchClear,
|
|
||||||
isReadOnly = false,
|
|
||||||
errorCodes: propErrorCodes = null
|
|
||||||
}) => {
|
}) => {
|
||||||
const [errorCodes, setErrorCodes] = useState([]);
|
const errorCodeColumns = [
|
||||||
const [loading, setLoading] = useState(false);
|
{ title: 'Error Code', dataIndex: 'error_code', key: 'error_code' },
|
||||||
const [pagination, setPagination] = useState({
|
{ title: 'Error Code Name', dataIndex: 'error_code_name', key: 'error_code_name' },
|
||||||
current_page: 1,
|
{
|
||||||
current_limit: 15,
|
title: 'Solutions',
|
||||||
total_limit: 0,
|
dataIndex: 'solution',
|
||||||
total_page: 0,
|
key: 'solution',
|
||||||
});
|
render: (solutions) => (
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
<div>
|
||||||
const pageSize = 15;
|
{solutions && solutions.length > 0 ? (
|
||||||
|
solutions.map((sol, index) => (
|
||||||
|
<div key={index} style={{ marginBottom: 4 }}>
|
||||||
|
<span style={{ fontSize: '12px' }}>
|
||||||
|
{sol.solution_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span style={{ color: '#999', fontSize: '12px' }}>No solutions</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Action',
|
||||||
|
key: 'action',
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => onPreview(record)}
|
||||||
|
style={{ color: '#1890ff', borderColor: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => onEdit(record)}
|
||||||
|
style={{ color: '#faad14', borderColor: '#faad14' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
type="text"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => onDelete(record.key)}
|
||||||
|
style={{ borderColor: '#ff4d4f' }}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const queryParams = useMemo(() => {
|
const dataSource = loading
|
||||||
const params = new URLSearchParams();
|
? Array.from({ length: 3 }, (_, index) => ({
|
||||||
params.set('page', currentPage.toString());
|
key: `loading-${index}`,
|
||||||
params.set('limit', pageSize.toString());
|
error_code: 'Loading...',
|
||||||
if (searchText) {
|
error_code_name: 'Loading...',
|
||||||
params.set('criteria', searchText);
|
solution: []
|
||||||
}
|
}))
|
||||||
return params;
|
: errorCodes;
|
||||||
}, [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 (
|
return (
|
||||||
<Card
|
<Table
|
||||||
title="Daftar Error Code"
|
columns={errorCodeColumns}
|
||||||
style={{ width: '100%', minWidth: '472px' }}
|
dataSource={dataSource}
|
||||||
styles={{ body: { padding: '12px' } }}
|
rowKey="key"
|
||||||
>
|
pagination={false}
|
||||||
<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;
|
export default ErrorCodeTable;
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Form, Input, Button, Switch, Radio, Typography, Space, Card, ConfigProvider } from 'antd';
|
import { Form, Input, Button, Switch, Radio, Upload, Typography, Space } from 'antd';
|
||||||
import { DeleteOutlined, EyeOutlined, FileOutlined } from '@ant-design/icons';
|
import { DeleteOutlined, UploadOutlined, EyeOutlined } from '@ant-design/icons';
|
||||||
import FileUploadHandler from './FileUploadHandler';
|
import { uploadFile, getFolderFromFileType } from '../../../../api/file-uploads';
|
||||||
import { NotifAlert } from '../../../../components/Global/ToastNotif';
|
import { NotifAlert } from '../../../../components/Global/ToastNotif';
|
||||||
import { getFileUrl, getFolderFromFileType } from '../../../../api/file-uploads';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
@@ -21,475 +20,223 @@ const SolutionFieldNew = ({
|
|||||||
onRemove,
|
onRemove,
|
||||||
onFileUpload,
|
onFileUpload,
|
||||||
onFileView,
|
onFileView,
|
||||||
fileList = [],
|
fileList = []
|
||||||
originalSolutionData = null
|
|
||||||
}) => {
|
}) => {
|
||||||
const form = Form.useFormInstance();
|
const [currentStatus, setCurrentStatus] = useState(solutionStatus ?? true);
|
||||||
const [currentFile, setCurrentFile] = useState(null);
|
|
||||||
const [isDeleted, setIsDeleted] = useState(false);
|
|
||||||
|
|
||||||
const fileUpload = Form.useWatch(['solution_items', fieldKey, 'fileUpload'], form);
|
// Watch form values
|
||||||
const file = Form.useWatch(['solution_items', fieldKey, 'file'], form);
|
const getFieldValue = () => {
|
||||||
const nameValue = Form.useWatch(['solution_items', fieldKey, 'name'], form);
|
try {
|
||||||
const fileNameValue = Form.useWatch(['solution_items', fieldKey, 'fileName'], form);
|
const form = document.querySelector(`[data-field="${fieldName}"]`)?.form;
|
||||||
const statusValue = Form.useWatch(['solution_items', fieldKey, 'status'], form) ?? true;
|
if (form) {
|
||||||
|
const formData = new FormData(form);
|
||||||
const pathSolution = Form.useWatch(['solution_items', fieldKey, 'path_solution'], form);
|
return formData.get(`${fieldName}.status`) === 'on';
|
||||||
|
}
|
||||||
const [deleteCounter, setDeleteCounter] = useState(0);
|
return currentStatus;
|
||||||
|
} catch {
|
||||||
React.useEffect(() => {
|
return currentStatus;
|
||||||
if (!nameValue || nameValue === '') {
|
|
||||||
setCurrentFile(null);
|
|
||||||
setIsDeleted(false);
|
|
||||||
setDeleteCounter(prev => prev + 1);
|
|
||||||
}
|
}
|
||||||
}, [nameValue]);
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
const getFileFromFormValues = () => {
|
setCurrentStatus(solutionStatus ?? true);
|
||||||
const hasValidFileUpload = fileUpload && typeof fileUpload === 'object' && Object.keys(fileUpload).length > 0;
|
}, [solutionStatus]);
|
||||||
const hasValidFile = file && typeof file === 'object' && Object.keys(file).length > 0;
|
const handleFileUpload = async (file) => {
|
||||||
const hasValidPath = pathSolution && pathSolution.trim() !== '';
|
try {
|
||||||
|
const isAllowedType = [
|
||||||
|
'application/pdf',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
].includes(file.type);
|
||||||
|
|
||||||
const wasExplicitlyDeleted =
|
if (!isAllowedType) {
|
||||||
(fileUpload === null || file === null || pathSolution === null) &&
|
NotifAlert({
|
||||||
!hasValidFileUpload &&
|
icon: 'error',
|
||||||
!hasValidFile &&
|
title: 'Error',
|
||||||
!hasValidPath;
|
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`,
|
||||||
|
});
|
||||||
if (wasExplicitlyDeleted) {
|
return;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (solutionType === 'text') {
|
const fileExtension = file.name.split('.').pop().toLowerCase();
|
||||||
return null;
|
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension);
|
||||||
}
|
const fileType = isImage ? 'image' : 'pdf';
|
||||||
|
const folder = getFolderFromFileType(fileType);
|
||||||
|
|
||||||
if (hasValidFileUpload) {
|
const uploadResponse = await uploadFile(file, folder);
|
||||||
return fileUpload;
|
const actualPath = uploadResponse.data?.path_solution || '';
|
||||||
}
|
|
||||||
if (hasValidFile) {
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
if (hasValidPath) {
|
|
||||||
return {
|
|
||||||
name: fileNameValue || pathSolution.split('/').pop() || 'File',
|
|
||||||
uploadPath: pathSolution,
|
|
||||||
url: pathSolution,
|
|
||||||
path: pathSolution
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
if (actualPath) {
|
||||||
};
|
// Store the file info with the solution field
|
||||||
|
file.uploadPath = actualPath;
|
||||||
const fileFromForm = getFileFromFormValues();
|
file.solutionId = fieldKey;
|
||||||
|
file.type_solution = fileType;
|
||||||
if (JSON.stringify(currentFile) !== JSON.stringify(fileFromForm)) {
|
onFileUpload(file);
|
||||||
setCurrentFile(fileFromForm);
|
NotifAlert({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
message: `${file.name} berhasil diupload!`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: `Gagal mengupload ${file.name}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading file:', error);
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [fileUpload, file, pathSolution, solutionType, deleteCounter, fileNameValue, fieldKey]);
|
};
|
||||||
|
|
||||||
|
|
||||||
const renderSolutionContent = () => {
|
const renderSolutionContent = () => {
|
||||||
if (solutionType === 'text') {
|
if (solutionType === 'text') {
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={['solution_items', fieldKey, 'text']}
|
name={[fieldName, 'text']}
|
||||||
rules={[{ required: true, message: 'Text solution wajib diisi!' }]}
|
rules={[{ required: true, message: 'Text solution wajib diisi!' }]}
|
||||||
>
|
>
|
||||||
<TextArea
|
<TextArea
|
||||||
placeholder="Enter solution text"
|
placeholder="Enter solution text"
|
||||||
rows={3}
|
rows={3}
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
style={{ fontSize: 12 }}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (solutionType === 'file') {
|
if (solutionType === 'file') {
|
||||||
const hasOriginalFile = originalSolutionData && (
|
const currentFiles = fileList.filter(file => file.solutionId === fieldKey);
|
||||||
originalSolutionData.path_solution ||
|
|
||||||
originalSolutionData.path_document
|
|
||||||
);
|
|
||||||
|
|
||||||
let displayFile = null;
|
return (
|
||||||
|
<div>
|
||||||
if (currentFile && Object.keys(currentFile).length > 0) {
|
<Form.Item
|
||||||
displayFile = currentFile;
|
name={[fieldName, 'file']}
|
||||||
}
|
rules={[{ required: true, message: 'File solution wajib diupload!' }]}
|
||||||
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 }}>
|
<Upload
|
||||||
<div style={{
|
beforeUpload={handleFileUpload}
|
||||||
display: 'flex',
|
showUploadList={false}
|
||||||
alignItems: 'center',
|
accept=".pdf,.jpg,.jpeg,.png,.gif"
|
||||||
justifyContent: 'center',
|
disabled={isReadOnly}
|
||||||
width: 48,
|
>
|
||||||
height: 48,
|
<Button
|
||||||
borderRadius: 8,
|
icon={<UploadOutlined />}
|
||||||
backgroundColor: '#f0f5ff',
|
disabled={isReadOnly}
|
||||||
flexShrink: 0
|
style={{ width: '100%' }}
|
||||||
}}>
|
>
|
||||||
<FileOutlined style={{ fontSize: 24, color: '#1890ff' }} />
|
Upload File (PDF/Image)
|
||||||
</div>
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
{currentFiles.length > 0 && (
|
||||||
<div style={{
|
<div style={{ marginTop: 8 }}>
|
||||||
fontSize: 13,
|
{currentFiles.map((file, index) => (
|
||||||
fontWeight: 600,
|
<div key={index} style={{
|
||||||
color: '#262626',
|
display: 'flex',
|
||||||
marginBottom: 4,
|
justifyContent: 'space-between',
|
||||||
overflow: 'hidden',
|
alignItems: 'center',
|
||||||
textOverflow: 'ellipsis',
|
padding: '4px 8px',
|
||||||
whiteSpace: 'nowrap'
|
border: '1px solid #d9d9d9',
|
||||||
|
borderRadius: 4,
|
||||||
|
marginBottom: 4
|
||||||
}}>
|
}}>
|
||||||
{displayFileName}
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<Text style={{ fontSize: 12 }}>{file.name}</Text>
|
||||||
|
<Text type="secondary" style={{ fontSize: 10 }}>
|
||||||
|
({(file.size / 1024).toFixed(1)} KB)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => onFileView(file.uploadPath, file.type_solution)}
|
||||||
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Card>
|
)}
|
||||||
);
|
</div>
|
||||||
} 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 null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider
|
<div style={{
|
||||||
theme={{
|
border: '1px solid #d9d9d9',
|
||||||
components: {
|
borderRadius: 8,
|
||||||
Switch: {
|
padding: 16,
|
||||||
colorPrimary: '#23A55A',
|
marginBottom: 16,
|
||||||
colorPrimaryHover: '#23A55A',
|
backgroundColor: isReadOnly ? '#f5f5f5' : 'white'
|
||||||
},
|
}}>
|
||||||
},
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||||
}}
|
<Text strong>Solution #{index + 1}</Text>
|
||||||
>
|
<Space>
|
||||||
<div style={{
|
<Form.Item
|
||||||
border: '1px solid #d9d9d9',
|
name={[fieldName, 'name']}
|
||||||
borderRadius: 6,
|
rules={[{ required: true, message: 'Solution name wajib diisi!' }]}
|
||||||
padding: 12,
|
style={{ margin: 0, width: 200 }}
|
||||||
marginBottom: 12,
|
>
|
||||||
backgroundColor: isReadOnly ? '#f5f5f5' : 'white'
|
<Input
|
||||||
}}>
|
placeholder="Solution name"
|
||||||
<div style={{
|
disabled={isReadOnly}
|
||||||
marginBottom: 8,
|
/>
|
||||||
gap: 8
|
</Form.Item>
|
||||||
}}>
|
|
||||||
<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 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<Form.Item name={[fieldName, 'status']} valuePropName="checked" noStyle>
|
||||||
<Form.Item name={['solution_items', fieldKey, 'status']} valuePropName="checked" noStyle>
|
<Switch
|
||||||
<Switch
|
disabled={isReadOnly}
|
||||||
size="small"
|
onChange={(checked) => {
|
||||||
disabled={isReadOnly}
|
onStatusChange(fieldKey, checked);
|
||||||
onChange={(checked) => {
|
setCurrentStatus(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={{
|
style={{
|
||||||
fontSize: 12,
|
backgroundColor: currentStatus ? '#23A55A' : '#bfbfbf'
|
||||||
padding: '2px 4px',
|
|
||||||
height: '24px'
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</Form.Item>
|
||||||
|
<Text style={{ fontSize: 12, color: '#666' }}>
|
||||||
|
{currentStatus ? 'Active' : 'Inactive'}
|
||||||
|
</Text>
|
||||||
</div>
|
</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>
|
|
||||||
|
|
||||||
|
{canRemove && !isReadOnly && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={onRemove}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={['solution_items', fieldKey, 'type']}
|
name={[fieldName, 'type']}
|
||||||
rules={[{ required: true, message: 'Solution type wajib diisi!' }]}
|
rules={[{ required: true, message: 'Solution type wajib diisi!' }]}
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
initialValue={solutionType || 'text'}
|
|
||||||
>
|
>
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
onChange={(e) => {
|
onChange={(e) => onTypeChange(fieldKey, e.target.value)}
|
||||||
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}
|
disabled={isReadOnly}
|
||||||
size="small"
|
|
||||||
>
|
>
|
||||||
<Radio value="text" style={{ fontSize: 12 }}>Text</Radio>
|
<Radio value="text">Text Solution</Radio>
|
||||||
<Radio value="file" style={{ fontSize: 12 }}>File</Radio>
|
<Radio value="file">File Solution</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name={['solution_items', fieldKey, 'status']}
|
|
||||||
initialValue={solutionStatus !== false ? true : false}
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
<input type="hidden" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
{renderSolutionContent()}
|
{renderSolutionContent()}
|
||||||
</div>
|
</div>
|
||||||
</ConfigProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Typography, Divider, Button, Form } from 'antd';
|
import { Form, Card, Typography, Divider, Button } from 'antd';
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import SolutionFieldNew from './SolutionField';
|
import SolutionFieldNew from './SolutionField';
|
||||||
|
|
||||||
@@ -10,64 +10,67 @@ const SolutionForm = ({
|
|||||||
solutionFields,
|
solutionFields,
|
||||||
solutionTypes,
|
solutionTypes,
|
||||||
solutionStatuses,
|
solutionStatuses,
|
||||||
|
fileList,
|
||||||
|
solutionsToDelete,
|
||||||
|
firstSolutionValid,
|
||||||
onAddSolutionField,
|
onAddSolutionField,
|
||||||
onRemoveSolutionField,
|
onRemoveSolutionField,
|
||||||
onSolutionTypeChange,
|
onSolutionTypeChange,
|
||||||
onSolutionStatusChange,
|
onSolutionStatusChange,
|
||||||
onSolutionFileUpload,
|
onSolutionFileUpload,
|
||||||
onFileView,
|
onFileView,
|
||||||
fileList,
|
|
||||||
isReadOnly = false,
|
isReadOnly = false,
|
||||||
solutionData = [],
|
onAddSolution,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: 0 }}>
|
<div>
|
||||||
|
<Form
|
||||||
|
form={solutionForm}
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={{
|
||||||
|
solution_status_0: true,
|
||||||
|
solution_type_0: 'text',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Divider orientation="left">Solution Items</Divider>
|
||||||
|
|
||||||
<Form form={solutionForm} layout="vertical">
|
{solutionFields.map((field, index) => (
|
||||||
<div style={{
|
<SolutionFieldNew
|
||||||
maxHeight: '400px',
|
key={field.key}
|
||||||
overflowY: 'auto',
|
fieldKey={field.key}
|
||||||
paddingRight: '8px'
|
fieldName={field.name}
|
||||||
}}>
|
index={index}
|
||||||
{solutionFields.map((field, displayIndex) => (
|
solutionType={solutionTypes[field.key]}
|
||||||
<SolutionFieldNew
|
solutionStatus={solutionStatuses[field.key]}
|
||||||
key={field}
|
onTypeChange={onSolutionTypeChange}
|
||||||
fieldKey={field}
|
onStatusChange={onSolutionStatusChange}
|
||||||
fieldName={['solution_items', field]}
|
onRemove={() => onRemoveSolutionField(field.key)}
|
||||||
index={displayIndex}
|
onFileUpload={onSolutionFileUpload}
|
||||||
solutionType={solutionTypes[field]}
|
onFileView={onFileView}
|
||||||
solutionStatus={solutionStatuses[field]}
|
fileList={fileList}
|
||||||
onTypeChange={onSolutionTypeChange}
|
isReadOnly={isReadOnly}
|
||||||
onStatusChange={onSolutionStatusChange}
|
canRemove={solutionFields.length > 1}
|
||||||
onRemove={() => onRemoveSolutionField(field)}
|
/>
|
||||||
onFileUpload={onSolutionFileUpload}
|
))}
|
||||||
onFileView={onFileView}
|
|
||||||
fileList={fileList}
|
|
||||||
isReadOnly={isReadOnly}
|
|
||||||
canRemove={solutionFields.length > 1 && displayIndex > 0}
|
|
||||||
originalSolutionData={solutionData[displayIndex]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<div style={{ marginBottom: 8, marginTop: 12 }}>
|
<>
|
||||||
<Button
|
<Form.Item>
|
||||||
type="dashed"
|
<Button
|
||||||
onClick={onAddSolutionField}
|
type="dashed"
|
||||||
icon={<PlusOutlined />}
|
onClick={onAddSolutionField}
|
||||||
style={{
|
icon={<PlusOutlined />}
|
||||||
width: '100%',
|
style={{ width: '100%' }}
|
||||||
borderColor: '#23A55A',
|
>
|
||||||
color: '#23A55A',
|
+ Add Solution
|
||||||
height: '32px',
|
</Button>
|
||||||
fontSize: '12px'
|
</Form.Item>
|
||||||
}}
|
<div style={{ marginTop: 16 }}>
|
||||||
>
|
<Text type="secondary">
|
||||||
Add sollution
|
* At least one solution is required for each error code.
|
||||||
</Button>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
152
src/pages/master/brandDevice/component/SparepartField.jsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Form, Select, Button, Switch, Typography, Space, Input, message } from 'antd';
|
||||||
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
|
import { getAllSparepart } from '../../../../api/sparepart';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const SparepartField = ({
|
||||||
|
fieldKey,
|
||||||
|
fieldName,
|
||||||
|
index,
|
||||||
|
sparepartType,
|
||||||
|
sparepartStatus,
|
||||||
|
isReadOnly = false,
|
||||||
|
canRemove = true,
|
||||||
|
onRemove,
|
||||||
|
spareparts = [],
|
||||||
|
onSparepartChange
|
||||||
|
}) => {
|
||||||
|
const [currentStatus, setCurrentStatus] = useState(sparepartStatus ?? true);
|
||||||
|
const [sparepartList, setSparepartList] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentStatus(sparepartStatus ?? true);
|
||||||
|
loadSpareparts();
|
||||||
|
}, [sparepartStatus]);
|
||||||
|
|
||||||
|
const loadSpareparts = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Get all spareparts from the API
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('limit', '100'); // Get all spareparts
|
||||||
|
|
||||||
|
const response = await getAllSparepart(params);
|
||||||
|
// Response structure should have { data: [...], statusCode: 200 }
|
||||||
|
if (response && (response.statusCode === 200 || response.data)) {
|
||||||
|
// If response has data array directly
|
||||||
|
const sparepartData = response.data?.data || response.data || [];
|
||||||
|
setSparepartList(sparepartData);
|
||||||
|
if (onSparepartChange) {
|
||||||
|
onSparepartChange(sparepartData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For demo purposes, use mock data if API fails
|
||||||
|
setSparepartList([
|
||||||
|
{ brand_sparepart_id: 1, sparepart_name: 'Compressor Oil Filter', brand_sparepart_description: 'Oil filter for compressor' },
|
||||||
|
{ brand_sparepart_id: 2, sparepart_name: 'Air Intake Filter', brand_sparepart_description: 'Air intake filter' },
|
||||||
|
{ brand_sparepart_id: 3, sparepart_name: 'Cooling Fan Motor', brand_sparepart_description: 'Motor for cooling fan' },
|
||||||
|
]);
|
||||||
|
if (onSparepartChange) {
|
||||||
|
onSparepartChange([
|
||||||
|
{ brand_sparepart_id: 1, sparepart_name: 'Compressor Oil Filter', brand_sparepart_description: 'Oil filter for compressor' },
|
||||||
|
{ brand_sparepart_id: 2, sparepart_name: 'Air Intake Filter', brand_sparepart_description: 'Air intake filter' },
|
||||||
|
{ brand_sparepart_id: 3, sparepart_name: 'Cooling Fan Motor', brand_sparepart_description: 'Motor for cooling fan' },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading spareparts:', error);
|
||||||
|
// Default mock data
|
||||||
|
const mockSpareparts = [
|
||||||
|
{ brand_sparepart_id: 1, sparepart_name: 'Compressor Oil Filter', brand_sparepart_description: 'Oil filter for compressor' },
|
||||||
|
{ brand_sparepart_id: 2, sparepart_name: 'Air Intake Filter', brand_sparepart_description: 'Air intake filter' },
|
||||||
|
{ brand_sparepart_id: 3, sparepart_name: 'Cooling Fan Motor', brand_sparepart_description: 'Motor for cooling fan' },
|
||||||
|
];
|
||||||
|
setSparepartList(mockSpareparts);
|
||||||
|
if (onSparepartChange) {
|
||||||
|
onSparepartChange(mockSpareparts);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sparepartOptions = sparepartList.map(sparepart => ({
|
||||||
|
label: sparepart.sparepart_name || sparepart.sparepart_name || `Sparepart ${sparepart.sparepart_id || sparepart.brand_sparepart_id}`,
|
||||||
|
value: sparepart.sparepart_id || sparepart.brand_sparepart_id,
|
||||||
|
description: sparepart.sparepart_description
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
border: '1px solid #d9d9d9',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
marginBottom: 16,
|
||||||
|
backgroundColor: isReadOnly ? '#f5f5f5' : 'white'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||||
|
<Text strong>Sparepart #{index + 1}</Text>
|
||||||
|
<Space>
|
||||||
|
<Form.Item
|
||||||
|
name={[fieldName, 'sparepart_id']}
|
||||||
|
rules={[{ required: false, message: 'Sparepart wajib dipilih!' }]} /* Making it optional since sparepart is optional */
|
||||||
|
style={{ margin: 0, width: 200 }}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="Pilih sparepart"
|
||||||
|
loading={loading}
|
||||||
|
disabled={isReadOnly}
|
||||||
|
options={sparepartOptions}
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
|
<Form.Item name={[fieldName, 'status']} valuePropName="checked" noStyle>
|
||||||
|
<Switch
|
||||||
|
disabled={isReadOnly}
|
||||||
|
onChange={(checked) => {
|
||||||
|
setCurrentStatus(checked);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
backgroundColor: currentStatus ? '#23A55A' : '#bfbfbf'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Text style={{ fontSize: 12, color: '#666' }}>
|
||||||
|
{currentStatus ? 'Active' : 'Inactive'}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{canRemove && !isReadOnly && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={onRemove}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sparepart Description */}
|
||||||
|
<Form.Item
|
||||||
|
name={[fieldName, 'description']}
|
||||||
|
label="Deskripsi"
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="Deskripsi sparepart"
|
||||||
|
rows={2}
|
||||||
|
disabled={isReadOnly}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SparepartField;
|
||||||
76
src/pages/master/brandDevice/component/SparepartForm.jsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Form, Card, Typography, Divider, Button } from 'antd';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import SparepartField from './SparepartField';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const SparepartForm = ({
|
||||||
|
sparepartForm,
|
||||||
|
sparepartFields,
|
||||||
|
onAddSparepartField,
|
||||||
|
onRemoveSparepartField,
|
||||||
|
isReadOnly = false,
|
||||||
|
spareparts = [],
|
||||||
|
onSparepartChange
|
||||||
|
}) => {
|
||||||
|
const [sparepartList, setSparepartList] = useState([]);
|
||||||
|
|
||||||
|
const handleSparepartChange = (list) => {
|
||||||
|
setSparepartList(list);
|
||||||
|
if (onSparepartChange) {
|
||||||
|
onSparepartChange(list);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form
|
||||||
|
form={sparepartForm}
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={{
|
||||||
|
sparepart_status_0: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Divider orientation="left">Sparepart Items</Divider>
|
||||||
|
|
||||||
|
{sparepartFields.map((field, index) => (
|
||||||
|
<SparepartField
|
||||||
|
key={field.key}
|
||||||
|
fieldKey={field.key}
|
||||||
|
fieldName={field.name}
|
||||||
|
index={index}
|
||||||
|
sparepartStatus={field.status}
|
||||||
|
onRemove={() => onRemoveSparepartField(field.key)}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
canRemove={sparepartFields.length > 1}
|
||||||
|
spareparts={sparepartList}
|
||||||
|
onSparepartChange={handleSparepartChange}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!isReadOnly && (
|
||||||
|
<>
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={onAddSparepartField}
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
+ Add Sparepart
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<Text type="secondary">
|
||||||
|
* Sparepart is optional and can be added for each error code if needed.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SparepartForm;
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
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;
|
|
||||||
265
src/pages/master/brandDevice/hooks/errorCode.js
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||||
|
|
||||||
|
export const useErrorCodeLogic = (errorCodeForm, fileList) => {
|
||||||
|
const [solutionFields, setSolutionFields] = useState([0]);
|
||||||
|
const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' });
|
||||||
|
const [solutionStatuses, setSolutionStatuses] = useState({ 0: true });
|
||||||
|
const [firstSolutionValid, setFirstSolutionValid] = useState(false);
|
||||||
|
const [solutionsToDelete, setSolutionsToDelete] = useState(new Set());
|
||||||
|
|
||||||
|
const checkPreviousSolutionValid = (currentSolutionIndex) => {
|
||||||
|
for (let i = 0; i < currentSolutionIndex; i++) {
|
||||||
|
const fieldId = solutionFields[i];
|
||||||
|
const solutionType = solutionTypes[fieldId];
|
||||||
|
|
||||||
|
const solutionName = errorCodeForm.getFieldValue(`solution_name_${fieldId}`);
|
||||||
|
if (!solutionName || solutionName.trim() === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (solutionType === 'text') {
|
||||||
|
const textSolution = errorCodeForm.getFieldValue(`text_solution_${fieldId}`);
|
||||||
|
if (!textSolution || textSolution.trim() === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (solutionType === 'file') {
|
||||||
|
const filesForSolution = fileList.filter(file => file.solutionId === fieldId);
|
||||||
|
if (filesForSolution.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkFirstSolutionValid = () => {
|
||||||
|
if (solutionFields.length === 0) {
|
||||||
|
setFirstSolutionValid(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const isValid = checkPreviousSolutionValid(1);
|
||||||
|
setFirstSolutionValid(isValid);
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddSolutionField = () => {
|
||||||
|
const currentSolutionCount = solutionFields.length;
|
||||||
|
const nextSolutionNumber = currentSolutionCount + 1;
|
||||||
|
|
||||||
|
if (!checkPreviousSolutionValid(currentSolutionCount)) {
|
||||||
|
let incompleteSolutionIndex = -1;
|
||||||
|
for (let i = 0; i < currentSolutionCount; i++) {
|
||||||
|
const fieldId = solutionFields[i];
|
||||||
|
const solutionType = solutionTypes[fieldId];
|
||||||
|
const solutionName = errorCodeForm.getFieldValue(`solution_name_${fieldId}`);
|
||||||
|
let hasContent = false;
|
||||||
|
|
||||||
|
if (solutionType === 'text') {
|
||||||
|
const textSolution = errorCodeForm.getFieldValue(`text_solution_${fieldId}`);
|
||||||
|
hasContent = textSolution && textSolution.trim();
|
||||||
|
} else if (solutionType === 'file') {
|
||||||
|
const filesForSolution = fileList.filter(file => file.solutionId === fieldId);
|
||||||
|
hasContent = filesForSolution.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!solutionName?.trim() || !hasContent) {
|
||||||
|
incompleteSolutionIndex = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Perhatian',
|
||||||
|
message: `Harap lengkapi Solution ${incompleteSolutionIndex} terlebih dahulu sebelum menambah Solution ${nextSolutionNumber}!`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newId = `new-${Date.now()}`;
|
||||||
|
setSolutionFields(prev => [...prev, newId]);
|
||||||
|
setSolutionTypes(prev => ({ ...prev, [newId]: 'text' }));
|
||||||
|
setSolutionStatuses(prev => ({ ...prev, [newId]: true }));
|
||||||
|
errorCodeForm.setFieldValue(`solution_status_${newId}`, true);
|
||||||
|
errorCodeForm.setFieldValue(`solution_type_${newId}`, 'text');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveSolutionField = (id) => {
|
||||||
|
const isNewSolution = !id.toString().startsWith('existing-');
|
||||||
|
|
||||||
|
if (isNewSolution) {
|
||||||
|
if (solutionFields.length > 1) {
|
||||||
|
setSolutionFields(solutionFields.filter(fieldId => fieldId !== id));
|
||||||
|
setSolutionTypes(prev => {
|
||||||
|
const newTypes = { ...prev };
|
||||||
|
delete newTypes[id];
|
||||||
|
return newTypes;
|
||||||
|
});
|
||||||
|
setSolutionStatuses(prev => {
|
||||||
|
const newStatuses = { ...prev };
|
||||||
|
delete newStatuses[id];
|
||||||
|
return newStatuses;
|
||||||
|
});
|
||||||
|
setSolutionsToDelete(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(id);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Perhatian',
|
||||||
|
message: 'Setiap error code harus memiliki minimal 1 solution!'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const solutionName = errorCodeForm.getFieldValue(`solution_name_${id}`);
|
||||||
|
const solutionType = solutionTypes[id];
|
||||||
|
let isEmpty = true;
|
||||||
|
|
||||||
|
const existingSolution = window.currentSolutionData?.[id];
|
||||||
|
const hasExistingData = existingSolution && (
|
||||||
|
(existingSolution.solution_name && existingSolution.solution_name.trim()) ||
|
||||||
|
(existingSolution.text_solution && existingSolution.text_solution.trim()) ||
|
||||||
|
(existingSolution.path_solution && existingSolution.path_solution.trim())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (solutionType === 'text') {
|
||||||
|
const textSolution = errorCodeForm.getFieldValue(`text_solution_${id}`);
|
||||||
|
isEmpty = !solutionName?.trim() && !textSolution?.trim() && !hasExistingData;
|
||||||
|
} else if (solutionType === 'file') {
|
||||||
|
const filesForSolution = fileList.filter(file => file.solutionId === id);
|
||||||
|
isEmpty = !solutionName?.trim() && filesForSolution.length === 0 && !hasExistingData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
if (solutionFields.length > 1) {
|
||||||
|
setSolutionFields(solutionFields.filter(fieldId => fieldId !== id));
|
||||||
|
setSolutionTypes(prev => {
|
||||||
|
const newTypes = { ...prev };
|
||||||
|
delete newTypes[id];
|
||||||
|
return newTypes;
|
||||||
|
});
|
||||||
|
setSolutionStatuses(prev => {
|
||||||
|
const newStatuses = { ...prev };
|
||||||
|
delete newStatuses[id];
|
||||||
|
return newStatuses;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.currentSolutionData) {
|
||||||
|
delete window.currentSolutionData[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
setSolutionsToDelete(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(id);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Perhatian',
|
||||||
|
message: 'Setiap error code harus memiliki minimal 1 solution!'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (solutionFields.length > 1) {
|
||||||
|
setSolutionsToDelete(prev => new Set(prev).add(id));
|
||||||
|
|
||||||
|
const solutionElement = document.querySelector(`[data-solution-id="${id}"]`);
|
||||||
|
if (solutionElement) {
|
||||||
|
solutionElement.style.opacity = '0.5';
|
||||||
|
solutionElement.style.border = '2px dashed #ff4d4f';
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifOk({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
message: 'Solution ditandai untuk dihapus. Klik "Update Error Code" untuk menyimpan perubahan.'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Perhatian',
|
||||||
|
message: 'Setiap error code harus memiliki minimal 1 solution!'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSolutionTypeChange = (fieldId, type) => {
|
||||||
|
setSolutionTypes(prev => ({ ...prev, [fieldId]: type }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSolutionStatusChange = (fieldId, status) => {
|
||||||
|
// Only update local state - form is already updated by Form.Item
|
||||||
|
setSolutionStatuses(prev => ({
|
||||||
|
...prev,
|
||||||
|
[fieldId]: status
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSolutionsForExistingRecord = (solutions, errorCodeForm) => {
|
||||||
|
const newSolutionFields = [];
|
||||||
|
const newSolutionTypes = {};
|
||||||
|
const newSolutionStatuses = {};
|
||||||
|
const newSolutionData = {};
|
||||||
|
|
||||||
|
solutions.forEach((solution, index) => {
|
||||||
|
const fieldId = `existing-${index}`;
|
||||||
|
newSolutionFields.push(fieldId);
|
||||||
|
newSolutionTypes[fieldId] = solution.type_solution || 'text';
|
||||||
|
newSolutionStatuses[fieldId] = solution.is_active !== false;
|
||||||
|
newSolutionData[fieldId] = {
|
||||||
|
...solution,
|
||||||
|
brand_code_solution_id: solution.brand_code_solution_id
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
errorCodeForm.setFieldsValue({
|
||||||
|
[`solution_name_${fieldId}`]: solution.solution_name,
|
||||||
|
[`text_solution_${fieldId}`]: solution.text_solution || '',
|
||||||
|
[`solution_status_${fieldId}`]: solution.is_active !== false,
|
||||||
|
[`solution_type_${fieldId}`]: solution.type_solution === 'image' || solution.type_solution === 'pdf' ? 'file' : solution.type_solution
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
setSolutionFields(newSolutionFields);
|
||||||
|
setSolutionTypes(newSolutionTypes);
|
||||||
|
setSolutionStatuses(newSolutionStatuses);
|
||||||
|
window.currentSolutionData = newSolutionData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetSolutionFields = () => {
|
||||||
|
setSolutionFields([0]);
|
||||||
|
setSolutionTypes({ 0: 'text' });
|
||||||
|
setSolutionStatuses({ 0: true });
|
||||||
|
setFirstSolutionValid(false);
|
||||||
|
setSolutionsToDelete(new Set());
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
checkFirstSolutionValid();
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [solutionFields, solutionTypes, fileList, errorCodeForm]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
solutionFields,
|
||||||
|
solutionTypes,
|
||||||
|
solutionStatuses,
|
||||||
|
firstSolutionValid,
|
||||||
|
solutionsToDelete,
|
||||||
|
handleAddSolutionField,
|
||||||
|
handleRemoveSolutionField,
|
||||||
|
handleSolutionTypeChange,
|
||||||
|
handleSolutionStatusChange,
|
||||||
|
resetSolutionFields,
|
||||||
|
checkFirstSolutionValid,
|
||||||
|
setSolutionsForExistingRecord
|
||||||
|
};
|
||||||
|
};
|
||||||
166
src/pages/master/brandDevice/hooks/solution.js
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export const useSolutionLogic = (solutionForm) => {
|
||||||
|
const [solutionFields, setSolutionFields] = useState([
|
||||||
|
{ name: ['solution_items', 0], key: 0 }
|
||||||
|
]);
|
||||||
|
const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' });
|
||||||
|
const [solutionStatuses, setSolutionStatuses] = useState({ 0: true });
|
||||||
|
const [solutionsToDelete, setSolutionsToDelete] = useState([]);
|
||||||
|
|
||||||
|
const handleAddSolutionField = () => {
|
||||||
|
const newKey = Date.now(); // Use timestamp for unique key
|
||||||
|
const newField = { name: ['solution_items', newKey], key: newKey };
|
||||||
|
|
||||||
|
setSolutionFields(prev => [...prev, newField]);
|
||||||
|
setSolutionTypes(prev => ({ ...prev, [newKey]: 'text' }));
|
||||||
|
setSolutionStatuses(prev => ({ ...prev, [newKey]: true }));
|
||||||
|
|
||||||
|
// Set default values for the new field
|
||||||
|
setTimeout(() => {
|
||||||
|
solutionForm.setFieldValue(['solution_items', newKey, 'name'], '');
|
||||||
|
solutionForm.setFieldValue(['solution_items', newKey, 'type'], 'text');
|
||||||
|
solutionForm.setFieldValue(['solution_items', newKey, 'text'], '');
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveSolutionField = (key) => {
|
||||||
|
if (solutionFields.length <= 1) {
|
||||||
|
return; // Keep at least one solution field
|
||||||
|
}
|
||||||
|
|
||||||
|
setSolutionFields(prev => prev.filter(field => field.key !== key));
|
||||||
|
|
||||||
|
// Clean up type and status
|
||||||
|
const newTypes = { ...solutionTypes };
|
||||||
|
const newStatuses = { ...solutionStatuses };
|
||||||
|
delete newTypes[key];
|
||||||
|
delete newStatuses[key];
|
||||||
|
|
||||||
|
setSolutionTypes(newTypes);
|
||||||
|
setSolutionStatuses(newStatuses);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSolutionTypeChange = (key, value) => {
|
||||||
|
setSolutionTypes(prev => ({ ...prev, [key]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSolutionStatusChange = (key, value) => {
|
||||||
|
setSolutionStatuses(prev => ({ ...prev, [key]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetSolutionFields = () => {
|
||||||
|
setSolutionFields([{ name: ['solution_items', 0], key: 0 }]);
|
||||||
|
setSolutionTypes({ 0: 'text' });
|
||||||
|
setSolutionStatuses({ 0: true });
|
||||||
|
|
||||||
|
// Reset form values
|
||||||
|
solutionForm.resetFields();
|
||||||
|
solutionForm.setFieldsValue({
|
||||||
|
solution_status_0: true,
|
||||||
|
solution_type_0: 'text',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkFirstSolutionValid = () => {
|
||||||
|
const values = solutionForm.getFieldsValue();
|
||||||
|
const firstSolution = values.solution_items?.[0];
|
||||||
|
|
||||||
|
if (!firstSolution || !firstSolution.name || firstSolution.name.trim() === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (solutionTypes[0] === 'text' && (!firstSolution.text || firstSolution.text.trim() === '')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSolutionData = () => {
|
||||||
|
const values = solutionForm.getFieldsValue();
|
||||||
|
|
||||||
|
const result = solutionFields.map(field => {
|
||||||
|
const key = field.key;
|
||||||
|
// Access form values using the key from field.name (AntD stores with comma)
|
||||||
|
const solutionPath = field.name.join(',');
|
||||||
|
const solution = values[solutionPath];
|
||||||
|
|
||||||
|
const validSolution = solution && solution.name && solution.name.trim() !== '';
|
||||||
|
|
||||||
|
if (validSolution) {
|
||||||
|
return {
|
||||||
|
solution_name: solution.name || 'Default Solution',
|
||||||
|
type_solution: solutionTypes[key] || 'text',
|
||||||
|
text_solution: solution.text || '',
|
||||||
|
path_solution: solution.file || '',
|
||||||
|
is_active: solution.status !== false, // Use form value directly
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSolutionsForExistingRecord = (solutions, form) => {
|
||||||
|
if (!solutions || solutions.length === 0) return;
|
||||||
|
|
||||||
|
const newFields = solutions.map((solution, index) => ({
|
||||||
|
name: ['solution_items', solution.id || index],
|
||||||
|
key: solution.id || index
|
||||||
|
}));
|
||||||
|
|
||||||
|
setSolutionFields(newFields);
|
||||||
|
|
||||||
|
// Set solution values
|
||||||
|
const solutionsValues = {};
|
||||||
|
const newTypes = {};
|
||||||
|
const newStatuses = {};
|
||||||
|
|
||||||
|
solutions.forEach((solution, index) => {
|
||||||
|
const key = solution.id || index;
|
||||||
|
solutionsValues[key] = {
|
||||||
|
name: solution.solution_name || '',
|
||||||
|
type: solution.type_solution || 'text',
|
||||||
|
text: solution.text_solution || '',
|
||||||
|
file: solution.path_solution || '',
|
||||||
|
};
|
||||||
|
newTypes[key] = solution.type_solution || 'text';
|
||||||
|
newStatuses[key] = solution.is_active !== false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set all form values at once
|
||||||
|
const formValues = {};
|
||||||
|
Object.keys(solutionsValues).forEach(key => {
|
||||||
|
const solution = solutionsValues[key];
|
||||||
|
formValues[`solution_items,${key}`] = {
|
||||||
|
name: solution.name,
|
||||||
|
type: solution.type,
|
||||||
|
text: solution.text,
|
||||||
|
file: solution.file,
|
||||||
|
status: solution.is_active !== false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
form.setFieldsValue(formValues);
|
||||||
|
setSolutionTypes(newTypes);
|
||||||
|
setSolutionStatuses(newStatuses);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
solutionFields,
|
||||||
|
solutionTypes,
|
||||||
|
solutionStatuses,
|
||||||
|
solutionsToDelete,
|
||||||
|
firstSolutionValid: checkFirstSolutionValid(),
|
||||||
|
handleAddSolutionField,
|
||||||
|
handleRemoveSolutionField,
|
||||||
|
handleSolutionTypeChange,
|
||||||
|
handleSolutionStatusChange,
|
||||||
|
resetSolutionFields,
|
||||||
|
checkFirstSolutionValid,
|
||||||
|
getSolutionData,
|
||||||
|
setSolutionsForExistingRecord,
|
||||||
|
};
|
||||||
|
};
|
||||||
141
src/pages/master/brandDevice/hooks/sparepart.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
export const useSparepartLogic = (sparepartForm) => {
|
||||||
|
const [sparepartFields, setSparepartFields] = useState([]);
|
||||||
|
const [sparepartTypes, setSparepartTypes] = useState({});
|
||||||
|
const [sparepartStatuses, setSparepartStatuses] = useState({});
|
||||||
|
const [sparepartsToDelete, setSparepartsToDelete] = useState(new Set());
|
||||||
|
|
||||||
|
const handleAddSparepartField = useCallback(() => {
|
||||||
|
const newKey = Date.now();
|
||||||
|
const newField = {
|
||||||
|
key: newKey,
|
||||||
|
name: sparepartFields.length,
|
||||||
|
isCreated: true,
|
||||||
|
};
|
||||||
|
setSparepartFields(prev => [...prev, newField]);
|
||||||
|
setSparepartTypes(prev => ({
|
||||||
|
...prev,
|
||||||
|
[newKey]: 'required'
|
||||||
|
}));
|
||||||
|
setSparepartStatuses(prev => ({
|
||||||
|
...prev,
|
||||||
|
[newKey]: true
|
||||||
|
}));
|
||||||
|
}, [sparepartFields.length]);
|
||||||
|
|
||||||
|
const handleRemoveSparepartField = useCallback((key) => {
|
||||||
|
setSparepartFields(prev => prev.filter(field => field.key !== key));
|
||||||
|
setSparepartTypes(prev => {
|
||||||
|
const newTypes = { ...prev };
|
||||||
|
delete newTypes[key];
|
||||||
|
return newTypes;
|
||||||
|
});
|
||||||
|
setSparepartStatuses(prev => {
|
||||||
|
const newStatuses = { ...prev };
|
||||||
|
delete newStatuses[key];
|
||||||
|
return newStatuses;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to delete list if it's not a new field
|
||||||
|
setSparepartsToDelete(prev => new Set([...prev, key]));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSparepartTypeChange = useCallback((key, type) => {
|
||||||
|
setSparepartTypes(prev => ({
|
||||||
|
...prev,
|
||||||
|
[key]: type
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSparepartStatusChange = useCallback((key, status) => {
|
||||||
|
setSparepartStatuses(prev => ({
|
||||||
|
...prev,
|
||||||
|
[key]: status
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetSparepartFields = useCallback(() => {
|
||||||
|
setSparepartFields([]);
|
||||||
|
setSparepartTypes({});
|
||||||
|
setSparepartStatuses({});
|
||||||
|
setSparepartsToDelete(new Set());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getSparepartData = useCallback(() => {
|
||||||
|
if (!sparepartForm) return [];
|
||||||
|
|
||||||
|
const values = sparepartForm.getFieldsValue();
|
||||||
|
const data = [];
|
||||||
|
|
||||||
|
sparepartFields.forEach((field, index) => {
|
||||||
|
const fieldData = {
|
||||||
|
sparepart_id: values[`sparepart_id_${field.name}`],
|
||||||
|
sparepart_name: values[`sparepart_name_${field.name}`],
|
||||||
|
sparepart_description: values[`sparepart_description_${field.name}`],
|
||||||
|
status: values[`sparepart_status_${field.name}`],
|
||||||
|
type: sparepartTypes[field.key] || 'required',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add if required fields are filled
|
||||||
|
if (fieldData.sparepart_id) {
|
||||||
|
data.push(fieldData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, [sparepartForm, sparepartFields, sparepartTypes]);
|
||||||
|
|
||||||
|
const setSparepartsForExistingRecord = useCallback((sparepartData, form) => {
|
||||||
|
resetSparepartFields();
|
||||||
|
|
||||||
|
if (!sparepartData || !Array.isArray(sparepartData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFields = sparepartData.map((sp, index) => ({
|
||||||
|
key: sp.brand_sparepart_id || sp.sparepart_id || `existing-${index}`,
|
||||||
|
name: index,
|
||||||
|
isCreated: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setSparepartFields(newFields);
|
||||||
|
|
||||||
|
// Set form values for existing spareparts
|
||||||
|
setTimeout(() => {
|
||||||
|
const formValues = {};
|
||||||
|
sparepartData.forEach((sp, index) => {
|
||||||
|
const sparepartId = sp.brand_sparepart_id || sp.sparepart_id || sp.sparepart_name;
|
||||||
|
formValues[`sparepart_id_${index}`] = sparepartId;
|
||||||
|
formValues[`sparepart_status_${index}`] = sp.is_active ?? sp.status ?? true;
|
||||||
|
formValues[`sparepart_description_${index}`] = sp.brand_sparepart_description || sp.description || sp.sparepart_name;
|
||||||
|
|
||||||
|
setSparepartTypes(prev => ({
|
||||||
|
...prev,
|
||||||
|
[sp.brand_sparepart_id || sp.sparepart_id || `existing-${index}`]: sp.type || sp.sparepart_type || 'required'
|
||||||
|
}));
|
||||||
|
|
||||||
|
setSparepartStatuses(prev => ({
|
||||||
|
...prev,
|
||||||
|
[sp.brand_sparepart_id || sp.sparepart_id || `existing-${index}`]: sp.is_active ?? sp.status ?? true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
form.setFieldsValue(formValues);
|
||||||
|
}, 0);
|
||||||
|
}, [resetSparepartFields]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sparepartFields,
|
||||||
|
sparepartTypes,
|
||||||
|
sparepartStatuses,
|
||||||
|
sparepartsToDelete,
|
||||||
|
handleAddSparepartField,
|
||||||
|
handleRemoveSparepartField,
|
||||||
|
handleSparepartTypeChange,
|
||||||
|
handleSparepartStatusChange,
|
||||||
|
resetSparepartFields,
|
||||||
|
getSparepartData,
|
||||||
|
setSparepartsForExistingRecord,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -23,7 +23,6 @@ const DetailDevice = (props) => {
|
|||||||
device_location: '',
|
device_location: '',
|
||||||
device_description: '',
|
device_description: '',
|
||||||
ip_address: '',
|
ip_address: '',
|
||||||
listen_channel: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [formData, setFormData] = useState(defaultData);
|
const [formData, setFormData] = useState(defaultData);
|
||||||
@@ -60,13 +59,9 @@ const DetailDevice = (props) => {
|
|||||||
device_name: formData.device_name,
|
device_name: formData.device_name,
|
||||||
is_active: formData.is_active,
|
is_active: formData.is_active,
|
||||||
device_location: formData.device_location,
|
device_location: formData.device_location,
|
||||||
device_description:
|
device_description: formData.device_description,
|
||||||
formData.device_description && formData.device_description.trim() !== ''
|
|
||||||
? formData.device_description
|
|
||||||
: ' ',
|
|
||||||
ip_address: formData.ip_address,
|
ip_address: formData.ip_address,
|
||||||
brand_id: formData.brand_id,
|
brand_id: formData.brand_id,
|
||||||
listen_channel: formData.listen_channel,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = formData.device_id
|
const response = formData.device_id
|
||||||
@@ -187,6 +182,7 @@ const DetailDevice = (props) => {
|
|||||||
defaultBorderColor: '#23A55A',
|
defaultBorderColor: '#23A55A',
|
||||||
defaultHoverColor: '#23A55A',
|
defaultHoverColor: '#23A55A',
|
||||||
defaultHoverBorderColor: '#23A55A',
|
defaultHoverBorderColor: '#23A55A',
|
||||||
|
defaultHoverColor: '#23A55A',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -330,16 +326,6 @@ const DetailDevice = (props) => {
|
|||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Text strong>Device Description</Text>
|
<Text strong>Device Description</Text>
|
||||||
<TextArea
|
<TextArea
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, {useEffect, useState } from 'react';
|
||||||
import { Modal, Button, ConfigProvider } from 'antd';
|
import { Modal, Button, ConfigProvider } from 'antd';
|
||||||
import { jsPDF } from 'jspdf';
|
import { jsPDF } from 'jspdf';
|
||||||
import logoPiEnergi from '../../../../assets/images/logo/pi-energi.png';
|
import logoPiEnergi from '../../../../assets/images/logo/pi-energi.png';
|
||||||
@@ -22,12 +22,12 @@ const GeneratePdf = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const generatePdf = async () => {
|
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({
|
const doc = new jsPDF({
|
||||||
orientation: 'portrait',
|
orientation: "portrait",
|
||||||
unit: 'mm',
|
unit: "mm",
|
||||||
format: 'a4',
|
format: "a4"
|
||||||
});
|
});
|
||||||
|
|
||||||
const width = 45;
|
const width = 45;
|
||||||
@@ -45,32 +45,32 @@ const GeneratePdf = (props) => {
|
|||||||
doc.setFontSize(11);
|
doc.setFontSize(11);
|
||||||
doc.setFont('helvetica', 'normal');
|
doc.setFont('helvetica', 'normal');
|
||||||
|
|
||||||
doc.setLineWidth(0.2);
|
doc.setLineWidth(0.2);
|
||||||
doc.line(10, 32, 200, 32);
|
doc.line(10, 32, 200, 32);
|
||||||
doc.setLineWidth(0.6);
|
doc.setLineWidth(0.6);
|
||||||
doc.line(10, 32.8, 200, 32.8);
|
doc.line(10, 32.8, 200, 32.8);
|
||||||
|
|
||||||
doc.text('Tanggal Pengajuan', 10, 42);
|
doc.text("Tanggal Pengajuan", 10, 42);
|
||||||
doc.text(':', 59, 42);
|
doc.text(":", 59, 42);
|
||||||
|
|
||||||
doc.text('Deskripsi Pekerjaan', 10, 48);
|
doc.text("Deskripsi Pekerjaan", 10, 48);
|
||||||
doc.text(':', 59, 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("No. Order", 10, 60);
|
||||||
doc.text(':', 59, 54);
|
doc.text(":", 59, 60);
|
||||||
doc.text('Spesifik Lokasi', 120, 54);
|
doc.text("Jum. Personil Terlihat", 120, 60);
|
||||||
doc.text(':', 160, 54);
|
doc.text(":", 160, 60);
|
||||||
|
|
||||||
doc.text('No. Order', 10, 60);
|
doc.text("Peralatan yang digunakan", 10, 66);
|
||||||
doc.text(':', 59, 60);
|
doc.text(":", 59, 66);
|
||||||
doc.text('Jum. Personil Terlihat', 120, 60);
|
|
||||||
doc.text(':', 160, 60);
|
|
||||||
|
|
||||||
doc.text('Peralatan yang digunakan', 10, 66);
|
doc.text("Jenis APD yang digunakan", 10, 72);
|
||||||
doc.text(':', 59, 66);
|
doc.text(":", 59, 72);
|
||||||
|
|
||||||
doc.text('Jenis APD yang digunakan', 10, 72);
|
|
||||||
doc.text(':', 59, 72);
|
|
||||||
|
|
||||||
const blob = doc.output('blob');
|
const blob = doc.output('blob');
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -84,7 +84,7 @@ const GeneratePdf = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
width="60%"
|
width='60%'
|
||||||
title="Preview PDF"
|
title="Preview PDF"
|
||||||
open={props.showPdf}
|
open={props.showPdf}
|
||||||
// open={true}
|
// open={true}
|
||||||
@@ -101,6 +101,7 @@ const GeneratePdf = (props) => {
|
|||||||
defaultBorderColor: '#23A55A',
|
defaultBorderColor: '#23A55A',
|
||||||
defaultHoverColor: '#23A55A',
|
defaultHoverColor: '#23A55A',
|
||||||
defaultHoverBorderColor: '#23A55A',
|
defaultHoverBorderColor: '#23A55A',
|
||||||
|
defaultHoverColor: '#23A55A',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -62,13 +62,6 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
|||||||
key: 'ip_address',
|
key: 'ip_address',
|
||||||
width: '10%',
|
width: '10%',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Listen Channel',
|
|
||||||
dataIndex: 'listen_channel',
|
|
||||||
key: 'listen_channel',
|
|
||||||
width: '10%',
|
|
||||||
render: (listen_channel) => listen_channel || '-'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Status',
|
title: 'Status',
|
||||||
dataIndex: 'is_active',
|
dataIndex: 'is_active',
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const DetailPlantSubSection = (props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(`📝 Input change: ${name} = ${value}`);
|
console.log(`📝 Input change: ${name} = ${value}`);
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
@@ -74,20 +74,16 @@ const DetailPlantSubSection = (props) => {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// console.log('💾 Current formData before save:', formData);
|
console.log('💾 Current formData before save:', formData);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
plant_sub_section_name: formData.plant_sub_section_name,
|
plant_sub_section_name: formData.plant_sub_section_name,
|
||||||
plant_sub_section_description:
|
plant_sub_section_description: formData.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
|
table_name_value: formData.table_name_value, // Fix field name
|
||||||
is_active: formData.is_active,
|
is_active: formData.is_active,
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log('📤 Payload to be sent:', payload);
|
console.log('📤 Payload to be sent:', payload);
|
||||||
|
|
||||||
const response =
|
const response =
|
||||||
props.actionMode === 'edit'
|
props.actionMode === 'edit'
|
||||||
@@ -130,17 +126,17 @@ const DetailPlantSubSection = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// console.log('🔄 Modal state changed:', {
|
console.log('🔄 Modal state changed:', {
|
||||||
// showModal: props.showModal,
|
showModal: props.showModal,
|
||||||
// actionMode: props.actionMode,
|
actionMode: props.actionMode,
|
||||||
// selectedData: props.selectedData,
|
selectedData: props.selectedData,
|
||||||
// });
|
});
|
||||||
|
|
||||||
if (props.selectedData) {
|
if (props.selectedData) {
|
||||||
// console.log('📋 Setting form data from selectedData:', props.selectedData);
|
console.log('📋 Setting form data from selectedData:', props.selectedData);
|
||||||
setFormData(props.selectedData);
|
setFormData(props.selectedData);
|
||||||
} else {
|
} else {
|
||||||
// console.log('📋 Resetting to default data');
|
console.log('📋 Resetting to default data');
|
||||||
setFormData(defaultData);
|
setFormData(defaultData);
|
||||||
}
|
}
|
||||||
}, [props.showModal, props.selectedData, props.actionMode]);
|
}, [props.showModal, props.selectedData, props.actionMode]);
|
||||||
|
|||||||
@@ -112,9 +112,9 @@ const DetailShift = (props) => {
|
|||||||
is_active: formData.is_active,
|
is_active: formData.is_active,
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log('Payload yang dikirim:', payload);
|
console.log('Payload yang dikirim:', payload);
|
||||||
// console.log('Type start_time:', typeof payload.start_time, payload.start_time);
|
console.log('Type start_time:', typeof payload.start_time, payload.start_time);
|
||||||
// console.log('Type end_time:', typeof payload.end_time, payload.end_time);
|
console.log('Type end_time:', typeof payload.end_time, payload.end_time);
|
||||||
|
|
||||||
const response =
|
const response =
|
||||||
props.actionMode === 'edit'
|
props.actionMode === 'edit'
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
Col,
|
Col,
|
||||||
Image,
|
Image,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { PlusOutlined, EyeOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||||
import { createSparepart, updateSparepart } from '../../../../api/sparepart';
|
import { createSparepart, updateSparepart } from '../../../../api/sparepart';
|
||||||
import { uploadFile } from '../../../../api/file-uploads';
|
import { uploadFile } from '../../../../api/file-uploads';
|
||||||
@@ -35,18 +35,16 @@ const DetailSparepart = (props) => {
|
|||||||
const [previewOpen, setPreviewOpen] = useState(false);
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
const [previewImage, setPreviewImage] = useState('');
|
const [previewImage, setPreviewImage] = useState('');
|
||||||
const [previewTitle, setPreviewTitle] = useState('');
|
const [previewTitle, setPreviewTitle] = useState('');
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
|
||||||
|
|
||||||
const defaultData = {
|
const defaultData = {
|
||||||
sparepart_id: '',
|
sparepart_id: '',
|
||||||
sparepart_name: '',
|
sparepart_name: '',
|
||||||
sparepart_description: '',
|
sparepart_description: '',
|
||||||
sparepart_model: '',
|
sparepart_model: '',
|
||||||
sparepart_item_type: null,
|
sparepart_item_type: '',
|
||||||
sparepart_qty: 0,
|
|
||||||
sparepart_unit: '',
|
sparepart_unit: '',
|
||||||
sparepart_merk: '',
|
sparepart_merk: '',
|
||||||
sparepart_stok: 'Not Available',
|
sparepart_stok: '0',
|
||||||
sparepart_foto: '',
|
sparepart_foto: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,10 +69,6 @@ const DetailSparepart = (props) => {
|
|||||||
|
|
||||||
const handleChange = ({ fileList: newFileList }) => setFileList(newFileList);
|
const handleChange = ({ fileList: newFileList }) => setFileList(newFileList);
|
||||||
|
|
||||||
const handleRemove = () => {
|
|
||||||
setFileList([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setConfirmLoading(true);
|
setConfirmLoading(true);
|
||||||
|
|
||||||
@@ -95,11 +89,11 @@ const DetailSparepart = (props) => {
|
|||||||
const newFile = fileList.length > 0 ? fileList[0] : null;
|
const newFile = fileList.length > 0 ? fileList[0] : null;
|
||||||
|
|
||||||
if (newFile && newFile.originFileObj) {
|
if (newFile && newFile.originFileObj) {
|
||||||
// console.log('Uploading file:', newFile.originFileObj);
|
console.log('Uploading file:', newFile.originFileObj);
|
||||||
const uploadResponse = await uploadFile(newFile.originFileObj, 'images');
|
const uploadResponse = await uploadFile(newFile.originFileObj, 'images');
|
||||||
|
|
||||||
// Log untuk debugging
|
// Log untuk debugging
|
||||||
// console.log('Upload response:', uploadResponse);
|
console.log('Upload response:', uploadResponse);
|
||||||
|
|
||||||
// Cek berbagai kemungkinan struktur respons dari API
|
// Cek berbagai kemungkinan struktur respons dari API
|
||||||
let uploadedUrl = null;
|
let uploadedUrl = null;
|
||||||
@@ -169,7 +163,7 @@ const DetailSparepart = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uploadedUrl) {
|
if (uploadedUrl) {
|
||||||
// console.log('Successfully extracted image URL:', uploadedUrl);
|
console.log('Successfully extracted image URL:', uploadedUrl);
|
||||||
imageUrl = uploadedUrl;
|
imageUrl = uploadedUrl;
|
||||||
} else {
|
} else {
|
||||||
console.error('Upload response structure:', uploadResponse);
|
console.error('Upload response structure:', uploadResponse);
|
||||||
@@ -209,10 +203,10 @@ const DetailSparepart = (props) => {
|
|||||||
sparepart_name: formData.sparepart_name, // Wajib
|
sparepart_name: formData.sparepart_name, // Wajib
|
||||||
};
|
};
|
||||||
|
|
||||||
payload.sparepart_description =
|
// Tambahkan field-field secara kondisional hanya jika nilainya tidak kosong
|
||||||
formData.sparepart_description && formData.sparepart_description.trim() !== ''
|
if (formData.sparepart_description && formData.sparepart_description.trim() !== '') {
|
||||||
? formData.sparepart_description
|
payload.sparepart_description = formData.sparepart_description;
|
||||||
: ' ';
|
}
|
||||||
if (formData.sparepart_model && formData.sparepart_model.trim() !== '') {
|
if (formData.sparepart_model && formData.sparepart_model.trim() !== '') {
|
||||||
payload.sparepart_model = formData.sparepart_model;
|
payload.sparepart_model = formData.sparepart_model;
|
||||||
}
|
}
|
||||||
@@ -225,24 +219,23 @@ const DetailSparepart = (props) => {
|
|||||||
if (formData.sparepart_merk && formData.sparepart_merk.trim() !== '') {
|
if (formData.sparepart_merk && formData.sparepart_merk.trim() !== '') {
|
||||||
payload.sparepart_merk = formData.sparepart_merk;
|
payload.sparepart_merk = formData.sparepart_merk;
|
||||||
}
|
}
|
||||||
// sparepart_qty disimpan sebagai angka kuantitas
|
if (formData.sparepart_stok && formData.sparepart_stok.trim() !== '') {
|
||||||
const qty = parseInt(formData.sparepart_qty) || 0;
|
payload.sparepart_stok = formData.sparepart_stok.toString();
|
||||||
payload.sparepart_qty = qty;
|
} else {
|
||||||
|
payload.sparepart_stok = '0'; // Set default value jika tidak diisi
|
||||||
// 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
|
// Sertakan sparepart_foto hanya jika nilainya tidak kosong, agar tidak memicu validasi
|
||||||
if (imageUrl && imageUrl.trim() !== '') {
|
if (imageUrl && imageUrl.trim() !== '') {
|
||||||
payload.sparepart_foto = imageUrl;
|
payload.sparepart_foto = imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('Sending payload:', payload);
|
console.log('Sending payload:', payload);
|
||||||
|
|
||||||
const response = formData.sparepart_id
|
const response = formData.sparepart_id
|
||||||
? await updateSparepart(formData.sparepart_id, payload)
|
? await updateSparepart(formData.sparepart_id, payload)
|
||||||
: await createSparepart(payload);
|
: await createSparepart(payload);
|
||||||
|
|
||||||
// console.log('API response:', response);
|
console.log('API response:', response);
|
||||||
|
|
||||||
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
||||||
NotifOk({
|
NotifOk({
|
||||||
@@ -286,33 +279,18 @@ const DetailSparepart = (props) => {
|
|||||||
if (props.selectedData) {
|
if (props.selectedData) {
|
||||||
setFormData(props.selectedData);
|
setFormData(props.selectedData);
|
||||||
if (props.selectedData.sparepart_foto) {
|
if (props.selectedData.sparepart_foto) {
|
||||||
let displayUrl = props.selectedData.sparepart_foto;
|
// Buat URL lengkap dengan token untuk file yang sudah ada
|
||||||
|
|
||||||
// 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();
|
const fileName = props.selectedData.sparepart_foto.split('/').pop();
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const baseURL = import.meta.env.VITE_API_SERVER || '';
|
||||||
|
const fullUrl = `${baseURL}/file-uploads/images/${encodeURIComponent(fileName)}${token ? `?token=${encodeURIComponent(token)}` : ''}`;
|
||||||
|
|
||||||
setFileList([
|
setFileList([
|
||||||
{
|
{
|
||||||
uid: '-1',
|
uid: '-1',
|
||||||
name: fileName,
|
name: fileName,
|
||||||
status: 'done',
|
status: 'done',
|
||||||
url: displayUrl,
|
url: fullUrl,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
@@ -386,159 +364,85 @@ const DetailSparepart = (props) => {
|
|||||||
{formData && (
|
{formData && (
|
||||||
<div>
|
<div>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{/* Kolom untuk foto */}
|
<Col span={12}>
|
||||||
<Col span={10} style={{ display: 'flex', flexDirection: 'column' }}>
|
<Text strong>Sparepart Name</Text>
|
||||||
<Text strong>Foto</Text>
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
<div
|
<Input
|
||||||
style={{
|
name="sparepart_name"
|
||||||
flexGrow: 1,
|
value={formData.sparepart_name}
|
||||||
display: 'flex',
|
onChange={handleInputChange}
|
||||||
alignItems: 'center',
|
placeholder="Enter Sparepart Name"
|
||||||
justifyContent: 'center',
|
readOnly={props.readOnly}
|
||||||
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>
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
{/* Kolom untuk field lainnya */}
|
<Text strong>Item Type</Text>
|
||||||
<Col span={14}>
|
<Select
|
||||||
<Row gutter={[16, 16]}>
|
name="sparepart_item_type"
|
||||||
<Col span={24}>
|
value={formData.sparepart_item_type}
|
||||||
<Text strong>Sparepart Name</Text>
|
onChange={(value) =>
|
||||||
<Text style={{ color: 'red' }}> *</Text>
|
handleSelectChange('sparepart_item_type', value)
|
||||||
<Input
|
}
|
||||||
name="sparepart_name"
|
placeholder="Select Item Type"
|
||||||
value={formData.sparepart_name}
|
disabled={props.readOnly}
|
||||||
onChange={handleInputChange}
|
style={{ width: '100%' }}
|
||||||
placeholder="Enter Sparepart Name"
|
>
|
||||||
readOnly={props.readOnly}
|
<Select.Option value="Air Dryer">Air Dryer</Select.Option>
|
||||||
/>
|
<Select.Option value="Compressor">Compressor</Select.Option>
|
||||||
</Col>
|
</Select>
|
||||||
<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>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Text strong>Stock</Text>
|
||||||
|
<Input
|
||||||
|
name="sparepart_stok"
|
||||||
|
value={formData.sparepart_stok}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Initial stock quantity"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Text strong>Unit</Text>
|
||||||
|
<Input
|
||||||
|
name="sparepart_unit"
|
||||||
|
value={formData.sparepart_unit}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="e.g., pcs, box, roll"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={24}>
|
||||||
|
<Text strong>Foto</Text>
|
||||||
|
<Upload
|
||||||
|
listType="picture-card"
|
||||||
|
fileList={fileList}
|
||||||
|
onPreview={handlePreview}
|
||||||
|
onChange={handleChange}
|
||||||
|
beforeUpload={() => false}
|
||||||
|
maxCount={1}
|
||||||
|
disabled={props.readOnly}
|
||||||
|
>
|
||||||
|
{fileList.length >= 1 ? null : uploadButton}
|
||||||
|
</Upload>
|
||||||
|
<Modal
|
||||||
|
open={previewOpen}
|
||||||
|
title={previewTitle}
|
||||||
|
footer={null}
|
||||||
|
onCancel={handlePreviewCancel}
|
||||||
|
>
|
||||||
|
<img alt="preview" style={{ width: '100%' }} src={previewImage} />
|
||||||
|
</Modal>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Text strong>Brand</Text>
|
<Text strong>Brand</Text>
|
||||||
<Input
|
<Input
|
||||||
@@ -561,7 +465,7 @@ const DetailSparepart = (props) => {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Text strong>Description</Text>
|
<Text strong>Description</Text>
|
||||||
<TextArea
|
<TextArea
|
||||||
@@ -576,14 +480,6 @@ const DetailSparepart = (props) => {
|
|||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Modal
|
|
||||||
open={previewOpen}
|
|
||||||
title={previewTitle}
|
|
||||||
footer={null}
|
|
||||||
onCancel={handlePreviewCancel}
|
|
||||||
>
|
|
||||||
<img alt="preview" style={{ width: '100%' }} src={previewImage} />
|
|
||||||
</Modal>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,18 +72,11 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
|||||||
render: (sparepart_merk) => sparepart_merk || '-'
|
render: (sparepart_merk) => sparepart_merk || '-'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Qty',
|
title: 'Stock',
|
||||||
dataIndex: 'sparepart_qty',
|
|
||||||
key: 'sparepart_qty',
|
|
||||||
width: '8%',
|
|
||||||
render: (sparepart_qty) => sparepart_qty || '0'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Status',
|
|
||||||
dataIndex: 'sparepart_stok',
|
dataIndex: 'sparepart_stok',
|
||||||
key: 'sparepart_stok',
|
key: 'sparepart_stok',
|
||||||
width: '8%',
|
width: '8%',
|
||||||
render: (sparepart_stok) => sparepart_stok || 'Not Available'
|
render: (sparepart_stok) => sparepart_stok || '0'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Action',
|
title: 'Action',
|
||||||
|
|||||||
@@ -21,15 +21,8 @@ const SparepartCardList = ({
|
|||||||
const [loadingQuantities, setLoadingQuantities] = useState({});
|
const [loadingQuantities, setLoadingQuantities] = useState({});
|
||||||
|
|
||||||
const handleQuantityChange = (id, value) => {
|
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 };
|
const newQuantities = { ...updateQuantities };
|
||||||
newQuantities[id] = clampedValue;
|
newQuantities[id] = value;
|
||||||
setUpdateQuantities(newQuantities);
|
setUpdateQuantities(newQuantities);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,19 +37,16 @@ const SparepartCardList = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentQty = Number(item.sparepart_qty) || 0;
|
const newStock = Number(item.sparepart_stok) + quantityToAdd;
|
||||||
const newQty = currentQty + quantityToAdd;
|
if (newStock < 0) {
|
||||||
if (newQty < 0) {
|
NotifAlert({ icon: 'error', title: 'Error', message: 'Stock cannot be negative.' });
|
||||||
NotifAlert({ icon: 'error', title: 'Error', message: 'Quantity cannot be negative.' });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoadingQuantities((prev) => ({ ...prev, [item.sparepart_id]: true }));
|
setLoadingQuantities((prev) => ({ ...prev, [item.sparepart_id]: true }));
|
||||||
|
|
||||||
// sparepart_qty disimpan sebagai angka kuantitas (update boleh 0 sesuai validasi update schema)
|
|
||||||
const payload = {
|
const payload = {
|
||||||
sparepart_qty: newQty,
|
sparepart_stok: newStock.toString(), // Convert number to string as required by API
|
||||||
sparepart_stok: newQty > 0 ? 'Available' : 'Not Available', // Otomatis tentukan status
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hanya tambahkan field jika nilainya tidak kosong untuk menghindari validasi error
|
// Hanya tambahkan field jika nilainya tidak kosong untuk menghindari validasi error
|
||||||
@@ -72,12 +62,6 @@ const SparepartCardList = ({
|
|||||||
if (item.sparepart_description && item.sparepart_description.trim() !== '') {
|
if (item.sparepart_description && item.sparepart_description.trim() !== '') {
|
||||||
payload.sparepart_description = item.sparepart_description;
|
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 {
|
try {
|
||||||
const response = await updateSparepart(item.sparepart_id, payload);
|
const response = await updateSparepart(item.sparepart_id, payload);
|
||||||
@@ -89,16 +73,6 @@ const SparepartCardList = ({
|
|||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: 'Stock updated successfully.',
|
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) {
|
if (onStockUpdate) {
|
||||||
onStockUpdate();
|
onStockUpdate();
|
||||||
}
|
}
|
||||||
@@ -165,8 +139,7 @@ const SparepartCardList = ({
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor: '#f0f0f0',
|
backgroundColor: '#f0f0f0',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
paddingTop:
|
paddingTop: '100%', /* Ini membuat tinggi sama dengan lebar (aspect ratio 1:1) */
|
||||||
'100%' /* Ini membuat tinggi sama dengan lebar (aspect ratio 1:1) */,
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -180,50 +153,30 @@ const SparepartCardList = ({
|
|||||||
imgSrc = item.sparepart_foto;
|
imgSrc = item.sparepart_foto;
|
||||||
} else {
|
} else {
|
||||||
// Gunakan format file URL seperti di brandDevice
|
// Gunakan format file URL seperti di brandDevice
|
||||||
const fileName = item.sparepart_foto
|
const fileName = item.sparepart_foto.split('/').pop();
|
||||||
.split('/')
|
|
||||||
.pop();
|
|
||||||
|
|
||||||
// Jika filename adalah default file, gunakan dari public assets
|
// Jika filename adalah default file, gunakan dari public assets
|
||||||
if (
|
if (fileName === 'defaultSparepartImg.jpg') {
|
||||||
fileName === 'defaultSparepartImg.jpg'
|
|
||||||
) {
|
|
||||||
imgSrc = `/assets/defaultSparepartImg.jpg`;
|
imgSrc = `/assets/defaultSparepartImg.jpg`;
|
||||||
} else {
|
} else {
|
||||||
// Gunakan API getFileUrl untuk mendapatkan URL yang benar untuk file upload
|
// Gunakan API getFileUrl untuk mendapatkan URL yang benar untuk file upload
|
||||||
const token =
|
const token = localStorage.getItem('token');
|
||||||
localStorage.getItem('token');
|
const baseURL = import.meta.env.VITE_API_SERVER || '';
|
||||||
const baseURL =
|
imgSrc = `${baseURL}/file-uploads/images/${encodeURIComponent(fileName)}${token ? `?token=${encodeURIComponent(token)}` : ''}`;
|
||||||
import.meta.env.VITE_API_SERVER ||
|
|
||||||
'';
|
|
||||||
imgSrc = `${baseURL}/file-uploads/images/${encodeURIComponent(
|
|
||||||
fileName
|
|
||||||
)}${
|
|
||||||
token
|
|
||||||
? `?token=${encodeURIComponent(
|
|
||||||
token
|
|
||||||
)}`
|
|
||||||
: ''
|
|
||||||
}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(
|
console.log('Image path being constructed:', imgSrc);
|
||||||
'Image path being constructed:',
|
|
||||||
imgSrc
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
imgSrc = 'https://via.placeholder.com/150';
|
imgSrc = 'https://via.placeholder.com/150';
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={{
|
||||||
style={{
|
position: 'absolute',
|
||||||
position: 'absolute',
|
top: 0,
|
||||||
top: 0,
|
left: 0,
|
||||||
left: 0,
|
width: '100%',
|
||||||
width: '100%',
|
height: '100%',
|
||||||
height: '100%',
|
}}>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
src={imgSrc}
|
src={imgSrc}
|
||||||
alt={item[header]}
|
alt={item[header]}
|
||||||
@@ -233,19 +186,10 @@ const SparepartCardList = ({
|
|||||||
objectFit: 'cover', // Mengisi container dan crop sisi berlebih
|
objectFit: 'cover', // Mengisi container dan crop sisi berlebih
|
||||||
}}
|
}}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
console.error(
|
console.error('Image failed to load:', imgSrc);
|
||||||
'Image failed to load:',
|
e.target.src = 'https://via.placeholder.com/150';
|
||||||
imgSrc
|
|
||||||
);
|
|
||||||
e.target.src =
|
|
||||||
'https://via.placeholder.com/150';
|
|
||||||
}}
|
}}
|
||||||
onLoad={() =>
|
onLoad={() => console.log('Image loaded successfully:', imgSrc)}
|
||||||
console.log(
|
|
||||||
'Image loaded successfully:',
|
|
||||||
imgSrc
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -305,8 +249,8 @@ const SparepartCardList = ({
|
|||||||
>
|
>
|
||||||
{item[header]}
|
{item[header]}
|
||||||
</Title>
|
</Title>
|
||||||
<Text type="secondary" style={{ display: 'block' }}>
|
<Text type="secondary">
|
||||||
Stok: {item.sparepart_stok || 'Not Available'}
|
Available Stock: {item.sparepart_stok || '0'}
|
||||||
</Text>
|
</Text>
|
||||||
<Divider style={{ margin: '8px 0' }} />
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
|
||||||
@@ -315,9 +259,9 @@ const SparepartCardList = ({
|
|||||||
style={{
|
style={{
|
||||||
marginBottom: '8px',
|
marginBottom: '8px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text type="secondary">Qty</Text>
|
|
||||||
<Button
|
<Button
|
||||||
icon={<MinusOutlined />}
|
icon={<MinusOutlined />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -326,16 +270,14 @@ const SparepartCardList = ({
|
|||||||
quantity - 1
|
quantity - 1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={isLoading}
|
||||||
isLoading || item.sparepart_qty + quantity <= 0
|
|
||||||
}
|
|
||||||
style={{ width: 28, height: 28 }}
|
style={{ width: 28, height: 28 }}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
strong
|
strong
|
||||||
style={{ padding: '0 8px', fontSize: '16px' }}
|
style={{ padding: '0 8px', fontSize: '16px' }}
|
||||||
>
|
>
|
||||||
{item.sparepart_qty + (quantity || 0)}
|
{quantity}
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
@@ -355,17 +297,15 @@ const SparepartCardList = ({
|
|||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
{quantity !== 0 && (
|
<Button
|
||||||
<Button
|
type={quantity === 0 ? 'default' : 'primary'}
|
||||||
type={quantity === 0 ? 'default' : 'primary'}
|
size="small"
|
||||||
size="small"
|
style={{ width: '100%' }}
|
||||||
style={{ width: '100%' }}
|
onClick={() => handleUpdateStock(item)}
|
||||||
onClick={() => handleUpdateStock(item)}
|
loading={isLoading}
|
||||||
loading={isLoading}
|
>
|
||||||
>
|
Update Stock
|
||||||
Update Stock
|
</Button>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const DetailStatus = (props) => {
|
|||||||
status_number: formData.status_number,
|
status_number: formData.status_number,
|
||||||
status_name: formData.status_name,
|
status_name: formData.status_name,
|
||||||
status_color: formData.status_color,
|
status_color: formData.status_color,
|
||||||
status_description: (formData.status_description && formData.status_description.trim() !== '') ? formData.status_description : ' ',
|
status_description: formData.status_description,
|
||||||
is_active: formData.is_active,
|
is_active: formData.is_active,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,10 @@ const DetailTag = (props) => {
|
|||||||
payload.unit = formData.unit.trim();
|
payload.unit = formData.unit.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
payload.tag_description = (formData.tag_description && formData.tag_description.trim() !== '') ? formData.tag_description.trim() : ' ';
|
// Add tag_description only if it has a value
|
||||||
|
if (formData.tag_description && formData.tag_description.trim() !== '') {
|
||||||
|
payload.tag_description = formData.tag_description.trim();
|
||||||
|
}
|
||||||
|
|
||||||
// Add device_id only if it has a value
|
// Add device_id only if it has a value
|
||||||
if (formData.device_id) {
|
if (formData.device_id) {
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ const ListUnit = memo(function ListUnit(props) {
|
|||||||
const handleDelete = async (param) => {
|
const handleDelete = async (param) => {
|
||||||
try {
|
try {
|
||||||
const response = await deleteUnit(param.unit_id);
|
const response = await deleteUnit(param.unit_id);
|
||||||
// console.log('deleteUnit response:', response);
|
console.log('deleteUnit response:', response);
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { memo, useState, useEffect } from 'react';
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
|
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
|
||||||
import { Typography, Row, Col } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import ListNotification from './component/ListNotification';
|
import ListNotification from './component/ListNotification';
|
||||||
import DetailNotification from './component/DetailNotification';
|
import DetailNotification from './component/DetailNotification';
|
||||||
|
|
||||||
@@ -10,7 +10,10 @@ const { Text } = Typography;
|
|||||||
const IndexNotification = memo(function IndexNotification() {
|
const IndexNotification = memo(function IndexNotification() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { setBreadcrumbItems } = useBreadcrumb();
|
const { setBreadcrumbItems } = useBreadcrumb();
|
||||||
|
|
||||||
|
const [actionMode, setActionMode] = useState('list');
|
||||||
const [selectedData, setSelectedData] = useState(null);
|
const [selectedData, setSelectedData] = useState(null);
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
@@ -29,34 +32,33 @@ const IndexNotification = memo(function IndexNotification() {
|
|||||||
}
|
}
|
||||||
}, [navigate, setBreadcrumbItems]);
|
}, [navigate, setBreadcrumbItems]);
|
||||||
|
|
||||||
const handleCloseDetail = () => {
|
useEffect(() => {
|
||||||
|
if (actionMode === 'preview') {
|
||||||
|
setIsModalVisible(true);
|
||||||
|
} else {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
}
|
||||||
|
}, [actionMode]);
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setActionMode('list');
|
||||||
setSelectedData(null);
|
setSelectedData(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// This handler will be passed to ListNotification to update the selected item
|
|
||||||
const handleSelectNotification = (data) => {
|
|
||||||
setSelectedData(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gutter={16}>
|
<React.Fragment>
|
||||||
<Col span={selectedData ? 16 : 24}>
|
<ListNotification
|
||||||
<ListNotification
|
actionMode={actionMode}
|
||||||
// The setActionMode is likely not needed anymore,
|
setActionMode={setActionMode}
|
||||||
// but we pass the selection handler
|
selectedData={selectedData}
|
||||||
setActionMode={() => {}} // Keep prop for safety, but can be empty
|
setSelectedData={setSelectedData}
|
||||||
setSelectedData={handleSelectNotification}
|
/>
|
||||||
/>
|
<DetailNotification
|
||||||
</Col>
|
visible={isModalVisible}
|
||||||
{selectedData && (
|
onCancel={handleCancel}
|
||||||
<Col span={8}>
|
selectedData={selectedData}
|
||||||
<DetailNotification
|
/>
|
||||||
selectedData={selectedData}
|
</React.Fragment>
|
||||||
onClose={handleCloseDetail}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,8 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { Row, Col, Tag, Card, Button } from 'antd';
|
import { Modal, Row, Col, Tag, Divider } from 'antd';
|
||||||
import {
|
import { CloseCircleFilled, WarningFilled, CheckCircleFilled, InfoCircleFilled } from '@ant-design/icons';
|
||||||
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) => {
|
const getIconAndColor = (type) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'critical':
|
case 'critical':
|
||||||
@@ -58,194 +36,133 @@ const DetailNotification = memo(function DetailNotification({ selectedData, onCl
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const notificationType = getTypeFromStatus();
|
const { IconComponent, color, bgColor, tagColor } = selectedData ? getIconAndColor(selectedData.type) : {};
|
||||||
const { IconComponent, color, bgColor, tagColor } = getIconAndColor(notificationType);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Modal
|
||||||
title="Detail Notifikasi"
|
title="Detail Notifikasi"
|
||||||
extra={<Button onClick={onClose}>Tutup</Button>}
|
open={visible}
|
||||||
style={{ height: '100%' }}
|
onCancel={onCancel}
|
||||||
bodyStyle={{ padding: '0 24px' }}
|
onOk={onCancel}
|
||||||
|
okText="Tutup"
|
||||||
|
cancelButtonProps={{ style: { display: 'none' } }}
|
||||||
|
width={700}
|
||||||
>
|
>
|
||||||
<div>
|
{selectedData && (
|
||||||
{/* Header with Icon and Status */}
|
<div>
|
||||||
<div
|
{/* Header with Icon and Status */}
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '8px',
|
|
||||||
marginBottom: '0',
|
|
||||||
padding: '2px 0',
|
|
||||||
backgroundColor: '#fafafa',
|
|
||||||
borderRadius: '8px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '32px',
|
|
||||||
height: '32px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
backgroundColor: bgColor,
|
|
||||||
color: color,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
gap: '16px',
|
||||||
fontSize: '18px',
|
marginBottom: '24px',
|
||||||
flexShrink: 0,
|
padding: '16px',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
borderRadius: '8px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{IconComponent && <IconComponent style={{ fontSize: '18px' }} />}
|
<div
|
||||||
|
style={{
|
||||||
|
width: '64px',
|
||||||
|
height: '64px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
color: color,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '32px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{IconComponent && <IconComponent style={{ fontSize: '32px' }} />}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Tag color={tagColor} style={{ marginBottom: '8px', fontSize: '12px' }}>
|
||||||
|
{selectedData.type.toUpperCase()}
|
||||||
|
</Tag>
|
||||||
|
<div style={{ fontSize: '16px', fontWeight: 600, color: '#262626' }}>
|
||||||
|
{selectedData.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<Tag color={tagColor} style={{ marginBottom: '2px', fontSize: '11px' }}>
|
<Divider style={{ margin: '16px 0' }} />
|
||||||
{notificationType.toUpperCase()}
|
|
||||||
|
{/* Information Grid */}
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={12}>
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
|
||||||
|
PLC
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
|
||||||
|
{selectedData.plc}
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={12}>
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
|
||||||
|
Engineer
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
|
||||||
|
{selectedData.engineer}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
|
||||||
|
Waktu
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
|
||||||
|
{selectedData.time}
|
||||||
|
</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'}
|
||||||
</Tag>
|
</Tag>
|
||||||
<div style={{ fontSize: '14px', fontWeight: 600, color: '#262626' }}>
|
|
||||||
{errorCodeData?.error_code_name || 'N/A'}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Information Grid */}
|
{/* Additional Info */}
|
||||||
<Row gutter={[16, 0]}>
|
|
||||||
<Col span={12}>
|
|
||||||
<div style={{ marginBottom: '2px' }}>
|
|
||||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
|
|
||||||
Kode Error
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '13px', color: '#262626', fontWeight: 500 }}>
|
|
||||||
{errorCodeData?.error_code || 'N/A'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<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, 0]}>
|
|
||||||
<Col span={12}>
|
|
||||||
<div style={{ marginBottom: '2px' }}>
|
|
||||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
|
|
||||||
Solusi
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '13px', color: '#262626', fontWeight: 500 }}>
|
|
||||||
{activeSolution?.solution_name || 'N/A'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<div style={{ marginBottom: '2px' }}>
|
|
||||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
|
|
||||||
Waktu Dibuat
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<div style={{ marginTop: '16px', marginBottom: '8px' }}>
|
|
||||||
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '4px' }}>
|
|
||||||
Deskripsi Error
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: '13px',
|
marginTop: '16px',
|
||||||
color: '#262626',
|
padding: '12px',
|
||||||
fontWeight: 500,
|
backgroundColor: '#f6f9ff',
|
||||||
padding: '8px',
|
borderRadius: '6px',
|
||||||
backgroundColor: '#fafafa',
|
border: '1px solid #d6e4ff',
|
||||||
borderRadius: '4px',
|
|
||||||
border: '1px solid #f0f0f0',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedData.message_error_issue || 'N/A'}
|
<div style={{ fontSize: '12px', color: '#595959' }}>
|
||||||
|
<strong>Catatan:</strong> Notifikasi ini telah dikirim ke engineer yang bersangkutan
|
||||||
|
untuk ditindaklanjuti sesuai dengan prosedur yang berlaku.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{/* Spareparts Information */}
|
</Modal>
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,64 +38,96 @@ import {
|
|||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
||||||
import {
|
import { getAllNotification } from '../../../api/notification';
|
||||||
getAllNotification,
|
|
||||||
getNotificationLogByNotificationId,
|
|
||||||
getNotificationDetail,
|
|
||||||
resendChatByUser,
|
|
||||||
resendChatAllUser,
|
|
||||||
searchData,
|
|
||||||
} from '../../../api/notification';
|
|
||||||
|
|
||||||
const { Text, Paragraph, Link: AntdLink } = Typography;
|
const { Text, Paragraph, Link: AntdLink } = Typography;
|
||||||
const OpenMail = ({ size = 22, color = 'black' }) => (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 640 640"
|
|
||||||
width={size}
|
|
||||||
height={size}
|
|
||||||
fill={color}
|
|
||||||
>
|
|
||||||
<path d="M576 480C576 515.3 547.5 544 512.1 544L128 544C92.6 544 64 515.3 64 480L64 228C64.1 212.5 71.8 198 84.5 189.2L270 61.3C300.1 40.6 339.8 40.6 369.9 61.3L555.5 189.2C568.3 198 575.9 212.5 576 228L576 480zM128 496L512.1 496C520.9 496 528 488.9 528 480L528 288.3L373.2 405.7C341.8 429.6 298.3 429.6 266.8 405.7L112 288.3L112 480C112 488.9 119.2 496 128 496zM527.6 228.4L342.7 100.8C329 91.4 311 91.4 297.3 100.8L112.4 228.4L295.8 367.5C310.1 378.3 329.9 378.3 344.2 367.5L527.6 228.4z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
// Transform API response to component format
|
// Transform API response to component format
|
||||||
const transformNotificationData = (apiData) => {
|
const transformNotificationData = (apiData) => {
|
||||||
return apiData.map((item, index) => ({
|
return apiData.map((item, index) => ({
|
||||||
id: `notification-${item.notification_error_id}-${index}`, // Unique key prefix with array index
|
id: `notification-${item.notification_error_id}-${index}`, // Unique key prefix with array index
|
||||||
type: item.is_read ? 'resolved' : item.is_delivered ? 'warning' : 'critical',
|
type: item.is_read ? 'resolved' : item.is_delivered ? 'warning' : 'critical',
|
||||||
title: item.error_code_name || 'Unknown Error',
|
title: item.device_name || 'Unknown Device',
|
||||||
color: item.error_code_color || 'Black',
|
issue: item.error_code_name || 'Unknown Error',
|
||||||
issue: item.error_code || item.error_code_name || 'Unknown Error',
|
description: `${item.error_code} - ${item.error_code_name}`,
|
||||||
description: `${item.error_code} - ${item.error_code_name || ''}`,
|
timestamp:
|
||||||
timestamp: item.created_at
|
new Date(item.created_at).toLocaleString('id-ID', {
|
||||||
? new Date(item.created_at).toLocaleString('id-ID', {
|
day: '2-digit',
|
||||||
day: '2-digit',
|
month: '2-digit',
|
||||||
month: '2-digit',
|
year: 'numeric',
|
||||||
year: 'numeric',
|
hour: '2-digit',
|
||||||
hour: '2-digit',
|
minute: '2-digit',
|
||||||
minute: '2-digit',
|
}) + ' WIB',
|
||||||
}) + ' WIB'
|
location: item.device_location || 'Location not specified',
|
||||||
: 'N/A',
|
details: item.message_error_issue || 'No details available',
|
||||||
location: item.plant_sub_section_name || item.device_location || 'Location not specified',
|
|
||||||
details: item.device_name || '-',
|
|
||||||
errId: item.notification_error_id || 0,
|
|
||||||
link: `/verification-sparepart/${item.notification_error_id}`, // Dummy URL untuk verifikasi spare part
|
link: `/verification-sparepart/${item.notification_error_id}`, // Dummy URL untuk verifikasi spare part
|
||||||
subsection: item.plant_sub_section_name || 'N/A',
|
subsection: item.solution_name || 'N/A',
|
||||||
isRead: item.is_read,
|
isRead: item.is_read,
|
||||||
status: item.is_read ? 'Resolved' : item.is_delivered ? 'Delivered' : 'Pending',
|
status: item.is_read ? 'Resolved' : item.is_delivered ? 'Delivered' : 'Pending',
|
||||||
tag: item.error_code,
|
tag: item.error_code,
|
||||||
errorCode: item.error_code,
|
errorCode: item.error_code,
|
||||||
solutionName: item.error_code?.solution?.[0]?.solution_name || 'N/A',
|
solutionName: item.solution_name,
|
||||||
typeSolution: item.error_code?.solution?.[0]?.type_solution || 'N/A',
|
typeSolution: item.type_solution,
|
||||||
pathSolution:
|
pathSolution: item.path_solution,
|
||||||
item.error_code?.solution?.[0]?.path_document ||
|
|
||||||
item.error_code?.solution?.[0]?.path_solution ||
|
|
||||||
'N/A',
|
|
||||||
error_code: item.error_code,
|
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Dummy data untuk 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',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Dummy data untuk log history
|
||||||
|
const logHistoryData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
timestamp: '04-11-2025 11:55 WIB',
|
||||||
|
addedBy: {
|
||||||
|
name: 'Budi Santoso',
|
||||||
|
phone: '081122334455',
|
||||||
|
},
|
||||||
|
description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
timestamp: '04-11-2025 11:45 WIB',
|
||||||
|
addedBy: {
|
||||||
|
name: 'John Doe',
|
||||||
|
phone: '081234567890',
|
||||||
|
},
|
||||||
|
description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
timestamp: '04-11-2025 11:40 WIB',
|
||||||
|
addedBy: {
|
||||||
|
name: 'Jane Smith',
|
||||||
|
phone: '087654321098',
|
||||||
|
},
|
||||||
|
description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const ListNotification = memo(function ListNotification(props) {
|
const ListNotification = memo(function ListNotification(props) {
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
const [activeTab, setActiveTab] = useState('all');
|
const [activeTab, setActiveTab] = useState('all');
|
||||||
@@ -105,10 +137,6 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
const [modalContent, setModalContent] = useState(null); // 'user', 'log', 'details', or null
|
const [modalContent, setModalContent] = useState(null); // 'user', 'log', 'details', or null
|
||||||
const [isAddingLog, setIsAddingLog] = useState(false);
|
const [isAddingLog, setIsAddingLog] = useState(false);
|
||||||
const [selectedNotification, setSelectedNotification] = useState(null);
|
const [selectedNotification, setSelectedNotification] = useState(null);
|
||||||
const [logHistoryData, setLogHistoryData] = useState([]);
|
|
||||||
const [logLoading, setLogLoading] = useState(false);
|
|
||||||
const [userHistoryData, setUserHistoryData] = useState([]);
|
|
||||||
const [userLoading, setUserLoading] = useState(false);
|
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
current_page: 1,
|
current_page: 1,
|
||||||
current_limit: 10,
|
current_limit: 10,
|
||||||
@@ -174,7 +202,7 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Fetch notifications with new pagination
|
// Fetch notifications with new pagination
|
||||||
const isReadFilter = activeTab === 'read' ? 1 : activeTab === 'unread' ? 0 : null;
|
const isReadFilter = activeTab === 'read' ? true : activeTab === 'unread' ? false : null;
|
||||||
fetchNotifications(page, pageSize, isReadFilter);
|
fetchNotifications(page, pageSize, isReadFilter);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,20 +214,20 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch notifications on component mount and when tab changes
|
// Fetch notifications on component mount and when tab changes
|
||||||
const isReadFilter = activeTab === 'read' ? 1 : activeTab === 'unread' ? 0 : null;
|
const isReadFilter = activeTab === 'read' ? true : activeTab === 'unread' ? false : null;
|
||||||
fetchNotifications(pagination.current_page, pagination.current_limit, isReadFilter);
|
fetchNotifications(pagination.current_page, pagination.current_limit, isReadFilter);
|
||||||
}, [activeTab]);
|
}, [activeTab]);
|
||||||
|
|
||||||
const getIconAndColor = (type) => {
|
const getIconAndColor = (type) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'critical':
|
case 'critical':
|
||||||
return { IconComponent: MailOutlined, color: '#faad14', bgColor: '#fff1f0' };
|
return { IconComponent: CloseCircleFilled, color: '#ff4d4f', bgColor: '#fff1f0' };
|
||||||
case 'warning':
|
case 'warning':
|
||||||
return { IconComponent: MailOutlined, color: '#1890ff', bgColor: '#fffbe6' };
|
return { IconComponent: WarningFilled, color: '#faad14', bgColor: '#fffbe6' };
|
||||||
case 'resolved':
|
case 'resolved':
|
||||||
return { IconComponent: MailOutlined, color: '#52c41a', bgColor: '#f6ffed' };
|
return { IconComponent: CheckCircleFilled, color: '#52c41a', bgColor: '#f6ffed' };
|
||||||
default:
|
default:
|
||||||
return { IconComponent: MailOutlined, color: '#1890ff', bgColor: '#e6f7ff' };
|
return { IconComponent: InfoCircleFilled, color: '#1890ff', bgColor: '#e6f7ff' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -210,9 +238,9 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
content: `Are you sure you want to resend the notification for "${notification.title}"?`,
|
content: `Are you sure you want to resend the notification for "${notification.title}"?`,
|
||||||
okText: 'Resend',
|
okText: 'Resend',
|
||||||
cancelText: 'Cancel',
|
cancelText: 'Cancel',
|
||||||
async onOk() {
|
onOk() {
|
||||||
console.log('Resending notification:', notification.id);
|
console.log('Resending notification:', notification.id);
|
||||||
await resendChatAllUser(notification.errId);
|
|
||||||
message.success(
|
message.success(
|
||||||
`Notification for "${notification.title}" has been resent successfully.`
|
`Notification for "${notification.title}" has been resent successfully.`
|
||||||
);
|
);
|
||||||
@@ -231,49 +259,13 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchSearch = async (data) => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await searchData(data);
|
|
||||||
if (response && response.data) {
|
|
||||||
const transformedData = transformNotificationData(response.data);
|
|
||||||
setNotifications(transformedData);
|
|
||||||
|
|
||||||
// Update pagination with API response or calculate from data
|
|
||||||
if (response.paging) {
|
|
||||||
setPagination({
|
|
||||||
current_page: response.paging.current_page || page,
|
|
||||||
current_limit: response.paging.current_limit || limit,
|
|
||||||
total_limit: response.paging.total_limit || transformedData.length,
|
|
||||||
total_page:
|
|
||||||
response.paging.total_page || Math.ceil(transformedData.length / limit),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fallback: calculate pagination from data
|
|
||||||
const totalItems = transformedData.length;
|
|
||||||
setPagination((prev) => ({
|
|
||||||
...prev,
|
|
||||||
current_page: page,
|
|
||||||
current_limit: limit,
|
|
||||||
total_limit: totalItems,
|
|
||||||
total_page: Math.ceil(totalItems / limit),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
fetchSearch(searchValue);
|
setSearchTerm(searchValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchClear = () => {
|
const handleSearchClear = () => {
|
||||||
setSearchValue('');
|
setSearchValue('');
|
||||||
fetchSearch('');
|
setSearchTerm('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUnreadCount = () => notifications.filter((n) => !n.isRead).length;
|
const getUnreadCount = () => notifications.filter((n) => !n.isRead).length;
|
||||||
@@ -288,78 +280,6 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch log history from API
|
|
||||||
const fetchLogHistory = async (notificationId) => {
|
|
||||||
try {
|
|
||||||
setLogLoading(true);
|
|
||||||
const response = await getNotificationLogByNotificationId(notificationId);
|
|
||||||
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 || 'N/A',
|
|
||||||
},
|
|
||||||
description: log.notification_error_log_description || '',
|
|
||||||
}));
|
|
||||||
setLogHistoryData(transformedLogs);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching log history:', err);
|
|
||||||
setLogHistoryData([]); // Set empty array on error
|
|
||||||
} finally {
|
|
||||||
setLogLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch user history from API
|
|
||||||
const fetchUserHistory = async (notificationId) => {
|
|
||||||
try {
|
|
||||||
setUserLoading(true);
|
|
||||||
|
|
||||||
const response = await getNotificationDetail(notificationId);
|
|
||||||
|
|
||||||
if (response && response.data && response.data.users) {
|
|
||||||
// Transform API data to component format
|
|
||||||
const transformedUsers = response.data.users.map((user) => ({
|
|
||||||
id: user.notification_error_user_id.toString(),
|
|
||||||
name: user.contact_name,
|
|
||||||
phone: user.contact_phone,
|
|
||||||
status: user.is_send ? 'Delivered' : 'Pending',
|
|
||||||
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',
|
|
||||||
}));
|
|
||||||
setUserHistoryData(transformedUsers);
|
|
||||||
} else {
|
|
||||||
setUserHistoryData([]);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching user history:', err);
|
|
||||||
setUserHistoryData([]); // Set empty array on error
|
|
||||||
} finally {
|
|
||||||
setUserLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabButtonStyle = (isActive) => ({
|
const tabButtonStyle = (isActive) => ({
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@@ -394,6 +314,7 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
borderColor: notification.isRead ? '#f0f0f0' : '#d6e4ff',
|
borderColor: notification.isRead ? '#f0f0f0' : '#d6e4ff',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
|
onClick={() => handleMarkAsRead(notification.id)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -416,11 +337,7 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{notification.type === 'resolved' ? (
|
<IconComponent style={{ fontSize: '22px' }} />
|
||||||
<OpenMail size={28.5} color={color} />
|
|
||||||
) : (
|
|
||||||
<IconComponent style={{ fontSize: '22px' }} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Row align="top">
|
<Row align="top">
|
||||||
@@ -435,12 +352,8 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
<div>
|
<div>
|
||||||
<Text strong>{notification.title}</Text>
|
<Text strong>{notification.title}</Text>
|
||||||
<div style={{ marginTop: '4px' }}>
|
<div style={{ marginTop: '4px' }}>
|
||||||
<Text
|
<Text style={{ color }}>
|
||||||
style={{
|
{notification.issue}
|
||||||
color: notification.color,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Error Code {notification.issue}
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -457,7 +370,7 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col flex="auto">
|
<Col flex="auto">
|
||||||
{/* <div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
@@ -480,18 +393,12 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
>
|
>
|
||||||
{notification.details}
|
{notification.details}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</div> */}
|
</div>
|
||||||
<Space
|
<Space
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
size={4}
|
size={4}
|
||||||
style={{ fontSize: '13px', color: '#8c8c8c' }}
|
style={{ fontSize: '13px', color: '#8c8c8c' }}
|
||||||
>
|
>
|
||||||
<Space>
|
|
||||||
<MobileOutlined />
|
|
||||||
<Text type="secondary">
|
|
||||||
{notification.details}
|
|
||||||
</Text>
|
|
||||||
</Space>
|
|
||||||
<Space>
|
<Space>
|
||||||
<ClockCircleOutlined />
|
<ClockCircleOutlined />
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
@@ -505,10 +412,17 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
|
<LinkOutlined />
|
||||||
|
<AntdLink
|
||||||
|
href={notification.link}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{notification.link}
|
||||||
|
</AntdLink>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
icon={<SendOutlined />}
|
icon={<SendOutlined />}
|
||||||
style={{ paddingLeft: '0px' }}
|
style={{ paddingLeft: '8px' }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleResend(notification);
|
handleResend(notification);
|
||||||
@@ -544,23 +458,13 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
border: '1px solid #1890ff',
|
border: '1px solid #1890ff',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
}}
|
}}
|
||||||
onClick={async (e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
setSelectedNotification(notification);
|
|
||||||
|
|
||||||
// Extract notification ID from the notification object
|
|
||||||
const notificationId =
|
|
||||||
notification.id.split('-')[1];
|
|
||||||
|
|
||||||
// Fetch user history for the selected notification
|
|
||||||
await fetchUserHistory(notificationId);
|
|
||||||
|
|
||||||
setModalContent('user');
|
setModalContent('user');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to={`/notification-detail/${
|
to={`/detail-notification/${
|
||||||
notification.id.split('-')[1]
|
notification.id.split('-')[1]
|
||||||
}`}
|
}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -595,15 +499,6 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// Set the selected notification for the log history
|
|
||||||
const notificationId =
|
|
||||||
notification.id.split('-')[1];
|
|
||||||
setSelectedNotification(notification);
|
|
||||||
|
|
||||||
// Fetch log history for the selected notification
|
|
||||||
fetchLogHistory(notificationId);
|
|
||||||
|
|
||||||
setModalContent('log');
|
setModalContent('log');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -622,189 +517,131 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
|
|
||||||
const renderUserHistory = () => (
|
const renderUserHistory = () => (
|
||||||
<>
|
<>
|
||||||
{userLoading ? (
|
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
||||||
<div style={{ textAlign: 'center', padding: '24px' }}>
|
{userHistoryData.map((user) => (
|
||||||
<Spin size="large" />
|
<Card key={user.id} style={{ borderColor: '#91d5ff' }}>
|
||||||
</div>
|
<Row align="middle" justify="space-between">
|
||||||
) : (
|
<Col>
|
||||||
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
<Space align="center">
|
||||||
{userHistoryData.map((user) => (
|
<Text strong>{user.name}</Text>
|
||||||
<Card key={user.id} style={{ borderColor: '#91d5ff' }}>
|
<Text>|</Text>
|
||||||
<Row align="middle" justify="space-between">
|
<Text>
|
||||||
<Col>
|
<MobileOutlined /> {user.phone}
|
||||||
<Space align="center">
|
</Text>
|
||||||
<Text strong>{user.name}</Text>
|
<Text>|</Text>
|
||||||
<Text>|</Text>
|
<Badge status="success" text={user.status} />
|
||||||
<Text>
|
</Space>
|
||||||
<MobileOutlined /> {user.phone}
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
</Text>
|
<Space align="center">
|
||||||
<Text>|</Text>
|
<CheckCircleFilled style={{ color: '#52c41a' }} />
|
||||||
<Badge
|
<Text type="secondary">
|
||||||
status={
|
Success Delivered at {user.timestamp}
|
||||||
user.status === 'Delivered' ? 'success' : 'default'
|
</Text>
|
||||||
}
|
</Space>
|
||||||
text={user.status}
|
</Col>
|
||||||
/>
|
<Col>
|
||||||
</Space>
|
<Button type="primary" ghost icon={<SendOutlined />}>
|
||||||
<Divider style={{ margin: '8px 0' }} />
|
Resend
|
||||||
<Space align="center">
|
</Button>
|
||||||
{user.status === 'Delivered' ? (
|
</Col>
|
||||||
<CheckCircleFilled style={{ color: '#52c41a' }} />
|
</Row>
|
||||||
) : (
|
</Card>
|
||||||
<ClockCircleOutlined style={{ color: '#faad14' }} />
|
))}
|
||||||
)}
|
</Space>
|
||||||
<Text type="secondary">
|
|
||||||
{user.status === 'Delivered'
|
|
||||||
? 'Success Delivered at'
|
|
||||||
: 'Status '}{' '}
|
|
||||||
{user.timestamp}
|
|
||||||
</Text>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
<Col>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
ghost
|
|
||||||
icon={<SendOutlined />}
|
|
||||||
onClick={async () => {
|
|
||||||
await resendChatByUser(user.id, user.phone);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Resend
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
{userHistoryData.length === 0 && (
|
|
||||||
<div style={{ textAlign: 'center', padding: '24px', color: '#8c8c8c' }}>
|
|
||||||
No user history available
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderLogHistory = () => (
|
const renderLogHistory = () => (
|
||||||
<>
|
<>
|
||||||
{logLoading ? (
|
<div style={{ padding: '0 16px', position: 'relative' }}>
|
||||||
<div style={{ textAlign: 'center', padding: '24px' }}>
|
{/* Garis vertikal yang menyambung */}
|
||||||
<Spin size="large" />
|
|
||||||
</div>
|
|
||||||
) : logHistoryData.length === 0 ? (
|
|
||||||
<div style={{ textAlign: 'center', padding: '24px', color: '#8c8c8c' }}>
|
|
||||||
Tidak ada log history
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: '400px',
|
position: 'absolute',
|
||||||
overflowY: 'auto',
|
top: '7px',
|
||||||
padding: '0 16px',
|
left: '23px',
|
||||||
position: 'relative',
|
bottom: '7px',
|
||||||
border: '1px solid #f0f0f0',
|
width: '2px',
|
||||||
borderRadius: '4px',
|
backgroundColor: '#91d5ff',
|
||||||
|
zIndex: 0,
|
||||||
}}
|
}}
|
||||||
>
|
></div>
|
||||||
<div style={{ position: 'relative' }}>
|
|
||||||
{/* Garis vertikal yang menyambung */}
|
{logHistoryData.map((log, index) => (
|
||||||
<div
|
<Row
|
||||||
|
key={log.id}
|
||||||
|
wrap={false}
|
||||||
|
style={{ marginBottom: '16px', position: 'relative', zIndex: 1 }}
|
||||||
|
>
|
||||||
|
{/* Kolom Kiri: Branch/Timeline */}
|
||||||
|
<Col
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'relative',
|
||||||
top: '7px',
|
display: 'flex',
|
||||||
left: '23px',
|
flexDirection: 'column',
|
||||||
bottom: '7px',
|
alignItems: 'center',
|
||||||
width: '2px',
|
marginRight: '16px',
|
||||||
backgroundColor: '#91d5ff',
|
|
||||||
zIndex: 0,
|
|
||||||
}}
|
}}
|
||||||
></div>
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '14px',
|
||||||
|
height: '14px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
border: '3px solid #1890ff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
zIndex: 1,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
{logHistoryData.map((log, index) => (
|
{/* Kolom Kanan: Card */}
|
||||||
<Row
|
<Col flex="auto">
|
||||||
key={log.id}
|
<Card size="small" style={{ borderColor: '#91d5ff' }}>
|
||||||
wrap={false}
|
<Row gutter={[16, 8]} align="middle">
|
||||||
style={{ marginBottom: '16px', position: 'relative', zIndex: 1 }}
|
<Col xs={24} md={12}>
|
||||||
>
|
<Space direction="vertical" size={4}>
|
||||||
{/* Kolom Kiri: Branch/Timeline */}
|
<Space>
|
||||||
<Col
|
<ClockCircleOutlined />
|
||||||
style={{
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
position: 'relative',
|
Added at {log.timestamp}
|
||||||
display: 'flex',
|
</Text>
|
||||||
flexDirection: 'column',
|
</Space>
|
||||||
alignItems: 'center',
|
<div>
|
||||||
marginRight: '16px',
|
<Text strong>Added by: {log.addedBy.name}</Text>
|
||||||
}}
|
<span
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '14px',
|
|
||||||
height: '14px',
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
border: '3px solid #1890ff',
|
|
||||||
borderRadius: '50%',
|
|
||||||
zIndex: 1,
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* Kolom Kanan: Card */}
|
|
||||||
<Col flex="auto">
|
|
||||||
<Card size="small" style={{ borderColor: '#91d5ff' }}>
|
|
||||||
<Row gutter={[16, 8]} align="top">
|
|
||||||
<Col xs={24} md={10}>
|
|
||||||
<Space direction="vertical" size={4}>
|
|
||||||
<Space>
|
|
||||||
<ClockCircleOutlined />
|
|
||||||
<Text
|
|
||||||
type="secondary"
|
|
||||||
style={{ fontSize: '12px' }}
|
|
||||||
>
|
|
||||||
Added at {log.timestamp}
|
|
||||||
</Text>
|
|
||||||
</Space>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Text strong>{log.addedBy.name}</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
border: '1px solid #52c41a',
|
|
||||||
color: '#52c41a',
|
|
||||||
padding: '2px 6px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '12px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MobileOutlined /> {log.addedBy.phone}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} md={14}>
|
|
||||||
<Text strong>Description:</Text>
|
|
||||||
<Paragraph
|
|
||||||
style={{
|
style={{
|
||||||
color: '#595959',
|
marginLeft: '8px',
|
||||||
margin: 0,
|
border: '1px solid #52c41a',
|
||||||
fontSize: '13px',
|
color: '#52c41a',
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '12px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{log.description}
|
<MobileOutlined /> {log.addedBy.phone}
|
||||||
</Paragraph>
|
</span>
|
||||||
</Col>
|
</div>
|
||||||
</Row>
|
</Space>
|
||||||
</Card>
|
</Col>
|
||||||
</Col>
|
<Col xs={24} md={12}>
|
||||||
</Row>
|
<Paragraph
|
||||||
))}
|
style={{
|
||||||
</div>
|
color: '#595959',
|
||||||
</div>
|
margin: 0,
|
||||||
)}
|
fontSize: '13px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{log.description}
|
||||||
|
</Paragraph>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -814,8 +651,8 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
const { IconComponent, color } = getIconAndColor(selectedNotification.type);
|
const { IconComponent, color } = getIconAndColor(selectedNotification.type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
<Row gutter={[16, 8]}>
|
<Row gutter={[16, 16]}>
|
||||||
{/* Kolom Kiri: Data Kompresor */}
|
{/* Kolom Kiri: Data Kompresor */}
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Card
|
<Card
|
||||||
@@ -824,7 +661,7 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
style={{ height: '100%', borderColor: '#d4380d' }}
|
style={{ height: '100%', borderColor: '#d4380d' }}
|
||||||
bodyStyle={{ padding: '12px' }}
|
bodyStyle={{ padding: '12px' }}
|
||||||
>
|
>
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
<Row gutter={16} align="middle">
|
<Row gutter={16} align="middle">
|
||||||
<Col>
|
<Col>
|
||||||
<div
|
<div
|
||||||
@@ -856,9 +693,9 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
<Text strong>Plant Subsection</Text>
|
<Text strong>Plant Subsection</Text>
|
||||||
<div>{selectedNotification.subsection}</div>
|
<div>{selectedNotification.subsection}</div>
|
||||||
<Text strong style={{ display: 'block', marginTop: '8px' }}>
|
<Text strong style={{ display: 'block', marginTop: '8px' }}>
|
||||||
Date & Time
|
Time
|
||||||
</Text>
|
</Text>
|
||||||
<div>{selectedNotification.timestamp}</div>
|
<div>{selectedNotification.timestamp.split(' ')[1]} WIB</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -929,7 +766,7 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<div>
|
<div>
|
||||||
<Row gutter={[16, 8]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Card
|
<Card
|
||||||
style={{
|
style={{
|
||||||
@@ -975,16 +812,7 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
bodyStyle={{ padding: '12px' }}
|
bodyStyle={{ padding: '12px' }}
|
||||||
onClick={() => {
|
onClick={() => setModalContent('log')}
|
||||||
// Set the selected notification for the log history if not already set
|
|
||||||
if (selectedNotification) {
|
|
||||||
const notificationId =
|
|
||||||
selectedNotification.id.split('-')[1];
|
|
||||||
// Fetch log history for the selected notification
|
|
||||||
fetchLogHistory(notificationId);
|
|
||||||
}
|
|
||||||
setModalContent('log');
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
<HistoryOutlined
|
<HistoryOutlined
|
||||||
@@ -997,7 +825,7 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row gutter={[16, 8]} style={{ marginTop: '0' }}>
|
<Row gutter={[16, 16]} style={{ marginTop: '16px' }}>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Card size="small" style={{ height: '100%' }}>
|
<Card size="small" style={{ height: '100%' }}>
|
||||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
@@ -1301,40 +1129,24 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
{logLoading ? (
|
{logHistoryData.map((log) => (
|
||||||
<div style={{ textAlign: 'center', padding: '12px' }}>
|
<Card
|
||||||
<Spin size="small" />
|
key={log.id}
|
||||||
</div>
|
size="small"
|
||||||
) : logHistoryData.length === 0 ? (
|
bodyStyle={{ padding: '8px 12px' }}
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '12px',
|
|
||||||
color: '#8c8c8c',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Tidak ada log history
|
<Paragraph
|
||||||
</div>
|
style={{ fontSize: '12px', margin: 0 }}
|
||||||
) : (
|
ellipsis={{ rows: 2 }}
|
||||||
logHistoryData.map((log) => (
|
|
||||||
<Card
|
|
||||||
key={log.id}
|
|
||||||
size="small"
|
|
||||||
bodyStyle={{ padding: '8px 12px' }}
|
|
||||||
>
|
>
|
||||||
<Paragraph
|
<Text strong>{log.addedBy.name}:</Text>{' '}
|
||||||
style={{ fontSize: '12px', margin: 0 }}
|
{log.description}
|
||||||
ellipsis={{ rows: 2 }}
|
</Paragraph>
|
||||||
>
|
<Text type="secondary" style={{ fontSize: '11px' }}>
|
||||||
<Text strong>{log.addedBy.name}:</Text>{' '}
|
{log.timestamp}
|
||||||
{log.description}
|
</Text>
|
||||||
</Paragraph>
|
</Card>
|
||||||
<Text type="secondary" style={{ fontSize: '11px' }}>
|
))}
|
||||||
{log.timestamp}
|
|
||||||
</Text>
|
|
||||||
</Card>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -1474,7 +1286,7 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||||
{modalContent === 'user' && 'History User Notification'}
|
{modalContent === 'user' && 'User History Notification'}
|
||||||
{modalContent === 'log' && 'Log History Notification'}
|
{modalContent === 'log' && 'Log History Notification'}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal, Typography, Card, Row, Col, Avatar, Tag, Button, Space } from 'antd';
|
import { Modal, Typography, Card, Row, Col, Avatar, Tag, Button, Space } from 'antd';
|
||||||
import {
|
import { UserOutlined, PhoneOutlined, CheckCircleOutlined, SyncOutlined, SendOutlined } from '@ant-design/icons';
|
||||||
UserOutlined,
|
|
||||||
PhoneOutlined,
|
|
||||||
CheckCircleOutlined,
|
|
||||||
SyncOutlined,
|
|
||||||
SendOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -47,17 +41,9 @@ const UserHistoryModal = ({ visible, onCancel, notificationData }) => {
|
|||||||
const getStatusTag = (status) => {
|
const getStatusTag = (status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'delivered':
|
case 'delivered':
|
||||||
return (
|
return <Tag icon={<CheckCircleOutlined />} color="success">Delivered</Tag>;
|
||||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
|
||||||
Delivered
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'sent':
|
case 'sent':
|
||||||
return (
|
return <Tag icon={<SyncOutlined spin />} color="processing">Sent</Tag>;
|
||||||
<Tag icon={<SyncOutlined spin />} color="processing">
|
|
||||||
Sent
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return <Tag color="error">Failed</Tag>;
|
return <Tag color="error">Failed</Tag>;
|
||||||
default:
|
default:
|
||||||
@@ -69,7 +55,7 @@ const UserHistoryModal = ({ visible, onCancel, notificationData }) => {
|
|||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<Text strong style={{ fontSize: '18px' }}>
|
<Text strong style={{ fontSize: '18px' }}>
|
||||||
History User Notification
|
User History Notification
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
open={visible}
|
open={visible}
|
||||||
@@ -92,13 +78,7 @@ const UserHistoryModal = ({ visible, onCancel, notificationData }) => {
|
|||||||
<Avatar size="large" icon={<UserOutlined />} />
|
<Avatar size="large" icon={<UserOutlined />} />
|
||||||
<div>
|
<div>
|
||||||
<Text strong>{user.name}</Text>
|
<Text strong>{user.name}</Text>
|
||||||
<div
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '4px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PhoneOutlined style={{ color: '#8c8c8c' }} />
|
<PhoneOutlined style={{ color: '#8c8c8c' }} />
|
||||||
<Text type="secondary">{user.phone}</Text>
|
<Text type="secondary">{user.phone}</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,37 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Row, Col, Card, Badge, Typography, Space, Divider } from 'antd';
|
import { Button, Row, Col, Card, Badge, Typography, Space, Divider } from 'antd';
|
||||||
import {
|
import { SendOutlined, MobileOutlined, CheckCircleFilled, ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
SendOutlined,
|
|
||||||
MobileOutlined,
|
|
||||||
CheckCircleFilled,
|
|
||||||
ArrowLeftOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
// Dummy data for user history
|
// Dummy data for user history
|
||||||
const userHistoryData = [
|
const userHistoryData = [
|
||||||
{
|
{ id: 1, name: 'John Doe', phone: '081234567890', status: 'Delivered', timestamp: '04-11-2025 11:40 WIB' },
|
||||||
id: 1,
|
{ id: 2, name: 'Jane Smith', phone: '087654321098', status: 'Delivered', timestamp: '04-11-2025 11:41 WIB' },
|
||||||
name: 'John Doe',
|
{ id: 3, name: 'Peter Jones', phone: '082345678901', status: 'Delivered', timestamp: '04-11-2025 11:42 WIB' },
|
||||||
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 }) => {
|
const UserHistory = ({ notification, onBack }) => {
|
||||||
@@ -41,9 +18,7 @@ const UserHistory = ({ notification, onBack }) => {
|
|||||||
<Col>
|
<Col>
|
||||||
<Space align="center">
|
<Space align="center">
|
||||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={onBack} />
|
<Button type="text" icon={<ArrowLeftOutlined />} onClick={onBack} />
|
||||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
<Typography.Title level={4} style={{ margin: 0 }}>User History Notification</Typography.Title>
|
||||||
History User Notification
|
|
||||||
</Typography.Title>
|
|
||||||
</Space>
|
</Space>
|
||||||
<Text type="secondary" style={{ marginLeft: '40px' }}>
|
<Text type="secondary" style={{ marginLeft: '40px' }}>
|
||||||
{notification.title} - {notification.issue}
|
{notification.title} - {notification.issue}
|
||||||
@@ -52,34 +27,25 @@ const UserHistory = ({ notification, onBack }) => {
|
|||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
||||||
{userHistoryData.map((user) => (
|
{userHistoryData.map(user => (
|
||||||
<Card
|
<Card key={user.id} style={{ backgroundColor: '#e6f7ff', borderColor: '#91d5ff' }}>
|
||||||
key={user.id}
|
|
||||||
style={{ backgroundColor: '#e6f7ff', borderColor: '#91d5ff' }}
|
|
||||||
>
|
|
||||||
<Row align="middle" justify="space-between">
|
<Row align="middle" justify="space-between">
|
||||||
<Col>
|
<Col>
|
||||||
<Space align="center">
|
<Space align="center">
|
||||||
<Text strong>{user.name}</Text>
|
<Text strong>{user.name}</Text>
|
||||||
<Text>|</Text>
|
<Text>|</Text>
|
||||||
<Text>
|
<Text><MobileOutlined /> {user.phone}</Text>
|
||||||
<MobileOutlined /> {user.phone}
|
|
||||||
</Text>
|
|
||||||
<Text>|</Text>
|
<Text>|</Text>
|
||||||
<Badge status="success" text={user.status} />
|
<Badge status="success" text={user.status} />
|
||||||
</Space>
|
</Space>
|
||||||
<Divider style={{ margin: '8px 0' }} />
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
<Space align="center">
|
<Space align="center">
|
||||||
<CheckCircleFilled style={{ color: '#52c41a' }} />
|
<CheckCircleFilled style={{ color: '#52c41a' }} />
|
||||||
<Text type="secondary">
|
<Text type="secondary">Success Delivered at {user.timestamp}</Text>
|
||||||
Success Delivered at {user.timestamp}
|
|
||||||
</Text>
|
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<Button type="primary" ghost icon={<SendOutlined />}>
|
<Button type="primary" ghost icon={<SendOutlined />}>Resend</Button>
|
||||||
Resend
|
|
||||||
</Button>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,955 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,246 +1,91 @@
|
|||||||
import React, { memo, useState, useEffect } from 'react';
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
import { Button, Row, Col, Card, DatePicker, Select, Typography, Table, Spin, Modal } from 'antd';
|
import { Button, Row, Col, Card, Input, DatePicker, Select, Typography } from 'antd';
|
||||||
|
import TableList from '../../../../components/Global/TableList';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { FileTextOutlined, DownloadOutlined, LoadingOutlined } from '@ant-design/icons';
|
import { FileTextOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
getAllHistoryValueReportPivot,
|
|
||||||
getAllHistoryValueReport,
|
getAllHistoryValueReport,
|
||||||
|
getAllHistoryValueReportPivot,
|
||||||
} from '../../../../api/history-value';
|
} from '../../../../api/history-value';
|
||||||
import { getAllPlantSection } from '../../../../api/master-plant-section';
|
import { getAllPlantSection } from '../../../../api/master-plant-section';
|
||||||
import jsPDF from 'jspdf';
|
|
||||||
import autoTable from 'jspdf-autotable';
|
|
||||||
import ExcelJS from 'exceljs';
|
|
||||||
import { saveAs } from 'file-saver';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const ListReport = memo(function ListReport(props) {
|
const ListReport = memo(function ListReport(props) {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'No',
|
||||||
|
key: 'no',
|
||||||
|
width: '5%',
|
||||||
|
align: 'center',
|
||||||
|
render: (_, __, index) => index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Datetime',
|
||||||
|
dataIndex: 'datetime',
|
||||||
|
key: 'datetime',
|
||||||
|
width: '15%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tag Name',
|
||||||
|
dataIndex: 'tag_name',
|
||||||
|
key: 'tag_name',
|
||||||
|
width: '70%',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: 'Value',
|
||||||
|
// dataIndex: 'val',
|
||||||
|
// key: 'val',
|
||||||
|
// width: '10%',
|
||||||
|
// render: (_, record) => Number(record.val).toFixed(4),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: 'Stat',
|
||||||
|
// dataIndex: 'status',
|
||||||
|
// key: 'status',
|
||||||
|
// width: '10%',
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
const dateNow = dayjs();
|
const dateNow = dayjs();
|
||||||
const dateNowFormated = dateNow.format('YYYY-MM-DD');
|
const dateNowFormated = dateNow.format('YYYY-MM-DD');
|
||||||
|
|
||||||
const [isLoadingModal, setIsLoadingModal] = useState(false);
|
const [trigerFilter, setTrigerFilter] = useState(false);
|
||||||
const [isLoadingTable, setIsLoadingTable] = useState(false);
|
|
||||||
const [tableData, setTableData] = useState([]);
|
|
||||||
const [columns, setColumns] = useState([]);
|
|
||||||
const [pivotData, setPivotData] = useState([]);
|
|
||||||
const [valueReportData, setValueReportData] = useState([]);
|
|
||||||
const [pagination, setPagination] = useState({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [plantSubSection, setPlantSubSection] = useState(0);
|
const [plantSubSection, setPlantSubSection] = useState(0);
|
||||||
const [plantSubSectionList, setPlantSubSectionList] = useState([]);
|
const [plantSubSectionList, setPlantSubSectionList] = useState([]);
|
||||||
const [startDate, setStartDate] = useState(dateNow);
|
const [startDate, setStartDate] = useState(dateNow);
|
||||||
const [endDate, setEndDate] = useState(dateNow);
|
const [endDate, setEndDate] = useState(dateNow);
|
||||||
const [periode, setPeriode] = useState(30);
|
const [periode, setPeriode] = useState(10);
|
||||||
|
|
||||||
const generateFullDayTimes = (dateString, intervalMinutes) => {
|
const defaultFilter = {
|
||||||
const times = [];
|
criteria: '',
|
||||||
const startOfDay = dayjs(dateString).startOf('day');
|
plant_sub_section_id: 0,
|
||||||
const endOfDay = dayjs(dateString).endOf('day');
|
from: dateNowFormated,
|
||||||
|
to: dateNowFormated,
|
||||||
let currentTime = startOfDay;
|
interval: periode,
|
||||||
|
|
||||||
while (currentTime.isBefore(endOfDay) || currentTime.isSame(endOfDay)) {
|
|
||||||
times.push(currentTime.format('YYYY-MM-DD HH:mm:ss'));
|
|
||||||
currentTime = currentTime.add(intervalMinutes, 'minute');
|
|
||||||
|
|
||||||
if (currentTime.isAfter(endOfDay)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return times;
|
|
||||||
};
|
};
|
||||||
|
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||||
|
|
||||||
const fetchData = async (page = 1, pageSize = 10, showModal = false) => {
|
const handleSearch = () => {
|
||||||
// if (!plantSubSection) {
|
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
||||||
// return;
|
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
||||||
// }
|
|
||||||
|
|
||||||
if (showModal) {
|
setFormDataFilter({
|
||||||
setIsLoadingModal(true);
|
criteria: '',
|
||||||
} else {
|
plant_sub_section_id: plantSubSection,
|
||||||
setIsLoadingTable(true);
|
from: formattedDateStart,
|
||||||
}
|
to: formattedDateEnd,
|
||||||
try {
|
interval: periode,
|
||||||
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
});
|
||||||
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
setTrigerFilter((prev) => !prev);
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
plant_sub_section_id: plantSubSection,
|
|
||||||
from: formattedDateStart,
|
|
||||||
to: formattedDateEnd,
|
|
||||||
interval: periode,
|
|
||||||
page: 1,
|
|
||||||
limit: 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pivotResponse = await getAllHistoryValueReportPivot(params);
|
|
||||||
const valueReportResponse = await getAllHistoryValueReportPivot(params);
|
|
||||||
|
|
||||||
if (pivotResponse && pivotResponse.data) {
|
|
||||||
console.log('API Pivot Response:', pivotResponse);
|
|
||||||
setPivotData(pivotResponse.data);
|
|
||||||
|
|
||||||
if (valueReportResponse && valueReportResponse.data) {
|
|
||||||
console.log('API Value Report Response:', valueReportResponse);
|
|
||||||
setValueReportData(valueReportResponse.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buat struktur pivot: waktu sebagai baris, tag sebagai kolom
|
|
||||||
const timeMap = new Map();
|
|
||||||
const tagSet = new Set();
|
|
||||||
|
|
||||||
// Kumpulkan semua waktu unik dan tag unik
|
|
||||||
pivotResponse.data.forEach((row) => {
|
|
||||||
const tagName = row.id;
|
|
||||||
tagSet.add(tagName);
|
|
||||||
|
|
||||||
const dataPoints = row.data || [];
|
|
||||||
dataPoints.forEach((item) => {
|
|
||||||
if (item && typeof item === 'object' && 'x' in item && 'y' in item) {
|
|
||||||
const datetime = item.x;
|
|
||||||
if (!timeMap.has(datetime)) {
|
|
||||||
timeMap.set(datetime, {});
|
|
||||||
}
|
|
||||||
timeMap.get(datetime)[tagName] = item.y;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Konversi ke array dan sort berdasarkan waktu
|
|
||||||
const sortedTimes = Array.from(timeMap.keys()).sort();
|
|
||||||
const sortedTags = Array.from(tagSet).sort();
|
|
||||||
|
|
||||||
// Buat data untuk table
|
|
||||||
const pivotTableData = sortedTimes.map((datetime, index) => {
|
|
||||||
const rowData = {
|
|
||||||
key: index,
|
|
||||||
datetime: datetime,
|
|
||||||
};
|
|
||||||
|
|
||||||
sortedTags.forEach((tagName) => {
|
|
||||||
rowData[tagName] = timeMap.get(datetime)[tagName];
|
|
||||||
});
|
|
||||||
|
|
||||||
return rowData;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Pivot table data sample:', pivotTableData.slice(0, 5));
|
|
||||||
console.log('Total pivot rows:', pivotTableData.length);
|
|
||||||
|
|
||||||
// Buat kolom dinamis
|
|
||||||
const dynamicColumns = [
|
|
||||||
{
|
|
||||||
title: 'No',
|
|
||||||
key: 'no',
|
|
||||||
width: 60,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'left',
|
|
||||||
render: (_, __, index) => {
|
|
||||||
return (page - 1) * pageSize + index + 1;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Datetime',
|
|
||||||
dataIndex: 'datetime',
|
|
||||||
key: 'datetime',
|
|
||||||
width: 180,
|
|
||||||
fixed: 'left',
|
|
||||||
sorter: (a, b) => new Date(a.datetime) - new Date(b.datetime),
|
|
||||||
},
|
|
||||||
...sortedTags.map((tagName) => ({
|
|
||||||
title: tagName,
|
|
||||||
dataIndex: tagName,
|
|
||||||
key: tagName,
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
render: (value) => {
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
return Number(value).toFixed(2);
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
setColumns(dynamicColumns);
|
|
||||||
|
|
||||||
// Pagination
|
|
||||||
const total = pivotTableData.length;
|
|
||||||
const startIndex = (page - 1) * pageSize;
|
|
||||||
const endIndex = startIndex + pageSize;
|
|
||||||
const paginatedData = pivotTableData.slice(startIndex, endIndex);
|
|
||||||
|
|
||||||
setTableData(paginatedData);
|
|
||||||
setPagination({
|
|
||||||
current: page,
|
|
||||||
pageSize: pageSize,
|
|
||||||
total: total,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching data:', error);
|
|
||||||
} finally {
|
|
||||||
if (showModal) {
|
|
||||||
setIsLoadingModal(false);
|
|
||||||
} else {
|
|
||||||
setIsLoadingTable(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
|
||||||
fetchData(pagination.current, pagination.pageSize, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearch = async () => {
|
|
||||||
setIsLoadingModal(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
|
||||||
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
plant_sub_section_id: plantSubSection,
|
|
||||||
from: formattedDateStart,
|
|
||||||
to: formattedDateEnd,
|
|
||||||
interval: periode,
|
|
||||||
page: 1,
|
|
||||||
limit: 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pivotResponse = await getAllHistoryValueReportPivot(params);
|
|
||||||
|
|
||||||
// Jika response sukses, proses data
|
|
||||||
if (pivotResponse && pivotResponse.data) {
|
|
||||||
await fetchData(1, pagination.pageSize, false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching data:', error);
|
|
||||||
// Error akan ditangkap oleh api-request.js dan muncul Swal otomatis
|
|
||||||
} finally {
|
|
||||||
setIsLoadingModal(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setPlantSubSection(0);
|
setPlantSubSection(0);
|
||||||
setStartDate(dateNow);
|
setStartDate(dateNow);
|
||||||
setEndDate(dateNow);
|
setEndDate(dateNow);
|
||||||
setPeriode(30);
|
setPeriode(5);
|
||||||
setTableData([]);
|
|
||||||
setColumns([]);
|
|
||||||
setPivotData([]);
|
|
||||||
setValueReportData([]);
|
|
||||||
setPagination({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlantSubSection = async () => {
|
const getPlantSubSection = async () => {
|
||||||
@@ -259,548 +104,8 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
getPlantSubSection();
|
getPlantSubSection();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isWithinOneDay = startDate.isSame(endDate, 'day');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isWithinOneDay && periode < 60) {
|
|
||||||
setPeriode(60);
|
|
||||||
}
|
|
||||||
}, [startDate, endDate, periode, isWithinOneDay]);
|
|
||||||
|
|
||||||
const periodeOptions = [
|
|
||||||
{ value: 5, label: '5 Minute', disabled: !isWithinOneDay },
|
|
||||||
{ value: 10, label: '10 Minute', disabled: !isWithinOneDay },
|
|
||||||
{ value: 30, label: '30 Minute', disabled: !isWithinOneDay },
|
|
||||||
{ value: 60, label: '1 Hour', disabled: false },
|
|
||||||
{ value: 120, label: '2 Hour', disabled: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
const exportToExcel = async () => {
|
|
||||||
if (pivotData.length === 0) {
|
|
||||||
alert('No data to export');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagMapping = {};
|
|
||||||
valueReportData.forEach(item => {
|
|
||||||
if (item.tag_name && item.tag_number) {
|
|
||||||
tagMapping[item.tag_name] = item.tag_number;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedSection = plantSubSectionList.find(
|
|
||||||
item => item.plant_sub_section_id === plantSubSection
|
|
||||||
);
|
|
||||||
const sectionName = selectedSection ? selectedSection.plant_sub_section_name : 'Unknown';
|
|
||||||
|
|
||||||
// Buat struktur pivot yang sama seperti di tabel
|
|
||||||
const timeMap = new Map();
|
|
||||||
const tagSet = new Set();
|
|
||||||
|
|
||||||
pivotData.forEach((row) => {
|
|
||||||
const tagName = row.id;
|
|
||||||
tagSet.add(tagName);
|
|
||||||
|
|
||||||
const dataPoints = row.data || [];
|
|
||||||
dataPoints.forEach((item) => {
|
|
||||||
if (item && typeof item === 'object' && 'x' in item && 'y' in item) {
|
|
||||||
const datetime = item.x;
|
|
||||||
if (!timeMap.has(datetime)) {
|
|
||||||
timeMap.set(datetime, {});
|
|
||||||
}
|
|
||||||
timeMap.get(datetime)[tagName] = item.y;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortedTimes = Array.from(timeMap.keys()).sort();
|
|
||||||
const sortedTags = Array.from(tagSet).sort();
|
|
||||||
|
|
||||||
const pivotTableData = sortedTimes.map((datetime) => {
|
|
||||||
const rowData = {
|
|
||||||
datetime: datetime,
|
|
||||||
};
|
|
||||||
|
|
||||||
sortedTags.forEach((tagName) => {
|
|
||||||
rowData[tagName] = timeMap.get(datetime)[tagName];
|
|
||||||
});
|
|
||||||
|
|
||||||
return rowData;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Excel Pivot data:', pivotTableData.slice(0, 5));
|
|
||||||
console.log('Total rows for Excel:', pivotTableData.length);
|
|
||||||
|
|
||||||
const workbook = new ExcelJS.Workbook();
|
|
||||||
const ws = workbook.addWorksheet('Pivot Report');
|
|
||||||
|
|
||||||
// Buat header info (3 baris pertama)
|
|
||||||
ws.addRow(['PT. PUPUK INDONESIA UTILITAS']);
|
|
||||||
ws.addRow(['GRESIK GAS COGENERATION PLANT']);
|
|
||||||
ws.addRow([`${sectionName}`]);
|
|
||||||
ws.addRow([]); // Baris kosong sebagai pemisah
|
|
||||||
|
|
||||||
// Buat header kolom dengan tag number
|
|
||||||
const headerRow = [
|
|
||||||
'Datetime',
|
|
||||||
...sortedTags.map(tag => tagMapping[tag] || tag)
|
|
||||||
];
|
|
||||||
ws.addRow(headerRow);
|
|
||||||
|
|
||||||
// Buat data rows - PERBAIKAN: Simpan sebagai number murni
|
|
||||||
pivotTableData.forEach((rowData) => {
|
|
||||||
const row = [dayjs(rowData.datetime).format('DD-MM-YYYY HH:mm')];
|
|
||||||
sortedTags.forEach((tagName) => {
|
|
||||||
const value = rowData[tagName];
|
|
||||||
// Simpan sebagai number, bukan string
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
row.push(Number(value));
|
|
||||||
} else {
|
|
||||||
row.push('-');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ws.addRow(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set column widths
|
|
||||||
ws.getColumn(1).width = 18; // Datetime column
|
|
||||||
for (let i = 2; i <= sortedTags.length + 1; i++) {
|
|
||||||
ws.getColumn(i).width = 12; // Tag columns
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge cells untuk header info
|
|
||||||
const totalCols = sortedTags.length + 1;
|
|
||||||
ws.mergeCells(1, 1, 1, totalCols); // Baris 1
|
|
||||||
ws.mergeCells(2, 1, 2, totalCols); // Baris 2
|
|
||||||
ws.mergeCells(3, 1, 3, totalCols); // Baris 3
|
|
||||||
|
|
||||||
// Style untuk header info (3 baris pertama - bold dan center)
|
|
||||||
for (let i = 1; i <= 3; i++) {
|
|
||||||
const cell = ws.getCell(i, 1);
|
|
||||||
cell.font = { bold: true, size: 12 };
|
|
||||||
cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Style untuk header kolom (bold, background color, center, border)
|
|
||||||
const headerRowIndex = 5; // Baris header
|
|
||||||
for (let col = 1; col <= totalCols; col++) {
|
|
||||||
const cell = ws.getCell(headerRowIndex, col);
|
|
||||||
cell.font = { bold: true, size: 11 };
|
|
||||||
cell.fill = {
|
|
||||||
type: 'pattern',
|
|
||||||
pattern: 'solid',
|
|
||||||
fgColor: { argb: 'FFDCDCDC' }
|
|
||||||
};
|
|
||||||
cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
|
|
||||||
cell.border = {
|
|
||||||
top: { style: 'thin', color: { argb: 'FF000000' } },
|
|
||||||
bottom: { style: 'thin', color: { argb: 'FF000000' } },
|
|
||||||
left: { style: 'thin', color: { argb: 'FF000000' } },
|
|
||||||
right: { style: 'thin', color: { argb: 'FF000000' } }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Style untuk data cells (border dan alignment) - PERBAIKAN: Format number dengan 2 desimal
|
|
||||||
for (let row = headerRowIndex + 1; row <= ws.rowCount; row++) {
|
|
||||||
for (let col = 1; col <= totalCols; col++) {
|
|
||||||
const cell = ws.getCell(row, col);
|
|
||||||
|
|
||||||
cell.alignment = {
|
|
||||||
horizontal: 'center',
|
|
||||||
vertical: 'middle',
|
|
||||||
wrapText: true
|
|
||||||
};
|
|
||||||
cell.border = {
|
|
||||||
top: { style: 'thin', color: { argb: 'FF000000' } },
|
|
||||||
bottom: { style: 'thin', color: { argb: 'FF000000' } },
|
|
||||||
left: { style: 'thin', color: { argb: 'FF000000' } },
|
|
||||||
right: { style: 'thin', color: { argb: 'FF000000' } }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format number dengan 2 desimal untuk kolom value (kolom 2 dst)
|
|
||||||
if (col > 1) {
|
|
||||||
const cellValue = cell.value;
|
|
||||||
// Hanya set format number jika cell berisi angka
|
|
||||||
if (typeof cellValue === 'number') {
|
|
||||||
cell.numFmt = '0.00';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate file name
|
|
||||||
const fileName = `Report_Pivot_${startDate.format('DD-MM-YYYY')}_to_${endDate.format('DD-MM-YYYY')}.xlsx`;
|
|
||||||
|
|
||||||
// Save file
|
|
||||||
const buffer = await workbook.xlsx.writeBuffer();
|
|
||||||
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
||||||
saveAs(blob, fileName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportToPDF = async () => {
|
|
||||||
if (pivotData.length === 0) {
|
|
||||||
alert('No data to export');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagMapping = {};
|
|
||||||
valueReportData.forEach(item => {
|
|
||||||
if (item.tag_name && item.tag_number) {
|
|
||||||
tagMapping[item.tag_name] = item.tag_number;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedSection = plantSubSectionList.find(item => item.plant_sub_section_id === plantSubSection);
|
|
||||||
const sectionName = selectedSection ? selectedSection.plant_sub_section_name : 'Unknown';
|
|
||||||
|
|
||||||
// Buat struktur pivot yang sama seperti di tabel
|
|
||||||
const timeMap = new Map();
|
|
||||||
const tagSet = new Set();
|
|
||||||
|
|
||||||
pivotData.forEach((row) => {
|
|
||||||
const tagName = row.id;
|
|
||||||
tagSet.add(tagName);
|
|
||||||
|
|
||||||
const dataPoints = row.data || [];
|
|
||||||
dataPoints.forEach((item) => {
|
|
||||||
if (item && typeof item === 'object' && 'x' in item && 'y' in item) {
|
|
||||||
const datetime = item.x;
|
|
||||||
if (!timeMap.has(datetime)) {
|
|
||||||
timeMap.set(datetime, {});
|
|
||||||
}
|
|
||||||
timeMap.get(datetime)[tagName] = item.y;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortedTimes = Array.from(timeMap.keys()).sort();
|
|
||||||
const sortedTags = Array.from(tagSet).sort();
|
|
||||||
|
|
||||||
const pivotTableData = sortedTimes.map((datetime) => {
|
|
||||||
const rowData = {
|
|
||||||
datetime: datetime,
|
|
||||||
};
|
|
||||||
|
|
||||||
sortedTags.forEach((tagName) => {
|
|
||||||
rowData[tagName] = timeMap.get(datetime)[tagName];
|
|
||||||
});
|
|
||||||
|
|
||||||
return rowData;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('PDF Pivot data:', pivotTableData.slice(0, 5));
|
|
||||||
console.log('Total rows for PDF:', pivotTableData.length);
|
|
||||||
|
|
||||||
const loadImage = (src) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => resolve(img);
|
|
||||||
img.onerror = reject;
|
|
||||||
img.src = src;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let logo1, logo2;
|
|
||||||
try {
|
|
||||||
logo1 = await loadImage('/assets/pupuk-indonesia-2.jpg');
|
|
||||||
logo2 = await loadImage('/assets/pupuk-indonesia-1.png');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading logos:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const doc = new jsPDF({ orientation: 'landscape' });
|
|
||||||
const pageWidth = doc.internal.pageSize.width;
|
|
||||||
const pageHeight = doc.internal.pageSize.height;
|
|
||||||
const marginLeft = 10;
|
|
||||||
const marginRight = 10;
|
|
||||||
const tableWidth = pageWidth - marginLeft - marginRight;
|
|
||||||
|
|
||||||
const DATETIME_COLUMN_WIDTH = 25;
|
|
||||||
const HEADER_LEFT_COLUMN_WIDTH = 40;
|
|
||||||
const MAX_TAG_COLUMNS_PER_PAGE = 15;
|
|
||||||
|
|
||||||
const drawFullHeader = (doc) => {
|
|
||||||
doc.setLineWidth(0.5);
|
|
||||||
doc.line(marginLeft, 10, marginLeft + tableWidth, 10);
|
|
||||||
doc.line(marginLeft, 10, marginLeft, 50);
|
|
||||||
doc.line(marginLeft + tableWidth, 10, marginLeft + tableWidth, 50);
|
|
||||||
|
|
||||||
const col1Width = HEADER_LEFT_COLUMN_WIDTH;
|
|
||||||
const col3Width = tableWidth * 0.20;
|
|
||||||
const col2Width = tableWidth - col1Width - col3Width;
|
|
||||||
|
|
||||||
doc.line(marginLeft + col1Width, 10, marginLeft + col1Width, 30);
|
|
||||||
doc.line(marginLeft + tableWidth - col3Width, 10, marginLeft + tableWidth - col3Width, 30);
|
|
||||||
doc.line(marginLeft, 30, marginLeft + tableWidth, 30);
|
|
||||||
|
|
||||||
if (logo1) {
|
|
||||||
const maxLogoHeight = 18;
|
|
||||||
const maxLogoWidth = col1Width - 4;
|
|
||||||
const logoAspectRatio = logo1.width / logo1.height;
|
|
||||||
let logoWidth, logoHeight;
|
|
||||||
|
|
||||||
if (logoAspectRatio > (maxLogoWidth / maxLogoHeight)) {
|
|
||||||
logoWidth = maxLogoWidth;
|
|
||||||
logoHeight = logoWidth / logoAspectRatio;
|
|
||||||
} else {
|
|
||||||
logoHeight = maxLogoHeight;
|
|
||||||
logoWidth = logoHeight * logoAspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logoX = marginLeft + (col1Width - logoWidth) / 2;
|
|
||||||
const logoY = 10 + (20 - logoHeight) / 2;
|
|
||||||
|
|
||||||
doc.addImage(logo1, 'JPEG', logoX, logoY, logoWidth, logoHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.setFontSize(12);
|
|
||||||
doc.setFont('helvetica', 'bold');
|
|
||||||
doc.text('PT. PUPUK INDONESIA UTILITAS', marginLeft + col1Width + col2Width / 2, 17, { align: 'center' });
|
|
||||||
doc.line(marginLeft + col1Width, 21, marginLeft + tableWidth - col3Width, 21);
|
|
||||||
doc.setFontSize(11);
|
|
||||||
doc.text('GRESIK GAS COGENERATION PLANT', marginLeft + col1Width + col2Width / 2, 27, { align: 'center' });
|
|
||||||
|
|
||||||
if (logo2) {
|
|
||||||
const maxLogoHeight = 18;
|
|
||||||
const maxLogoWidth = col3Width - 4;
|
|
||||||
const logoAspectRatio = logo2.width / logo2.height;
|
|
||||||
let logoWidth, logoHeight;
|
|
||||||
|
|
||||||
if (logoAspectRatio > (maxLogoWidth / maxLogoHeight)) {
|
|
||||||
logoWidth = maxLogoWidth;
|
|
||||||
logoHeight = logoWidth / logoAspectRatio;
|
|
||||||
} else {
|
|
||||||
logoHeight = maxLogoHeight;
|
|
||||||
logoWidth = logoHeight * logoAspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logoX = marginLeft + tableWidth - col3Width + (col3Width - logoWidth) / 2;
|
|
||||||
const logoY = 10 + (20 - logoHeight) / 2;
|
|
||||||
|
|
||||||
doc.addImage(logo2, 'PNG', logoX, logoY, logoWidth, logoHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.setFontSize(9);
|
|
||||||
doc.setFont('helvetica', 'bold');
|
|
||||||
doc.setFontSize(10);
|
|
||||||
doc.text(`${sectionName}`, marginLeft + col1Width + col2Width / 2, 38, { align: 'center' });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hitung total kolom tag chunks
|
|
||||||
const totalTagColumns = sortedTags.length;
|
|
||||||
const totalTagChunks = Math.ceil(totalTagColumns / MAX_TAG_COLUMNS_PER_PAGE);
|
|
||||||
|
|
||||||
// PERBAIKAN: Variabel untuk tracking total halaman yang sebenarnya
|
|
||||||
let actualTotalPages = 0;
|
|
||||||
const pageInfoArray = []; // Array untuk menyimpan info setiap page
|
|
||||||
|
|
||||||
// Loop pertama: hitung dulu total halaman yang akan dibuat
|
|
||||||
for (let pageChunk = 0; pageChunk < totalTagChunks; pageChunk++) {
|
|
||||||
const startTagIndex = pageChunk * MAX_TAG_COLUMNS_PER_PAGE;
|
|
||||||
const endTagIndex = Math.min(startTagIndex + MAX_TAG_COLUMNS_PER_PAGE, totalTagColumns);
|
|
||||||
const pageTagColumns = sortedTags.slice(startTagIndex, endTagIndex);
|
|
||||||
const isFirstPage = (pageChunk === 0);
|
|
||||||
|
|
||||||
// Simulasi autoTable untuk menghitung jumlah halaman
|
|
||||||
const tempDoc = new jsPDF({ orientation: 'landscape' });
|
|
||||||
const headerRow = ['Datetime', ...pageTagColumns.map(tag => tagMapping[tag] || tag)];
|
|
||||||
|
|
||||||
const pdfRows = pivotTableData.map((rowData) => {
|
|
||||||
const row = [dayjs(rowData.datetime).format('DD-MM-YYYY HH:mm')];
|
|
||||||
pageTagColumns.forEach((tagName) => {
|
|
||||||
const value = rowData[tagName];
|
|
||||||
row.push(value !== undefined && value !== null ? Number(value).toFixed(2) : '-');
|
|
||||||
});
|
|
||||||
return row;
|
|
||||||
});
|
|
||||||
|
|
||||||
const availableWidthForTags = tableWidth - DATETIME_COLUMN_WIDTH;
|
|
||||||
const TAG_COLUMN_WIDTH = availableWidthForTags / pageTagColumns.length;
|
|
||||||
|
|
||||||
const tagColumnStyles = {};
|
|
||||||
for (let i = 0; i < pageTagColumns.length; i++) {
|
|
||||||
tagColumnStyles[i + 1] = {
|
|
||||||
cellWidth: TAG_COLUMN_WIDTH,
|
|
||||||
halign: 'center'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let pagesForThisChunk = 0;
|
|
||||||
|
|
||||||
autoTable(tempDoc, {
|
|
||||||
head: [headerRow],
|
|
||||||
body: pdfRows,
|
|
||||||
startY: isFirstPage ? 50 : 15,
|
|
||||||
theme: 'grid',
|
|
||||||
rowPageBreak: 'avoid',
|
|
||||||
styles: {
|
|
||||||
fontSize: 7,
|
|
||||||
cellPadding: 1.5,
|
|
||||||
minCellHeight: 8,
|
|
||||||
lineColor: [0, 0, 0],
|
|
||||||
lineWidth: 0.1,
|
|
||||||
halign: 'center',
|
|
||||||
valign: 'middle',
|
|
||||||
overflow: 'linebreak',
|
|
||||||
},
|
|
||||||
headStyles: {
|
|
||||||
fillColor: [220, 220, 220],
|
|
||||||
textColor: [0, 0, 0],
|
|
||||||
fontStyle: 'bold',
|
|
||||||
halign: 'center',
|
|
||||||
valign: 'middle',
|
|
||||||
lineColor: [0, 0, 0],
|
|
||||||
lineWidth: 0.3,
|
|
||||||
},
|
|
||||||
columnStyles: {
|
|
||||||
0: {
|
|
||||||
cellWidth: DATETIME_COLUMN_WIDTH,
|
|
||||||
fontStyle: 'bold',
|
|
||||||
halign: 'center',
|
|
||||||
valign: 'middle'
|
|
||||||
},
|
|
||||||
...tagColumnStyles
|
|
||||||
},
|
|
||||||
margin: { left: marginLeft, right: marginRight, top: 15 },
|
|
||||||
tableWidth: tableWidth,
|
|
||||||
pageBreak: 'auto',
|
|
||||||
didDrawPage: () => {
|
|
||||||
pagesForThisChunk++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
pageInfoArray.push({
|
|
||||||
chunkIndex: pageChunk,
|
|
||||||
pagesCount: pagesForThisChunk,
|
|
||||||
startPage: actualTotalPages + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
actualTotalPages += pagesForThisChunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Total pages akan dibuat:', actualTotalPages);
|
|
||||||
|
|
||||||
// Loop kedua: buat PDF yang sebenarnya dengan nomor halaman yang benar
|
|
||||||
let globalPageNumber = 1;
|
|
||||||
|
|
||||||
for (let pageChunk = 0; pageChunk < totalTagChunks; pageChunk++) {
|
|
||||||
if (pageChunk > 0) {
|
|
||||||
doc.addPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTagIndex = pageChunk * MAX_TAG_COLUMNS_PER_PAGE;
|
|
||||||
const endTagIndex = Math.min(startTagIndex + MAX_TAG_COLUMNS_PER_PAGE, totalTagColumns);
|
|
||||||
const pageTagColumns = sortedTags.slice(startTagIndex, endTagIndex);
|
|
||||||
const isFirstPage = (pageChunk === 0);
|
|
||||||
|
|
||||||
if (isFirstPage) {
|
|
||||||
drawFullHeader(doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerRow = ['Datetime', ...pageTagColumns.map(tag => tagMapping[tag] || tag)];
|
|
||||||
|
|
||||||
const pdfRows = pivotTableData.map((rowData) => {
|
|
||||||
const row = [dayjs(rowData.datetime).format('DD-MM-YYYY HH:mm')];
|
|
||||||
|
|
||||||
pageTagColumns.forEach((tagName) => {
|
|
||||||
const value = rowData[tagName];
|
|
||||||
row.push(value !== undefined && value !== null ? Number(value).toFixed(2) : '-');
|
|
||||||
});
|
|
||||||
|
|
||||||
return row;
|
|
||||||
});
|
|
||||||
|
|
||||||
const availableWidthForTags = tableWidth - DATETIME_COLUMN_WIDTH;
|
|
||||||
const TAG_COLUMN_WIDTH = availableWidthForTags / pageTagColumns.length;
|
|
||||||
|
|
||||||
const tagColumnStyles = {};
|
|
||||||
for (let i = 0; i < pageTagColumns.length; i++) {
|
|
||||||
tagColumnStyles[i + 1] = {
|
|
||||||
cellWidth: TAG_COLUMN_WIDTH,
|
|
||||||
halign: 'center'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
autoTable(doc, {
|
|
||||||
head: [headerRow],
|
|
||||||
body: pdfRows,
|
|
||||||
startY: isFirstPage ? 43 : 15,
|
|
||||||
theme: 'grid',
|
|
||||||
rowPageBreak: 'avoid',
|
|
||||||
styles: {
|
|
||||||
fontSize: 7,
|
|
||||||
cellPadding: 1.5,
|
|
||||||
minCellHeight: 8,
|
|
||||||
lineColor: [0, 0, 0],
|
|
||||||
lineWidth: 0.5,
|
|
||||||
halign: 'center',
|
|
||||||
valign: 'middle',
|
|
||||||
overflow: 'linebreak',
|
|
||||||
},
|
|
||||||
headStyles: {
|
|
||||||
fillColor: [220, 220, 220],
|
|
||||||
textColor: [0, 0, 0],
|
|
||||||
fontStyle: 'bold',
|
|
||||||
halign: 'center',
|
|
||||||
valign: 'middle',
|
|
||||||
lineColor: [0, 0, 0],
|
|
||||||
lineWidth: 0.5,
|
|
||||||
},
|
|
||||||
columnStyles: {
|
|
||||||
0: {
|
|
||||||
cellWidth: DATETIME_COLUMN_WIDTH,
|
|
||||||
fontStyle: 'bold',
|
|
||||||
halign: 'center',
|
|
||||||
valign: 'middle'
|
|
||||||
},
|
|
||||||
...tagColumnStyles
|
|
||||||
},
|
|
||||||
margin: { left: marginLeft, right: marginRight, top: 15 },
|
|
||||||
tableWidth: tableWidth,
|
|
||||||
pageBreak: 'auto',
|
|
||||||
didDrawPage: (data) => {
|
|
||||||
doc.setFontSize(8);
|
|
||||||
doc.setFont('helvetica', 'normal');
|
|
||||||
doc.text(
|
|
||||||
`Page ${globalPageNumber} of ${actualTotalPages}`,
|
|
||||||
doc.internal.pageSize.width / 2,
|
|
||||||
doc.internal.pageSize.height - 10,
|
|
||||||
{ align: 'center' }
|
|
||||||
);
|
|
||||||
globalPageNumber++;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.save(`Report_Pivot_${startDate.format('DD-MM-YYYY')}_to_${endDate.format('DD-MM-YYYY')}.pdf`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Modal
|
|
||||||
open={isLoadingModal}
|
|
||||||
footer={null}
|
|
||||||
closable={false}
|
|
||||||
centered
|
|
||||||
width={400}
|
|
||||||
bodyStyle={{
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '40px 20px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Spin
|
|
||||||
indicator={<LoadingOutlined style={{ fontSize: 48, color: '#1890ff' }} spin />}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: '24px' }}>
|
|
||||||
<Typography.Title level={4} style={{ marginBottom: '8px' }}>
|
|
||||||
Please Wait
|
|
||||||
</Typography.Title>
|
|
||||||
<Typography.Text type="secondary">
|
|
||||||
System is generating report data...
|
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
@@ -862,8 +167,14 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
value={periode}
|
value={periode}
|
||||||
onChange={setPeriode}
|
onChange={setPeriode}
|
||||||
style={{ width: '100%', marginTop: '4px' }}
|
style={{ width: '100%', marginTop: '4px' }}
|
||||||
options={periodeOptions}
|
options={[
|
||||||
/>
|
{ value: 5, label: '5 Minute' },
|
||||||
|
{ value: 10, label: '10 Minute' },
|
||||||
|
{ value: 30, label: '30 Minute' },
|
||||||
|
{ value: 60, label: '1 Hour' },
|
||||||
|
{ value: 120, label: '2 Hour' },
|
||||||
|
]}
|
||||||
|
></Select>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -874,33 +185,10 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
danger
|
danger
|
||||||
icon={<FileTextOutlined />}
|
icon={<FileTextOutlined />}
|
||||||
onClick={handleSearch}
|
onClick={handleSearch}
|
||||||
disabled={false}
|
|
||||||
>
|
>
|
||||||
Show
|
Show
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<DownloadOutlined />}
|
|
||||||
onClick={exportToPDF}
|
|
||||||
disabled={pivotData.length === 0}
|
|
||||||
style={{ backgroundColor: '#1890ff', borderColor: '#1890ff' }}
|
|
||||||
>
|
|
||||||
Export PDF
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
<Col>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<DownloadOutlined />}
|
|
||||||
onClick={exportToExcel}
|
|
||||||
disabled={pivotData.length === 0}
|
|
||||||
style={{ backgroundColor: '#28a745', borderColor: '#28a745' }}
|
|
||||||
>
|
|
||||||
Export Excel
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
<Col>
|
<Col>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
@@ -911,26 +199,18 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} style={{ marginTop: '16px' }}>
|
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||||
<Spin spinning={isLoadingTable}>
|
<TableList
|
||||||
<div style={{ overflowX: 'auto', width: '100%' }}>
|
firstLoad={false}
|
||||||
<Table
|
mobile
|
||||||
columns={columns}
|
cardColor={'#d38943ff'}
|
||||||
dataSource={tableData}
|
header={'datetime'}
|
||||||
pagination={{
|
getData={getAllHistoryValueReportPivot}
|
||||||
...pagination,
|
queryParams={formDataFilter}
|
||||||
showSizeChanger: true,
|
columns={columns}
|
||||||
showTotal: (total) => `Total ${total} data`,
|
columnDynamic={'columns'}
|
||||||
pageSizeOptions: ['10', '20', '50', '100'],
|
triger={trigerFilter}
|
||||||
}}
|
/>
|
||||||
onChange={handleTableChange}
|
|
||||||
scroll={{ x: 'max-content', y: 500 }}
|
|
||||||
bordered
|
|
||||||
size="small"
|
|
||||||
sticky
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Spin>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -938,4 +218,4 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ListReport;
|
export default ListReport;
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
import React, { memo, useState, useEffect } from 'react';
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
import { Button, Row, Col, Card, DatePicker, Select, Typography, Modal, Spin } from 'antd';
|
import { Button, Row, Col, Card, Input, DatePicker, Select, Typography } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { FileTextOutlined, LoadingOutlined } from '@ant-design/icons';
|
import { FileTextOutlined } from '@ant-design/icons';
|
||||||
import {
|
import { ResponsiveLine } from '@nivo/line';
|
||||||
LineChart,
|
|
||||||
Line,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
CartesianGrid,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
ResponsiveContainer
|
|
||||||
} from 'recharts';
|
|
||||||
import './trending.css';
|
import './trending.css';
|
||||||
import { getAllPlantSection } from '../../../api/master-plant-section';
|
import { getAllPlantSection } from '../../../api/master-plant-section';
|
||||||
import { getAllHistoryValueTrendingPivot } from '../../../api/history-value';
|
import { getAllHistoryValueTrendingPivot } from '../../../api/history-value';
|
||||||
@@ -27,7 +18,6 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
const [startDate, setStartDate] = useState(dateNow);
|
const [startDate, setStartDate] = useState(dateNow);
|
||||||
const [endDate, setEndDate] = useState(dateNow);
|
const [endDate, setEndDate] = useState(dateNow);
|
||||||
const [periode, setPeriode] = useState(60);
|
const [periode, setPeriode] = useState(60);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const defaultFilter = {
|
const defaultFilter = {
|
||||||
criteria: '',
|
criteria: '',
|
||||||
@@ -39,83 +29,51 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||||
|
|
||||||
const [trendingValue, setTrendingValue] = useState([]);
|
const [trendingValue, setTrendingValue] = useState([]);
|
||||||
const [chartData, setChartData] = useState([]);
|
|
||||||
const [metrics, setMetrics] = useState([]);
|
|
||||||
|
|
||||||
// Palet warna
|
|
||||||
const colorPalette = [
|
|
||||||
'#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
|
|
||||||
'#ec4899', '#14b8a6', '#f97316', '#6366f1', '#84cc16'
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
setIsLoading(true);
|
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
||||||
|
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
||||||
try {
|
|
||||||
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
|
||||||
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
|
||||||
|
|
||||||
const newFilter = {
|
const newFilter = {
|
||||||
criteria: '',
|
criteria: '',
|
||||||
plant_sub_section_id: plantSubSection,
|
plant_sub_section_id: plantSubSection,
|
||||||
from: formattedDateStart,
|
from: formattedDateStart,
|
||||||
to: formattedDateEnd,
|
to: formattedDateEnd,
|
||||||
interval: periode,
|
interval: periode,
|
||||||
};
|
};
|
||||||
|
|
||||||
setFormDataFilter(newFilter);
|
setFormDataFilter(newFilter);
|
||||||
|
|
||||||
const param = new URLSearchParams(newFilter);
|
const param = new URLSearchParams(newFilter);
|
||||||
const response = await getAllHistoryValueTrendingPivot(param);
|
const response = await getAllHistoryValueTrendingPivot(param);
|
||||||
|
|
||||||
if (response?.data?.length > 0) {
|
if (response?.data?.length > 0) {
|
||||||
transformDataForRecharts(response.data);
|
// 🔹 Bersihkan dan format data agar aman untuk Nivo
|
||||||
} else {
|
const cleanedData = response.data.map((serie) => ({
|
||||||
setTrendingValue([]);
|
id: serie.id ?? 'Unknown',
|
||||||
setChartData([]);
|
data: Array.isArray(serie.data)
|
||||||
setMetrics([]);
|
? serie.data.map((d) => ({
|
||||||
}
|
x: d?.x ?? null,
|
||||||
} catch (error) {
|
y:
|
||||||
console.error('Error fetching trending data:', error);
|
d?.y !== null && d?.y !== undefined
|
||||||
} finally {
|
? Number(d.y).toFixed(4) // format 4 angka di belakang koma
|
||||||
setIsLoading(false);
|
: null,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
setTrendingValue(cleanedData);
|
||||||
|
} else {
|
||||||
|
// 🔹 Jika tidak ada data dari API
|
||||||
|
setTrendingValue([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const transformDataForRecharts = (nivoData) => {
|
|
||||||
setTrendingValue(nivoData);
|
|
||||||
|
|
||||||
const metricNames = nivoData.map(serie => serie.id);
|
|
||||||
setMetrics(metricNames);
|
|
||||||
|
|
||||||
const timeMap = new Map();
|
|
||||||
|
|
||||||
nivoData.forEach(serie => {
|
|
||||||
serie.data.forEach(point => {
|
|
||||||
if (!timeMap.has(point.x)) {
|
|
||||||
timeMap.set(point.x, { time: point.x });
|
|
||||||
}
|
|
||||||
const entry = timeMap.get(point.x);
|
|
||||||
entry[serie.id] = point.y !== null && point.y !== undefined
|
|
||||||
? parseFloat(point.y)
|
|
||||||
: null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const transformedData = Array.from(timeMap.values()).sort((a, b) =>
|
|
||||||
new Date(a.time) - new Date(b.time)
|
|
||||||
);
|
|
||||||
|
|
||||||
setChartData(transformedData);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setPlantSubSection(0);
|
setPlantSubSection(0);
|
||||||
setStartDate(dateNow);
|
setStartDate(dateNow);
|
||||||
setEndDate(dateNow);
|
setEndDate(dateNow);
|
||||||
setPeriode(60);
|
setPeriode(5);
|
||||||
setChartData([]);
|
|
||||||
setMetrics([]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlantSubSection = async () => {
|
const getPlantSubSection = async () => {
|
||||||
@@ -130,171 +88,12 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fungsi untuk menentukan apakah rentang tanggal lebih dari 1 hari
|
|
||||||
const isMultipleDays = () => {
|
|
||||||
return !startDate.isSame(endDate, 'day');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format sumbu X yang otomatis menyesuaikan
|
|
||||||
const formatXAxis = (tickItem) => {
|
|
||||||
const date = new Date(tickItem);
|
|
||||||
const hours = date.getHours().toString().padStart(2, '0');
|
|
||||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
|
||||||
|
|
||||||
// Jika rentang lebih dari 1 hari, tampilkan tanggal + waktu
|
|
||||||
if (isMultipleDays()) {
|
|
||||||
const day = date.getDate().toString().padStart(2, '0');
|
|
||||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
||||||
return `${day}/${month} ${hours}:${minutes}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jika hanya 1 hari, tampilkan waktu saja
|
|
||||||
return `${hours}:${minutes}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CustomTooltip = ({ active, payload, label }) => {
|
|
||||||
if (active && payload && payload.length) {
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.98)',
|
|
||||||
padding: '12px',
|
|
||||||
border: '1px solid #ccc',
|
|
||||||
borderRadius: '8px',
|
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
|
|
||||||
}}>
|
|
||||||
<p style={{ margin: 0, fontWeight: 'bold', marginBottom: '8px' }}>
|
|
||||||
{new Date(label).toLocaleString('id-ID')}
|
|
||||||
</p>
|
|
||||||
{payload.map((entry, index) => (
|
|
||||||
<p key={index} style={{
|
|
||||||
margin: '4px 0',
|
|
||||||
color: entry.color,
|
|
||||||
fontSize: '13px'
|
|
||||||
}}>
|
|
||||||
<strong>{entry.name}:</strong> {Number(entry.value).toFixed(4)}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderChart = () => {
|
|
||||||
if (!chartData || chartData.length === 0) {
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
textAlign: 'center',
|
|
||||||
marginTop: '100px',
|
|
||||||
color: '#999',
|
|
||||||
fontSize: '16px'
|
|
||||||
}}>
|
|
||||||
Tidak ada data untuk ditampilkan
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart
|
|
||||||
data={chartData}
|
|
||||||
margin={{ top: 20, right: 200, left: 80, bottom: 40 }}
|
|
||||||
>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#e0e0e0" />
|
|
||||||
<XAxis
|
|
||||||
dataKey="time"
|
|
||||||
angle={-45}
|
|
||||||
textAnchor="end"
|
|
||||||
height={100}
|
|
||||||
tick={{ fontSize: 11 }}
|
|
||||||
tickFormatter={formatXAxis}
|
|
||||||
label={{
|
|
||||||
value: 'Waktu',
|
|
||||||
position: 'bottom',
|
|
||||||
offset: -50,
|
|
||||||
style: { fontSize: 14, fontWeight: 'bold' }
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
tick={{ fontSize: 11 }}
|
|
||||||
label={{
|
|
||||||
value: 'Nilai',
|
|
||||||
angle: -90,
|
|
||||||
position: 'right',
|
|
||||||
offset: -70,
|
|
||||||
dy: 0,
|
|
||||||
style: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fill: '#059669',
|
|
||||||
textAnchor: 'middle'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
tickFormatter={(value) => Number(value).toFixed(2)}
|
|
||||||
/>
|
|
||||||
<Tooltip content={<CustomTooltip />} />
|
|
||||||
<Legend
|
|
||||||
layout="vertical"
|
|
||||||
align="right"
|
|
||||||
verticalAlign="middle"
|
|
||||||
wrapperStyle={{
|
|
||||||
position: 'absolute',
|
|
||||||
right: 150,
|
|
||||||
top: '35%',
|
|
||||||
transform: 'translateY(-50%)'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{metrics.map((metric, index) => {
|
|
||||||
const color = colorPalette[index % colorPalette.length];
|
|
||||||
return (
|
|
||||||
<Line
|
|
||||||
key={metric}
|
|
||||||
type="monotone"
|
|
||||||
dataKey={metric}
|
|
||||||
stroke={color}
|
|
||||||
strokeWidth={2}
|
|
||||||
dot={chartData.length < 50}
|
|
||||||
name={metric}
|
|
||||||
connectNulls={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getPlantSubSection();
|
getPlantSubSection();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{/* Loading Modal */}
|
|
||||||
<Modal
|
|
||||||
open={isLoading}
|
|
||||||
footer={null}
|
|
||||||
closable={false}
|
|
||||||
centered
|
|
||||||
width={400}
|
|
||||||
bodyStyle={{
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '40px 20px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Spin
|
|
||||||
indicator={<LoadingOutlined style={{ fontSize: 48, color: '#1890ff' }} spin />}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: '24px' }}>
|
|
||||||
<Typography.Title level={4} style={{ marginBottom: '8px' }}>
|
|
||||||
Please Wait
|
|
||||||
</Typography.Title>
|
|
||||||
<Typography.Text type="secondary">
|
|
||||||
System is generating trending data...
|
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
@@ -363,11 +162,10 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
{ value: 60, label: '1 Hour' },
|
{ value: 60, label: '1 Hour' },
|
||||||
{ value: 120, label: '2 Hour' },
|
{ value: 120, label: '2 Hour' },
|
||||||
]}
|
]}
|
||||||
/>
|
></Select>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row gutter={8} style={{ marginTop: '16px' }}>
|
<Row gutter={8} style={{ marginTop: '16px' }}>
|
||||||
<Col>
|
<Col>
|
||||||
<Button
|
<Button
|
||||||
@@ -389,9 +187,108 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '24px' }}>
|
<div style={{ height: '500px', marginTop: '16px' }}>
|
||||||
{renderChart()}
|
{trendingValue && trendingValue.length > 0 ? (
|
||||||
|
<ResponsiveLine
|
||||||
|
data={trendingValue} // [{ id, data: [{x, y}] }]
|
||||||
|
// data={
|
||||||
|
// trendingValue && trendingValue.length
|
||||||
|
// ? trendingValue
|
||||||
|
// : [{ id, data: [{ x, y }] }]
|
||||||
|
// }
|
||||||
|
margin={{ top: 40, right: 100, bottom: 70, left: 70 }}
|
||||||
|
xScale={{
|
||||||
|
type: 'time',
|
||||||
|
format: '%Y-%m-%d %H:%M',
|
||||||
|
useUTC: false,
|
||||||
|
precision: 'minute',
|
||||||
|
}}
|
||||||
|
xFormat="time:%Y-%m-%d %H:%M"
|
||||||
|
yScale={{
|
||||||
|
type: 'linear',
|
||||||
|
min: 'auto',
|
||||||
|
max: 'auto',
|
||||||
|
stacked: false,
|
||||||
|
reverse: false,
|
||||||
|
}}
|
||||||
|
yFormat={(value) => Number(value).toFixed(4)} // ✅ format 4 angka di belakang koma
|
||||||
|
axisBottom={{
|
||||||
|
format: '%Y-%m-%d %H:%M', // ✅ tampilkan tanggal + jam
|
||||||
|
tickValues: 'every 2 hours', // tampilkan setiap 2 jam (bisa ubah ke every 30 minutes)
|
||||||
|
tickSize: 5,
|
||||||
|
tickPadding: 5,
|
||||||
|
tickRotation: -45,
|
||||||
|
legend: 'Tanggal & Waktu',
|
||||||
|
legendOffset: 60,
|
||||||
|
legendPosition: 'middle',
|
||||||
|
}}
|
||||||
|
axisLeft={{
|
||||||
|
tickSize: 5,
|
||||||
|
tickPadding: 5,
|
||||||
|
tickRotation: 0,
|
||||||
|
legend: 'Nilai (Avg)',
|
||||||
|
legendOffset: -60,
|
||||||
|
legendPosition: 'middle',
|
||||||
|
format: (value) => Number(value).toFixed(4), // ✅ tampilkan 4 angka di sumbu Y
|
||||||
|
}}
|
||||||
|
curve="monotoneX"
|
||||||
|
colors={{ scheme: 'category10' }}
|
||||||
|
pointSize={6}
|
||||||
|
pointColor={{ theme: 'background' }}
|
||||||
|
pointBorderWidth={2}
|
||||||
|
pointBorderColor={{ from: 'serieColor' }}
|
||||||
|
enablePointLabel={false}
|
||||||
|
enableGridX={true}
|
||||||
|
enableGridY={true}
|
||||||
|
useMesh={true}
|
||||||
|
tooltip={({ point }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'white',
|
||||||
|
padding: '6px 9px',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '6px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong>{point.serieId}</strong>
|
||||||
|
<br />
|
||||||
|
{point.data.xFormatted}
|
||||||
|
<br />
|
||||||
|
<span style={{ color: point.serieColor }}>
|
||||||
|
{Number(point.data.y).toFixed(4)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
legends={[
|
||||||
|
{
|
||||||
|
anchor: 'bottom-right',
|
||||||
|
direction: 'column',
|
||||||
|
justify: false,
|
||||||
|
translateX: 100,
|
||||||
|
translateY: 0,
|
||||||
|
itemsSpacing: 2,
|
||||||
|
itemDirection: 'left-to-right',
|
||||||
|
itemWidth: 120,
|
||||||
|
itemHeight: 20,
|
||||||
|
itemOpacity: 0.85,
|
||||||
|
symbolSize: 12,
|
||||||
|
symbolShape: 'circle',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: '40px',
|
||||||
|
color: '#999',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tidak ada data untuk ditampilkan
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -399,4 +296,4 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ReportTrending;
|
export default ReportTrending;
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const ChangePasswordModal = (props) => {
|
|||||||
try {
|
try {
|
||||||
const response = await changePassword(props.selectedUser.user_id, formData.newPassword);
|
const response = await changePassword(props.selectedUser.user_id, formData.newPassword);
|
||||||
|
|
||||||
// console.log('Change Password Response:', response);
|
console.log('Change Password Response:', response);
|
||||||
|
|
||||||
if (response && response.statusCode === 200) {
|
if (response && response.statusCode === 200) {
|
||||||
NotifOk({
|
NotifOk({
|
||||||
|
|||||||
@@ -220,27 +220,35 @@ const DetailUser = (props) => {
|
|||||||
|
|
||||||
// For update mode: only send email if it has changed
|
// For update mode: only send email if it has changed
|
||||||
if (FormData.user_id) {
|
if (FormData.user_id) {
|
||||||
|
// Only include email if it has changed from original
|
||||||
if (FormData.user_email !== originalEmail) {
|
if (FormData.user_email !== originalEmail) {
|
||||||
payload.user_email = FormData.user_email;
|
payload.user_email = FormData.user_email;
|
||||||
}
|
}
|
||||||
|
// Add is_active for update mode
|
||||||
payload.is_active = FormData.is_active;
|
payload.is_active = FormData.is_active;
|
||||||
} else {
|
} else {
|
||||||
|
// For create mode: always send email
|
||||||
payload.user_email = FormData.user_email;
|
payload.user_email = FormData.user_email;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only add role_id if it exists (backend requires number >= 1, no null)
|
||||||
if (FormData.role_id) {
|
if (FormData.role_id) {
|
||||||
payload.role_id = FormData.role_id;
|
payload.role_id = FormData.role_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add password and name for new user (create mode)
|
// Add password and name for new user (create mode)
|
||||||
if (!FormData.user_id) {
|
if (!FormData.user_id) {
|
||||||
payload.user_name = FormData.user_name;
|
payload.user_name = FormData.user_name; // Username only for create
|
||||||
payload.user_password = FormData.password;
|
payload.user_password = FormData.password; // Backend expects 'user_password'
|
||||||
|
// Don't send confirmPassword, is_sa for create
|
||||||
}
|
}
|
||||||
|
// For update mode:
|
||||||
|
// - Don't send 'user_name' (username is immutable)
|
||||||
|
// - is_active is now sent for update mode
|
||||||
|
// - Only send email if it has changed
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// console.log('Payload being sent:', payload);
|
console.log('Payload being sent:', payload);
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
if (!FormData.user_id) {
|
if (!FormData.user_id) {
|
||||||
@@ -249,10 +257,11 @@ const DetailUser = (props) => {
|
|||||||
response = await updateUser(FormData.user_id, payload);
|
response = await updateUser(FormData.user_id, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('Save User Response:', response);
|
console.log('Save User Response:', response);
|
||||||
|
|
||||||
// Check if response is successful
|
// Check if response is successful
|
||||||
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
||||||
|
// If in edit mode and newPassword is provided, change password
|
||||||
if (FormData.user_id && FormData.newPassword) {
|
if (FormData.user_id && FormData.newPassword) {
|
||||||
try {
|
try {
|
||||||
const passwordResponse = await changePassword(
|
const passwordResponse = await changePassword(
|
||||||
@@ -376,9 +385,9 @@ const DetailUser = (props) => {
|
|||||||
search: '',
|
search: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log('Fetching roles with params:', queryParams.toString());
|
console.log('Fetching roles with params:', queryParams.toString());
|
||||||
const response = await getAllRole(queryParams);
|
const response = await getAllRole(queryParams);
|
||||||
// console.log('Fetched roles response:', response);
|
console.log('Fetched roles response:', response);
|
||||||
|
|
||||||
// Handle different response structures
|
// Handle different response structures
|
||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
@@ -399,7 +408,7 @@ const DetailUser = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setRoleList(roles);
|
setRoleList(roles);
|
||||||
// console.log('Setting role list:', roles);
|
console.log('Setting role list:', roles);
|
||||||
} else {
|
} else {
|
||||||
// Add mock data as fallback
|
// Add mock data as fallback
|
||||||
console.warn('No response data, using mock data');
|
console.warn('No response data, using mock data');
|
||||||
@@ -409,7 +418,7 @@ const DetailUser = (props) => {
|
|||||||
{ role_id: 3, role_name: 'User', role_level: 3 },
|
{ role_id: 3, role_name: 'User', role_level: 3 },
|
||||||
];
|
];
|
||||||
setRoleList(mockRoles);
|
setRoleList(mockRoles);
|
||||||
// console.log('Setting mock role list:', mockRoles);
|
console.log('Setting mock role list:', mockRoles);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching roles:', error);
|
console.error('Error fetching roles:', error);
|
||||||
@@ -420,7 +429,7 @@ const DetailUser = (props) => {
|
|||||||
{ role_id: 3, role_name: 'User', role_level: 3 },
|
{ role_id: 3, role_name: 'User', role_level: 3 },
|
||||||
];
|
];
|
||||||
setRoleList(mockRoles);
|
setRoleList(mockRoles);
|
||||||
// console.log('Setting mock role list due to error:', mockRoles);
|
console.log('Setting mock role list due to error:', mockRoles);
|
||||||
|
|
||||||
// Only show error notification if we don't have fallback data
|
// Only show error notification if we don't have fallback data
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
@@ -1137,7 +1146,9 @@ const DetailUser = (props) => {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
{errors.role_id && (
|
{errors.role_id && (
|
||||||
<Text style={{ color: 'red', fontSize: '12px' }}>{errors.role_id}</Text>
|
<Text style={{ color: 'red', fontSize: '12px' }}>
|
||||||
|
{errors.role_id}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog, showApproval
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Action',
|
title: 'Aksi',
|
||||||
key: 'aksi',
|
key: 'aksi',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: '12%',
|
width: '12%',
|
||||||
|
|||||||