Compare commits
96 Commits
96d6367dbd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cc6a52ccbf | |||
|
|
c2163cec5e | ||
|
|
d5866ceae4 | ||
| 03d5646565 | |||
| 6fdb259246 | |||
| 0aad43c751 | |||
| d988d47e30 | |||
|
|
e08eaaa43e | ||
|
|
f6ca54f5b4 | ||
|
|
a9b8053bd8 | ||
| 4d8af01316 | |||
| 600c101c68 | |||
| 8f32f29c03 | |||
|
|
14a6884f43 | ||
|
|
8e151ffe0b | ||
| 8f64843613 | |||
|
|
fe8f6d1002 | ||
|
|
5281e288a9 | ||
| 5a7d64a05b | |||
|
|
4ed05cc640 | ||
| e74b802a60 | |||
|
|
14e97fead2 | ||
|
|
0935d7c9f5 | ||
|
|
3266641f81 | ||
|
|
739c55c0bc | ||
|
|
5b4485d20d | ||
| f436865b7f | |||
| 98057beb0f | |||
|
|
b342289888 | ||
| d03bbf2a41 | |||
|
|
ec094b8f55 | ||
| 48437c3c50 | |||
| b6d941ba2d | |||
| 167abcaa43 | |||
| beb8ccbaee | |||
| 797f6c2383 | |||
| a33c9b3b92 | |||
| 016c77a586 | |||
| 36ebab7f9a | |||
| a5b1fbef74 | |||
|
|
cb0c53daea | ||
| 978e020305 | |||
| 4508738958 | |||
| 58d1f5c7ab | |||
| eb23612444 | |||
| c10a5bf62d | |||
| bee196e299 | |||
| d19f555c7c | |||
| 1d7253f9a1 | |||
| d8a1878ab1 | |||
| e4af2d6e18 | |||
| 8cf21643ea | |||
|
|
6b75f6f4b9 | ||
|
|
dc78add71d | ||
| 1ce922ff4c | |||
| 3a4b0f0748 | |||
| 4bffbb3798 | |||
| b9cdfcb1e9 | |||
| 49ba00d886 | |||
| cf1ccb0fd0 | |||
| fb790e5e37 | |||
| ea3adf40cc | |||
| 2ff50342e8 | |||
| 1f8ee62721 | |||
| 6cc5042956 | |||
| 5079f8d316 | |||
| 7073390de7 | |||
| 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,7 +22,8 @@
|
|||||||
"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.1",
|
"jspdf": "^3.0.4",
|
||||||
|
"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",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"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": {
|
||||||
|
|||||||
BIN
public/assets/pupuk-indonesia-1.png
Normal file
|
After Width: | Height: | Size: 309 KiB |
BIN
public/assets/pupuk-indonesia-2.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
@@ -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" />
|
||||||
|
|||||||
11
src/App.jsx
@@ -21,7 +21,6 @@ import IndexShift from './pages/master/shift/IndexShift';
|
|||||||
// Brand device
|
// Brand device
|
||||||
import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice';
|
import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice';
|
||||||
import EditBrandDevice from './pages/master/brandDevice/EditBrandDevice';
|
import EditBrandDevice from './pages/master/brandDevice/EditBrandDevice';
|
||||||
import AddEditErrorCode from './pages/master/brandDevice/AddEditErrorCode';
|
|
||||||
import ViewBrandDevice from './pages/master/brandDevice/ViewBrandDevice';
|
import ViewBrandDevice from './pages/master/brandDevice/ViewBrandDevice';
|
||||||
import ViewFilePage from './pages/master/brandDevice/ViewFilePage';
|
import ViewFilePage from './pages/master/brandDevice/ViewFilePage';
|
||||||
|
|
||||||
@@ -37,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/detailNotification/IndexDetailNotification';
|
import DetailNotificationTab from './pages/notificationDetail/IndexNotificationDetail';
|
||||||
import IndexVerificationSparepart from './pages/verificationSparepart/IndexVerificationSparepart';
|
import IndexVerificationSparepart from './pages/verificationSparepart/IndexVerificationSparepart';
|
||||||
|
|
||||||
import SvgTest from './pages/home/SvgTest';
|
import SvgTest from './pages/home/SvgTest';
|
||||||
@@ -54,6 +53,7 @@ import IndexHistoryEvent from './pages/history/event/IndexHistoryEvent';
|
|||||||
|
|
||||||
// Image Viewer
|
// Image Viewer
|
||||||
import ImageViewer from './Utils/ImageViewer';
|
import ImageViewer from './Utils/ImageViewer';
|
||||||
|
import RedirectWa from './pages/blank/RedirectWa';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
@@ -65,7 +65,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="/detail-notification/:notificationId"
|
path="/notification-detail/:notificationId"
|
||||||
element={<DetailNotificationTab />}
|
element={<DetailNotificationTab />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
@@ -73,6 +73,8 @@ 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 />} />
|
||||||
@@ -118,9 +120,6 @@ const App = () => {
|
|||||||
path="brand-device/view/temp/files/:fileName"
|
path="brand-device/view/temp/files/:fileName"
|
||||||
element={<ViewFilePage />}
|
element={<ViewFilePage />}
|
||||||
/>
|
/>
|
||||||
<Route path="brand-device/:brandId/error-code/add" element={<AddEditErrorCode />} />
|
|
||||||
<Route path="brand-device/:brandId/error-code/edit/:errorCodeId" element={<AddEditErrorCode />} />
|
|
||||||
<Route path="brand-device/add/error-code/edit/:errorCodeId" element={<AddEditErrorCode />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/report" element={<ProtectedRoute />}>
|
<Route path="/report" element={<ProtectedRoute />}>
|
||||||
|
|||||||
@@ -27,4 +27,79 @@ const getNotificationDetail = async (id) => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { getAllNotification, getNotificationById, getNotificationDetail };
|
// 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,27 +26,29 @@
|
|||||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 490.992px 230.229px;" d="M 646.097 240.002 L 676.271 240.002"/>
|
<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 transform="matrix(0.826913, 0, 0, 0.698383, 0.443817, 3.138935)">
|
<g>
|
||||||
<rect x="752" y="355.455" width="42.438" height="3.527" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
|
<rect x="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="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);"/>
|
<g>
|
||||||
<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.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);"/>
|
||||||
<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"/>
|
<rect x="625.711" y="248.983" width="28.143" height="2.361" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);"/>
|
||||||
|
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);" cx="640.016" cy="271.807" rx="9.717" ry="7.689"/>
|
||||||
|
</g>
|
||||||
</g>
|
</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)">°F</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1;" x="698.476" y="373.93" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">°C</text>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 12px; stroke-width: 1; font-weight: bold;" x="748" y="347.676" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HEATER</text>
|
<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"/>
|
||||||
@@ -108,12 +110,12 @@
|
|||||||
<rect x="427.269" y="377.282" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
<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;">
|
||||||
@@ -177,17 +179,17 @@
|
|||||||
<rect x="43.443" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
<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>
|
||||||
@@ -205,15 +207,15 @@
|
|||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HTD</text>
|
<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);"/>
|
||||||
@@ -227,12 +229,33 @@
|
|||||||
<rect x="870.356" y="344.9" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
<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="334.165" cy="232.104" rx="13.582" ry="12.517"/>
|
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="336.418" cy="237.483" rx="13.582" ry="12.517"/>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="379.214" y="423.395" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="379.214" y="423.395" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
||||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.623" cy="320.662" rx="13.582" ry="12.517"/>
|
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.418" cy="321.016" rx="13.582" ry="12.517"/>
|
||||||
|
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.623" cy="320.662" rx="13.582" ry="12.517" id="c_4021"/>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="897.237" y="299.014" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="897.237" y="299.014" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
|
||||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.502" cy="233.796" rx="13.582" ry="12.517"/>
|
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.772" cy="233.876" rx="13.582" ry="12.517"/>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="896" y="425.818" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
<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: 40 KiB After Width: | Height: | Size: 41 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="439.023" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" x="12.119"/>
|
<rect y="10.407" width="972.648" height="440.159" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" x="12.119"/>
|
||||||
<g transform="matrix(0.826913, 0, 0, 0.698383, 74.03907, 53.375034)">
|
<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,27 +26,26 @@
|
|||||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 490.992px 230.229px;" d="M 646.097 240.002 L 676.271 240.002"/>
|
<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 transform="matrix(0.826913, 0, 0, 0.698383, 0.443817, 3.138935)">
|
<g>
|
||||||
<rect x="752" y="355.455" width="42.438" height="3.527" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
|
<rect x="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="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);"/>
|
<g>
|
||||||
<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.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);"/>
|
||||||
<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"/>
|
<rect x="625.711" y="248.983" width="28.143" height="2.361" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);"/>
|
||||||
|
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);" cx="640.016" cy="271.807" rx="9.717" ry="7.689"/>
|
||||||
|
</g>
|
||||||
</g>
|
</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"/>
|
||||||
@@ -108,12 +107,10 @@
|
|||||||
<rect x="427.269" y="377.282" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
<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;">
|
||||||
@@ -177,17 +174,14 @@
|
|||||||
<rect x="43.443" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
<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>
|
||||||
@@ -205,15 +199,12 @@
|
|||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HTD</text>
|
<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);"/>
|
||||||
@@ -227,12 +218,33 @@
|
|||||||
<rect x="870.356" y="344.9" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
<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="334.165" cy="232.104" rx="13.582" ry="12.517"/>
|
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="336.418" cy="237.483" rx="13.582" ry="12.517"/>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="379.214" y="423.395" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="379.214" y="423.395" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
||||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.623" cy="320.662" rx="13.582" ry="12.517"/>
|
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.418" cy="321.016" rx="13.582" ry="12.517"/>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="897.237" y="299.014" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="897.237" y="299.014" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
|
||||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.502" cy="233.796" rx="13.582" ry="12.517"/>
|
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.772" cy="233.876" rx="13.582" ry="12.517"/>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="896" y="425.818" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
<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: 40 KiB After Width: | Height: | Size: 41 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="439.023" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" x="12.119"/>
|
<rect y="10.407" width="972.648" height="440.159" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" x="12.119"/>
|
||||||
<g transform="matrix(0.826913, 0, 0, 0.698383, 74.03907, 53.375034)">
|
<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,27 +26,26 @@
|
|||||||
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 1.222; transform-origin: 490.992px 230.229px;" d="M 646.097 240.002 L 676.271 240.002"/>
|
<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 transform="matrix(0.826913, 0, 0, 0.698383, 0.443817, 3.138935)">
|
<g>
|
||||||
<rect x="752" y="355.455" width="42.438" height="3.527" style="stroke: rgb(0, 0, 0); stroke-width: 1; fill: rgb(243, 243, 243);"/>
|
<rect x="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="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);"/>
|
<g>
|
||||||
<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.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);"/>
|
||||||
<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"/>
|
<rect x="625.711" y="248.983" width="28.143" height="2.361" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);"/>
|
||||||
|
<ellipse style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(243, 243, 243);" cx="640.016" cy="271.807" rx="9.717" ry="7.689"/>
|
||||||
|
</g>
|
||||||
</g>
|
</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"/>
|
||||||
@@ -108,12 +107,10 @@
|
|||||||
<rect x="427.269" y="377.282" width="62.018" height="9.968" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
<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;">
|
||||||
@@ -177,17 +174,14 @@
|
|||||||
<rect x="43.443" y="280.75" width="82.691" height="27.103" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.763;"/>
|
<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>
|
||||||
@@ -205,15 +199,12 @@
|
|||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 15px; stroke-width: 1;" x="183.349" y="288.807" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">HTD</text>
|
<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);"/>
|
||||||
@@ -227,12 +218,34 @@
|
|||||||
<rect x="870.356" y="344.9" width="82.691" height="42.35" style="stroke: rgb(0, 0, 0); stroke-width: 0.763; fill: rgb(244, 248, 248);"/>
|
<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="334.165" cy="232.104" rx="13.582" ry="12.517"/>
|
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="336.418" cy="237.483" rx="13.582" ry="12.517"/>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="379.214" y="423.395" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="379.214" y="423.395" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
||||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.623" cy="320.662" rx="13.582" ry="12.517"/>
|
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="335.418" cy="321.016" rx="13.582" ry="12.517"/>
|
||||||
|
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="897.237" y="299.014" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
|
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="897.237" y="299.014" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">REGEN</text>
|
||||||
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.502" cy="233.796" rx="13.582" ry="12.517"/>
|
<ellipse style="fill: rgb(255, 204, 63); stroke: rgb(0, 0, 0); stroke-width: 0.763;" cx="761.772" cy="233.876" rx="13.582" ry="12.517"/>
|
||||||
<text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 14px; stroke-width: 1; font-weight: bold;" x="896" y="425.818" transform="matrix(0.826913, 0, 0, 0.698383, 2.097643, 3.138935)">DRYING</text>
|
<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: 40 KiB After Width: | Height: | Size: 41 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,24 +70,35 @@ async function ApiRequest({ method = 'GET', params = {}, prefix = '/', token = t
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const rawToken = localStorage.getItem('token');
|
const tokenRedirect = sessionStorage.getItem('token_redirect');
|
||||||
|
|
||||||
|
let rawToken = '';
|
||||||
|
|
||||||
|
if (tokenRedirect !== null) {
|
||||||
|
rawToken = tokenRedirect;
|
||||||
|
// console.log(`sessionStorage: ${tokenRedirect}`);
|
||||||
|
} else {
|
||||||
|
rawToken = localStorage.getItem('token');
|
||||||
|
// console.log(`localStorage: ${rawToken}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (token && rawToken) {
|
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;
|
||||||
@@ -132,17 +143,10 @@ 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,7 +2,16 @@
|
|||||||
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 = ['cod/air_dryer/air_dryer1'];
|
const topics = [
|
||||||
|
'PIU_COD/AIR_DRYER/OVERVIEW',
|
||||||
|
'PIU_COD/AIR_DRYER/AIR_DRYER_A',
|
||||||
|
'PIU_COD/AIR_DRYER/AIR_DRYER_B',
|
||||||
|
'PIU_COD/AIR_DRYER/AIR_DRYER_C',
|
||||||
|
'PIU_COD/COMPRESSOR/OVERVIEW',
|
||||||
|
'PIU_COD/COMPRESSOR/COMPRESSOR_A',
|
||||||
|
'PIU_COD/COMPRESSOR/COMPRESSOR_B',
|
||||||
|
'PIU_COD/COMPRESSOR/COMPRESSOR_C'
|
||||||
|
];
|
||||||
const options = {
|
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),
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
49
src/pages/blank/RedirectWa.jsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { verifyRedirect } from '../../api/auth';
|
||||||
|
import { encryptData } from '../../components/Global/Formatter';
|
||||||
|
import NotFound from './NotFound';
|
||||||
|
import Waiting from './Waiting';
|
||||||
|
import NotificationDetailTab from '../notificationDetail/IndexNotificationDetail';
|
||||||
|
|
||||||
|
export default function RedirectWa() {
|
||||||
|
const [idData, setIdData] = useState(0);
|
||||||
|
const [ready, setReady] = useState(0);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// URLSearchParams untuk ambil query
|
||||||
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
const token = queryParams.get('token');
|
||||||
|
|
||||||
|
const handleInitForm = async (encodedToken) => {
|
||||||
|
const originalToken = decodeURIComponent(encodedToken);
|
||||||
|
// console.log(originalToken);
|
||||||
|
|
||||||
|
const response = await verifyRedirect({
|
||||||
|
tokenRedirect: originalToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('tes', response);
|
||||||
|
|
||||||
|
const tokenResult = JSON.stringify(response.data?.data?.accessToken);
|
||||||
|
|
||||||
|
sessionStorage.setItem('token_redirect', tokenResult);
|
||||||
|
response.data.auth = true;
|
||||||
|
sessionStorage.setItem('session', encryptData(response?.data));
|
||||||
|
|
||||||
|
setIdData(response.data.data.idData);
|
||||||
|
|
||||||
|
setReady(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleInitForm(token);
|
||||||
|
}, [idData]);
|
||||||
|
|
||||||
|
if (ready == 0) return <Waiting />;
|
||||||
|
|
||||||
|
if (idData === 0) return <NotFound />;
|
||||||
|
|
||||||
|
return <NotificationDetailTab id={idData} />;
|
||||||
|
}
|
||||||
@@ -267,9 +267,6 @@ 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) {
|
||||||
@@ -309,11 +306,10 @@ 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(); // Refetch all contacts when data is saved
|
fetchContacts();
|
||||||
}
|
}
|
||||||
}, [props.lastSavedContact]);
|
}, [props.lastSavedContact]);
|
||||||
|
|
||||||
// Get contacts (already filtered by backend)
|
|
||||||
const getFilteredContacts = () => {
|
const getFilteredContacts = () => {
|
||||||
return filteredContacts;
|
return filteredContacts;
|
||||||
};
|
};
|
||||||
@@ -326,7 +322,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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,411 +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,
|
|
||||||
} from 'antd';
|
|
||||||
import {
|
|
||||||
ArrowLeftOutlined,
|
|
||||||
CloseCircleFilled,
|
|
||||||
WarningFilled,
|
|
||||||
CheckCircleFilled,
|
|
||||||
InfoCircleFilled,
|
|
||||||
CloseOutlined,
|
|
||||||
BookOutlined,
|
|
||||||
ToolOutlined,
|
|
||||||
HistoryOutlined,
|
|
||||||
FilePdfOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { getNotificationDetail } from '../../api/notification';
|
|
||||||
import UserHistoryModal from '../notification/component/UserHistoryModal';
|
|
||||||
import LogHistoryCard from '../notification/component/LogHistoryCard';
|
|
||||||
|
|
||||||
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;
|
|
||||||
const solutionData = 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 Device',
|
|
||||||
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: solutionData?.solution_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,
|
|
||||||
spareparts: errorCodeData?.spareparts || [],
|
|
||||||
solution: solutionData, // Include the solution data
|
|
||||||
error_code: errorCodeData,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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', 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);
|
|
||||||
|
|
||||||
// Fetch using the actual API
|
|
||||||
const response = await getNotificationDetail(notificationId);
|
|
||||||
|
|
||||||
if (response && response.data) {
|
|
||||||
const transformedData = transformNotificationData(response.data);
|
|
||||||
setNotification(transformedData);
|
|
||||||
} 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' }}>
|
|
||||||
<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={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' }}>{notification.issue}</Text></div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div>
|
|
||||||
<Text strong>Plant Subsection</Text>
|
|
||||||
<div>{notification.location}</div>
|
|
||||||
<Text strong style={{ display: 'block', marginTop: '8px' }}>Time</Text>
|
|
||||||
<div>{notification.timestamp.split(' ')[1]} WIB</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 Tengah: Informasi Teknis */}
|
|
||||||
<Col xs={24} lg={8}>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Kolom Kanan: Log History */}
|
|
||||||
<Col xs={24} lg={8}>
|
|
||||||
<LogHistoryCard notificationData={notification} />
|
|
||||||
</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} 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%' }}>
|
|
||||||
{notification.solution && (
|
|
||||||
<>
|
|
||||||
{notification.solution.path_document ? (
|
|
||||||
<Card size="small" bodyStyle={{ padding: '8px 12px' }} 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' }} /> {notification.solution.file_upload_name || 'Solution Document.pdf'}</Text>
|
|
||||||
<Link href={notification.solution.path_document} target="_blank" style={{ fontSize: '12px', display: 'block' }}>lihat disini</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
) : null}
|
|
||||||
{notification.solution.type_solution === 'text' && notification.solution.text_solution ? (
|
|
||||||
<Card size="small" bodyStyle={{ padding: '8px 12px' }} extra={<Text type="secondary" style={{ fontSize: '10px' }}>{notification.solution.type_solution.toUpperCase()}</Text>}>
|
|
||||||
<Paragraph style={{ fontSize: '12px', margin: 0 }}>
|
|
||||||
<Text strong>{notification.issue}:</Text> {notification.solution.text_solution}
|
|
||||||
</Paragraph>
|
|
||||||
</Card>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!notification.solution && (
|
|
||||||
<div style={{ textAlign: 'center', padding: '20px', color: '#8c8c8c' }}>
|
|
||||||
Tidak ada dokumen solusi tersedia
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</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%' }}>
|
|
||||||
{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>
|
|
||||||
</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}
|
|
||||||
/>
|
|
||||||
</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_GGCP/Devices/PB';
|
const topicMqtt = 'PIU_COD/AIR_DRYER/AIR_DRYER_A';
|
||||||
|
|
||||||
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_GGCP/Devices/PB';
|
const topicMqtt = 'PIU_COD/AIR_DRYER/AIR_DRYER_B';
|
||||||
|
|
||||||
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_GGCP/Devices/PB';
|
const topicMqtt = 'PIU_COD/AIR_DRYER/AIR_DRYER_C';
|
||||||
|
|
||||||
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_GGCP/Devices/PB';
|
const topicMqtt = 'PIU_COD/COMPRESSOR/COMPRESSOR_A';
|
||||||
|
|
||||||
const SvgCompressorA = () => {
|
const SvgCompressorA = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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 = 'cod/air_dryer/air_dryer1';
|
const topicMqtt = 'PIU_COD/COMPRESSOR/COMPRESSOR_B';
|
||||||
|
|
||||||
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_GGCP/Devices/PB';
|
const topicMqtt = 'PIU_COD/COMPRESSOR/COMPRESSOR_C';
|
||||||
|
|
||||||
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_GGCP/Devices/PB';
|
const topicMqtt = 'PIU_COD/AIR_DRYER/OVERVIEW';
|
||||||
|
|
||||||
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_GGCP/Devices/PB';
|
const topicMqtt = 'PIU_COD/COMPRESSOR/OVERVIEW';
|
||||||
|
|
||||||
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 ? (
|
||||||
|
|||||||
@@ -1,467 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
Spin,
|
|
||||||
Upload,
|
|
||||||
} from 'antd';
|
|
||||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
|
||||||
import { getErrorCodeById, createErrorCode, updateErrorCode } from '../../../api/master-brand';
|
|
||||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
|
||||||
import ErrorCodeForm from './component/ErrorCodeForm';
|
|
||||||
import SolutionForm from './component/SolutionForm';
|
|
||||||
import { useSolutionLogic } from './hooks/solution';
|
|
||||||
import SparepartSelect from './component/SparepartSelect';
|
|
||||||
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
|
|
||||||
|
|
||||||
const { Title } = Typography;
|
|
||||||
|
|
||||||
const AddEditErrorCode = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { brandId: routeBrandId, errorCodeId } = useParams();
|
|
||||||
const { setBreadcrumbItems } = useBreadcrumb();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const currentBrandId = routeBrandId;
|
|
||||||
|
|
||||||
const isFromAddBrand = location.pathname.includes('/master/brand-device/') && location.pathname.includes('/error-code/') &&
|
|
||||||
(location.pathname.includes('/add') || (location.pathname.includes('/edit/') && !location.pathname.includes('/edit/')));
|
|
||||||
|
|
||||||
const [errorCodeForm] = Form.useForm();
|
|
||||||
const [solutionForm] = Form.useForm();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
|
||||||
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
|
|
||||||
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
|
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
|
||||||
const [fileList, setFileList] = useState([]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
solutionFields,
|
|
||||||
solutionTypes,
|
|
||||||
solutionStatuses,
|
|
||||||
solutionsToDelete,
|
|
||||||
firstSolutionValid,
|
|
||||||
handleAddSolutionField,
|
|
||||||
handleRemoveSolutionField,
|
|
||||||
handleSolutionTypeChange,
|
|
||||||
handleSolutionStatusChange,
|
|
||||||
resetSolutionFields,
|
|
||||||
getSolutionData,
|
|
||||||
setSolutionsForExistingRecord,
|
|
||||||
} = useSolutionLogic(solutionForm);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isEditMode = errorCodeId && errorCodeId !== 'add';
|
|
||||||
setIsEdit(isEditMode);
|
|
||||||
|
|
||||||
if (!isEditMode) {
|
|
||||||
resetSolutionFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
setBreadcrumbItems([
|
|
||||||
{
|
|
||||||
title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}>• Master</span>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: (
|
|
||||||
<span
|
|
||||||
style={{ fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}
|
|
||||||
onClick={() => navigate('/master/brand-device')}
|
|
||||||
>
|
|
||||||
Brand Device
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: (
|
|
||||||
<span
|
|
||||||
style={{ fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}
|
|
||||||
onClick={() => navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`)}
|
|
||||||
>
|
|
||||||
Edit Brand Device
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: (
|
|
||||||
<span style={{ fontSize: '14px', fontWeight: 'bold' }}>
|
|
||||||
{isEditMode ? 'Edit Error Code' : 'Add Error Code'}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (isEditMode && errorCodeId) {
|
|
||||||
const tempId = errorCodeId.startsWith('existing_') ? errorCodeId : `existing_${errorCodeId}`;
|
|
||||||
loadExistingErrorCode(tempId);
|
|
||||||
}
|
|
||||||
}, [currentBrandId, errorCodeId, navigate, setBreadcrumbItems]);
|
|
||||||
|
|
||||||
const loadExistingErrorCode = async (tempId) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
let errorIdToUse = tempId;
|
|
||||||
if (tempId.startsWith('existing_')) {
|
|
||||||
errorIdToUse = tempId.replace('existing_', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorCodeResponse = await getErrorCodeById(errorIdToUse);
|
|
||||||
|
|
||||||
if (errorCodeResponse && errorCodeResponse.statusCode === 200) {
|
|
||||||
const errorData = errorCodeResponse.data;
|
|
||||||
|
|
||||||
if (errorData) {
|
|
||||||
errorCodeForm.setFieldsValue({
|
|
||||||
error_code: errorData.error_code,
|
|
||||||
error_code_name: errorData.error_code_name || '',
|
|
||||||
error_code_description: errorData.error_code_description || '',
|
|
||||||
error_code_color: errorData.error_code_color || '#000000',
|
|
||||||
status: errorData.is_active !== false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (errorData.path_icon) {
|
|
||||||
setErrorCodeIcon({
|
|
||||||
name: errorData.path_icon.split('/').pop(),
|
|
||||||
uploadPath: errorData.path_icon,
|
|
||||||
url: errorData.path_icon,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorData.solution && errorData.solution.length > 0) {
|
|
||||||
setSolutionsForExistingRecord(errorData.solution, solutionForm);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorData.spareparts && errorData.spareparts.length > 0) {
|
|
||||||
const sparepartIds = errorData.spareparts.map(sp => sp.sparepart_id);
|
|
||||||
setSelectedSparepartIds(sparepartIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errorCodeForm.setFieldsValue({
|
|
||||||
error_code: '',
|
|
||||||
error_code_name: '',
|
|
||||||
error_code_description: '',
|
|
||||||
error_code_color: '#000000',
|
|
||||||
status: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'warning',
|
|
||||||
title: 'Peringatan',
|
|
||||||
message: 'Error code not found. Creating new error code.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Error',
|
|
||||||
message: 'Failed to load error code data',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
try {
|
|
||||||
await errorCodeForm.validateFields();
|
|
||||||
|
|
||||||
const solutionData = getSolutionData();
|
|
||||||
|
|
||||||
|
|
||||||
if (!solutionData || solutionData.length === 0) {
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'warning',
|
|
||||||
title: 'Perhatian',
|
|
||||||
message: 'Harap lengkapi minimal 1 solution',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const invalidSolutions = solutionData.filter(solution => {
|
|
||||||
if (solution.type_solution === 'text') {
|
|
||||||
return !solution.text_solution || solution.text_solution.trim() === '';
|
|
||||||
} else if (solution.type_solution !== 'text') {
|
|
||||||
return !solution.path_solution || solution.path_solution.trim() === '';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
if (invalidSolutions.length > 0) {
|
|
||||||
const invalidNames = invalidSolutions.map(s => s.solution_name).join(', ');
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'warning',
|
|
||||||
title: 'Perhatian',
|
|
||||||
message: `Harap lengkapi solution berikut:\n${invalidSolutions.map(s =>
|
|
||||||
`- ${s.solution_name}: ${s.type_solution === 'text' ? 'Text solution wajib diisi' : 'File wajib diupload'}`
|
|
||||||
).join('\n')}`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorCodeValues = errorCodeForm.getFieldsValue();
|
|
||||||
|
|
||||||
setConfirmLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
error_code_name: errorCodeValues.error_code_name,
|
|
||||||
error_code_description: errorCodeValues.error_code_description || '',
|
|
||||||
error_code_color: errorCodeValues.error_code_color || '#000000',
|
|
||||||
path_icon: errorCodeIcon?.path_icon || errorCodeIcon?.uploadPath || '',
|
|
||||||
is_active: errorCodeValues.status !== undefined ? errorCodeValues.status : true,
|
|
||||||
solution: solutionData || [],
|
|
||||||
spareparts: selectedSparepartIds || []
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isEdit) {
|
|
||||||
payload.error_code = errorCodeValues.error_code;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let response;
|
|
||||||
|
|
||||||
if (isEdit && errorCodeId) {
|
|
||||||
let cleanErrorCodeId = errorCodeId;
|
|
||||||
if (errorCodeId.startsWith('existing_')) {
|
|
||||||
cleanErrorCodeId = errorCodeId.replace('existing_', '');
|
|
||||||
}
|
|
||||||
response = await updateErrorCode(currentBrandId, cleanErrorCodeId, payload);
|
|
||||||
} else {
|
|
||||||
response = await createErrorCode(currentBrandId, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
|
||||||
NotifOk({
|
|
||||||
icon: 'success',
|
|
||||||
title: 'Berhasil',
|
|
||||||
message: isEdit ? 'Error Code berhasil diupdate!' : 'Error Code berhasil ditambahkan!',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isFromAddBrand) {
|
|
||||||
navigate(`/master/brand-device/add`);
|
|
||||||
} else {
|
|
||||||
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`, {
|
|
||||||
state: { refreshErrorCodes: true }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Gagal',
|
|
||||||
message: response?.message || 'Gagal menyimpan error code',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Gagal',
|
|
||||||
message: error.message || 'Gagal menyimpan error code. Silakan coba lagi.',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setConfirmLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Error',
|
|
||||||
message: 'Gagal menyimpan error code. Silakan coba lagi.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
if (isFromAddBrand) {
|
|
||||||
navigate(`/master/brand-device/add`);
|
|
||||||
} else {
|
|
||||||
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleErrorCodeIconUpload = (iconData) => {
|
|
||||||
if (!iconData || !iconData.uploadPath) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedIconData = {
|
|
||||||
name: iconData.name,
|
|
||||||
uploadPath: iconData.uploadPath,
|
|
||||||
url: iconData.uploadPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
setErrorCodeIcon(formattedIconData);
|
|
||||||
return formattedIconData;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleErrorCodeIconRemove = () => {
|
|
||||||
setErrorCodeIcon(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSolutionFileUpload = (fileObject) => {
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
errorCodeForm.resetFields();
|
|
||||||
errorCodeForm.setFieldsValue({
|
|
||||||
status: true,
|
|
||||||
});
|
|
||||||
setErrorCodeIcon(null);
|
|
||||||
resetSolutionFields();
|
|
||||||
setSelectedSparepartIds([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
{/* Header */}
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 24
|
|
||||||
}}>
|
|
||||||
<Title level={4} style={{ margin: 0 }}>
|
|
||||||
{isEdit ? 'Edit Error Code' : 'Add Error Code'}
|
|
||||||
</Title>
|
|
||||||
<Button
|
|
||||||
icon={<ArrowLeftOutlined />}
|
|
||||||
onClick={handleCancel}
|
|
||||||
>
|
|
||||||
Back to Brand Device
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div style={{ position: 'relative', minHeight: 500 }}>
|
|
||||||
{loading && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
zIndex: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Spin size="large" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Row gutter={[24, 24]}>
|
|
||||||
{/* Error Code Form */}
|
|
||||||
<Col xs={24} lg={8}>
|
|
||||||
<Card
|
|
||||||
title="Error Code Details"
|
|
||||||
size="small"
|
|
||||||
style={{ height: 'fit-content' }}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={errorCodeForm}
|
|
||||||
layout="vertical"
|
|
||||||
initialValues={{
|
|
||||||
status: true,
|
|
||||||
error_code_color: '#000000',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ErrorCodeForm
|
|
||||||
errorCodeForm={errorCodeForm}
|
|
||||||
isErrorCodeFormReadOnly={false}
|
|
||||||
errorCodeIcon={errorCodeIcon}
|
|
||||||
onErrorCodeIconUpload={handleErrorCodeIconUpload}
|
|
||||||
onErrorCodeIconRemove={handleErrorCodeIconRemove}
|
|
||||||
isEdit={isEdit}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* Solutions Form */}
|
|
||||||
<Col xs={24} lg={8}>
|
|
||||||
<Card
|
|
||||||
title="Solutions"
|
|
||||||
size="small"
|
|
||||||
style={{ height: 'fit-content' }}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={solutionForm}
|
|
||||||
layout="vertical"
|
|
||||||
initialValues={{}}
|
|
||||||
>
|
|
||||||
<SolutionForm
|
|
||||||
solutionForm={solutionForm}
|
|
||||||
solutionFields={solutionFields}
|
|
||||||
solutionTypes={solutionTypes}
|
|
||||||
solutionStatuses={solutionStatuses}
|
|
||||||
firstSolutionValid={firstSolutionValid}
|
|
||||||
checkFirstSolutionValid={() => {
|
|
||||||
return checkFirstSolutionValid();
|
|
||||||
}}
|
|
||||||
onAddSolutionField={handleAddSolutionField}
|
|
||||||
onRemoveSolutionField={handleRemoveSolutionField}
|
|
||||||
onSolutionTypeChange={handleSolutionTypeChange}
|
|
||||||
onSolutionStatusChange={handleSolutionStatusChange}
|
|
||||||
onSolutionFileUpload={handleSolutionFileUpload}
|
|
||||||
onFileView={(fileData) => {
|
|
||||||
if (fileData && fileData.url) {
|
|
||||||
window.open(fileData.url, '_blank');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
isReadOnly={false}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* Sparepart Selection */}
|
|
||||||
<Col xs={24} lg={8}>
|
|
||||||
<Card
|
|
||||||
title="Spareparts"
|
|
||||||
size="small"
|
|
||||||
style={{ height: 'fit-content' }}
|
|
||||||
>
|
|
||||||
<SparepartSelect
|
|
||||||
selectedSparepartIds={selectedSparepartIds}
|
|
||||||
onSparepartChange={setSelectedSparepartIds}
|
|
||||||
isReadOnly={false}
|
|
||||||
brandId={currentBrandId}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
{/* Save Button */}
|
|
||||||
<div style={{ marginTop: 24, textAlign: 'right' }}>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
loading={confirmLoading}
|
|
||||||
style={{
|
|
||||||
backgroundColor: '#23A55A',
|
|
||||||
borderColor: '#23A55A',
|
|
||||||
}}
|
|
||||||
onClick={handleSave}
|
|
||||||
>
|
|
||||||
{isEdit ? 'Update Error Code' : 'Save Error Code'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddEditErrorCode;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Divider,
|
Divider,
|
||||||
@@ -13,17 +13,16 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Space,
|
Space,
|
||||||
Input,
|
Input,
|
||||||
|
ConfigProvider
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { EyeOutlined, EditOutlined, DeleteOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons';
|
|
||||||
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
|
||||||
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
|
||||||
import { getBrandById, getErrorCodesByBrandId, getErrorCodeById, deleteErrorCode, updateErrorCode as updateErrorCodeAPI, createErrorCode as createErrorCodeAPI } from '../../../api/master-brand';
|
import { getBrandById, updateBrand, getErrorCodesByBrandId, deleteErrorCode, updateErrorCode as updateErrorCodeAPI, createErrorCode as createErrorCodeAPI } from '../../../api/master-brand';
|
||||||
import { getFileUrl } from '../../../api/file-uploads';
|
import { getFileUrl } from '../../../api/file-uploads';
|
||||||
import { SendRequest } from '../../../components/Global/ApiRequest';
|
import { SendRequest } from '../../../components/Global/ApiRequest';
|
||||||
import BrandForm from './component/BrandForm';
|
import BrandForm from './component/BrandForm';
|
||||||
import ErrorCodeForm from './component/ErrorCodeForm';
|
import ErrorCodeForm from './component/ErrorCodeForm';
|
||||||
import SolutionForm from './component/SolutionForm';
|
import SolutionForm from './component/SolutionForm';
|
||||||
import FormActions from './component/FormActions';
|
|
||||||
import SparepartSelect from './component/SparepartSelect';
|
import SparepartSelect from './component/SparepartSelect';
|
||||||
import ListErrorCode from './component/ListErrorCode';
|
import ListErrorCode from './component/ListErrorCode';
|
||||||
|
|
||||||
@@ -57,22 +56,42 @@ const EditBrandDevice = () => {
|
|||||||
const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' });
|
const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' });
|
||||||
const [solutionStatuses, setSolutionStatuses] = useState({ 0: true });
|
const [solutionStatuses, setSolutionStatuses] = useState({ 0: true });
|
||||||
const [currentSolutionData, setCurrentSolutionData] = useState([]);
|
const [currentSolutionData, setCurrentSolutionData] = useState([]);
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
|
||||||
const getSolutionData = () => {
|
const getSolutionData = () => {
|
||||||
if (!solutionForm) return [];
|
if (!solutionForm) return [];
|
||||||
try {
|
try {
|
||||||
const values = solutionForm.getFieldsValue(true);
|
const values = solutionForm.getFieldsValue(true);
|
||||||
|
const solutions = [];
|
||||||
|
|
||||||
let solutions = [];
|
solutionFields.forEach(fieldKey => {
|
||||||
|
let solution = null;
|
||||||
|
|
||||||
if (values.solution_items) {
|
if (values.solution_items && values.solution_items[fieldKey]) {
|
||||||
if (Array.isArray(values.solution_items)) {
|
solution = values.solution_items[fieldKey];
|
||||||
solutions = values.solution_items.filter(Boolean);
|
|
||||||
} else if (typeof values.solution_items === 'object') {
|
|
||||||
solutions = Object.values(values.solution_items).filter(Boolean);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!solution || !solution.name || solution.name.trim() === '') {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const solutionType = solutionTypes[fieldKey] || solution.type || 'text';
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
if (solutionType === 'text') {
|
||||||
|
isValid = solution.text && solution.text.trim() !== '';
|
||||||
|
} else if (solutionType === 'file') {
|
||||||
|
const hasPathSolution = solution.path_solution && solution.path_solution.trim() !== '';
|
||||||
|
const hasFileUpload = (solution.fileUpload && typeof solution.fileUpload === 'object' && Object.keys(solution.fileUpload).length > 0);
|
||||||
|
const hasFile = (solution.file && typeof solution.file === 'object' && Object.keys(solution.file).length > 0);
|
||||||
|
isValid = hasPathSolution || hasFileUpload || hasFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
solutions.push(solution);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return solutions;
|
return solutions;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return [];
|
||||||
@@ -80,18 +99,26 @@ const EditBrandDevice = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resetSolutionFields = () => {
|
const resetSolutionFields = () => {
|
||||||
|
setSolutionFields([0]);
|
||||||
|
setSolutionTypes({ 0: 'text' });
|
||||||
|
setSolutionStatuses({ 0: true });
|
||||||
|
|
||||||
if (solutionForm && solutionForm.resetFields) {
|
if (solutionForm && solutionForm.resetFields) {
|
||||||
solutionForm.resetFields();
|
solutionForm.resetFields();
|
||||||
|
setTimeout(() => {
|
||||||
solutionForm.setFieldsValue({
|
solutionForm.setFieldsValue({
|
||||||
solution_items: {
|
solution_items: {
|
||||||
0: {
|
0: {
|
||||||
name: '',
|
name: '',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: '',
|
text: '',
|
||||||
status: true
|
status: true,
|
||||||
|
file: null,
|
||||||
|
fileUpload: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
setCurrentSolutionData([]);
|
setCurrentSolutionData([]);
|
||||||
};
|
};
|
||||||
@@ -115,7 +142,7 @@ const EditBrandDevice = () => {
|
|||||||
|
|
||||||
const isFileType = solution.type_solution && solution.type_solution !== 'text';
|
const isFileType = solution.type_solution && solution.type_solution !== 'text';
|
||||||
newSolutionTypes[fieldKey] = isFileType ? 'file' : 'text';
|
newSolutionTypes[fieldKey] = isFileType ? 'file' : 'text';
|
||||||
newSolutionStatuses[fieldKey] = solution.is_active !== false;
|
newSolutionStatuses[fieldKey] = solution.is_active;
|
||||||
|
|
||||||
let fileObject = null;
|
let fileObject = null;
|
||||||
if (isFileType && (solution.path_solution || solution.path_document)) {
|
if (isFileType && (solution.path_solution || solution.path_document)) {
|
||||||
@@ -135,7 +162,7 @@ const EditBrandDevice = () => {
|
|||||||
name: solution.solution_name || '',
|
name: solution.solution_name || '',
|
||||||
type: isFileType ? 'file' : 'text',
|
type: isFileType ? 'file' : 'text',
|
||||||
text: solution.text_solution || '',
|
text: solution.text_solution || '',
|
||||||
status: solution.is_active !== false,
|
status: solution.is_active,
|
||||||
file: fileObject,
|
file: fileObject,
|
||||||
fileUpload: fileObject,
|
fileUpload: fileObject,
|
||||||
path_solution: solution.path_solution || solution.path_document || null,
|
path_solution: solution.path_solution || solution.path_document || null,
|
||||||
@@ -327,13 +354,13 @@ const EditBrandDevice = () => {
|
|||||||
}
|
}
|
||||||
}, [currentStep, id]);
|
}, [currentStep, id]);
|
||||||
|
|
||||||
// Auto refresh error codes when returning from add/edit error code
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.state?.refreshErrorCodes) {
|
if (location.state?.refreshErrorCodes) {
|
||||||
// Trigger refresh of error codes list
|
|
||||||
setTrigerFilter(prev => !prev);
|
setTrigerFilter(prev => !prev);
|
||||||
|
|
||||||
// Clear the state to prevent infinite refresh
|
|
||||||
const state = { ...location.state };
|
const state = { ...location.state };
|
||||||
delete state.refreshErrorCodes;
|
delete state.refreshErrorCodes;
|
||||||
navigate(location.pathname + location.search, {
|
navigate(location.pathname + location.search, {
|
||||||
@@ -343,61 +370,70 @@ const EditBrandDevice = () => {
|
|||||||
}
|
}
|
||||||
}, [location, navigate]);
|
}, [location, navigate]);
|
||||||
|
|
||||||
const addErrorCode = (newErrorCode) => {
|
|
||||||
// Generate unique tempId with timestamp and random number
|
|
||||||
const uniqueId = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
const errorCodeWithId = {
|
|
||||||
...newErrorCode,
|
|
||||||
tempId: uniqueId,
|
|
||||||
status: 'new'
|
|
||||||
};
|
|
||||||
setTempErrorCodes(prev => [...prev, errorCodeWithId]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateErrorCode = (tempId, updatedData) => {
|
|
||||||
setTempErrorCodes(prev =>
|
|
||||||
prev.map(ec => ec.tempId === tempId ? { ...ec, ...updatedData, status: 'modified' } : ec)
|
|
||||||
);
|
|
||||||
setExistingErrorCodes(prev =>
|
|
||||||
prev.map(ec => ec.tempId === tempId ? { ...ec, ...updatedData, status: 'modified' } : ec)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteLocalErrorCode = (tempId, permanent = false) => {
|
|
||||||
if (permanent) {
|
|
||||||
setTempErrorCodes(prev => prev.filter(ec => ec.tempId !== tempId));
|
|
||||||
setExistingErrorCodes(prev => prev.filter(ec => ec.tempId !== tempId));
|
|
||||||
} else {
|
|
||||||
setTempErrorCodes(prev =>
|
|
||||||
prev.map(ec => ec.tempId === tempId ? { ...ec, status: 'deleted' } : ec)
|
|
||||||
);
|
|
||||||
setExistingErrorCodes(prev =>
|
|
||||||
prev.map(ec => ec.tempId === tempId ? { ...ec, status: 'deleted' } : ec)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getErrorCodeById = (tempId) => {
|
|
||||||
const inTemp = tempErrorCodes.find(ec => ec.tempId === tempId);
|
|
||||||
if (inTemp) return inTemp;
|
|
||||||
|
|
||||||
const inExisting = existingErrorCodes.find(ec => ec.tempId === tempId);
|
|
||||||
return inExisting;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNextStep = async () => {
|
const handleNextStep = async () => {
|
||||||
try {
|
try {
|
||||||
await brandForm.validateFields();
|
setConfirmLoading(true);
|
||||||
|
|
||||||
|
const brandValues = brandForm.getFieldsValue();
|
||||||
|
|
||||||
|
if (!brandValues.brand_name || brandValues.brand_name.trim() === '') {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Perhatian',
|
||||||
|
message: 'Brand Name wajib diisi!',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!brandValues.brand_manufacture || brandValues.brand_manufacture.trim() === '') {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Perhatian',
|
||||||
|
message: 'Manufacturer wajib diisi!',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const brandApiData = {
|
||||||
|
brand_name: brandValues.brand_name.trim(),
|
||||||
|
brand_type: brandValues.brand_type || '',
|
||||||
|
brand_manufacture: brandValues.brand_manufacture.trim(),
|
||||||
|
brand_model: brandValues.brand_model || '',
|
||||||
|
is_active: brandValues.is_active !== undefined ? brandValues.is_active : true
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await updateBrand(id, brandApiData);
|
||||||
|
|
||||||
|
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
||||||
|
NotifOk({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
message: 'Brand device berhasil diupdate.',
|
||||||
|
});
|
||||||
|
|
||||||
const currentBrandId = id;
|
const currentBrandId = id;
|
||||||
if (currentBrandId) {
|
if (currentBrandId) {
|
||||||
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`);
|
navigate(`/master/brand-device/edit/${currentBrandId}?tab=error-codes`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
NotifAlert({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Gagal',
|
||||||
|
message: response?.message || 'Gagal mengupdate brand device',
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'warning',
|
icon: 'error',
|
||||||
title: 'Perhatian',
|
title: 'Gagal',
|
||||||
message: 'Harap isi semua kolom wajib untuk brand device!',
|
message: error.message || 'Gagal mengupdate brand device',
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setConfirmLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -406,6 +442,7 @@ const EditBrandDevice = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleErrorCodeIconUpload = (iconData) => {
|
const handleErrorCodeIconUpload = (iconData) => {
|
||||||
setErrorCodeIcon(iconData);
|
setErrorCodeIcon(iconData);
|
||||||
};
|
};
|
||||||
@@ -424,15 +461,12 @@ const EditBrandDevice = () => {
|
|||||||
setIsErrorCodeFormReadOnly(false);
|
setIsErrorCodeFormReadOnly(false);
|
||||||
setEditingErrorCodeKey(null);
|
setEditingErrorCodeKey(null);
|
||||||
setSelectedSparepartIds([]);
|
setSelectedSparepartIds([]);
|
||||||
};
|
setSelectedErrorCode(null);
|
||||||
|
|
||||||
const handleCreateNewErrorCode = () => {
|
|
||||||
resetErrorCodeForm();
|
|
||||||
setCurrentSolutionData([]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveErrorCode = async () => {
|
const handleSaveErrorCode = async () => {
|
||||||
try {
|
try {
|
||||||
|
setConfirmLoading(true);
|
||||||
const errorCodeValues = await errorCodeForm.validateFields();
|
const errorCodeValues = await errorCodeForm.validateFields();
|
||||||
const solutionData = getSolutionData();
|
const solutionData = getSolutionData();
|
||||||
|
|
||||||
@@ -445,14 +479,14 @@ const EditBrandDevice = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!solutionData || solutionData.length === 0) {
|
// if (!solutionData || solutionData.length === 0) {
|
||||||
NotifAlert({
|
// NotifAlert({
|
||||||
icon: 'warning',
|
// icon: 'warning',
|
||||||
title: 'Perhatian',
|
// title: 'Perhatian',
|
||||||
message: 'Setiap error code harus memiliki minimal 1 solution!',
|
// message: 'Setiap error code harus memiliki minimal 1 solution!',
|
||||||
});
|
// });
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const formattedSolutions = solutionData.map(solution => {
|
const formattedSolutions = solutionData.map(solution => {
|
||||||
const solutionType = solution.type || 'text';
|
const solutionType = solution.type || 'text';
|
||||||
@@ -487,6 +521,7 @@ const EditBrandDevice = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
|
error_code: errorCodeValues.error_code,
|
||||||
error_code_name: errorCodeValues.error_code_name,
|
error_code_name: errorCodeValues.error_code_name,
|
||||||
error_code_description: errorCodeValues.error_code_description || '',
|
error_code_description: errorCodeValues.error_code_description || '',
|
||||||
error_code_color: errorCodeValues.error_code_color || '#000000',
|
error_code_color: errorCodeValues.error_code_color || '#000000',
|
||||||
@@ -496,10 +531,6 @@ const EditBrandDevice = () => {
|
|||||||
spareparts: selectedSparepartIds || []
|
spareparts: selectedSparepartIds || []
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!editingErrorCodeKey || !editingErrorCodeKey.startsWith('existing_')) {
|
|
||||||
payload.error_code = errorCodeValues.error_code;
|
|
||||||
}
|
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
if (editingErrorCodeKey && editingErrorCodeKey.startsWith('existing_')) {
|
if (editingErrorCodeKey && editingErrorCodeKey.startsWith('existing_')) {
|
||||||
@@ -517,16 +548,6 @@ const EditBrandDevice = () => {
|
|||||||
message: editingErrorCodeKey ? 'Error code berhasil diupdate!' : 'Error code berhasil ditambahkan!',
|
message: editingErrorCodeKey ? 'Error code berhasil diupdate!' : 'Error code berhasil ditambahkan!',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear temp error codes after successful save to prevent duplication
|
|
||||||
setTempErrorCodes(prev => prev.filter(ec => {
|
|
||||||
// Keep existing error codes that weren't just saved
|
|
||||||
if (ec.status === 'existing' || ec.tempId.startsWith('existing_')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Remove the newly saved error code to prevent duplication
|
|
||||||
return ec.tempId !== editingErrorCodeKey;
|
|
||||||
}));
|
|
||||||
|
|
||||||
setTrigerFilter(prev => !prev);
|
setTrigerFilter(prev => !prev);
|
||||||
resetErrorCodeForm();
|
resetErrorCodeForm();
|
||||||
} else {
|
} else {
|
||||||
@@ -542,6 +563,8 @@ const EditBrandDevice = () => {
|
|||||||
title: 'Perhatian',
|
title: 'Perhatian',
|
||||||
message: error.message || 'Harap isi semua kolom wajib!',
|
message: error.message || 'Harap isi semua kolom wajib!',
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setConfirmLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -576,157 +599,6 @@ const EditBrandDevice = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePreviewErrorCode = (record) => {
|
|
||||||
const errorCode = getErrorCodeById(record.tempId || record.error_code_id);
|
|
||||||
loadErrorCodeData(errorCode, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditErrorCode = (record) => {
|
|
||||||
const errorCode = getErrorCodeById(record.tempId || record.error_code_id);
|
|
||||||
loadErrorCodeData(errorCode, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const handleDeleteErrorCode = async (record) => {
|
|
||||||
NotifConfirmDialog({
|
|
||||||
icon: 'question',
|
|
||||||
title: 'Konfirmasi Hapus',
|
|
||||||
message: `Apakah Anda yakin ingin menghapus error code "${record.error_code}"?`,
|
|
||||||
onConfirm: async () => {
|
|
||||||
try {
|
|
||||||
if (record.status === 'existing' && record.error_code_id) {
|
|
||||||
const response = await deleteErrorCode(id, record.error_code_id);
|
|
||||||
if (response && response.statusCode === 200) {
|
|
||||||
NotifOk({
|
|
||||||
icon: 'success',
|
|
||||||
title: 'Berhasil',
|
|
||||||
message: 'Error code berhasil dihapus!',
|
|
||||||
});
|
|
||||||
setTrigerFilter(prev => !prev);
|
|
||||||
} else {
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Error',
|
|
||||||
message: response?.message || 'Failed to delete error code',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const tempId = record.tempId;
|
|
||||||
deleteLocalErrorCode(tempId, true);
|
|
||||||
NotifOk({
|
|
||||||
icon: 'success',
|
|
||||||
title: 'Berhasil',
|
|
||||||
message: 'Error code berhasil dihapus!',
|
|
||||||
});
|
|
||||||
setTrigerFilter(prev => !prev);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
NotifAlert({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Error',
|
|
||||||
message: error.message || 'Failed to delete error code',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCancel: () => { }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const mergedErrorCodes = useMemo(() => {
|
|
||||||
const allErrorCodes = [
|
|
||||||
...existingErrorCodes.map(ec => ({ ...ec, status: 'existing' })),
|
|
||||||
...tempErrorCodes
|
|
||||||
];
|
|
||||||
|
|
||||||
const activeErrorCodes = allErrorCodes.filter(ec => ec.status !== 'deleted');
|
|
||||||
|
|
||||||
if (searchText) {
|
|
||||||
return activeErrorCodes.filter(ec =>
|
|
||||||
ec.error_code.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
||||||
ec.error_code_name.toLowerCase().includes(searchText.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return activeErrorCodes;
|
|
||||||
}, [existingErrorCodes, tempErrorCodes, searchText]);
|
|
||||||
|
|
||||||
const errorCodeColumns = (showPreviewModal, showEditModal, showDeleteDialog) => [
|
|
||||||
{
|
|
||||||
title: 'No',
|
|
||||||
key: 'no',
|
|
||||||
width: '5%',
|
|
||||||
align: 'center',
|
|
||||||
render: (_, __, index) => index + 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Error Code',
|
|
||||||
dataIndex: 'error_code',
|
|
||||||
key: 'error_code',
|
|
||||||
width: '20%',
|
|
||||||
render: (text, record) => (
|
|
||||||
<Space>
|
|
||||||
{text}
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Error Name',
|
|
||||||
dataIndex: 'error_code_name',
|
|
||||||
key: 'error_code_name',
|
|
||||||
width: '25%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Status',
|
|
||||||
dataIndex: 'is_active',
|
|
||||||
key: 'is_active',
|
|
||||||
width: '10%',
|
|
||||||
align: 'center',
|
|
||||||
render: (_, { is_active }) => (
|
|
||||||
<Tag color={is_active ? 'green' : 'red'}>
|
|
||||||
{is_active ? 'Active' : 'Inactive'}
|
|
||||||
</Tag>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Action',
|
|
||||||
key: 'action',
|
|
||||||
align: 'center',
|
|
||||||
width: '15%',
|
|
||||||
render: (_, record) => (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
icon={<EyeOutlined />}
|
|
||||||
onClick={() => showPreviewModal(record)}
|
|
||||||
style={{
|
|
||||||
color: '#1890ff',
|
|
||||||
borderColor: '#1890ff',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => showEditModal(record)}
|
|
||||||
style={{
|
|
||||||
color: '#faad14',
|
|
||||||
borderColor: '#faad14',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
onClick={() => showDeleteDialog(record)}
|
|
||||||
style={{
|
|
||||||
borderColor: '#ff4d4f',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleBrandFormValuesChange = useCallback((changedValues, allValues) => {
|
|
||||||
setBrandInfo(allValues);
|
|
||||||
}, [setBrandInfo]);
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
setTrigerFilter((prev) => !prev);
|
setTrigerFilter((prev) => !prev);
|
||||||
};
|
};
|
||||||
@@ -736,113 +608,6 @@ const EditBrandDevice = () => {
|
|||||||
setTrigerFilter((prev) => !prev);
|
setTrigerFilter((prev) => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getErrorCodesData = async (params) => {
|
|
||||||
try {
|
|
||||||
const criteria = params.get('criteria') || '';
|
|
||||||
const page = parseInt(params.get('page')) || 1;
|
|
||||||
const limit = parseInt(params.get('limit')) || 10;
|
|
||||||
const currentBrandId = id;
|
|
||||||
|
|
||||||
if (!currentBrandId) {
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
pagination: {
|
|
||||||
current_page: page,
|
|
||||||
current_limit: limit,
|
|
||||||
total_limit: 0,
|
|
||||||
total_page: 0,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryParams = new URLSearchParams({
|
|
||||||
page: page.toString(),
|
|
||||||
limit: limit.toString(),
|
|
||||||
...(criteria && { criteria })
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await getErrorCodesByBrandId(currentBrandId, queryParams);
|
|
||||||
|
|
||||||
if (response && response.statusCode === 200) {
|
|
||||||
const apiData = response.data || [];
|
|
||||||
|
|
||||||
const contextExistingMap = new Map();
|
|
||||||
existingErrorCodes.forEach(ec => {
|
|
||||||
if (ec.status === 'modified' || ec.status === 'deleted') {
|
|
||||||
contextExistingMap.set(ec.error_code_id, ec);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const existingCodes = apiData.map(ec => {
|
|
||||||
const contextModified = contextExistingMap.get(ec.error_code_id);
|
|
||||||
if (contextModified) {
|
|
||||||
return {
|
|
||||||
...contextModified,
|
|
||||||
tempId: `existing_${ec.error_code_id}`,
|
|
||||||
solution: contextModified.solution || ec.solution || [],
|
|
||||||
spareparts: contextModified.spareparts || ec.spareparts || []
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...ec,
|
|
||||||
tempId: `existing_${ec.error_code_id}`,
|
|
||||||
status: 'existing',
|
|
||||||
solution: ec.solution || [],
|
|
||||||
spareparts: ec.spareparts || []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeExistingCodes = existingCodes.filter(ec => ec.status !== 'deleted');
|
|
||||||
|
|
||||||
const allErrorCodes = [...activeExistingCodes, ...tempErrorCodes.filter(ec => ec.status !== 'deleted')];
|
|
||||||
|
|
||||||
let filteredData = allErrorCodes;
|
|
||||||
|
|
||||||
if (search) {
|
|
||||||
filteredData = allErrorCodes.filter(ec =>
|
|
||||||
ec.error_code.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
ec.error_code_name.toLowerCase().includes(search.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const startIndex = 0;
|
|
||||||
const endIndex = startIndex + limit;
|
|
||||||
const paginatedData = filteredData.slice(startIndex, endIndex);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: paginatedData,
|
|
||||||
pagination: {
|
|
||||||
current_page: page,
|
|
||||||
current_limit: limit,
|
|
||||||
total_limit: filteredData.length,
|
|
||||||
total_page: Math.ceil(filteredData.length / limit),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
pagination: {
|
|
||||||
current_page: page,
|
|
||||||
current_limit: limit,
|
|
||||||
total_limit: 0,
|
|
||||||
total_page: 0,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
pagination: {
|
|
||||||
current_page: 1,
|
|
||||||
current_limit: 10,
|
|
||||||
total_limit: 0,
|
|
||||||
total_page: 0,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStepContent = () => {
|
const renderStepContent = () => {
|
||||||
if (currentStep === 0) {
|
if (currentStep === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -868,7 +633,6 @@ const EditBrandDevice = () => {
|
|||||||
)}
|
)}
|
||||||
<BrandForm
|
<BrandForm
|
||||||
form={brandForm}
|
form={brandForm}
|
||||||
onValuesChange={handleBrandFormValuesChange}
|
|
||||||
isEdit={true}
|
isEdit={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -960,6 +724,12 @@ const EditBrandDevice = () => {
|
|||||||
}}>
|
}}>
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
<span style={{
|
<span style={{
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
@@ -976,6 +746,31 @@ const EditBrandDevice = () => {
|
|||||||
}}></span>
|
}}></span>
|
||||||
Error Code Form
|
Error Code Form
|
||||||
</span>
|
</span>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={handleSaveErrorCode}
|
||||||
|
loading={confirmLoading}
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#23A55A',
|
||||||
|
borderColor: '#23A55A',
|
||||||
|
borderRadius: '8px',
|
||||||
|
height: '40px',
|
||||||
|
padding: '0 24px',
|
||||||
|
fontWeight: '500',
|
||||||
|
boxShadow: '0 2px 4px rgba(35, 165, 90, 0.2)',
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.target.style.boxShadow = '0 4px 8px rgba(35, 165, 90, 0.3)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.target.style.boxShadow = '0 2px 4px rgba(35, 165, 90, 0.2)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{editingErrorCodeKey ? 'Update Error Code' : 'Save Error Code'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -1001,24 +796,6 @@ const EditBrandDevice = () => {
|
|||||||
transition: 'all 0.3s ease',
|
transition: 'all 0.3s ease',
|
||||||
boxShadow: '0 1px 3px rgba(0,0,0,0.04)'
|
boxShadow: '0 1px 3px rgba(0,0,0,0.04)'
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '8px',
|
|
||||||
marginBottom: '12px',
|
|
||||||
paddingBottom: '8px',
|
|
||||||
borderBottom: '1px solid #f5f5f5'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
width: '3px',
|
|
||||||
height: '16px',
|
|
||||||
backgroundColor: '#23A55A',
|
|
||||||
borderRadius: '2px'
|
|
||||||
}}></div>
|
|
||||||
<h4 style={{ margin: 0, color: '#262626', fontSize: '14px', fontWeight: '600' }}>
|
|
||||||
Error Code Details
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<ErrorCodeForm
|
<ErrorCodeForm
|
||||||
errorCodeForm={errorCodeForm}
|
errorCodeForm={errorCodeForm}
|
||||||
isErrorCodeFormReadOnly={isErrorCodeFormReadOnly}
|
isErrorCodeFormReadOnly={isErrorCodeFormReadOnly}
|
||||||
@@ -1131,11 +908,14 @@ const EditBrandDevice = () => {
|
|||||||
borderTop: '1px solid #f0f0f0',
|
borderTop: '1px solid #f0f0f0',
|
||||||
marginTop: '12px'
|
marginTop: '12px'
|
||||||
}}>
|
}}>
|
||||||
{/* Cancel Button - Only show when editing existing error code */}
|
|
||||||
{editingErrorCodeKey && (
|
{editingErrorCodeKey && (
|
||||||
<Button
|
<Button
|
||||||
size="large"
|
size="large"
|
||||||
onClick={handleCreateNewErrorCode}
|
onClick={() => {
|
||||||
|
resetErrorCodeForm();
|
||||||
|
resetSolutionFields();
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderColor: '#d9d9d9',
|
borderColor: '#d9d9d9',
|
||||||
@@ -1158,33 +938,6 @@ const EditBrandDevice = () => {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Save Button - Always show on right */}
|
|
||||||
<div style={{ marginLeft: editingErrorCodeKey ? '0' : 'auto' }}>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
onClick={handleSaveErrorCode}
|
|
||||||
style={{
|
|
||||||
backgroundColor: '#23A55A',
|
|
||||||
borderColor: '#23A55A',
|
|
||||||
borderRadius: '8px',
|
|
||||||
height: '40px',
|
|
||||||
padding: '0 24px',
|
|
||||||
fontWeight: '500',
|
|
||||||
boxShadow: '0 2px 4px rgba(35, 165, 90, 0.2)',
|
|
||||||
transition: 'all 0.3s ease'
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.target.style.boxShadow = '0 4px 8px rgba(35, 165, 90, 0.3)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.target.style.boxShadow = '0 2px 4px rgba(35, 165, 90, 0.2)';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{editingErrorCodeKey ? 'Update Error Code' : 'Simpan Error Code'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -1197,6 +950,16 @@ const EditBrandDevice = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
components: {
|
||||||
|
Switch: {
|
||||||
|
colorPrimary: '#23A55A',
|
||||||
|
colorPrimaryHover: '#23A55A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<Steps current={currentStep} style={{ marginBottom: 24 }}>
|
<Steps current={currentStep} style={{ marginBottom: 24 }}>
|
||||||
<Step title="Brand Device Details" />
|
<Step title="Brand Device Details" />
|
||||||
@@ -1220,7 +983,7 @@ const EditBrandDevice = () => {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleNextStep}
|
onClick={handleNextStep}
|
||||||
loading={loading}
|
loading={confirmLoading}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#23A55A',
|
backgroundColor: '#23A55A',
|
||||||
borderColor: '#23A55A',
|
borderColor: '#23A55A',
|
||||||
@@ -1238,12 +1001,13 @@ const EditBrandDevice = () => {
|
|||||||
borderColor: '#23A55A',
|
borderColor: '#23A55A',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Selesai
|
Done
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -412,7 +412,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 +429,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',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const BrandForm = ({
|
|||||||
onValuesChange,
|
onValuesChange,
|
||||||
isEdit = false,
|
isEdit = false,
|
||||||
brandInfo = null,
|
brandInfo = null,
|
||||||
|
readOnly = false,
|
||||||
}) => {
|
}) => {
|
||||||
const isActive = Form.useWatch('is_active', form) ?? true;
|
const isActive = Form.useWatch('is_active', form) ?? true;
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ const BrandForm = ({
|
|||||||
<Form.Item name="is_active" valuePropName="checked" noStyle>
|
<Form.Item name="is_active" valuePropName="checked" noStyle>
|
||||||
<Switch
|
<Switch
|
||||||
style={{ backgroundColor: isActive ? '#23A55A' : '#bfbfbf' }}
|
style={{ backgroundColor: isActive ? '#23A55A' : '#bfbfbf' }}
|
||||||
|
disabled={readOnly}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Text style={{ marginLeft: 8 }}>
|
<Text style={{ marginLeft: 8 }}>
|
||||||
@@ -61,18 +63,18 @@ const BrandForm = ({
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label="Brand Name"
|
label="Brand Name"
|
||||||
name="brand_name"
|
name="brand_name"
|
||||||
rules={[{ required: true, message: 'Brand Name wajib diisi!' }]}
|
rules={[{ required: !readOnly, message: 'Brand Name wajib diisi!' }]}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input disabled={readOnly} />
|
||||||
</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: true, message: 'Manufacturer wajib diisi!' }]}
|
rules={[{ required: !readOnly, message: 'Manufacturer wajib diisi!' }]}
|
||||||
>
|
>
|
||||||
<Input placeholder="Enter Manufacturer" />
|
<Input placeholder="Enter Manufacturer" disabled={readOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -80,12 +82,12 @@ const BrandForm = ({
|
|||||||
<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)" />
|
<Input placeholder="Enter Brand Type (Optional)" disabled={readOnly} />
|
||||||
</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)" />
|
<Input placeholder="Enter Model (Optional)" disabled={readOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ const CustomSparepartCard = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const truncateText = (text, maxLength = 15) => {
|
||||||
|
if (!text) return 'Unnamed';
|
||||||
|
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
|
||||||
|
};
|
||||||
|
|
||||||
const handleCardClick = () => {
|
const handleCardClick = () => {
|
||||||
if (!isReadOnly && onCardClick) {
|
if (!isReadOnly && onCardClick) {
|
||||||
onCardClick(sparepart);
|
onCardClick(sparepart);
|
||||||
@@ -125,212 +130,90 @@ const CustomSparepartCard = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card
|
<div
|
||||||
hoverable={!!onCardClick && !isReadOnly}
|
style={{
|
||||||
style={getCardStyle()}
|
border: '1px solid #f0f0f0',
|
||||||
styles={{
|
borderRadius: '6px',
|
||||||
body: {
|
padding: '12px 16px',
|
||||||
padding: 0,
|
marginBottom: '8px',
|
||||||
height: 'calc(100% - 48px)',
|
backgroundColor: 'white',
|
||||||
|
cursor: onCardClick && !isReadOnly ? 'pointer' : 'default',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column'
|
alignItems: 'center',
|
||||||
}
|
justifyContent: 'space-between'
|
||||||
}}
|
}}
|
||||||
actions={getCardActions()}
|
|
||||||
onClick={handleCardClick}
|
onClick={handleCardClick}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', height: '100%' }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
{/* Image Section */}
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '4px' }}>
|
||||||
<div style={{
|
<Text
|
||||||
width: size === 'small' ? '90px' : '110px',
|
strong
|
||||||
flexShrink: 0,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
padding: size === 'small' ? '12px' : '16px',
|
|
||||||
backgroundColor: '#fafafa',
|
|
||||||
borderRight: '1px solid #f0f0f0',
|
|
||||||
position: 'relative'
|
|
||||||
}}>
|
|
||||||
{sparepart.sparepart_item_type && (
|
|
||||||
<Tag
|
|
||||||
color="blue"
|
|
||||||
style={{
|
style={{
|
||||||
marginBottom: '8px',
|
fontSize: '14px',
|
||||||
fontSize: '10px',
|
color: '#262626',
|
||||||
fontWeight: 500
|
marginRight: '12px'
|
||||||
}}
|
}}
|
||||||
|
title={sparepart.sparepart_name || sparepart.name || 'Unnamed'}
|
||||||
>
|
>
|
||||||
{sparepart.sparepart_item_type}
|
{truncateText(sparepart.sparepart_name || sparepart.name || 'Unnamed')}
|
||||||
</Tag>
|
</Text>
|
||||||
)}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: size === 'small' ? '65px' : '75px',
|
|
||||||
height: size === 'small' ? '65px' : '75px',
|
|
||||||
backgroundColor: '#f0f0f0',
|
|
||||||
borderRadius: '8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
border: '1px solid #e8e8e8'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={getImageSrc()}
|
|
||||||
alt={sparepart.sparepart_name || 'Sparepart'}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
objectFit: 'cover'
|
|
||||||
}}
|
|
||||||
onError={(e) => {
|
|
||||||
e.target.src = 'https://via.placeholder.com/75';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Selection Indicator */}
|
|
||||||
{isSelected && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '8px',
|
|
||||||
right: '8px',
|
|
||||||
backgroundColor: '#52c41a',
|
|
||||||
borderRadius: '50%',
|
|
||||||
width: '18px',
|
|
||||||
height: '18px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
|
||||||
zIndex: 2
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CheckOutlined style={{ color: 'white', fontSize: '10px' }} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content Section */}
|
|
||||||
<div style={{
|
|
||||||
flex: 1,
|
|
||||||
padding: size === 'small' ? '12px' : '16px',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
overflow: 'hidden'
|
|
||||||
}}>
|
|
||||||
<div style={{ flex: 1, overflow: 'hidden' }}>
|
|
||||||
<Title
|
|
||||||
level={size === 'small' ? 5 : 4}
|
|
||||||
style={{
|
|
||||||
margin: `0 0 ${size === 'small' ? '6px' : '8px'} 0`,
|
|
||||||
fontSize: size === 'small' ? '12px' : '14px',
|
|
||||||
fontWeight: 600,
|
|
||||||
lineHeight: '1.4',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
display: '-webkit-box',
|
|
||||||
WebkitLineClamp: size === 'small' ? 2 : 3,
|
|
||||||
WebkitBoxOrient: 'vertical'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sparepart.sparepart_name || sparepart.name || 'Unnamed'}
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
{/* Stock and Quantity Information */}
|
|
||||||
<div style={{ marginBottom: size === 'small' ? '6px' : '8px' }}>
|
|
||||||
{/* Stock Status Tag */}
|
|
||||||
<div style={{ marginBottom: '4px' }}>
|
|
||||||
<Tag
|
<Tag
|
||||||
color={sparepart.sparepart_stok === 'Available' ? 'green' : 'red'}
|
color={sparepart.sparepart_stok === 'Available' ? 'green' : 'red'}
|
||||||
style={{
|
style={{ fontSize: '11px', margin: 0 }}
|
||||||
fontSize: size === 'small' ? '9px' : '10px',
|
|
||||||
padding: '0 4px',
|
|
||||||
margin: 0,
|
|
||||||
height: 'auto'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{sparepart.sparepart_stok || 'Not Available'}
|
{sparepart.sparepart_stok || 'Not Available'}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quantity */}
|
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<Text
|
<Text style={{ fontSize: '12px', color: '#666', marginRight: '4px' }}>
|
||||||
style={{
|
|
||||||
fontSize: size === 'small' ? '10px' : '11px',
|
|
||||||
fontWeight: 500,
|
|
||||||
color: '#262626',
|
|
||||||
marginRight: '4px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
qty:
|
qty:
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: size === 'small' ? '10px' : '11px',
|
fontSize: '12px',
|
||||||
color: sparepart.sparepart_qty > 0 ? '#52c41a' : '#ff4d4f',
|
fontWeight: 600,
|
||||||
fontWeight: 600
|
color: '#262626'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{sparepart.sparepart_qty || 0}
|
{sparepart.sparepart_qty || 0}
|
||||||
{sparepart.sparepart_unit ? ` ${sparepart.sparepart_unit}` : ''}
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: size === 'small' ? '4px' : '6px' }}>
|
<Space size="small">
|
||||||
<Text
|
{showPreview && (
|
||||||
code
|
<Button
|
||||||
style={{
|
type="text"
|
||||||
fontSize: size === 'small' ? '10px' : '11px',
|
icon={<EyeOutlined />}
|
||||||
backgroundColor: '#f5f5f5',
|
size="small"
|
||||||
padding: '2px 6px',
|
onClick={(e) => {
|
||||||
borderRadius: '3px'
|
e.stopPropagation();
|
||||||
|
handlePreview();
|
||||||
}}
|
}}
|
||||||
>
|
title="Preview"
|
||||||
{sparepart.sparepart_code || 'No code'}
|
/>
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(sparepart.sparepart_merk || sparepart.sparepart_model) && (
|
|
||||||
<div style={{
|
|
||||||
fontSize: size === 'small' ? '9px' : '10px',
|
|
||||||
color: '#666',
|
|
||||||
lineHeight: '1.4',
|
|
||||||
marginBottom: '4px'
|
|
||||||
}}>
|
|
||||||
{sparepart.sparepart_merk && (
|
|
||||||
<div>Brand: {sparepart.sparepart_merk}</div>
|
|
||||||
)}
|
)}
|
||||||
{sparepart.sparepart_model && (
|
{showDelete && !isReadOnly && (
|
||||||
<div>Model: {sparepart.sparepart_model}</div>
|
<Button
|
||||||
)}
|
type="text"
|
||||||
</div>
|
icon={<DeleteOutlined />}
|
||||||
)}
|
size="small"
|
||||||
</div>
|
danger
|
||||||
|
onClick={(e) => {
|
||||||
<Text
|
e.stopPropagation();
|
||||||
type="secondary"
|
onDelete?.(sparepart);
|
||||||
style={{
|
|
||||||
fontSize: size === 'small' ? '9px' : '10px',
|
|
||||||
marginTop: 'auto',
|
|
||||||
paddingTop: '4px',
|
|
||||||
borderTop: '1px solid #f0f0f0'
|
|
||||||
}}
|
}}
|
||||||
>
|
title="Remove"
|
||||||
{sparepart.updated_at && dayjs(sparepart.updated_at).format('DD MMM YYYY')}
|
/>
|
||||||
</Text>
|
)}
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Preview Modal */}
|
|
||||||
<Modal
|
<Modal
|
||||||
title="Sparepart Details"
|
title="Sparepart Details"
|
||||||
open={previewModalVisible}
|
open={previewModalVisible}
|
||||||
@@ -374,7 +257,7 @@ const CustomSparepartCard = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Item Type Tag */}
|
|
||||||
{sparepart.sparepart_item_type && (
|
{sparepart.sparepart_item_type && (
|
||||||
<div style={{ marginBottom: '12px' }}>
|
<div style={{ marginBottom: '12px' }}>
|
||||||
<Tag color="blue" style={{ fontSize: '14px', padding: '4px 12px' }}>
|
<Tag color="blue" style={{ fontSize: '14px', padding: '4px 12px' }}>
|
||||||
@@ -383,7 +266,7 @@ const CustomSparepartCard = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Stock Status and Quantity */}
|
|
||||||
<div style={{
|
<div style={{
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
background: '#f8f9fa',
|
background: '#f8f9fa',
|
||||||
@@ -412,12 +295,12 @@ const CustomSparepartCard = ({
|
|||||||
|
|
||||||
<Col span={14}>
|
<Col span={14}>
|
||||||
<div>
|
<div>
|
||||||
{/* Sparepart Name */}
|
|
||||||
<Title level={3} style={{ marginBottom: '20px', color: '#262626' }}>
|
<Title level={3} style={{ marginBottom: '20px', color: '#262626' }}>
|
||||||
{sparepart.sparepart_name || 'Unnamed'}
|
{sparepart.sparepart_name || 'Unnamed'}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
{/* Basic Information */}
|
|
||||||
<div style={{ marginBottom: '24px' }}>
|
<div style={{ marginBottom: '24px' }}>
|
||||||
<Row gutter={[16, 12]}>
|
<Row gutter={[16, 12]}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
@@ -480,7 +363,7 @@ const CustomSparepartCard = ({
|
|||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Timeline Information */}
|
|
||||||
{sparepart.created_at && (
|
{sparepart.created_at && (
|
||||||
<div>
|
<div>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ const ErrorCodeForm = ({
|
|||||||
onErrorCodeIconRemove,
|
onErrorCodeIconRemove,
|
||||||
isEdit = false,
|
isEdit = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [statusValue, setStatusValue] = useState(true);
|
|
||||||
const [currentIcon, setCurrentIcon] = useState(null);
|
const [currentIcon, setCurrentIcon] = useState(null);
|
||||||
|
const statusWatch = Form.useWatch('status', errorCodeForm) ?? true;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (errorCodeIcon && typeof errorCodeIcon === 'object' && Object.keys(errorCodeIcon).length > 0) {
|
if (errorCodeIcon && typeof errorCodeIcon === 'object' && Object.keys(errorCodeIcon).length > 0) {
|
||||||
@@ -201,56 +201,19 @@ const ErrorCodeForm = ({
|
|||||||
error_code_color: '#000000'
|
error_code_color: '#000000'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item
|
{/* Header bar with color picker, icon upload, and status toggle */}
|
||||||
label="Status"
|
<div style={{
|
||||||
name="status"
|
display: 'flex',
|
||||||
valuePropName="checked"
|
justifyContent: 'space-between',
|
||||||
>
|
alignItems: 'flex-start',
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
marginBottom: '16px',
|
||||||
<Switch
|
gap: '16px'
|
||||||
defaultChecked={true}
|
}}>
|
||||||
onChange={(checked) => {
|
{/* Color picker on left */}
|
||||||
setStatusValue(checked);
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text style={{ marginLeft: 8 }}>
|
|
||||||
{statusValue ? 'Active' : 'Inactive'}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label="Error Name"
|
|
||||||
name="error_code_name"
|
|
||||||
rules={[{ required: true, message: 'Error name wajib diisi!' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="Enter error name" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item label="Description" name="error_code_description">
|
|
||||||
<Input.TextArea
|
|
||||||
placeholder="Enter error description"
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item label="Color & Icon">
|
|
||||||
<div style={{ display: 'flex', gap: '12px', alignItems: 'flex-start' }}>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="error_code_color"
|
name="error_code_color"
|
||||||
noStyle
|
noStyle
|
||||||
style={{ flex: '0 0 auto' }}
|
|
||||||
getValueFromEvent={(e) => e.target.value}
|
getValueFromEvent={(e) => e.target.value}
|
||||||
getValueProps={(value) => ({ value: value || '#000000' })}
|
getValueProps={(value) => ({ value: value || '#000000' })}
|
||||||
>
|
>
|
||||||
@@ -261,15 +224,61 @@ const ErrorCodeForm = ({
|
|||||||
height: '40px',
|
height: '40px',
|
||||||
border: '1px solid #d9d9d9',
|
border: '1px solid #d9d9d9',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
|
cursor: isErrorCodeFormReadOnly ? 'not-allowed' : 'pointer',
|
||||||
}}
|
}}
|
||||||
|
disabled={isErrorCodeFormReadOnly}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item noStyle style={{ flex: '1 1 auto' }}>
|
{/* Icon upload beside color picker */}
|
||||||
|
<div style={{ flex: 1, maxWidth: '300px' }}>
|
||||||
{renderIconUpload()}
|
{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>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Form.Item label="Description" name="error_code_description">
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="Enter error description"
|
||||||
|
rows={3}
|
||||||
|
disabled={isErrorCodeFormReadOnly}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
|||||||
@@ -394,9 +394,7 @@ const FileUploadHandler = ({
|
|||||||
</Upload>
|
</Upload>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* renderExistingFile() is disabled because SolutionField.jsx already handles file card rendering */}
|
|
||||||
{/* This prevents duplicate card rendering */}
|
|
||||||
{/* {renderExistingFile()} */}
|
|
||||||
|
|
||||||
{showPreview && (
|
{showPreview && (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -142,7 +142,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 +156,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
|
|||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: `Brand ${brand_name} deleted successfully.`,
|
message: `Brand ${brand_name} deleted successfully.`,
|
||||||
});
|
});
|
||||||
doFilter(); // Refresh data
|
doFilter();
|
||||||
} else {
|
} else {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ const ListErrorCode = ({
|
|||||||
searchText,
|
searchText,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onSearch,
|
onSearch,
|
||||||
onSearchClear
|
onSearchClear,
|
||||||
|
isReadOnly = false,
|
||||||
|
errorCodes: propErrorCodes = null
|
||||||
}) => {
|
}) => {
|
||||||
const [errorCodes, setErrorCodes] = useState([]);
|
const [errorCodes, setErrorCodes] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -25,7 +27,7 @@ const ListErrorCode = ({
|
|||||||
total_page: 0,
|
total_page: 0,
|
||||||
});
|
});
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const pageSize = 15; // Fixed limit 15 items per page
|
const pageSize = 15;
|
||||||
|
|
||||||
const queryParams = useMemo(() => {
|
const queryParams = useMemo(() => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@@ -79,8 +81,15 @@ const ListErrorCode = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isReadOnly && propErrorCodes) {
|
||||||
|
|
||||||
|
setErrorCodes(propErrorCodes);
|
||||||
|
setLoading(false);
|
||||||
|
} else {
|
||||||
|
|
||||||
fetchErrorCodes();
|
fetchErrorCodes();
|
||||||
}, [brandId, queryParams, tempErrorCodes, trigerFilter]);
|
}
|
||||||
|
}, [brandId, queryParams, tempErrorCodes, trigerFilter, isReadOnly, propErrorCodes]);
|
||||||
|
|
||||||
const handlePrevious = () => {
|
const handlePrevious = () => {
|
||||||
if (pagination.current_page > 1) {
|
if (pagination.current_page > 1) {
|
||||||
@@ -117,7 +126,7 @@ const ListErrorCode = ({
|
|||||||
title: 'Hapus Error Code',
|
title: 'Hapus Error Code',
|
||||||
message: `Apakah Anda yakin ingin menghapus error code ${item.error_code}?`,
|
message: `Apakah Anda yakin ingin menghapus error code ${item.error_code}?`,
|
||||||
onConfirm: () => performDelete(item),
|
onConfirm: () => performDelete(item),
|
||||||
onCancel: () => {},
|
onCancel: () => { },
|
||||||
confirmButtonText: 'Hapus'
|
confirmButtonText: 'Hapus'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -125,7 +134,7 @@ const ListErrorCode = ({
|
|||||||
|
|
||||||
const performDelete = async (item) => {
|
const performDelete = async (item) => {
|
||||||
try {
|
try {
|
||||||
// Additional validation
|
|
||||||
if (!item.error_code_id || item.error_code_id === 'undefined') {
|
if (!item.error_code_id || item.error_code_id === 'undefined') {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
@@ -205,7 +214,6 @@ const ListErrorCode = ({
|
|||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
height: '32px',
|
height: '32px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: '300px'
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Form, Input, Button, Switch, Radio, Typography, Space, Card } from 'antd';
|
import { Form, Input, Button, Switch, Radio, Typography, Space, Card, ConfigProvider } from 'antd';
|
||||||
import { DeleteOutlined, EyeOutlined, FileOutlined } from '@ant-design/icons';
|
import { DeleteOutlined, EyeOutlined, FileOutlined } from '@ant-design/icons';
|
||||||
import FileUploadHandler from './FileUploadHandler';
|
import FileUploadHandler from './FileUploadHandler';
|
||||||
import { NotifAlert } from '../../../../components/Global/ToastNotif';
|
import { NotifAlert } from '../../../../components/Global/ToastNotif';
|
||||||
@@ -30,24 +30,22 @@ const SolutionFieldNew = ({
|
|||||||
|
|
||||||
const fileUpload = Form.useWatch(['solution_items', fieldKey, 'fileUpload'], form);
|
const fileUpload = Form.useWatch(['solution_items', fieldKey, 'fileUpload'], form);
|
||||||
const file = Form.useWatch(['solution_items', fieldKey, 'file'], form);
|
const file = Form.useWatch(['solution_items', fieldKey, 'file'], form);
|
||||||
|
|
||||||
const getSolutionData = () => {
|
|
||||||
try {
|
|
||||||
return form.getFieldValue(['solution_items', fieldKey]) || {};
|
|
||||||
} catch (error) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const nameValue = Form.useWatch(['solution_items', fieldKey, 'name'], form);
|
const nameValue = Form.useWatch(['solution_items', fieldKey, 'name'], form);
|
||||||
const typeValue = Form.useWatch(['solution_items', fieldKey, 'type'], form);
|
|
||||||
const textValue = Form.useWatch(['solution_items', fieldKey, 'text'], form);
|
|
||||||
const fileNameValue = Form.useWatch(['solution_items', fieldKey, 'fileName'], form);
|
const fileNameValue = Form.useWatch(['solution_items', fieldKey, 'fileName'], form);
|
||||||
|
const statusValue = Form.useWatch(['solution_items', fieldKey, 'status'], form) ?? true;
|
||||||
|
|
||||||
const pathSolution = Form.useWatch(['solution_items', fieldKey, 'path_solution'], form);
|
const pathSolution = Form.useWatch(['solution_items', fieldKey, 'path_solution'], form);
|
||||||
|
|
||||||
const [deleteCounter, setDeleteCounter] = useState(0);
|
const [deleteCounter, setDeleteCounter] = useState(0);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!nameValue || nameValue === '') {
|
||||||
|
setCurrentFile(null);
|
||||||
|
setIsDeleted(false);
|
||||||
|
setDeleteCounter(prev => prev + 1);
|
||||||
|
}
|
||||||
|
}, [nameValue]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const getFileFromFormValues = () => {
|
const getFileFromFormValues = () => {
|
||||||
const hasValidFileUpload = fileUpload && typeof fileUpload === 'object' && Object.keys(fileUpload).length > 0;
|
const hasValidFileUpload = fileUpload && typeof fileUpload === 'object' && Object.keys(fileUpload).length > 0;
|
||||||
@@ -363,6 +361,16 @@ const SolutionFieldNew = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
components: {
|
||||||
|
Switch: {
|
||||||
|
colorPrimary: '#23A55A',
|
||||||
|
colorPrimaryHover: '#23A55A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{
|
<div style={{
|
||||||
border: '1px solid #d9d9d9',
|
border: '1px solid #d9d9d9',
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
@@ -392,10 +400,6 @@ const SolutionFieldNew = ({
|
|||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
onStatusChange(fieldKey, checked);
|
onStatusChange(fieldKey, checked);
|
||||||
}}
|
}}
|
||||||
defaultChecked={solutionStatus !== false}
|
|
||||||
style={{
|
|
||||||
backgroundColor: solutionStatus !== false ? '#23A55A' : '#bfbfbf'
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Text style={{
|
<Text style={{
|
||||||
@@ -403,7 +407,7 @@ const SolutionFieldNew = ({
|
|||||||
color: '#666',
|
color: '#666',
|
||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap'
|
||||||
}}>
|
}}>
|
||||||
{solutionStatus !== false ? 'Active' : 'Inactive'}
|
{statusValue ? 'Active' : 'Inactive'}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -485,6 +489,7 @@ const SolutionFieldNew = ({
|
|||||||
|
|
||||||
{renderSolutionContent()}
|
{renderSolutionContent()}
|
||||||
</div>
|
</div>
|
||||||
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Select, Typography, Tag, Spin, Empty, Button, Row, Col } from 'antd';
|
import { Select, Typography, Tag, Spin, Empty, Button } from 'antd';
|
||||||
import { PlusOutlined, DeleteOutlined, CheckOutlined, EyeOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
import { PlusOutlined, DeleteOutlined, CheckOutlined, EyeOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { getAllSparepart } from '../../../../api/sparepart';
|
import { getAllSparepart } from '../../../../api/sparepart';
|
||||||
import CustomSparepartCard from './CustomSparepartCard';
|
import CustomSparepartCard from './CustomSparepartCard';
|
||||||
@@ -36,7 +36,7 @@ const SparepartSelect = ({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('limit', '1000');
|
params.set('limit', '10');
|
||||||
|
|
||||||
if (searchQuery && searchQuery.trim() !== '') {
|
if (searchQuery && searchQuery.trim() !== '') {
|
||||||
params.set('criteria', searchQuery.trim());
|
params.set('criteria', searchQuery.trim());
|
||||||
@@ -96,8 +96,8 @@ const SparepartSelect = ({
|
|||||||
const isAlreadySelected = selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id);
|
const isAlreadySelected = selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col xs={24} sm={24} md={12} lg={12} key={sparepart.sparepart_id}>
|
|
||||||
<CustomSparepartCard
|
<CustomSparepartCard
|
||||||
|
key={sparepart.sparepart_id}
|
||||||
sparepart={sparepart}
|
sparepart={sparepart}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
@@ -105,17 +105,13 @@ const SparepartSelect = ({
|
|||||||
showDelete={isAlreadySelected && !isReadOnly}
|
showDelete={isAlreadySelected && !isReadOnly}
|
||||||
onCardClick={!isAlreadySelected && !isReadOnly ? () => handleSparepartSelect(sparepart.sparepart_id) : undefined}
|
onCardClick={!isAlreadySelected && !isReadOnly ? () => handleSparepartSelect(sparepart.sparepart_id) : undefined}
|
||||||
onDelete={() => handleRemoveSparepart(sparepart.sparepart_id)}
|
onDelete={() => handleRemoveSparepart(sparepart.sparepart_id)}
|
||||||
style={{
|
|
||||||
border: isAlreadySelected ? '2px solid #52c41a' : undefined,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Fixed Search Section */}
|
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<div style={{
|
<div style={{
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
@@ -141,7 +137,7 @@ const SparepartSelect = ({
|
|||||||
>
|
>
|
||||||
{spareparts
|
{spareparts
|
||||||
.filter(sparepart => !selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id))
|
.filter(sparepart => !selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id))
|
||||||
.slice(0, 5)
|
.slice(0, 10)
|
||||||
.map((sparepart) => (
|
.map((sparepart) => (
|
||||||
<Option key={sparepart.sparepart_id} value={sparepart.sparepart_id}>
|
<Option key={sparepart.sparepart_id} value={sparepart.sparepart_id}>
|
||||||
<div>
|
<div>
|
||||||
@@ -156,16 +152,16 @@ const SparepartSelect = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Scrollable Selected Items Section */}
|
|
||||||
<div>
|
<div>
|
||||||
{selectedSpareparts.length > 0 ? (
|
{selectedSpareparts.length > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<Title level={5} style={{ marginBottom: 16 }}>
|
<Title level={5} style={{ marginBottom: 16 }}>
|
||||||
Selected Spareparts ({selectedSpareparts.length})
|
Selected Spareparts ({selectedSpareparts.length})
|
||||||
</Title>
|
</Title>
|
||||||
<Row gutter={[16, 16]}>
|
<div>
|
||||||
{selectedSpareparts.map(sparepart => renderSparepartCard(sparepart, true))}
|
{selectedSpareparts.map(sparepart => renderSparepartCard(sparepart, true))}
|
||||||
</Row>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Empty
|
<Empty
|
||||||
|
|||||||
@@ -1,263 +0,0 @@
|
|||||||
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) => {
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,453 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
export const useSolutionLogic = (solutionForm) => {
|
|
||||||
const [solutionFields, setSolutionFields] = useState([0]);
|
|
||||||
const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' });
|
|
||||||
const [solutionStatuses, setSolutionStatuses] = useState({ 0: true });
|
|
||||||
const [solutionsToDelete, setSolutionsToDelete] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (solutionForm) {
|
|
||||||
solutionForm.setFieldsValue({
|
|
||||||
solution_items: {
|
|
||||||
0: {
|
|
||||||
name: 'Solution 1',
|
|
||||||
status: true,
|
|
||||||
type: 'text',
|
|
||||||
text: 'Solution description',
|
|
||||||
file: null,
|
|
||||||
fileUpload: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}, [solutionForm]);
|
|
||||||
|
|
||||||
const handleAddSolutionField = () => {
|
|
||||||
const newKey = Date.now();
|
|
||||||
|
|
||||||
setSolutionFields(prev => [...prev, newKey]);
|
|
||||||
setSolutionTypes(prev => ({ ...prev, [newKey]: 'text' }));
|
|
||||||
setSolutionStatuses(prev => ({ ...prev, [newKey]: true }));
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const currentFormValues = solutionForm.getFieldsValue(true);
|
|
||||||
const existingNames = [];
|
|
||||||
|
|
||||||
Object.keys(currentFormValues).forEach(key => {
|
|
||||||
if (key.startsWith('solution_items,') || key.startsWith('solution_items.')) {
|
|
||||||
const solutionData = currentFormValues[key];
|
|
||||||
if (solutionData && solutionData.name) {
|
|
||||||
existingNames.push(solutionData.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (currentFormValues.solution_items) {
|
|
||||||
Object.values(currentFormValues.solution_items).forEach(solution => {
|
|
||||||
if (solution && solution.name) {
|
|
||||||
existingNames.push(solution.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let solutionNumber = solutionFields.length + 1;
|
|
||||||
let defaultName = `Solution ${solutionNumber}`;
|
|
||||||
|
|
||||||
while (existingNames.includes(defaultName)) {
|
|
||||||
solutionNumber++;
|
|
||||||
defaultName = `Solution ${solutionNumber}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'name'], defaultName);
|
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'type'], 'text');
|
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'text'], 'Solution description');
|
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'status'], true);
|
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'file'], null);
|
|
||||||
solutionForm.setFieldValue(['solution_items', newKey, 'fileUpload'], null);
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveSolutionField = (key) => {
|
|
||||||
if (solutionFields.length <= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSolutionFields(prev => prev.filter(field => field !== key));
|
|
||||||
|
|
||||||
const newTypes = { ...solutionTypes };
|
|
||||||
const newStatuses = { ...solutionStatuses };
|
|
||||||
delete newTypes[key];
|
|
||||||
delete newStatuses[key];
|
|
||||||
|
|
||||||
setSolutionTypes(newTypes);
|
|
||||||
setSolutionStatuses(newStatuses);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
solutionForm.setFieldValue(['solution_items', key], undefined);
|
|
||||||
solutionForm.setFieldValue(['solution_items', key, 'name'], undefined);
|
|
||||||
solutionForm.setFieldValue(['solution_items', key, 'type'], undefined);
|
|
||||||
solutionForm.setFieldValue(['solution_items', key, 'text'], undefined);
|
|
||||||
solutionForm.setFieldValue(['solution_items', key, 'status'], undefined);
|
|
||||||
solutionForm.setFieldValue(['solution_items', key, 'file'], undefined);
|
|
||||||
solutionForm.setFieldValue(['solution_items', key, 'fileUpload'], undefined);
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSolutionTypeChange = (key, value) => {
|
|
||||||
setSolutionTypes(prev => ({ ...prev, [key]: value }));
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const fieldName = ['solution_items', key];
|
|
||||||
const currentSolutionData = solutionForm.getFieldsValue([fieldName]) || {};
|
|
||||||
const solutionData = currentSolutionData[`solution_items,${key}`] || currentSolutionData[`solution_items.${key}`] || {};
|
|
||||||
|
|
||||||
if (value === 'text') {
|
|
||||||
const updatedSolutionData = {
|
|
||||||
...solutionData,
|
|
||||||
fileUpload: null,
|
|
||||||
file: null,
|
|
||||||
path_solution: null,
|
|
||||||
fileName: null,
|
|
||||||
text: solutionData.text || 'Solution description'
|
|
||||||
};
|
|
||||||
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'fileUpload'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'file'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'path_solution'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'fileName'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'text'], updatedSolutionData.text);
|
|
||||||
} else if (value === 'file') {
|
|
||||||
const updatedSolutionData = {
|
|
||||||
...solutionData,
|
|
||||||
text: '',
|
|
||||||
fileUpload: null,
|
|
||||||
file: null,
|
|
||||||
path_solution: null,
|
|
||||||
fileName: null
|
|
||||||
};
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'text'], '');
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'fileUpload'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'file'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'path_solution'], null);
|
|
||||||
solutionForm.setFieldValue([...fieldName, 'fileName'], null);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSolutionStatusChange = (key, value) => {
|
|
||||||
setSolutionStatuses(prev => ({ ...prev, [key]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetSolutionFields = () => {
|
|
||||||
setSolutionFields([0]);
|
|
||||||
setSolutionTypes({ 0: 'text' });
|
|
||||||
setSolutionStatuses({ 0: true });
|
|
||||||
|
|
||||||
if (!solutionForm || !solutionForm.resetFields) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
solutionForm.resetFields();
|
|
||||||
setTimeout(() => {
|
|
||||||
solutionForm.setFieldsValue({
|
|
||||||
solution_items: {
|
|
||||||
0: {
|
|
||||||
name: 'Solution 1',
|
|
||||||
status: true,
|
|
||||||
type: 'text',
|
|
||||||
text: 'Solution description',
|
|
||||||
file: null,
|
|
||||||
fileUpload: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
solutionForm.setFieldValue(['solution_items', 0, 'name'], 'Solution 1');
|
|
||||||
solutionForm.setFieldValue(['solution_items', 0, 'type'], 'text');
|
|
||||||
solutionForm.setFieldValue(['solution_items', 0, 'text'], 'Solution description');
|
|
||||||
solutionForm.setFieldValue(['solution_items', 0, 'status'], true);
|
|
||||||
solutionForm.setFieldValue(['solution_items', 0, 'file'], null);
|
|
||||||
solutionForm.setFieldValue(['solution_items', 0, 'fileUpload'], null);
|
|
||||||
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkFirstSolutionValid = () => {
|
|
||||||
if (!solutionForm || !solutionForm.getFieldsValue) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const values = solutionForm.getFieldsValue();
|
|
||||||
|
|
||||||
const firstField = solutionFields[0];
|
|
||||||
if (!firstField) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const solutionKey = firstField.key || firstField;
|
|
||||||
const commaPath = `solution_items,${solutionKey}`;
|
|
||||||
const dotPath = `solution_items.${solutionKey}`;
|
|
||||||
const firstSolution = values[commaPath] || values[dotPath];
|
|
||||||
|
|
||||||
if (!firstSolution || !firstSolution.name || firstSolution.name.trim() === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (solutionTypes[solutionKey] === 'text' && (!firstSolution.text || firstSolution.text.trim() === '')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSolutionData = () => {
|
|
||||||
try {
|
|
||||||
const values = solutionForm.getFieldsValue(true);
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
solutionFields.forEach(key => {
|
|
||||||
let solution = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
solution = solutionForm.getFieldValue(['solution_items', key]);
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!solution && values.solution_items && values.solution_items[key]) {
|
|
||||||
solution = values.solution_items[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!solution) {
|
|
||||||
const commaKey = `solution_items,${key}`;
|
|
||||||
solution = values[commaKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!solution) {
|
|
||||||
const dotKey = `solution_items.${key}`;
|
|
||||||
solution = values[dotKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!solution) {
|
|
||||||
const allKeys = Object.keys(values);
|
|
||||||
const foundKey = allKeys.find(k =>
|
|
||||||
k.includes(key.toString()) &&
|
|
||||||
k.includes('solution_items')
|
|
||||||
);
|
|
||||||
if (foundKey) {
|
|
||||||
solution = values[foundKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!solution) {
|
|
||||||
const rawValues = solutionForm.getFieldsValue();
|
|
||||||
|
|
||||||
if (rawValues.solution_items && rawValues.solution_items[key]) {
|
|
||||||
solution = rawValues.solution_items[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!solution) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const hasName = solution.name && solution.name.trim() !== '';
|
|
||||||
|
|
||||||
if (!hasName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const solutionType = solutionTypes[key] || solution.type || 'text';
|
|
||||||
let isValidType = true;
|
|
||||||
|
|
||||||
if (solutionType === 'text') {
|
|
||||||
isValidType = solution.text && solution.text.trim() !== '';
|
|
||||||
if (!isValidType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (solutionType === 'file') {
|
|
||||||
const hasPathSolution = solution.path_solution && solution.path_solution.trim() !== '';
|
|
||||||
const hasFileUpload = (solution.fileUpload && typeof solution.fileUpload === 'object' && Object.keys(solution.fileUpload).length > 0);
|
|
||||||
const hasFile = (solution.file && typeof solution.file === 'object' && Object.keys(solution.file).length > 0);
|
|
||||||
|
|
||||||
isValidType = hasPathSolution || hasFileUpload || hasFile;
|
|
||||||
if (!isValidType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let pathSolution = '';
|
|
||||||
let fileObject = null;
|
|
||||||
const typeSolution = solutionTypes[key] || solution.type || 'text';
|
|
||||||
|
|
||||||
if (typeSolution === 'file') {
|
|
||||||
if (solution.fileUpload && typeof solution.fileUpload === 'object' && Object.keys(solution.fileUpload).length > 0) {
|
|
||||||
pathSolution = solution.fileUpload.path_solution || solution.fileUpload.uploadPath || '';
|
|
||||||
fileObject = solution.fileUpload;
|
|
||||||
} else if (solution.file && typeof solution.file === 'object' && Object.keys(solution.file).length > 0) {
|
|
||||||
pathSolution = solution.file.path_solution || solution.file.uploadPath || '';
|
|
||||||
fileObject = solution.file;
|
|
||||||
} else if (solution.file && typeof solution.file === 'string' && solution.file.trim() !== '') {
|
|
||||||
pathSolution = solution.file;
|
|
||||||
} else if (solution.path_solution && solution.path_solution.trim() !== '') {
|
|
||||||
pathSolution = solution.path_solution;
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalTypeSolution = typeSolution;
|
|
||||||
if (typeSolution === 'file') {
|
|
||||||
if (fileObject && fileObject.type_solution) {
|
|
||||||
finalTypeSolution = fileObject.type_solution;
|
|
||||||
} else {
|
|
||||||
finalTypeSolution = 'image';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalSolution = {
|
|
||||||
solution_name: solution.name,
|
|
||||||
type_solution: finalTypeSolution,
|
|
||||||
is_active: solution.status !== false && solution.status !== undefined ? solution.status : (solutionStatuses[key] !== false),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeSolution === 'text') {
|
|
||||||
finalSolution.text_solution = solution.text || '';
|
|
||||||
finalSolution.path_solution = '';
|
|
||||||
} else {
|
|
||||||
finalSolution.text_solution = '';
|
|
||||||
finalSolution.path_solution = pathSolution;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(finalSolution);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setSolutionsForExistingRecord = (solutions, form) => {
|
|
||||||
if (!solutions || solutions.length === 0) return;
|
|
||||||
|
|
||||||
const newFields = solutions.map((solution, index) => solution.id || index);
|
|
||||||
|
|
||||||
setSolutionFields(newFields);
|
|
||||||
|
|
||||||
const solutionsValues = {};
|
|
||||||
const newTypes = {};
|
|
||||||
const newStatuses = {};
|
|
||||||
|
|
||||||
solutions.forEach((solution, index) => {
|
|
||||||
const key = solution.brand_code_solution_id || solution.id || index;
|
|
||||||
|
|
||||||
let fileObject = null;
|
|
||||||
if (solution.path_solution && solution.path_solution.trim() !== '') {
|
|
||||||
const fileName = solution.file_upload_name || solution.path_solution.split('/').pop() || `file_${index}`;
|
|
||||||
|
|
||||||
fileObject = {
|
|
||||||
uploadPath: solution.path_solution,
|
|
||||||
path_solution: solution.path_solution,
|
|
||||||
name: fileName,
|
|
||||||
type_solution: solution.type_solution || 'image',
|
|
||||||
isExisting: true,
|
|
||||||
size: 0,
|
|
||||||
type: solution.type_solution === 'pdf' ? 'application/pdf' : 'image/jpeg',
|
|
||||||
fileExtension: solution.type_solution === 'pdf' ? 'pdf' : (fileName.split('.').pop().toLowerCase() || 'jpg')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFileType = solution.type_solution && solution.type_solution !== 'text' && fileObject;
|
|
||||||
|
|
||||||
solutionsValues[key] = {
|
|
||||||
name: solution.solution_name || '',
|
|
||||||
type: isFileType ? 'file' : 'text',
|
|
||||||
text: solution.text_solution || '',
|
|
||||||
file: fileObject,
|
|
||||||
fileUpload: fileObject,
|
|
||||||
status: solution.is_active !== false,
|
|
||||||
path_solution: solution.path_solution || ''
|
|
||||||
};
|
|
||||||
newTypes[key] = isFileType ? 'file' : 'text';
|
|
||||||
newStatuses[key] = solution.is_active !== false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const nestedFormValues = {
|
|
||||||
solution_items: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(solutionsValues).forEach(key => {
|
|
||||||
const solution = solutionsValues[key];
|
|
||||||
nestedFormValues.solution_items[key] = {
|
|
||||||
name: solution.name,
|
|
||||||
type: solution.type,
|
|
||||||
text: solution.text,
|
|
||||||
file: solution.file,
|
|
||||||
fileUpload: solution.fileUpload,
|
|
||||||
status: solution.status,
|
|
||||||
path_solution: solution.path_solution
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
form.setFieldsValue(nestedFormValues);
|
|
||||||
|
|
||||||
const fallbackFormValues = {};
|
|
||||||
Object.keys(solutionsValues).forEach(key => {
|
|
||||||
const solution = solutionsValues[key];
|
|
||||||
fallbackFormValues[`solution_items,${key}`] = {
|
|
||||||
name: solution.name,
|
|
||||||
type: solution.type,
|
|
||||||
text: solution.text,
|
|
||||||
file: solution.file,
|
|
||||||
fileUpload: solution.fileUpload,
|
|
||||||
status: solution.status,
|
|
||||||
path_solution: solution.path_solution
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
form.setFieldsValue(fallbackFormValues);
|
|
||||||
|
|
||||||
Object.keys(solutionsValues).forEach(key => {
|
|
||||||
const solution = solutionsValues[key];
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'name'], solution.name);
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'type'], solution.type);
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'text'], solution.text);
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'file'], solution.file);
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'fileUpload'], solution.fileUpload);
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'status'], solution.status);
|
|
||||||
form.setFieldValue([`solution_items,${key}`, 'path_solution'], solution.path_solution);
|
|
||||||
|
|
||||||
form.setFieldValue(['solution_items', key, 'name'], solution.name);
|
|
||||||
form.setFieldValue(['solution_items', key, 'type'], solution.type);
|
|
||||||
form.setFieldValue(['solution_items', key, 'text'], solution.text);
|
|
||||||
form.setFieldValue(['solution_items', key, 'file'], solution.file);
|
|
||||||
form.setFieldValue(['solution_items', key, 'fileUpload'], solution.fileUpload);
|
|
||||||
form.setFieldValue(['solution_items', key, 'status'], solution.status);
|
|
||||||
form.setFieldValue(['solution_items', key, 'path_solution'], solution.path_solution);
|
|
||||||
});
|
|
||||||
|
|
||||||
setSolutionTypes(newTypes);
|
|
||||||
setSolutionStatuses(newStatuses);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
solutionFields,
|
|
||||||
solutionTypes,
|
|
||||||
solutionStatuses,
|
|
||||||
solutionsToDelete,
|
|
||||||
firstSolutionValid: checkFirstSolutionValid(),
|
|
||||||
handleAddSolutionField,
|
|
||||||
handleRemoveSolutionField,
|
|
||||||
handleSolutionTypeChange,
|
|
||||||
handleSolutionStatusChange,
|
|
||||||
resetSolutionFields,
|
|
||||||
checkFirstSolutionValid,
|
|
||||||
getSolutionData,
|
|
||||||
setSolutionsForExistingRecord,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -60,7 +60,10 @@ 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: (formData.device_description && formData.device_description.trim() !== '') ? formData.device_description : ' ',
|
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,
|
listen_channel: formData.listen_channel,
|
||||||
@@ -184,7 +187,6 @@ const DetailDevice = (props) => {
|
|||||||
defaultBorderColor: '#23A55A',
|
defaultBorderColor: '#23A55A',
|
||||||
defaultHoverColor: '#23A55A',
|
defaultHoverColor: '#23A55A',
|
||||||
defaultHoverBorderColor: '#23A55A',
|
defaultHoverBorderColor: '#23A55A',
|
||||||
defaultHoverColor: '#23A55A',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -50,27 +50,27 @@ const GeneratePdf = (props) => {
|
|||||||
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('No. Permit', 10, 54);
|
||||||
doc.text(":", 59, 54);
|
doc.text(':', 59, 54);
|
||||||
doc.text("Spesifik Lokasi", 120, 54);
|
doc.text('Spesifik Lokasi', 120, 54);
|
||||||
doc.text(":", 160, 54);
|
doc.text(':', 160, 54);
|
||||||
|
|
||||||
doc.text("No. Order", 10, 60);
|
doc.text('No. Order', 10, 60);
|
||||||
doc.text(":", 59, 60);
|
doc.text(':', 59, 60);
|
||||||
doc.text("Jum. Personil Terlihat", 120, 60);
|
doc.text('Jum. Personil Terlihat', 120, 60);
|
||||||
doc.text(":", 160, 60);
|
doc.text(':', 160, 60);
|
||||||
|
|
||||||
doc.text("Peralatan yang digunakan", 10, 66);
|
doc.text('Peralatan yang digunakan', 10, 66);
|
||||||
doc.text(":", 59, 66);
|
doc.text(':', 59, 66);
|
||||||
|
|
||||||
doc.text("Jenis APD yang digunakan", 10, 72);
|
doc.text('Jenis APD yang digunakan', 10, 72);
|
||||||
doc.text(":", 59, 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,7 +101,6 @@ const GeneratePdf = (props) => {
|
|||||||
defaultBorderColor: '#23A55A',
|
defaultBorderColor: '#23A55A',
|
||||||
defaultHoverColor: '#23A55A',
|
defaultHoverColor: '#23A55A',
|
||||||
defaultHoverBorderColor: '#23A55A',
|
defaultHoverBorderColor: '#23A55A',
|
||||||
defaultHoverColor: '#23A55A',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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,16 +74,20 @@ 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: (formData.plant_sub_section_description && formData.plant_sub_section_description.trim() !== '') ? formData.plant_sub_section_description : ' ',
|
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'
|
||||||
@@ -126,17 +130,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'
|
||||||
|
|||||||
@@ -95,11 +95,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 +169,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,7 +209,10 @@ const DetailSparepart = (props) => {
|
|||||||
sparepart_name: formData.sparepart_name, // Wajib
|
sparepart_name: formData.sparepart_name, // Wajib
|
||||||
};
|
};
|
||||||
|
|
||||||
payload.sparepart_description = (formData.sparepart_description && formData.sparepart_description.trim() !== '') ? formData.sparepart_description : ' ';
|
payload.sparepart_description =
|
||||||
|
formData.sparepart_description && formData.sparepart_description.trim() !== ''
|
||||||
|
? 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;
|
||||||
}
|
}
|
||||||
@@ -233,13 +236,13 @@ const DetailSparepart = (props) => {
|
|||||||
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({
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ const DetailNotification = memo(function DetailNotification({ selectedData, onCl
|
|||||||
|
|
||||||
// Get error code data from the nested structure
|
// Get error code data from the nested structure
|
||||||
const errorCodeData = selectedData.error_code;
|
const errorCodeData = selectedData.error_code;
|
||||||
const solutionData = errorCodeData?.solution?.[0] || {};
|
// Get active solution (is_active: true) or first solution
|
||||||
const sparepartsData = errorCodeData?.spareparts || [];
|
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
|
// Determine notification type based on is_read status
|
||||||
const getTypeFromStatus = () => {
|
const getTypeFromStatus = () => {
|
||||||
@@ -137,7 +138,7 @@ const DetailNotification = memo(function DetailNotification({ selectedData, onCl
|
|||||||
Solusi
|
Solusi
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '13px', color: '#262626', fontWeight: 500 }}>
|
<div style={{ fontSize: '13px', color: '#262626', fontWeight: 500 }}>
|
||||||
{solutionData?.solution_name || 'N/A'}
|
{activeSolution?.solution_name || 'N/A'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -38,7 +38,14 @@ 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 { getAllNotification } from '../../../api/notification';
|
import {
|
||||||
|
getAllNotification,
|
||||||
|
getNotificationLogByNotificationId,
|
||||||
|
getNotificationDetail,
|
||||||
|
resendChatByUser,
|
||||||
|
resendChatAllUser,
|
||||||
|
searchData,
|
||||||
|
} from '../../../api/notification';
|
||||||
|
|
||||||
const { Text, Paragraph, Link: AntdLink } = Typography;
|
const { Text, Paragraph, Link: AntdLink } = Typography;
|
||||||
|
|
||||||
@@ -47,87 +54,37 @@ 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.device_name || 'Unknown Device',
|
title: item.error_code_name || 'Unknown Error',
|
||||||
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.solution_name || 'N/A',
|
subsection: item.plant_sub_section_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.solution_name,
|
solutionName: item.error_code?.solution?.[0]?.solution_name || 'N/A',
|
||||||
typeSolution: item.type_solution,
|
typeSolution: item.error_code?.solution?.[0]?.type_solution || 'N/A',
|
||||||
pathSolution: item.path_solution,
|
pathSolution:
|
||||||
|
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');
|
||||||
@@ -137,6 +94,10 @@ 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,
|
||||||
@@ -238,9 +199,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',
|
||||||
onOk() {
|
async 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.`
|
||||||
);
|
);
|
||||||
@@ -259,13 +220,49 @@ 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 = () => {
|
||||||
setSearchTerm(searchValue);
|
fetchSearch(searchValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchClear = () => {
|
const handleSearchClear = () => {
|
||||||
setSearchValue('');
|
setSearchValue('');
|
||||||
setSearchTerm('');
|
fetchSearch('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUnreadCount = () => notifications.filter((n) => !n.isRead).length;
|
const getUnreadCount = () => notifications.filter((n) => !n.isRead).length;
|
||||||
@@ -280,6 +277,78 @@ 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',
|
||||||
@@ -314,7 +383,6 @@ 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={{
|
||||||
@@ -353,7 +421,7 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
<Text strong>{notification.title}</Text>
|
<Text strong>{notification.title}</Text>
|
||||||
<div style={{ marginTop: '4px' }}>
|
<div style={{ marginTop: '4px' }}>
|
||||||
<Text style={{ color }}>
|
<Text style={{ color }}>
|
||||||
{notification.issue}
|
Error Code {notification.issue}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -370,7 +438,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',
|
||||||
@@ -393,12 +461,18 @@ 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">
|
||||||
@@ -412,17 +486,10 @@ 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: '8px' }}
|
style={{ paddingLeft: '0px' }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleResend(notification);
|
handleResend(notification);
|
||||||
@@ -458,13 +525,23 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
border: '1px solid #1890ff',
|
border: '1px solid #1890ff',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={async (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={`/detail-notification/${
|
to={`/notification-detail/${
|
||||||
notification.id.split('-')[1]
|
notification.id.split('-')[1]
|
||||||
}`}
|
}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -499,6 +576,15 @@ 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');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -517,6 +603,11 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
|
|
||||||
const renderUserHistory = () => (
|
const renderUserHistory = () => (
|
||||||
<>
|
<>
|
||||||
|
{userLoading ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '24px' }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
||||||
{userHistoryData.map((user) => (
|
{userHistoryData.map((user) => (
|
||||||
<Card key={user.id} style={{ borderColor: '#91d5ff' }}>
|
<Card key={user.id} style={{ borderColor: '#91d5ff' }}>
|
||||||
@@ -529,31 +620,75 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
<MobileOutlined /> {user.phone}
|
<MobileOutlined /> {user.phone}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>|</Text>
|
<Text>|</Text>
|
||||||
<Badge status="success" text={user.status} />
|
<Badge
|
||||||
|
status={
|
||||||
|
user.status === 'Delivered' ? 'success' : 'default'
|
||||||
|
}
|
||||||
|
text={user.status}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
<Divider style={{ margin: '8px 0' }} />
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
<Space align="center">
|
<Space align="center">
|
||||||
|
{user.status === 'Delivered' ? (
|
||||||
<CheckCircleFilled style={{ color: '#52c41a' }} />
|
<CheckCircleFilled style={{ color: '#52c41a' }} />
|
||||||
|
) : (
|
||||||
|
<ClockCircleOutlined style={{ color: '#faad14' }} />
|
||||||
|
)}
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
Success Delivered at {user.timestamp}
|
{user.status === 'Delivered'
|
||||||
|
? 'Success Delivered at'
|
||||||
|
: 'Status '}{' '}
|
||||||
|
{user.timestamp}
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<Button type="primary" ghost icon={<SendOutlined />}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
icon={<SendOutlined />}
|
||||||
|
onClick={async () => {
|
||||||
|
await resendChatByUser(user.id, user.phone);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Resend
|
Resend
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
{userHistoryData.length === 0 && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '24px', color: '#8c8c8c' }}>
|
||||||
|
No user history available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderLogHistory = () => (
|
const renderLogHistory = () => (
|
||||||
<>
|
<>
|
||||||
<div style={{ padding: '0 16px', position: 'relative' }}>
|
{logLoading ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '24px' }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
) : logHistoryData.length === 0 ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '24px', color: '#8c8c8c' }}>
|
||||||
|
Tidak ada log history
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '400px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '0 16px',
|
||||||
|
position: 'relative',
|
||||||
|
border: '1px solid #f0f0f0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
{/* Garis vertikal yang menyambung */}
|
{/* Garis vertikal yang menyambung */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -599,20 +734,28 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
{/* Kolom Kanan: Card */}
|
{/* Kolom Kanan: Card */}
|
||||||
<Col flex="auto">
|
<Col flex="auto">
|
||||||
<Card size="small" style={{ borderColor: '#91d5ff' }}>
|
<Card size="small" style={{ borderColor: '#91d5ff' }}>
|
||||||
<Row gutter={[16, 8]} align="middle">
|
<Row gutter={[16, 8]} align="top">
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={10}>
|
||||||
<Space direction="vertical" size={4}>
|
<Space direction="vertical" size={4}>
|
||||||
<Space>
|
<Space>
|
||||||
<ClockCircleOutlined />
|
<ClockCircleOutlined />
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
<Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ fontSize: '12px' }}
|
||||||
|
>
|
||||||
Added at {log.timestamp}
|
Added at {log.timestamp}
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text strong>
|
||||||
|
{log.addedBy.name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Text strong>Added by: {log.addedBy.name}</Text>
|
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
marginLeft: '8px',
|
|
||||||
border: '1px solid #52c41a',
|
border: '1px solid #52c41a',
|
||||||
color: '#52c41a',
|
color: '#52c41a',
|
||||||
padding: '2px 6px',
|
padding: '2px 6px',
|
||||||
@@ -625,7 +768,8 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={14}>
|
||||||
|
<Text strong>Description:</Text>
|
||||||
<Paragraph
|
<Paragraph
|
||||||
style={{
|
style={{
|
||||||
color: '#595959',
|
color: '#595959',
|
||||||
@@ -642,6 +786,8 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -693,9 +839,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' }}>
|
||||||
Time
|
Date & Time
|
||||||
</Text>
|
</Text>
|
||||||
<div>{selectedNotification.timestamp.split(' ')[1]} WIB</div>
|
<div>{selectedNotification.timestamp}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -812,7 +958,16 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
bodyStyle={{ padding: '12px' }}
|
bodyStyle={{ padding: '12px' }}
|
||||||
onClick={() => setModalContent('log')}
|
onClick={() => {
|
||||||
|
// 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
|
||||||
@@ -837,7 +992,8 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
<Text type="secondary" style={{ fontSize: '10px' }}>
|
<Text type="secondary" style={{ fontSize: '10px' }}>
|
||||||
PDF
|
PDF
|
||||||
</Text>
|
</Text>
|
||||||
} >
|
}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -1128,7 +1284,22 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
{logHistoryData.map((log) => (
|
{logLoading ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '12px' }}>
|
||||||
|
<Spin size="small" />
|
||||||
|
</div>
|
||||||
|
) : logHistoryData.length === 0 ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '12px',
|
||||||
|
color: '#8c8c8c',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tidak ada log history
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
logHistoryData.map((log) => (
|
||||||
<Card
|
<Card
|
||||||
key={log.id}
|
key={log.id}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -1145,7 +1316,8 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
{log.timestamp}
|
{log.timestamp}
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -1285,7 +1457,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' && 'User History Notification'}
|
{modalContent === 'user' && 'History User Notification'}
|
||||||
{modalContent === 'log' && 'Log History Notification'}
|
{modalContent === 'log' && 'Log History Notification'}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
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 { UserOutlined, PhoneOutlined, CheckCircleOutlined, SyncOutlined, SendOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
UserOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
SendOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -41,9 +47,17 @@ const UserHistoryModal = ({ visible, onCancel, notificationData }) => {
|
|||||||
const getStatusTag = (status) => {
|
const getStatusTag = (status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'delivered':
|
case 'delivered':
|
||||||
return <Tag icon={<CheckCircleOutlined />} color="success">Delivered</Tag>;
|
return (
|
||||||
|
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||||
|
Delivered
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
case 'sent':
|
case 'sent':
|
||||||
return <Tag icon={<SyncOutlined spin />} color="processing">Sent</Tag>;
|
return (
|
||||||
|
<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:
|
||||||
@@ -55,7 +69,7 @@ const UserHistoryModal = ({ visible, onCancel, notificationData }) => {
|
|||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<Text strong style={{ fontSize: '18px' }}>
|
<Text strong style={{ fontSize: '18px' }}>
|
||||||
User History Notification
|
History User Notification
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
open={visible}
|
open={visible}
|
||||||
@@ -78,7 +92,13 @@ 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 style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<div
|
||||||
|
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,14 +1,37 @@
|
|||||||
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 { SendOutlined, MobileOutlined, CheckCircleFilled, ArrowLeftOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
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: 2, name: 'Jane Smith', phone: '087654321098', status: 'Delivered', timestamp: '04-11-2025 11:41 WIB' },
|
id: 1,
|
||||||
{ id: 3, name: 'Peter Jones', phone: '082345678901', status: 'Delivered', timestamp: '04-11-2025 11:42 WIB' },
|
name: 'John Doe',
|
||||||
|
phone: '081234567890',
|
||||||
|
status: 'Delivered',
|
||||||
|
timestamp: '04-11-2025 11:40 WIB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Jane Smith',
|
||||||
|
phone: '087654321098',
|
||||||
|
status: 'Delivered',
|
||||||
|
timestamp: '04-11-2025 11:41 WIB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Peter Jones',
|
||||||
|
phone: '082345678901',
|
||||||
|
status: 'Delivered',
|
||||||
|
timestamp: '04-11-2025 11:42 WIB',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const UserHistory = ({ notification, onBack }) => {
|
const UserHistory = ({ notification, onBack }) => {
|
||||||
@@ -18,7 +41,9 @@ 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 }}>User History Notification</Typography.Title>
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||||
|
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}
|
||||||
@@ -27,25 +52,34 @@ 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 key={user.id} style={{ backgroundColor: '#e6f7ff', borderColor: '#91d5ff' }}>
|
<Card
|
||||||
|
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><MobileOutlined /> {user.phone}</Text>
|
<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">Success Delivered at {user.timestamp}</Text>
|
<Text type="secondary">
|
||||||
|
Success Delivered at {user.timestamp}
|
||||||
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<Button type="primary" ghost icon={<SendOutlined />}>Resend</Button>
|
<Button type="primary" ghost icon={<SendOutlined />}>
|
||||||
|
Resend
|
||||||
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
955
src/pages/notificationDetail/IndexNotificationDetail.jsx
Normal file
@@ -0,0 +1,955 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Card,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Typography,
|
||||||
|
Space,
|
||||||
|
Button,
|
||||||
|
Spin,
|
||||||
|
Result,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Avatar,
|
||||||
|
Tag,
|
||||||
|
Badge,
|
||||||
|
Divider,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
ArrowLeftOutlined,
|
||||||
|
CloseCircleFilled,
|
||||||
|
WarningFilled,
|
||||||
|
CheckCircleFilled,
|
||||||
|
InfoCircleFilled,
|
||||||
|
CloseOutlined,
|
||||||
|
BookOutlined,
|
||||||
|
ToolOutlined,
|
||||||
|
HistoryOutlined,
|
||||||
|
FilePdfOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
LoadingOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
SendOutlined,
|
||||||
|
MobileOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
getNotificationDetail,
|
||||||
|
createNotificationLog,
|
||||||
|
getNotificationLogByNotificationId,
|
||||||
|
updateIsRead,
|
||||||
|
resendNotificationToUser,
|
||||||
|
resendChatByUser,
|
||||||
|
} from '../../api/notification';
|
||||||
|
|
||||||
|
const { Content } = Layout;
|
||||||
|
const { Text, Paragraph, Link } = Typography;
|
||||||
|
|
||||||
|
// Transform API response to component format
|
||||||
|
const transformNotificationData = (apiData) => {
|
||||||
|
// Extract nested data
|
||||||
|
const errorCodeData = apiData.error_code;
|
||||||
|
// Get active solution (is_active: true)
|
||||||
|
const activeSolution =
|
||||||
|
errorCodeData?.solution?.find((sol) => sol.is_active) || errorCodeData?.solution?.[0] || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `notification-${apiData.notification_error_id}-0`,
|
||||||
|
type: apiData.is_read ? 'resolved' : apiData.is_delivered ? 'warning' : 'critical',
|
||||||
|
title: errorCodeData?.error_code_name || 'Unknown Error',
|
||||||
|
issue: errorCodeData?.error_code || 'Unknown Error',
|
||||||
|
description: apiData.message_error_issue || 'No details available',
|
||||||
|
timestamp: apiData.created_at
|
||||||
|
? new Date(apiData.created_at).toLocaleString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}) + ' WIB'
|
||||||
|
: 'N/A',
|
||||||
|
location: apiData.plant_sub_section_name || 'Location not specified',
|
||||||
|
details: apiData.message_error_issue || 'No details available',
|
||||||
|
isRead: apiData.is_read || false,
|
||||||
|
isDelivered: apiData.is_delivered || false,
|
||||||
|
isSend: apiData.is_send || false,
|
||||||
|
status: apiData.is_read ? 'Resolved' : apiData.is_delivered ? 'Delivered' : 'Pending',
|
||||||
|
tag: errorCodeData?.error_code,
|
||||||
|
plc: 'N/A', // PLC not available in API response
|
||||||
|
notification_error_id: apiData.notification_error_id,
|
||||||
|
error_code_id: apiData.error_code_id,
|
||||||
|
error_chanel: apiData.error_chanel,
|
||||||
|
spareparts: errorCodeData?.spareparts || [],
|
||||||
|
solution: {
|
||||||
|
...activeSolution,
|
||||||
|
path_document: activeSolution.path_document
|
||||||
|
? activeSolution.path_document.replace(
|
||||||
|
'/detail-notification/pdf/',
|
||||||
|
'/notification-detail/pdf/'
|
||||||
|
)
|
||||||
|
: activeSolution.path_document,
|
||||||
|
}, // Include the active solution data with fixed URL
|
||||||
|
error_code: errorCodeData,
|
||||||
|
device_info: {
|
||||||
|
device_code: apiData.device_code,
|
||||||
|
device_name: apiData.device_name,
|
||||||
|
device_location: apiData.device_location,
|
||||||
|
brand_name: apiData.brand_name,
|
||||||
|
},
|
||||||
|
users: apiData.users || [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to get actual users from notification data
|
||||||
|
const getUsersFromNotification = (notification) => {
|
||||||
|
if (!notification || !notification.users) return [];
|
||||||
|
|
||||||
|
return notification.users.map((user) => ({
|
||||||
|
id: user.notification_error_user_id.toString(),
|
||||||
|
name: user.contact_name,
|
||||||
|
phone: user.contact_phone,
|
||||||
|
status: user.is_send ? 'Delivered' : 'Pending',
|
||||||
|
loading: user.loading || false,
|
||||||
|
timestamp: user.updated_at
|
||||||
|
? new Date(user.updated_at)
|
||||||
|
.toLocaleString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
.replace('.', ':') + ' WIB'
|
||||||
|
: 'N/A',
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusTag = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'delivered':
|
||||||
|
return (
|
||||||
|
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||||
|
Delivered
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'sent':
|
||||||
|
return (
|
||||||
|
<Tag icon={<SyncOutlined spin />} color="processing">
|
||||||
|
Sent
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'failed':
|
||||||
|
return <Tag color="error">Failed</Tag>;
|
||||||
|
default:
|
||||||
|
return <Tag color="default">{status}</Tag>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIconAndColor = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'critical':
|
||||||
|
return { IconComponent: CloseCircleFilled, color: '#ff4d4f', bgColor: '#fff1f0' };
|
||||||
|
case 'warning':
|
||||||
|
return { IconComponent: WarningFilled, color: '#faad14', bgColor: '#fffbe6' };
|
||||||
|
case 'resolved':
|
||||||
|
return { IconComponent: CheckCircleFilled, color: '#52c41a', bgColor: '#f6ffed' };
|
||||||
|
default:
|
||||||
|
return { IconComponent: InfoCircleFilled, color: '#1890ff', bgColor: '#e6f7ff' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotificationDetailTab = (props) => {
|
||||||
|
const params = useParams(); // Mungkin perlu disesuaikan jika route berbeda
|
||||||
|
const notificationId = props.id ?? params.notificationId;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [notification, setNotification] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [isAddingLog, setIsAddingLog] = useState(false);
|
||||||
|
|
||||||
|
// Log history states
|
||||||
|
const [logHistoryData, setLogHistoryData] = useState([]);
|
||||||
|
const [logLoading, setLogLoading] = useState(false);
|
||||||
|
const [newLogDescription, setNewLogDescription] = useState('');
|
||||||
|
const [submitLoading, setSubmitLoading] = useState(false);
|
||||||
|
|
||||||
|
// Fetch log history from API
|
||||||
|
const fetchLogHistory = async (notifId) => {
|
||||||
|
try {
|
||||||
|
setLogLoading(true);
|
||||||
|
const response = await getNotificationLogByNotificationId(notifId);
|
||||||
|
if (response && response.data) {
|
||||||
|
// Transform API data to component format
|
||||||
|
const transformedLogs = response.data.map((log) => ({
|
||||||
|
id: log.notification_error_log_id,
|
||||||
|
timestamp: log.created_at
|
||||||
|
? new Date(log.created_at).toLocaleString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}) + ' WIB'
|
||||||
|
: 'N/A',
|
||||||
|
addedBy: {
|
||||||
|
name: log.contact_name || 'Unknown',
|
||||||
|
phone: log.contact_phone || '',
|
||||||
|
},
|
||||||
|
description: log.notification_error_log_description || '',
|
||||||
|
}));
|
||||||
|
setLogHistoryData(transformedLogs);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching log history:', err);
|
||||||
|
} finally {
|
||||||
|
setLogLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle submit new log
|
||||||
|
const handleSubmitLog = async () => {
|
||||||
|
if (!newLogDescription.trim()) {
|
||||||
|
message.warning('Mohon isi deskripsi log terlebih dahulu');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSubmitLoading(true);
|
||||||
|
const payload = {
|
||||||
|
notification_error_id: parseInt(notificationId),
|
||||||
|
notification_error_log_description: newLogDescription.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await createNotificationLog(payload);
|
||||||
|
|
||||||
|
if (response && response.statusCode === 200) {
|
||||||
|
message.success('Log berhasil ditambahkan');
|
||||||
|
setNewLogDescription('');
|
||||||
|
setIsAddingLog(false);
|
||||||
|
// Refresh log history
|
||||||
|
fetchLogHistory(notificationId);
|
||||||
|
} else {
|
||||||
|
throw new Error(response?.message || 'Gagal menambahkan log');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error submitting log:', err);
|
||||||
|
message.error(err.message || 'Gagal menambahkan log');
|
||||||
|
} finally {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDetail = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Fetch using the actual API
|
||||||
|
const response = await getNotificationDetail(notificationId);
|
||||||
|
|
||||||
|
if (response && response.data) {
|
||||||
|
const transformedData = transformNotificationData(response.data);
|
||||||
|
setNotification(transformedData);
|
||||||
|
|
||||||
|
// Fetch log history
|
||||||
|
fetchLogHistory(notificationId);
|
||||||
|
|
||||||
|
// Fetch using the actual API
|
||||||
|
const resUpdate = await updateIsRead(notificationId);
|
||||||
|
} else {
|
||||||
|
throw new Error('Notification not found');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
console.error('Error fetching notification detail:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDetail();
|
||||||
|
}, [notificationId]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spin size="large" />
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !notification) {
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Result
|
||||||
|
status="404"
|
||||||
|
title="404"
|
||||||
|
subTitle="Sorry, the notification you visited does not exist."
|
||||||
|
extra={
|
||||||
|
<Button type="primary" onClick={() => navigate('/notification')}>
|
||||||
|
Back to List
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { color } = getIconAndColor(notification.type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout style={{ padding: '24px', backgroundColor: '#f0f2f5' }}>
|
||||||
|
<Content>
|
||||||
|
<Card>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
paddingBottom: '16px',
|
||||||
|
marginBottom: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!props.id && (
|
||||||
|
<Row justify="space-between" align="middle">
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
onClick={() => navigate('/notification')}
|
||||||
|
style={{ paddingLeft: 0 }}
|
||||||
|
>
|
||||||
|
Back to notification list
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#f6ffed',
|
||||||
|
border: '1px solid #b7eb8f',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '8px 16px',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Title level={4} style={{ margin: 0, color: '#262626' }}>
|
||||||
|
Error Notification Detail
|
||||||
|
</Typography.Title>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
|
<Row gutter={[8, 8]}>
|
||||||
|
{/* Kolom Kiri: Data Kompresor */}
|
||||||
|
<Col xs={24} lg={8}>
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
style={{ height: '100%', borderColor: '#d4380d' }}
|
||||||
|
bodyStyle={{ padding: '16px' }}
|
||||||
|
>
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size="large"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Row gutter={16} align="middle">
|
||||||
|
<Col>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: '#d4380d',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: '18px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Text>{notification.title}</Text>
|
||||||
|
<div style={{ marginTop: '2px' }}>
|
||||||
|
<Text strong style={{ fontSize: '16px' }}>
|
||||||
|
Error Code {notification.issue}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div>
|
||||||
|
<Text strong>Plant Subsection</Text>
|
||||||
|
<div>{notification.location}</div>
|
||||||
|
<Text
|
||||||
|
strong
|
||||||
|
style={{ display: 'block', marginTop: '8px' }}
|
||||||
|
>
|
||||||
|
Date & Time
|
||||||
|
</Text>
|
||||||
|
<div>{notification.timestamp}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Kolom Tengah: Informasi Teknis */}
|
||||||
|
<Col xs={24} lg={8}>
|
||||||
|
<Card
|
||||||
|
title="Device Information"
|
||||||
|
size="small"
|
||||||
|
style={{ height: '100%' }}
|
||||||
|
>
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size="middle"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Text strong>Error Channel</Text>
|
||||||
|
<div>{notification.error_chanel || 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text strong>Device Code</Text>
|
||||||
|
<div>
|
||||||
|
{notification.device_info?.device_code || 'N/A'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text strong>Device Name</Text>
|
||||||
|
<div>
|
||||||
|
{notification.device_info?.device_name || 'N/A'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text strong>Device Location</Text>
|
||||||
|
<div>
|
||||||
|
{notification.device_info?.device_location || 'N/A'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text strong>Brand</Text>
|
||||||
|
<div>
|
||||||
|
{notification.device_info?.brand_name || 'N/A'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Kolom Kanan: User History */}
|
||||||
|
<Col xs={24} lg={8}>
|
||||||
|
<Card title="User History" size="small" style={{ height: '100%' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxHeight: '400px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '2px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size={2}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{getUsersFromNotification(notification).map((user) => (
|
||||||
|
<Card
|
||||||
|
key={user.id}
|
||||||
|
size="small"
|
||||||
|
style={{ width: '100%', margin: 0 }}
|
||||||
|
>
|
||||||
|
<Row align="middle" justify="space-between">
|
||||||
|
<Col>
|
||||||
|
<Space align="center">
|
||||||
|
<Text strong>{user.name}</Text>
|
||||||
|
<Text>|</Text>
|
||||||
|
<Text>
|
||||||
|
<MobileOutlined /> {user.phone}
|
||||||
|
</Text>
|
||||||
|
<Text>|</Text>
|
||||||
|
<Badge
|
||||||
|
status={
|
||||||
|
user.status === 'Delivered'
|
||||||
|
? 'success'
|
||||||
|
: 'default'
|
||||||
|
}
|
||||||
|
text={user.status}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
<Space align="center">
|
||||||
|
{user.status === 'Delivered' ? (
|
||||||
|
<CheckCircleFilled
|
||||||
|
style={{ color: '#52c41a' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ClockCircleOutlined
|
||||||
|
style={{ color: '#faad14' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text type="secondary">
|
||||||
|
{user.status === 'Delivered'
|
||||||
|
? 'Success Delivered at'
|
||||||
|
: 'Status '}{' '}
|
||||||
|
{user.timestamp}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
icon={<SendOutlined />}
|
||||||
|
onClick={async () => {
|
||||||
|
await resendChatByUser(
|
||||||
|
user.id,
|
||||||
|
user.phone
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Resend
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={[8, 8]}>
|
||||||
|
<Col xs={24} md={8}>
|
||||||
|
<div>
|
||||||
|
<Card
|
||||||
|
hoverable
|
||||||
|
bodyStyle={{ padding: '12px'}}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<BookOutlined
|
||||||
|
style={{ fontSize: '16px', color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
strong
|
||||||
|
style={{ fontSize: '16px', color: '#262626' }}
|
||||||
|
>
|
||||||
|
Handling Guideline
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size="small"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{notification.error_code?.solution &&
|
||||||
|
notification.error_code.solution.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{notification.error_code.solution
|
||||||
|
.filter((sol) => sol.is_active) // Hanya tampilkan solusi yang aktif
|
||||||
|
.map((sol, index) => (
|
||||||
|
<div
|
||||||
|
key={
|
||||||
|
sol.brand_code_solution_id ||
|
||||||
|
index
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{sol.path_document ? (
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
bodyStyle={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
marginBottom: '4px',
|
||||||
|
}}
|
||||||
|
hoverable
|
||||||
|
extra={
|
||||||
|
<Text
|
||||||
|
type="secondary"
|
||||||
|
style={{
|
||||||
|
fontSize:
|
||||||
|
'10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
PDF
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent:
|
||||||
|
'space-between',
|
||||||
|
alignItems:
|
||||||
|
'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize:
|
||||||
|
'12px',
|
||||||
|
color: '#262626',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FilePdfOutlined
|
||||||
|
style={{
|
||||||
|
marginRight:
|
||||||
|
'8px',
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
{sol.file_upload_name ||
|
||||||
|
'Solution Document.pdf'}
|
||||||
|
</Text>
|
||||||
|
<Link
|
||||||
|
href={sol.path_document.replace(
|
||||||
|
'/detail-notification/pdf/',
|
||||||
|
'/notification-detail/pdf/'
|
||||||
|
)}
|
||||||
|
target="_blank"
|
||||||
|
style={{
|
||||||
|
fontSize:
|
||||||
|
'12px',
|
||||||
|
display:
|
||||||
|
'block',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
lihat disini
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
{sol.type_solution === 'text' &&
|
||||||
|
sol.text_solution ? (
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
title={
|
||||||
|
<Text strong>
|
||||||
|
{sol.solution_name}:
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
bodyStyle={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
marginBottom: '4px',
|
||||||
|
}}
|
||||||
|
extra={
|
||||||
|
<Text
|
||||||
|
type="secondary"
|
||||||
|
style={{
|
||||||
|
fontSize:
|
||||||
|
'10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sol.type_solution.toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop:
|
||||||
|
'4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sol.text_solution}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '20px',
|
||||||
|
color: '#8c8c8c',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tidak ada dokumen solusi tersedia
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={8}>
|
||||||
|
<div>
|
||||||
|
<Card
|
||||||
|
hoverable
|
||||||
|
bodyStyle={{ padding: '12px'}}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<ToolOutlined
|
||||||
|
style={{ fontSize: '16px', color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
strong
|
||||||
|
style={{ fontSize: '16px', color: '#262626' }}
|
||||||
|
>
|
||||||
|
Spare Part
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size="small"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{notification.spareparts &&
|
||||||
|
notification.spareparts.length > 0 ? (
|
||||||
|
notification.spareparts.map((sparepart, index) => (
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
key={index}
|
||||||
|
bodyStyle={{ padding: '12px' }}
|
||||||
|
hoverable
|
||||||
|
>
|
||||||
|
<Row gutter={16} align="top">
|
||||||
|
<Col
|
||||||
|
span={7}
|
||||||
|
style={{ textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '60px',
|
||||||
|
backgroundColor: '#f0f0f0',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: '4px',
|
||||||
|
marginBottom: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToolOutlined
|
||||||
|
style={{
|
||||||
|
fontSize: '24px',
|
||||||
|
color: '#bfbfbf',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color:
|
||||||
|
sparepart.sparepart_stok ===
|
||||||
|
'Available' ||
|
||||||
|
sparepart.sparepart_stok ===
|
||||||
|
'available'
|
||||||
|
? '#52c41a'
|
||||||
|
: '#ff4d4f',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sparepart.sparepart_stok}
|
||||||
|
</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={17}>
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size={4}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Text strong>
|
||||||
|
{sparepart.sparepart_name}
|
||||||
|
</Text>
|
||||||
|
<Paragraph
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
margin: 0,
|
||||||
|
color: '#595959',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sparepart.sparepart_description ||
|
||||||
|
'Deskripsi tidak tersedia'}
|
||||||
|
</Paragraph>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: '1px solid #d9d9d9',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '4px 8px',
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#8c8c8c',
|
||||||
|
marginTop: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Kode:{' '}
|
||||||
|
{sparepart.sparepart_code} |
|
||||||
|
Qty:{' '}
|
||||||
|
{sparepart.sparepart_qty} |
|
||||||
|
Unit:{' '}
|
||||||
|
{sparepart.sparepart_unit}
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '20px',
|
||||||
|
color: '#8c8c8c',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tidak ada spare parts terkait
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={8}>
|
||||||
|
<div>
|
||||||
|
<Card bodyStyle={{ padding: '12px'}}>
|
||||||
|
<Space>
|
||||||
|
<HistoryOutlined
|
||||||
|
style={{ fontSize: '16px', color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
strong
|
||||||
|
style={{ fontSize: '16px', color: '#262626' }}
|
||||||
|
>
|
||||||
|
Log Activity
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size="small"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
bodyStyle={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
backgroundColor: isAddingLog
|
||||||
|
? '#fafafa'
|
||||||
|
: '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{isAddingLog && (
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
strong
|
||||||
|
style={{ fontSize: '12px' }}
|
||||||
|
>
|
||||||
|
Add New Log / Update Progress
|
||||||
|
</Text>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={2}
|
||||||
|
placeholder="Tuliskan update penanganan di sini..."
|
||||||
|
value={newLogDescription}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewLogDescription(
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={submitLoading}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type={isAddingLog ? 'primary' : 'dashed'}
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
icon={
|
||||||
|
submitLoading ? (
|
||||||
|
<LoadingOutlined />
|
||||||
|
) : (
|
||||||
|
!isAddingLog && <PlusOutlined />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={
|
||||||
|
isAddingLog
|
||||||
|
? handleSubmitLog
|
||||||
|
: () => setIsAddingLog(true)
|
||||||
|
}
|
||||||
|
loading={submitLoading}
|
||||||
|
disabled={submitLoading}
|
||||||
|
>
|
||||||
|
{isAddingLog ? 'Submit Log' : 'Add Log'}
|
||||||
|
</Button>
|
||||||
|
{isAddingLog && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
onClick={() => {
|
||||||
|
setIsAddingLog(false);
|
||||||
|
setNewLogDescription('');
|
||||||
|
}}
|
||||||
|
disabled={submitLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
{logHistoryData.map((log) => (
|
||||||
|
<Card
|
||||||
|
key={log.id}
|
||||||
|
size="small"
|
||||||
|
bodyStyle={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paragraph
|
||||||
|
style={{ fontSize: '12px', margin: 0 }}
|
||||||
|
// ellipsis={{ rows: 2 }}
|
||||||
|
>
|
||||||
|
<Text strong>{log.addedBy.name}:</Text>{' '}
|
||||||
|
{log.description}
|
||||||
|
</Paragraph>
|
||||||
|
<Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
{log.timestamp}
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationDetailTab;
|
||||||
@@ -1,91 +1,246 @@
|
|||||||
import React, { memo, useState, useEffect } from 'react';
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
import { Button, Row, Col, Card, Input, DatePicker, Select, Typography } from 'antd';
|
import { Button, Row, Col, Card, DatePicker, Select, Typography, Table, Spin, Modal } from 'antd';
|
||||||
import TableList from '../../../../components/Global/TableList';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { FileTextOutlined } from '@ant-design/icons';
|
import { FileTextOutlined, DownloadOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
getAllHistoryValueReport,
|
|
||||||
getAllHistoryValueReportPivot,
|
getAllHistoryValueReportPivot,
|
||||||
|
getAllHistoryValueReport,
|
||||||
} 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 [trigerFilter, setTrigerFilter] = useState(false);
|
const [isLoadingModal, setIsLoadingModal] = 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(10);
|
const [periode, setPeriode] = useState(30);
|
||||||
|
|
||||||
const defaultFilter = {
|
const generateFullDayTimes = (dateString, intervalMinutes) => {
|
||||||
criteria: '',
|
const times = [];
|
||||||
plant_sub_section_id: 0,
|
const startOfDay = dayjs(dateString).startOf('day');
|
||||||
from: dateNowFormated,
|
const endOfDay = dayjs(dateString).endOf('day');
|
||||||
to: dateNowFormated,
|
|
||||||
interval: periode,
|
let currentTime = startOfDay;
|
||||||
|
|
||||||
|
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 handleSearch = () => {
|
const fetchData = async (page = 1, pageSize = 10, showModal = false) => {
|
||||||
|
// if (!plantSubSection) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (showModal) {
|
||||||
|
setIsLoadingModal(true);
|
||||||
|
} else {
|
||||||
|
setIsLoadingTable(true);
|
||||||
|
}
|
||||||
|
try {
|
||||||
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
||||||
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
||||||
|
|
||||||
setFormDataFilter({
|
const params = new URLSearchParams({
|
||||||
criteria: '',
|
|
||||||
plant_sub_section_id: plantSubSection,
|
plant_sub_section_id: plantSubSection,
|
||||||
from: formattedDateStart,
|
from: formattedDateStart,
|
||||||
to: formattedDateEnd,
|
to: formattedDateEnd,
|
||||||
interval: periode,
|
interval: periode,
|
||||||
|
page: 1,
|
||||||
|
limit: 1000,
|
||||||
});
|
});
|
||||||
setTrigerFilter((prev) => !prev);
|
|
||||||
|
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(5);
|
setPeriode(30);
|
||||||
|
setTableData([]);
|
||||||
|
setColumns([]);
|
||||||
|
setPivotData([]);
|
||||||
|
setValueReportData([]);
|
||||||
|
setPagination({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlantSubSection = async () => {
|
const getPlantSubSection = async () => {
|
||||||
@@ -104,8 +259,548 @@ 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}>
|
||||||
@@ -167,14 +862,8 @@ 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={[
|
options={periodeOptions}
|
||||||
{ 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>
|
||||||
@@ -185,10 +874,33 @@ 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}
|
||||||
@@ -199,18 +911,26 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
<Col xs={24} style={{ marginTop: '16px' }}>
|
||||||
<TableList
|
<Spin spinning={isLoadingTable}>
|
||||||
firstLoad={false}
|
<div style={{ overflowX: 'auto', width: '100%' }}>
|
||||||
mobile
|
<Table
|
||||||
cardColor={'#d38943ff'}
|
|
||||||
header={'datetime'}
|
|
||||||
getData={getAllHistoryValueReportPivot}
|
|
||||||
queryParams={formDataFilter}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
columnDynamic={'columns'}
|
dataSource={tableData}
|
||||||
triger={trigerFilter}
|
pagination={{
|
||||||
|
...pagination,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (total) => `Total ${total} data`,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100'],
|
||||||
|
}}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
scroll={{ x: 'max-content', y: 500 }}
|
||||||
|
bordered
|
||||||
|
size="small"
|
||||||
|
sticky
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import React, { memo, useState, useEffect } from 'react';
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
import { Button, Row, Col, Card, Input, DatePicker, Select, Typography } from 'antd';
|
import { Button, Row, Col, Card, DatePicker, Select, Typography, Modal, Spin } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { FileTextOutlined } from '@ant-design/icons';
|
import { FileTextOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||||
import { ResponsiveLine } from '@nivo/line';
|
import {
|
||||||
|
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';
|
||||||
@@ -18,6 +27,7 @@ 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: '',
|
||||||
@@ -29,8 +39,19 @@ 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);
|
||||||
|
|
||||||
|
try {
|
||||||
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
||||||
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
||||||
|
|
||||||
@@ -48,32 +69,53 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
const response = await getAllHistoryValueTrendingPivot(param);
|
const response = await getAllHistoryValueTrendingPivot(param);
|
||||||
|
|
||||||
if (response?.data?.length > 0) {
|
if (response?.data?.length > 0) {
|
||||||
// 🔹 Bersihkan dan format data agar aman untuk Nivo
|
transformDataForRecharts(response.data);
|
||||||
const cleanedData = response.data.map((serie) => ({
|
|
||||||
id: serie.id ?? 'Unknown',
|
|
||||||
data: Array.isArray(serie.data)
|
|
||||||
? serie.data.map((d) => ({
|
|
||||||
x: d?.x ?? null,
|
|
||||||
y:
|
|
||||||
d?.y !== null && d?.y !== undefined
|
|
||||||
? Number(d.y).toFixed(4) // format 4 angka di belakang koma
|
|
||||||
: null,
|
|
||||||
}))
|
|
||||||
: [],
|
|
||||||
}));
|
|
||||||
|
|
||||||
setTrendingValue(cleanedData);
|
|
||||||
} else {
|
} else {
|
||||||
// 🔹 Jika tidak ada data dari API
|
|
||||||
setTrendingValue([]);
|
setTrendingValue([]);
|
||||||
|
setChartData([]);
|
||||||
|
setMetrics([]);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching trending data:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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(5);
|
setPeriode(60);
|
||||||
|
setChartData([]);
|
||||||
|
setMetrics([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlantSubSection = async () => {
|
const getPlantSubSection = async () => {
|
||||||
@@ -88,12 +130,171 @@ 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}>
|
||||||
@@ -162,10 +363,11 @@ 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
|
||||||
@@ -187,108 +389,9 @@ 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' }}>
|
|
||||||
<div style={{ height: '500px', marginTop: '16px' }}>
|
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '24px' }}>
|
||||||
{trendingValue && trendingValue.length > 0 ? (
|
{renderChart()}
|
||||||
<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>
|
||||||
|
|||||||
@@ -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,35 +220,27 @@ 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; // Username only for create
|
payload.user_name = FormData.user_name;
|
||||||
payload.user_password = FormData.password; // Backend expects 'user_password'
|
payload.user_password = FormData.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) {
|
||||||
@@ -257,11 +249,10 @@ 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(
|
||||||
@@ -385,9 +376,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) {
|
||||||
@@ -408,7 +399,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');
|
||||||
@@ -418,7 +409,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);
|
||||||
@@ -429,7 +420,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') {
|
||||||
@@ -1146,9 +1137,7 @@ const DetailUser = (props) => {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
{errors.role_id && (
|
{errors.role_id && (
|
||||||
<Text style={{ color: 'red', fontSize: '12px' }}>
|
<Text style={{ color: 'red', fontSize: '12px' }}>{errors.role_id}</Text>
|
||||||
{errors.role_id}
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||