Compare commits

...

150 Commits

Author SHA1 Message Date
zain94rif
c2163cec5e Merge branch 'lavoce' of https://gitea.idetama.id/yogiedigital/cod-fe into lavoce 2026-01-08 17:25:23 +07:00
zain94rif
d5866ceae4 fix(api): search use api 2026-01-08 17:25:17 +07:00
6fdb259246 fixing validate solution optional in brand error code 2026-01-08 14:32:27 +07:00
0aad43c751 add label error code in list notification 2026-01-08 14:24:32 +07:00
d988d47e30 fixing layout mobile detail notification 2026-01-08 14:17:38 +07:00
zain94rif
e08eaaa43e fix(text): add 'error code' & move solution name 2026-01-08 13:55:01 +07:00
zain94rif
f6ca54f5b4 Merge branch 'lavoce' of https://gitea.idetama.id/yogiedigital/cod-fe into lavoce 2026-01-08 13:07:27 +07:00
zain94rif
a9b8053bd8 fix: change message 'Setiap error code harus memiliki minimal 1 solution!' disabled 2026-01-08 13:07:21 +07:00
600c101c68 fixing typo detail notification user 2026-01-08 12:17:13 +07:00
zain94rif
14a6884f43 Merge branch 'lavoce' of https://gitea.idetama.id/yogiedigital/cod-fe into lavoce 2026-01-07 17:07:46 +07:00
zain94rif
8e151ffe0b fix(comp): modified the card in notification detail 2026-01-07 17:07:42 +07:00
8f64843613 fixing redirect wa session token 2026-01-07 16:10:23 +07:00
zain94rif
fe8f6d1002 fix: move update is read's api after fetchLogHistory 2026-01-07 14:40:36 +07:00
zain94rif
5281e288a9 feat(var): add update at from create at 2026-01-07 11:00:35 +07:00
zain94rif
4ed05cc640 fix: resize add log card 2026-01-07 10:30:03 +07:00
zain94rif
14e97fead2 feat(api): add update is_read for detail 2026-01-06 16:12:58 +07:00
zain94rif
0935d7c9f5 fix(var): use notification_error_id from item, not from users 2026-01-06 09:53:15 +07:00
zain94rif
3266641f81 fix(api): fixing put to post 2026-01-05 14:48:46 +07:00
zain94rif
739c55c0bc fix(api): fixing endpoint notification 2026-01-05 14:26:12 +07:00
zain94rif
5b4485d20d feat(api): add API for resend chat user 2026-01-05 14:08:02 +07:00
98057beb0f Merge pull request 'fix-lav-notification' (#31) from fix-lav-notification into lavoce
Reviewed-on: #31
2026-01-05 03:40:24 +00:00
zain94rif
b342289888 fix(view): adjustment view page notification 2026-01-05 10:39:01 +07:00
d03bbf2a41 Repair topic mqtt 2026-01-05 10:38:24 +07:00
zain94rif
ec094b8f55 fix(text): change 'User History' to 'History User' 2026-01-05 09:38:27 +07:00
b6d941ba2d refactor: comment out console logs for cleaner production code 2025-12-29 10:58:03 +07:00
167abcaa43 refactor: enhance notification log layout and styling for better readability 2025-12-24 11:51:39 +07:00
beb8ccbaee feat: integration notification functionality and user history fetching 2025-12-23 22:10:11 +07:00
797f6c2383 refactor: clean up comments and streamline payload handling in user detail form 2025-12-23 20:10:33 +07:00
016c77a586 fixing redirect detail notification tab 2025-12-23 12:17:17 +07:00
36ebab7f9a refactor: remove unused UserHistoryModal and related state management 2025-12-23 10:30:44 +07:00
a5b1fbef74 repair: layout & sparepart brand-device 2025-12-23 10:26:30 +07:00
Athif
cb0c53daea update Menu Report 2025-12-23 02:20:16 +07:00
978e020305 feat: update notification data transformation and enhance user history display 2025-12-22 20:49:48 +07:00
4508738958 feat: implement log history fetching and display in ListNotification component 2025-12-22 18:47:42 +07:00
eb23612444 Repair and replace svg 2025-12-22 16:37:11 +07:00
bee196e299 feat: add notification log creation and retrieval functionality 2025-12-22 14:34:14 +07:00
d19f555c7c repair: layout error code form brand-device 2025-12-19 12:43:56 +07:00
1d7253f9a1 repair: sparepart select brand-device 2025-12-19 12:43:19 +07:00
d8a1878ab1 refactor: adjust spacing and layout in NotificationDetailTab for improved readability 2025-12-18 21:11:49 +07:00
e4af2d6e18 refactor: improve timestamp display and code formatting in notification detail 2025-12-18 19:34:20 +07:00
8cf21643ea refactor: update notification handling to prioritize active solutions and improve data extraction 2025-12-18 17:57:25 +07:00
Athif
6b75f6f4b9 update menu report 2025-12-18 15:03:39 +07:00
Athif
dc78add71d Perbaikan Menu Report dan Trending 2025-12-18 13:02:52 +07:00
1ce922ff4c minor fix notif 2025-12-18 12:36:46 +07:00
3a4b0f0748 repair: ErrorCode brand-device 2025-12-18 10:51:35 +07:00
4bffbb3798 repair: add clear selected error code 2025-12-13 14:25:30 +07:00
b9cdfcb1e9 repair: sollution field, handle clear form 2025-12-13 14:15:35 +07:00
49ba00d886 repair: view brand device, add: read only 2025-12-12 23:02:09 +07:00
cf1ccb0fd0 repair: sollution brand-device 2025-12-12 16:55:05 +07:00
fb790e5e37 repair: error code brand-device 2025-12-12 16:54:52 +07:00
ea3adf40cc repair: brandDevice add edit page 2025-12-12 16:54:30 +07:00
2ff50342e8 repair: sollution brand-device 2025-12-12 15:58:11 +07:00
1f8ee62721 fix: improve formatting and consistency in DetailDevice and GeneratePdf components 2025-12-12 15:53:29 +07:00
96d6367dbd add: image viewer 2025-12-12 12:46:05 +07:00
8afff23ffe update: brand device 2025-12-12 12:45:46 +07:00
512282f367 repair: sollution brand-device 2025-12-12 12:45:00 +07:00
4fab5df300 repair: error code brand-device 2025-12-12 12:44:16 +07:00
9e8191f8f8 repair: sparepart brand-device 2025-12-12 12:43:14 +07:00
13255f9713 feat: add getNotificationDetail API function and integrate it into DetailNotification component for enhanced notification handling 2025-12-11 17:07:06 +07:00
e23215b6c1 refactor: adjust layout and styling in DetailNotification and ListNotification components; remove pagination in LogHistoryCard for improved UI 2025-12-11 09:53:44 +07:00
a014d6b370 feat: replace LogHistoryModal with LogHistoryCard and update DetailNotification for improved log history display 2025-12-09 13:32:53 +07:00
3225a0865e repair: delete error code 2025-12-09 11:06:28 +07:00
5703ff0e8d repair: add edit brand device 2025-12-08 16:45:49 +07:00
03be3a6a99 fix: update column title from 'Aksi' to 'Action' in ListUser component for clarity 2025-12-08 13:48:15 +07:00
fe5f081b92 feat: update handling of description fields in multiple components to ensure non-empty values are stored 2025-12-05 13:13:30 +07:00
acaf1b3946 feat: add listen channel field to DetailDevice component and update ListDevice to display it; enhance quantity handling in SparepartCardList to prevent negative adjustments 2025-12-04 15:59:57 +07:00
147171373c feat: update quantity display and stock update button visibility in SparepartCardList component 2025-12-04 13:59:25 +07:00
f22e120204 update ui brand device 2025-12-04 01:17:25 +07:00
1bc98de564 feat: enhance image URL handling in DetailSparepart component and remove status field display 2025-12-03 16:58:58 +07:00
991a3eaa66 feat: update quantity handling in DetailSparepart and SparepartCardList components to allow zero values 2025-12-03 16:45:44 +07:00
7a5a9aafd1 feat: update sparepart quantity and stock status handling in DetailSparepart and SparepartCardList components 2025-12-03 13:26:54 +07:00
0694497f8d feat: remove contact_type from DetailContact component 2025-12-03 10:49:16 +07:00
c82d6d39c1 feat: conditionally display status field in add mode for DetailContact component 2025-12-02 15:39:20 +07:00
edf20050db feat: hide tabs and improve UI elements 2025-12-02 14:05:35 +07:00
2e98dc168a feat: enhance DetailSparepart component with image upload and preview functionality 2025-12-02 13:48:56 +07:00
1797058526 repair: brandDevice sparepart integration 2025-12-02 11:10:36 +07:00
1c2ddca9d4 fix: update isReadFilter 2025-12-01 10:51:11 +07:00
61ca7249cd repair 2025-11-28 14:16:45 +07:00
a98edbe658 Add scroll for overflow menus 2025-11-28 14:10:22 +07:00
fbc5473f2b feat: implement sparepart selection functionality and refactor related components 2025-11-28 14:00:17 +07:00
55a47c3a25 Repair svg compressor B 2025-11-28 12:50:09 +07:00
94e011e5c7 Repair topic mqtt 2025-11-28 12:49:56 +07:00
db9b40f2fc Repair compresso 2025-11-28 12:28:17 +07:00
5fdfb47f9e fix: update error code icon handling to use URL if available 2025-11-27 17:39:39 +07:00
55c50f6f7f Refactor code structure for improved readability and maintainability 2025-11-27 16:58:27 +07:00
ed4570e8dd feat: add onGetData callback to TableList and enhance DetailSparepart with improved image handling 2025-11-27 13:30:45 +07:00
572042ab53 feat: enhance DetailSparepart component with image upload and preview functionality 2025-11-27 11:36:23 +07:00
afcb85a323 feat: add image upload functionality and stock update feature in sparepart components 2025-11-26 17:16:09 +07:00
14f8a5d472 feat: enhance notification fetching with pagination and filtering options 2025-11-26 11:44:50 +07:00
309d191bce feat: add custom card component for sparepart display in ListSparepart 2025-11-25 16:49:46 +07:00
7e5105392c feat: add activity log functionality with log history display and input for new logs 2025-11-25 10:22:55 +07:00
7e16bf63aa refactor: consolidate SolutionField components and remove obsolete files 2025-11-24 21:44:44 +07:00
3e384f89b1 feat: integrate sparepart management into AddBrandDevice and EditBrandDevice components 2025-11-24 21:25:24 +07:00
b05e3fe5d9 feat: implement sparepart management with CRUD operations and UI components 2025-11-24 20:40:38 +07:00
1986368c1c feat: update button label from 'Add Data' to 'Add Role' in ListRole component 2025-11-24 15:58:23 +07:00
1cd9cf765c fix dual action 2025-11-24 15:57:12 +07:00
908788f41d feat: enhance error code management with table display and action buttons 2025-11-24 13:05:33 +07:00
899695f548 feat: add sparepart management page and update routing in App component 2025-11-24 12:17:31 +07:00
7d2b18a94d refactor: remove sparepart handling from AddBrandDevice and EditBrandDevice components 2025-11-24 11:59:26 +07:00
1eab3fe845 feat: refactor notification detail handling and add verification spare part page 2025-11-21 16:25:50 +07:00
c4f290bfcb fix: improve success message in DetailContact 2025-11-20 19:52:03 +07:00
f304a28493 enhance search and pagination functionality to ListNotification component 2025-11-20 19:45:45 +07:00
73b5cd6e97 feat: add detail notification page and update notification links 2025-11-20 16:41:21 +07:00
2d0c28bc48 fix: validation in DetailContact component 2025-11-20 15:55:03 +07:00
1413d0ef33 fix: header list tag plant sub section 2025-11-20 15:40:06 +07:00
fde71818e2 fix: enhance tag duplication check and handle varied response structures in DetailTag component 2025-11-20 15:25:07 +07:00
85017cd88c fix: update header label from 'tag_name' to 'brand_name' in ListBrandDevice component 2025-11-20 15:14:30 +07:00
f1c7ae5e20 fix: update header label for clarity in ListPlantSubSection component 2025-11-20 15:09:16 +07:00
5989948bf9 refactor: simplify log history display in ListNotification component 2025-11-20 11:00:46 +07:00
6a21b65808 konect brand device on device 2025-11-19 15:57:38 +07:00
b5fbf2f745 fix color contact 2025-11-19 15:40:15 +07:00
3b7ba28053 fix color number contact 2025-11-19 15:02:47 +07:00
f4caac55e6 feat: add API function to retrieve all notifications 2025-11-18 19:55:11 +07:00
34e38b3969 refactor: Improve code formatting and readability in SparepartForm component 2025-11-18 13:09:39 +07:00
3198b71f7e feat: Enhance delete confirmation with brand name in notification message 2025-11-18 12:43:00 +07:00
8405568e85 fix: Update Brand Code input to always be disabled with consistent placeholder and styling 2025-11-18 11:51:28 +07:00
ecf59fa9c6 refactor: Improve code readability and structure in EditBrandDevice component 2025-11-18 11:34:03 +07:00
0e8078c29f feat: Enhance notification system with device alerts, user history, and log history modals 2025-11-18 04:04:25 +07:00
1d06963f67 Load solutions and spareparts into forms with image handling in EditBrandDevice 2025-11-17 22:37:58 +07:00
8de195d961 Add custom validation for contact name and display type badge in contact list 2025-11-17 12:29:42 +07:00
da9cf0d554 hidden jadwal shift and master-shift 2025-11-15 14:57:25 +07:00
8cf5878d46 Add contact management functionality with CRUD operations and UI enhancements 2025-11-15 13:36:38 +07:00
7dd38aa50c Change inactive contact status color to red for better visibility 2025-11-13 15:22:45 +07:00
b2bcaa6b5f Add ErrorCodeListModal component for managing error codes with enhanced UI and functionality 2025-11-13 14:22:49 +07:00
5822dbbc82 Refactor Add and Edit Brand Device components to include solutions and spareparts forms, enhancing error code management and UI layout 2025-11-13 14:22:42 +07:00
de8f0ba2b6 Add forms and hooks for managing error codes and spare parts 2025-11-13 14:22:23 +07:00
08f8c4708f add menu contact 2025-11-13 13:58:52 +07:00
85db9e0a52 Enhance notification list with search functionality and improved layout 2025-11-13 13:16:31 +07:00
0916ea7103 Repair file route air dryer and cmpressor 2025-11-10 14:56:52 +07:00
5952858dca Repair sub menu air dryer and compressor 2025-11-10 14:56:31 +07:00
7064ec8587 Repair and add overview air dryer compressor 2025-11-10 14:56:08 +07:00
d2f9d6aacc Add svg compressor 2025-11-10 13:33:09 +07:00
7ef6d71be8 Repair header 2025-11-10 13:32:55 +07:00
d955cc7942 Repair beautiful apps 2025-11-04 17:01:27 +07:00
2ac707611f Repair bg-signin 2025-11-04 15:48:20 +07:00
71f42f4149 Add side bar menus mobile mode 2025-11-04 13:41:25 +07:00
4544e33d01 fix is_active error code ft vinix 2025-10-30 15:15:26 +07:00
114ef96de8 translate UI text from Indonesian to English for consistency 2025-10-28 19:10:30 +07:00
259ee474aa refactor ErrorCodeForm component for improved readability and structure 2025-10-28 13:15:22 +07:00
da14ed4e74 add error code icon functionality and color selection in forms 2025-10-28 13:07:25 +07:00
3738adf85a integration jadwal shift 2025-10-28 12:54:24 +07:00
6b727c84d8 integration history event alarm 2025-10-28 12:24:25 +07:00
9a483aa873 30:70 interface brand device 2025-10-28 12:20:54 +07:00
1d408ef3c1 change color sidebar 2025-10-28 12:12:30 +07:00
e8c3f259bf replace: unit code field 2025-10-28 12:09:25 +07:00
7050d7ca84 fix preview brand device error code , solution 2025-10-28 12:07:31 +07:00
fd361f21cf Fixing report 2025-10-28 11:47:15 +07:00
47f7c7b682 integration api brandDevice, file upload in brand device 2025-10-28 11:07:54 +07:00
39d8be10cc progress history report 2025-10-27 10:28:42 +07:00
5a8e2dee2f integration api history alarm and event alarm 2025-10-25 23:54:22 +07:00
96 changed files with 21173 additions and 1949 deletions

View File

@@ -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": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -10,13 +10,19 @@ import Home from './pages/home/Home';
import Blank from './pages/blank/Blank'; import Blank from './pages/blank/Blank';
// Master // Master
import IndexDevice from './pages/master/device/IndexDevice'; import IndexPlantSubSection from './pages/master/plantSubSection/IndexPlantSubSection';
import IndexTag from './pages/master/tag/IndexTag';
import IndexUnit from './pages/master/unit/IndexUnit';
import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice'; import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice';
import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice'; import IndexDevice from './pages/master/device/IndexDevice';
import IndexUnit from './pages/master/unit/IndexUnit';
import IndexTag from './pages/master/tag/IndexTag';
import IndexStatus from './pages/master/status/IndexStatus'; import IndexStatus from './pages/master/status/IndexStatus';
import IndexSparepart from './pages/master/sparepart/IndexSparepart';
import IndexShift from './pages/master/shift/IndexShift'; import IndexShift from './pages/master/shift/IndexShift';
// Brand device
import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice';
import EditBrandDevice from './pages/master/brandDevice/EditBrandDevice';
import ViewBrandDevice from './pages/master/brandDevice/ViewBrandDevice';
import ViewFilePage from './pages/master/brandDevice/ViewFilePage';
// Jadwal Shift // Jadwal Shift
import IndexJadwalShift from './pages/jadwalShift/IndexJadwalShift'; import IndexJadwalShift from './pages/jadwalShift/IndexJadwalShift';
@@ -29,18 +35,25 @@ import IndexReport from './pages/report/report/IndexReport';
import IndexNotification from './pages/notification/IndexNotification'; 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 DetailNotificationTab from './pages/notificationDetail/IndexNotificationDetail';
import IndexVerificationSparepart from './pages/verificationSparepart/IndexVerificationSparepart';
import SvgTest from './pages/home/SvgTest'; import SvgTest from './pages/home/SvgTest';
import SvgOverview from './pages/home/SvgOverview'; import SvgOverviewCompressor from './pages/home/SvgOverviewCompressor';
import SvgCompressorA from './pages/home/SvgCompressorA'; import SvgCompressorA from './pages/home/SvgCompressorA';
import SvgCompressorB from './pages/home/SvgCompressorB'; import SvgCompressorB from './pages/home/SvgCompressorB';
import SvgCompressorC from './pages/home/SvgCompressorC'; import SvgCompressorC from './pages/home/SvgCompressorC';
import SvgOverviewAirDryer from './pages/home/SvgOverviewAirDryer';
import SvgAirDryerA from './pages/home/SvgAirDryerA'; import SvgAirDryerA from './pages/home/SvgAirDryerA';
import SvgAirDryerB from './pages/home/SvgAirDryerB'; import SvgAirDryerB from './pages/home/SvgAirDryerB';
import SvgAirDryerC from './pages/home/SvgAirDryerC'; import SvgAirDryerC from './pages/home/SvgAirDryerC';
import IndexHistoryAlarm from './pages/history/alarm/IndexHistoryAlarm'; import IndexHistoryAlarm from './pages/history/alarm/IndexHistoryAlarm';
import IndexHistoryEvent from './pages/history/event/IndexHistoryEvent'; import IndexHistoryEvent from './pages/history/event/IndexHistoryEvent';
import IndexPlantSubSection from './pages/master/plantSubSection/IndexPlantSubSection';
// Image Viewer
import ImageViewer from './Utils/ImageViewer';
import RedirectWa from './pages/blank/RedirectWa';
const App = () => { const App = () => {
return ( return (
@@ -51,6 +64,16 @@ const App = () => {
<Route path="/signin" element={<SignIn />} /> <Route path="/signin" element={<SignIn />} />
<Route path="/signup" element={<SignUp />} /> <Route path="/signup" element={<SignUp />} />
<Route path="/svg" element={<SvgTest />} /> <Route path="/svg" element={<SvgTest />} />
<Route
path="/notification-detail/:notificationId"
element={<DetailNotificationTab />}
/>
<Route
path="/verification-sparepart/:notificationId"
element={<IndexVerificationSparepart />}
/>
<Route path="/redirect" element={<RedirectWa />} />
{/* Protected Routes */} {/* Protected Routes */}
<Route path="/dashboard" element={<ProtectedRoute />}> <Route path="/dashboard" element={<ProtectedRoute />}>
@@ -58,11 +81,14 @@ const App = () => {
<Route path="blank" element={<Blank />} /> <Route path="blank" element={<Blank />} />
</Route> </Route>
<Route path="/image-viewer/:fileName" element={<ImageViewer />} />
<Route path="/dashboard-svg" element={<ProtectedRoute />}> <Route path="/dashboard-svg" element={<ProtectedRoute />}>
<Route path="overview" element={<SvgOverview />} /> <Route path="overview-compressor" element={<SvgOverviewCompressor />} />
<Route path="compressor-a" element={<SvgCompressorA />} /> <Route path="compressor-a" element={<SvgCompressorA />} />
<Route path="compressor-b" element={<SvgCompressorB />} /> <Route path="compressor-b" element={<SvgCompressorB />} />
<Route path="compressor-c" element={<SvgCompressorC />} /> <Route path="compressor-c" element={<SvgCompressorC />} />
<Route path="overview-airdryer" element={<SvgOverviewAirDryer />} />
<Route path="airdryer-a" element={<SvgAirDryerA />} /> <Route path="airdryer-a" element={<SvgAirDryerA />} />
<Route path="airdryer-b" element={<SvgAirDryerB />} /> <Route path="airdryer-b" element={<SvgAirDryerB />} />
<Route path="airdryer-c" element={<SvgAirDryerC />} /> <Route path="airdryer-c" element={<SvgAirDryerC />} />
@@ -72,11 +98,28 @@ const App = () => {
<Route path="device" element={<IndexDevice />} /> <Route path="device" element={<IndexDevice />} />
<Route path="tag" element={<IndexTag />} /> <Route path="tag" element={<IndexTag />} />
<Route path="unit" element={<IndexUnit />} /> <Route path="unit" element={<IndexUnit />} />
<Route path="brand-device" element={<IndexBrandDevice />} /> <Route path="sparepart" element={<IndexSparepart />} />
<Route path="brand-device/add" element={<AddBrandDevice />} />
<Route path="plant-sub-section" element={<IndexPlantSubSection />} /> <Route path="plant-sub-section" element={<IndexPlantSubSection />} />
<Route path="shift" element={<IndexShift />} /> <Route path="shift" element={<IndexShift />} />
<Route path="status" element={<IndexStatus />} /> <Route path="status" element={<IndexStatus />} />
{/* Brand Device Routes */}
<Route path="brand-device" element={<IndexBrandDevice />} />
<Route path="brand-device/add" element={<AddBrandDevice />} />
<Route path="brand-device/edit/:id" element={<EditBrandDevice />} />
<Route path="brand-device/view/:id" element={<ViewBrandDevice />} />
<Route
path="brand-device/edit/:id/files/:fileType/:fileName"
element={<ViewFilePage />}
/>
<Route
path="brand-device/view/:id/files/:fileType/:fileName"
element={<ViewFilePage />}
/>
<Route
path="brand-device/view/temp/files/:fileName"
element={<ViewFilePage />}
/>
</Route> </Route>
<Route path="/report" element={<ProtectedRoute />}> <Route path="/report" element={<ProtectedRoute />}>
@@ -101,11 +144,14 @@ const App = () => {
<Route index element={<IndexUser />} /> <Route index element={<IndexUser />} />
</Route> </Route>
<Route path="/contact" element={<ProtectedRoute />}>
<Route index element={<IndexContact />} />
</Route>
<Route path="/jadwal-shift" element={<ProtectedRoute />}> <Route path="/jadwal-shift" element={<ProtectedRoute />}>
<Route index element={<IndexJadwalShift />} /> <Route index element={<IndexJadwalShift />} />
</Route> </Route>
{/* Catch-all */}
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>

248
src/Utils/ImageViewer.jsx Normal file
View File

@@ -0,0 +1,248 @@
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { getFileUrl, getFolderFromFileType } from '../api/file-uploads';
const ImageViewer = () => {
const { fileName } = useParams();
const [fileUrl, setFileUrl] = useState('');
const [error, setError] = useState('');
const [zoom, setZoom] = useState(1);
const [isImage, setIsImage] = useState(false);
useEffect(() => {
if (!fileName) {
setError('No file specified');
return;
}
try {
const decodedFileName = decodeURIComponent(fileName);
const fileExtension = decodedFileName.split('.').pop()?.toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
setIsImage(imageExtensions.includes(fileExtension));
const folder = getFolderFromFileType(fileExtension);
const url = getFileUrl(folder, decodedFileName);
setFileUrl(url);
document.title = `File Viewer - ${decodedFileName}`;
} catch (error) {
setError('Failed to load file');
}
}, [fileName]);
useEffect(() => {
const handleKeyDown = (e) => {
if (!isImage) return;
if (e.key === '+' || e.key === '=') {
setZoom(prev => Math.min(prev + 0.1, 3));
} else if (e.key === '-' || e.key === '_') {
setZoom(prev => Math.max(prev - 0.1, 0.1));
} else if (e.key === '0') {
setZoom(1);
} else if (e.key === 'Escape') {
window.close();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isImage]);
const handleWheel = (e) => {
if (!isImage || !e.ctrlKey) return;
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
setZoom(prev => Math.min(Math.max(prev + delta, 0.1), 3));
};
const handleZoomIn = () => setZoom(prev => Math.min(prev + 0.1, 3));
const handleZoomOut = () => setZoom(prev => Math.max(prev - 0.1, 0.1));
const handleResetZoom = () => setZoom(1);
if (error) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
fontFamily: 'Arial, sans-serif',
backgroundColor: '#f5f5f5'
}}>
<div style={{ textAlign: 'center' }}>
<h1>Error</h1>
<p>{error}</p>
</div>
</div>
);
}
if (!isImage) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
fontFamily: 'Arial, sans-serif',
backgroundColor: '#f5f5f5'
}}>
<div style={{ textAlign: 'center' }}>
<h1>File Type Not Supported</h1>
<p>Image viewer only supports image files.</p>
<p>Please use direct file preview for PDFs and other documents.</p>
</div>
</div>
);
}
return (
<div
style={{
margin: 0,
padding: 0,
height: '100vh',
width: '100vw',
backgroundColor: '#000',
overflow: 'hidden',
position: 'relative'
}}
onWheel={handleWheel}
>
{isImage && (
<div style={{
position: 'fixed',
top: '20px',
right: '20px',
display: 'flex',
gap: '10px',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
padding: '10px',
borderRadius: '8px',
zIndex: 1000
}}>
<button
onClick={handleZoomOut}
style={{
backgroundColor: 'rgba(255, 255, 255, 0.2)',
color: '#fff',
border: '1px solid rgba(255, 255, 255, 0.3)',
padding: '8px 12px',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '16px'
}}
title="Zoom Out (-)"
>
</button>
<span style={{
color: '#fff',
padding: '8px 12px',
minWidth: '60px',
textAlign: 'center',
fontSize: '14px'
}}>
{Math.round(zoom * 100)}%
</span>
<button
onClick={handleZoomIn}
style={{
backgroundColor: 'rgba(255, 255, 255, 0.2)',
color: '#fff',
border: '1px solid rgba(255, 255, 255, 0.3)',
padding: '8px 12px',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '16px'
}}
title="Zoom In (+)"
>
+
</button>
<button
onClick={handleResetZoom}
style={{
backgroundColor: 'rgba(255, 255, 255, 0.2)',
color: '#fff',
border: '1px solid rgba(255, 255, 255, 0.3)',
padding: '8px 12px',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
title="Reset Zoom (0)"
>
Reset
</button>
</div>
)}
{isImage && fileUrl ? (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
overflow: 'auto'
}}>
<img
src={fileUrl}
alt={decodeURIComponent(fileName)}
style={{
maxWidth: 'none',
maxHeight: 'none',
transform: `scale(${zoom})`,
transformOrigin: 'center',
transition: 'transform 0.1s ease-out',
cursor: zoom > 1 ? 'move' : 'default'
}}
onError={() => setError('Failed to load image')}
draggable={false}
/>
</div>
) : isImage ? (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
color: '#fff',
fontFamily: 'Arial, sans-serif'
}}>
<p>Loading image...</p>
</div>
) : null}
{isImage && (
<div style={{
position: 'fixed',
bottom: '20px',
left: '20px',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
color: '#fff',
padding: '10px 15px',
borderRadius: '8px',
fontSize: '12px',
zIndex: 1000
}}>
<div>Mouse wheel + Ctrl: Zoom</div>
<div>Keyboard: +/ Zoom, 0: Reset, ESC: Close</div>
</div>
)}
</div>
);
};
export default ImageViewer;

56
src/api/contact.jsx Normal file
View File

@@ -0,0 +1,56 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllContact = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `contact?${queryParams.toString()}`,
});
return response.data;
};
const getContactById = async (id) => {
const response = await SendRequest({
method: 'get',
prefix: `contact/${id}`,
});
return response.data;
};
const createContact = async (queryParams) => {
const response = await SendRequest({
method: 'post',
prefix: `contact`,
params: queryParams,
});
return response.data;
};
const updateContact = async (id, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `contact/${id}`,
params: queryParams,
});
return response.data;
};
const deleteContact = async (id) => {
const response = await SendRequest({
method: 'delete',
prefix: `contact/${id}`,
});
return response.data;
};
export {
getAllContact,
getContactById,
createContact,
updateContact,
deleteContact,
};

135
src/api/file-uploads.jsx Normal file
View File

@@ -0,0 +1,135 @@
import { SendRequest } from '../components/Global/ApiRequest';
import axios from 'axios';
const API_BASE_URL = import.meta.env.VITE_API_SERVER;
// Get file from uploads directory
const getFile = async (folder, filename) => {
const token = localStorage.getItem('token');
if (!token) {
throw new Error('No authentication token found');
}
const response = await axios.get(`${API_BASE_URL}/file-uploads/${folder}/${encodeURIComponent(filename)}`, {
responseType: 'blob',
headers: {
'Authorization': `Bearer ${token.replace(/"/g, '')}`
}
});
return response.data;
};
// Download file as blob with proper handling
const downloadFile = async (folder, filename) => {
try {
const response = await getFile(folder, filename);
const blob = new Blob([response], {
type: 'application/octet-stream'
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
return { success: true, filename };
} catch (error) {
console.error('Error downloading file:', error);
throw error;
}
};
// Get file info (metadata)
const getFileInfo = async (folder, filename) => {
const response = await SendRequest({
method: 'head',
prefix: `file-uploads/${folder}/${encodeURIComponent(filename)}`
});
if (response.error) {
throw new Error(response.message);
}
return {
contentType: response.headers?.['content-type'],
contentLength: response.headers?.['content-length'],
lastModified: response.headers?.['last-modified'],
filename: filename,
folder: folder
};
};
// Get file URL for iframe
const getFileUrl = (folder, filename) => {
const token = localStorage.getItem('token');
if (token) {
return `${API_BASE_URL}/file-uploads/${folder}/${encodeURIComponent(filename)}?token=${encodeURIComponent(token)}`;
}
return `${API_BASE_URL}/file-uploads/${folder}/${encodeURIComponent(filename)}`;
};
// Check if file exists
const checkFileExists = async (folder, filename) => {
const response = await SendRequest({
method: 'head',
prefix: `file-uploads/${folder}/${encodeURIComponent(filename)}`
});
if (response.error && response.statusCode === 404) {
return false;
} else if (response.error) {
throw new Error(response.message);
}
return true;
};
const getFileType = (filename) => {
const ext = filename.split('.').pop().toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
const pdfExtensions = ['pdf'];
if (imageExtensions.includes(ext)) {
return 'image';
} else if (pdfExtensions.includes(ext)) {
return 'pdf';
}
return 'unknown';
};
// Upload file to server
const uploadFile = async (file, folder) => {
const formData = new FormData();
formData.append('file', file);
formData.append('folder', folder);
const response = await SendRequest({
method: 'post',
prefix: 'file-uploads',
params: formData
});
return response.data;
};
const getFolderFromFileType = (fileType) => {
return fileType === 'pdf' ? 'pdf' : 'images';
};
export {
getFile,
downloadFile,
getFileInfo,
getFileUrl,
checkFileExists,
getFileType,
getFolderFromFileType,
uploadFile
};

54
src/api/history-value.jsx Normal file
View File

@@ -0,0 +1,54 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllHistoryAlarm = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `history/alarm?${queryParams.toString()}`,
});
return response.data;
};
const getAllHistoryEvent = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `history/event?${queryParams.toString()}`,
});
return response.data;
};
const getAllHistoryValueReport = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `history/value-report?${queryParams.toString()}`,
});
return response.data;
};
const getAllHistoryValueReportPivot = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `history/value-report-pivot?${queryParams.toString()}`,
});
return response.data;
};
const getAllHistoryValueTrendingPivot = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `history/value-trending?${queryParams.toString()}`,
});
return response.data;
};
export {
getAllHistoryAlarm,
getAllHistoryEvent,
getAllHistoryValueReport,
getAllHistoryValueReportPivot,
getAllHistoryValueTrendingPivot,
};

View File

@@ -3,7 +3,7 @@ import { SendRequest } from '../components/Global/ApiRequest';
const getAllJadwalShift = async (queryParams) => { const getAllJadwalShift = async (queryParams) => {
const response = await SendRequest({ const response = await SendRequest({
method: 'get', method: 'get',
prefix: `jadwal-shift?${queryParams.toString()}`, prefix: `user-schedule?${queryParams.toString()}`,
}); });
return response.data; return response.data;
@@ -12,7 +12,7 @@ const getAllJadwalShift = async (queryParams) => {
const getJadwalShiftById = async (id) => { const getJadwalShiftById = async (id) => {
const response = await SendRequest({ const response = await SendRequest({
method: 'get', method: 'get',
prefix: `jadwal-shift/${id}`, prefix: `user-schedule/${id}`,
}); });
return response.data; return response.data;
@@ -21,7 +21,7 @@ const getJadwalShiftById = async (id) => {
const createJadwalShift = async (queryParams) => { const createJadwalShift = async (queryParams) => {
const response = await SendRequest({ const response = await SendRequest({
method: 'post', method: 'post',
prefix: `jadwal-shift`, prefix: `user-schedule`,
params: queryParams, params: queryParams,
}); });
@@ -31,7 +31,7 @@ const createJadwalShift = async (queryParams) => {
const updateJadwalShift = async (id, queryParams) => { const updateJadwalShift = async (id, queryParams) => {
const response = await SendRequest({ const response = await SendRequest({
method: 'put', method: 'put',
prefix: `jadwal-shift/${id}`, prefix: `user-schedule/${id}`,
params: queryParams, params: queryParams,
}); });
@@ -41,7 +41,7 @@ const updateJadwalShift = async (id, queryParams) => {
const deleteJadwalShift = async (id) => { const deleteJadwalShift = async (id) => {
const response = await SendRequest({ const response = await SendRequest({
method: 'delete', method: 'delete',
prefix: `jadwal-shift/${id}`, prefix: `user-schedule/${id}`,
}); });
return response.data; return response.data;
}; };

View File

@@ -47,4 +47,63 @@ const deleteBrand = async (id) => {
return response.data; return response.data;
}; };
export { getAllBrands, getBrandById, createBrand, updateBrand, deleteBrand }; const getErrorCodesByBrandId = async (brandId, queryParams) => {
const query = queryParams ? `?${queryParams.toString()}` : '';
const response = await SendRequest({
method: 'get',
prefix: `error-code/brand/${brandId}${query}`,
});
return response.data;
};
const getErrorCodeById = async (id) => {
const response = await SendRequest({
method: 'get',
prefix: `error-code/${id}`,
});
return response.data;
};
const createErrorCode = async (brandId, queryParams) => {
const response = await SendRequest({
method: 'post',
prefix: `error-code/brand/${brandId}`,
params: queryParams,
});
return response.data;
};
const updateErrorCode = async (brandId, errorCodeId, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `error-code/brand/${brandId}/${errorCodeId}`,
params: queryParams,
});
return response.data;
};
const deleteErrorCode = async (brandId, errorCode) => {
const response = await SendRequest({
method: 'delete',
prefix: `error-code/brand/${brandId}/${errorCode}`,
});
return response.data;
};
export {
getAllBrands,
getBrandById,
createBrand,
updateBrand,
deleteBrand,
getErrorCodesByBrandId,
getErrorCodeById,
createErrorCode,
updateErrorCode,
deleteErrorCode
};

105
src/api/notification.jsx Normal file
View File

@@ -0,0 +1,105 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllNotification = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `notification?${queryParams.toString()}`,
});
return response.data;
};
const getNotificationById = async (id) => {
const response = await SendRequest({
method: 'get',
prefix: `notification/${id}`,
});
return response.data;
};
const getNotificationDetail = async (id) => {
const response = await SendRequest({
method: 'get',
prefix: `notification/${id}`,
});
return response.data;
};
// Create new notification log
const createNotificationLog = async (data) => {
const response = await SendRequest({
method: 'post',
prefix: 'notification-log',
params: data,
});
return response.data;
};
// Get notification logs by notification_error_id
const getNotificationLogByNotificationId = async (notificationId) => {
const response = await SendRequest({
method: 'get',
prefix: `notification-log/notification_error/${notificationId}`,
});
return response.data;
};
// update is_read status
const updateIsRead = async (notificationId) => {
const response = await SendRequest({
method: 'put',
prefix: `notification/${notificationId}`,
});
return response.data;
};
// Resend notification to specific user
const resendNotificationToUser = async (notificationId, userId) => {
const response = await SendRequest({
method: 'post',
prefix: `notification/${notificationId}/resend/${userId}`,
});
return response.data;
};
// Resend Chat by User
const resendChatByUser = async (notificationId, userPhone) => {
const response = await SendRequest({
method: 'post',
prefix: `notification-user/resend/${notificationId}/${userPhone}`,
});
return response.data;
};
// Resend Chat All User
const resendChatAllUser = async (notificationId) => {
const response = await SendRequest({
method: 'post',
prefix: `notification/resend/${notificationId}`,
});
return response.data;
};
// Searching
const searchData = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `notification?criteria=${queryParams}`,
});
return response.data;
};
export {
getAllNotification,
getNotificationById,
getNotificationDetail,
createNotificationLog,
getNotificationLogByNotificationId,
updateIsRead,
resendNotificationToUser,
resendChatByUser,
resendChatAllUser,
searchData,
};

50
src/api/sparepart.jsx Normal file
View File

@@ -0,0 +1,50 @@
import { SendRequest } from '../components/Global/ApiRequest';
const getAllSparepart = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `sparepart?${queryParams.toString()}`,
});
return response.data;
};
const getSparepartById = async (id) => {
const response = await SendRequest({
method: 'get',
prefix: `sparepart/${id}`,
});
return response.data;
};
const createSparepart = async (queryParams) => {
const response = await SendRequest({
method: 'post',
prefix: `sparepart`,
params: queryParams,
});
return response.data;
};
const updateSparepart = async (id, queryParams) => {
const response = await SendRequest({
method: 'put',
prefix: `sparepart/${id}`,
params: queryParams,
});
return response.data;
};
const deleteSparepart = async (id) => {
const response = await SendRequest({
method: 'delete',
prefix: `sparepart/${id}`,
});
return response.data;
};
export { getAllSparepart, getSparepartById, createSparepart, updateSparepart, deleteSparepart };

BIN
src/assets/bg-cod-1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -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

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 177 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 177 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -0,0 +1,443 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com" viewBox="0 0 950 500">
<defs>
<bx:grid x="0" y="0" width="25" height="25"/>
</defs>
<rect x="12.226" y="12.005" width="924.818" height="476.396" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0);"/>
<rect x="25" y="75" width="900" height="400" style="stroke: rgb(0, 0, 0); fill: rgb(255, 255, 255);"/>
<rect x="50" y="100.548" width="100.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
<rect x="50" y="125" width="100.168" height="50" style="stroke: rgb(0, 0, 0); stroke-width: 0.693; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.693;" cx="125.124" cy="137.355" rx="11.269" ry="10.987"/>
<g transform="matrix(1.13391, 0, 0, 1.234446, -9.410634, 162.99009)" style="">
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 363.181 109.151 L 363.181 110.989 L 460.721 110.989 L 469.183 107.313 L 469.183 105.958 L 460.721 109.151 L 363.181 109.151 Z"/>
<path style="fill: rgb(115, 135, 166); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 361.061 109.151 L 361.061 53.909 L 462.836 53.909 L 462.836 109.151 L 361.061 109.151 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 445.801 105.475 L 445.801 57.586 L 454.374 57.586 L 454.374 105.475 L 445.801 105.475 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 433.101 105.475 L 433.101 57.586 L 441.571 57.586 L 441.571 105.475 L 433.101 105.475 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 420.411 105.475 L 420.411 57.586 L 428.871 57.586 L 428.871 105.475 L 420.411 105.475 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 407.721 105.475 L 407.721 57.586 L 416.181 57.586 L 416.181 105.475 L 407.721 105.475 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 395.021 105.475 L 395.021 57.586 L 403.481 57.586 L 403.481 105.475 L 395.021 105.475 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 382.221 105.475 L 382.221 57.586 L 390.791 57.586 L 390.791 105.475 L 382.221 105.475 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 369.521 105.475 L 369.521 57.586 L 377.991 57.586 L 377.991 105.475 L 369.521 105.475 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.641 81.579 L 371.641 76.064 L 375.871 76.064 L 375.871 81.579 L 371.641 81.579 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.531 94.446 L 371.531 88.931 L 375.871 88.931 L 375.871 94.446 L 371.531 94.446 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.641 87.093 L 371.641 83.417 L 375.981 83.417 L 375.981 87.093 L 371.641 87.093 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.641 99.96 L 371.641 96.284 L 375.871 96.284 L 375.871 99.96 L 371.641 99.96 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 81.579 L 384.331 76.064 L 388.561 76.064 L 388.561 81.579 L 384.331 81.579 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 94.446 L 384.331 88.931 L 388.561 88.931 L 388.561 94.446 L 384.331 94.446 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 87.093 L 384.331 83.417 L 388.671 83.417 L 388.671 87.093 L 384.331 87.093 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 99.96 L 384.331 96.284 L 388.561 96.284 L 388.561 99.96 L 384.331 99.96 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.031 81.579 L 397.031 76.064 L 401.371 76.064 L 401.371 81.579 L 397.031 81.579 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.031 94.446 L 397.031 88.931 L 401.261 88.931 L 401.261 94.446 L 397.031 94.446 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.141 87.093 L 397.141 83.417 L 401.371 83.417 L 401.371 87.093 L 397.141 87.093 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.031 99.96 L 397.031 96.284 L 401.371 96.284 L 401.371 99.96 L 397.031 99.96 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 385.451 105.475 L 385.451 101.798 L 387.561 101.798 L 387.561 105.475 L 385.451 105.475 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 372.751 105.475 L 372.751 101.798 L 374.871 101.798 L 374.871 105.475 L 372.751 105.475 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 372.751 61.262 L 372.751 57.586 L 374.871 57.586 L 374.871 61.262 L 372.751 61.262 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 385.451 61.262 L 385.451 57.586 L 387.561 57.586 L 387.561 61.262 L 385.451 61.262 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 398.141 61.262 L 398.141 57.586 L 400.261 57.586 L 400.261 61.262 L 398.141 61.262 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 398.141 105.475 L 398.141 101.798 L 400.261 101.798 L 400.261 105.475 L 398.141 105.475 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 80.611 L 451.141 80.611 L 451.141 82.449 L 449.031 82.449 L 449.031 80.611 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 76.935 L 451.141 76.935 L 451.141 78.87 L 449.031 78.87 L 449.031 76.935 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 84.287 L 451.141 84.287 L 451.141 86.126 L 449.031 86.126 L 449.031 84.287 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 73.258 L 451.141 73.258 L 451.141 75.097 L 449.031 75.097 L 449.031 73.258 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 87.964 L 451.141 87.964 L 451.141 89.802 L 449.031 89.802 L 449.031 87.964 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 69.582 L 451.141 69.582 L 451.141 71.42 L 449.031 71.42 L 449.031 69.582 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 91.64 L 451.141 91.64 L 451.141 93.478 L 449.031 93.478 L 449.031 91.64 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 80.611 L 438.451 80.611 L 438.451 82.449 L 436.331 82.449 L 436.331 80.611 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 76.935 L 438.451 76.935 L 438.451 78.87 L 436.331 78.87 L 436.331 76.935 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 84.287 L 438.451 84.287 L 438.451 86.126 L 436.331 86.126 L 436.331 84.287 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 73.258 L 438.451 73.258 L 438.451 75.097 L 436.331 75.097 L 436.331 73.258 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 87.964 L 438.451 87.964 L 438.451 89.802 L 436.331 89.802 L 436.331 87.964 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 69.582 L 438.451 69.582 L 438.451 71.42 L 436.331 71.42 L 436.331 69.582 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 91.64 L 438.451 91.64 L 438.451 93.478 L 436.331 93.478 L 436.331 91.64 Z"/>
<path style="fill: rgb(89, 109, 140); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 462.836 53.909 L 471.299 50.233 L 471.299 105.475 L 462.836 109.151 L 462.836 53.909 Z"/>
<path style="fill: rgb(191, 211, 242); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 462.836 53.909 L 471.299 50.233 L 369.521 50.233 L 361.061 53.909 L 462.836 53.909 Z"/>
</g>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre;" x="56" y="116.982">AirDryer A</text>
<rect x="50" y="150" width="100" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0);"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.368" y="141.716">On/Off</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="120.949" y="168.123">H</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.007" y="167.208">####</text>
<rect x="49.832" y="225.548" width="100.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
<rect x="49.832" y="250" width="100.168" height="50" style="stroke: rgb(0, 0, 0); stroke-width: 0.693; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.693;" cx="124.956" cy="262.355" rx="11.269" ry="10.987"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="55.832" y="241.982">AirDryer B</text>
<rect x="49.832" y="275" width="100" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.2" y="266.716">On/Off</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="120.781" y="293.123">H</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="55.839" y="292.208">####</text>
<rect x="49.832" y="350.548" width="100.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
<rect x="49.832" y="375" width="100.168" height="50" style="stroke: rgb(0, 0, 0); stroke-width: 0.693; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.693;" cx="124.956" cy="387.355" rx="11.269" ry="10.987"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="55.832" y="366.982">AirDryer C</text>
<rect x="49.832" y="400" width="100" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.2" y="391.716">On/Off</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="120.781" y="418.123">H</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="55.839" y="417.208">####</text>
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 4, 255);" d="M 250 125 L 350 125 L 350 275 L 400 275"/>
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 4, 255);" d="M 250 250 L 350 250 L 350 275 L 400 275"/>
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 4, 255);" d="M 250 375 L 350 375 L 350 275 L 400 275"/>
<g transform="matrix(1, 0, 0, 1, 49.999999, -99.999998)" style="">
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 292.93 L 717.857 292.651 L 717.857 292.442 L 717.857 292.163 L 717.857 291.954 L 717.857 291.744 L 717.857 291.535 L 717.857 291.396 L 717.914 291.186 L 717.914 291.046 L 717.972 290.907 L 718.029 290.837 L 718.086 290.698 L 718.143 290.558 L 718.201 290.488 L 718.258 290.419 L 718.372 290.349 L 718.487 290.279 L 718.602 290.209 L 718.716 290.14 L 718.888 290.14 L 719.06 290.07 L 719.231 290.07 L 719.46 290 L 719.632 290 L 719.919 290 L 720.147 290 L 720.434 290 L 720.72 290 L 721.063 290 L 721.407 290 L 721.751 290 L 722.151 290 L 770.706 290 L 771.106 290 L 771.45 290 L 771.794 290 L 772.137 290 L 772.423 290 L 772.71 290 L 772.939 290 L 773.225 290 L 773.397 290.07 L 773.626 290.07 L 773.798 290.14 L 773.969 290.14 L 774.141 290.209 L 774.256 290.279 L 774.37 290.349 L 774.485 290.419 L 774.599 290.488 L 774.657 290.558 L 774.714 290.698 L 774.771 290.837 L 774.828 290.977 L 774.886 291.117 L 774.943 291.256 L 774.943 291.465 L 775 291.674 L 775 291.884 L 775 292.093 L 775 292.303 L 775 292.581 L 775 292.86 L 775 293.209 L 775 293.488 L 775 335.558 L 775 335.907 L 775 336.256 L 775 336.604 L 775 336.884 L 775 337.163 L 775 337.442 L 775 337.72 L 775 337.93 L 774.943 338.14 L 774.943 338.349 L 774.943 338.488 L 774.886 338.628 L 774.886 338.767 L 774.828 338.907 L 774.771 339.047 L 774.714 339.117 L 774.657 339.256 L 774.599 339.326 L 774.485 339.396 L 774.427 339.465 L 774.313 339.465 L 774.198 339.535 L 774.084 339.535 L 773.969 339.604 L 773.798 339.604 L 773.683 339.604 L 773.511 339.604 L 773.339 339.674 L 773.168 339.674 L 772.939 339.674 L 772.71 339.674 L 772.481 339.674 L 721.579 339.674 L 721.235 339.674 L 720.892 339.674 L 720.663 339.674 L 720.377 339.674 L 720.09 339.604 L 719.861 339.604 L 719.69 339.604 L 719.46 339.604 L 719.289 339.535 L 719.117 339.535 L 718.945 339.465 L 718.831 339.465 L 718.659 339.396 L 718.544 339.326 L 718.43 339.256 L 718.372 339.117 L 718.258 339.047 L 718.201 338.907 L 718.143 338.767 L 718.086 338.628 L 718.029 338.488 L 717.972 338.349 L 717.914 338.14 L 717.914 337.93 L 717.914 337.72 L 717.857 337.442 L 717.857 337.163 L 717.857 336.884 L 717.857 336.604 L 717.857 336.256 L 717.857 335.907 L 717.857 335.558 L 717.857 292.93 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 732.114 339.674 L 760.743 339.674 L 760.743 342.884 L 732.114 342.884 L 732.114 339.674 Z"/>
<path style="fill: rgb(67, 67, 67); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 722.151 299.419 L 722.151 299.07 L 722.151 298.791 L 722.151 298.442 L 722.151 298.163 L 722.151 297.954 L 722.151 297.675 L 722.209 297.465 L 722.209 297.256 L 722.209 297.046 L 722.266 296.837 L 722.323 296.698 L 722.323 296.558 L 722.381 296.419 L 722.495 296.279 L 722.553 296.209 L 722.61 296.07 L 722.725 296 L 722.838 295.93 L 722.953 295.86 L 723.067 295.791 L 723.239 295.791 L 723.354 295.721 L 723.526 295.721 L 723.754 295.651 L 723.926 295.651 L 724.155 295.651 L 724.385 295.651 L 724.671 295.651 L 724.9 295.651 L 725.243 295.651 L 725.53 295.651 L 725.873 295.651 L 767.557 295.651 L 767.843 295.651 L 768.129 295.651 L 768.358 295.651 L 768.587 295.651 L 768.816 295.651 L 769.045 295.721 L 769.217 295.721 L 769.389 295.791 L 769.561 295.791 L 769.675 295.86 L 769.79 295.86 L 769.962 295.93 L 770.076 296 L 770.133 296.07 L 770.247 296.139 L 770.305 296.209 L 770.362 296.349 L 770.419 296.419 L 770.477 296.558 L 770.534 296.698 L 770.591 296.837 L 770.591 296.907 L 770.649 297.046 L 770.649 297.256 L 770.649 297.396 L 770.706 297.604 L 770.706 297.744 L 770.706 297.954 L 770.706 298.163 L 770.706 298.442 L 770.706 298.651 L 770.706 298.861 L 770.706 327.744 L 770.706 328.093 L 770.706 328.372 L 770.706 328.581 L 770.706 328.861 L 770.763 329.07 L 770.763 329.279 L 770.763 329.488 L 770.763 329.628 L 770.763 329.838 L 770.763 329.977 L 770.706 330.117 L 770.706 330.186 L 770.649 330.326 L 770.649 330.465 L 770.591 330.535 L 770.534 330.604 L 770.477 330.674 L 770.419 330.744 L 770.305 330.814 L 770.19 330.883 L 770.076 330.883 L 769.962 330.883 L 769.79 330.953 L 769.618 330.953 L 769.446 330.953 L 769.274 331.023 L 769.045 331.023 L 768.759 331.023 L 768.53 331.023 L 768.244 331.023 L 767.9 331.023 L 767.557 331.023 L 725.873 331.023 L 725.53 331.023 L 725.243 331.023 L 724.9 331.023 L 724.671 331.023 L 724.385 331.023 L 724.155 331.023 L 723.926 331.023 L 723.754 331.023 L 723.526 331.023 L 723.354 331.023 L 723.239 331.023 L 723.067 330.953 L 722.953 330.953 L 722.838 330.883 L 722.725 330.814 L 722.61 330.744 L 722.553 330.674 L 722.495 330.604 L 722.381 330.465 L 722.323 330.326 L 722.323 330.186 L 722.266 330.046 L 722.209 329.838 L 722.209 329.628 L 722.209 329.419 L 722.151 329.209 L 722.151 328.93 L 722.151 328.651 L 722.151 328.302 L 722.151 327.954 L 722.151 327.604 L 722.151 327.186 L 722.151 299.419 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 350 L 775 350 L 775 347.558 L 760.743 342.884 L 732.114 342.884 L 717.857 347.558 L 717.857 350 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 770.133 334.023 L 770.133 337.512 L 756.391 337.512 L 756.391 334.023 L 770.133 334.023 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 347.558 L 775 347.558"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 675 285.361 L 703.571 285.361 L 703.571 350 L 675 350 L 675 285.361 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 287.463 L 701.169 287.463 L 701.169 350 L 677.317 350 L 677.317 287.463 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 290.616 L 701.169 290.616 L 701.169 297.898 L 677.317 297.898 L 677.317 290.616 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 297.898 L 701.169 297.898 L 701.169 305.181 L 677.317 305.181 L 677.317 297.898 Z"/>
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 293.694 L 698.767 293.694 L 698.767 294.745 L 679.72 294.745 L 679.72 293.694 Z"/>
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 300 L 698.767 300 L 698.767 303.078 L 679.72 303.078 L 679.72 300 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 275 L 698.767 275 L 703.571 285.361 L 675 285.361 L 679.72 275 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 695.163 300 L 701.169 300 L 701.169 303.078 L 695.163 303.078 L 695.163 300 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 300 L 698.767 300 L 698.767 303.078 L 697.566 303.078 L 697.566 300 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 291.592 L 692.847 291.592 L 692.847 292.643 L 697.566 292.643 L 697.566 291.592 Z"/>
<circle style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="890.673" cy="114.148" r="1.051" transform="matrix(1.142857, 0, 0, 1.000013, -320.346635, 178.493448)"/>
</g>
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(6, 255, 0);" d="M 525 250 L 575 250 L 575 200 L 725 200"/>
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(6, 255, 0);" d="M 525 250 L 575 250 L 575 400 L 725 400"/>
<rect x="399.832" y="149.864" width="125.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
<rect x="400" y="175" width="125" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="425.352" y="167.628">PLC AirDryer</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="426.101" y="192.839">IP : 192.168.0.3</text>
<rect x="725" y="99.864" width="125.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
<rect x="725.168" y="125" width="125" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="759.52" y="117.628">PC Station</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="751.269" y="142.839">IP : 192.168.0.2</text>
<g transform="matrix(1, 0, 0, 1, 49.999999, 99.999998)" style="">
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 292.93 L 717.857 292.651 L 717.857 292.442 L 717.857 292.163 L 717.857 291.954 L 717.857 291.744 L 717.857 291.535 L 717.857 291.396 L 717.914 291.186 L 717.914 291.046 L 717.972 290.907 L 718.029 290.837 L 718.086 290.698 L 718.143 290.558 L 718.201 290.488 L 718.258 290.419 L 718.372 290.349 L 718.487 290.279 L 718.602 290.209 L 718.716 290.14 L 718.888 290.14 L 719.06 290.07 L 719.231 290.07 L 719.46 290 L 719.632 290 L 719.919 290 L 720.147 290 L 720.434 290 L 720.72 290 L 721.063 290 L 721.407 290 L 721.751 290 L 722.151 290 L 770.706 290 L 771.106 290 L 771.45 290 L 771.794 290 L 772.137 290 L 772.423 290 L 772.71 290 L 772.939 290 L 773.225 290 L 773.397 290.07 L 773.626 290.07 L 773.798 290.14 L 773.969 290.14 L 774.141 290.209 L 774.256 290.279 L 774.37 290.349 L 774.485 290.419 L 774.599 290.488 L 774.657 290.558 L 774.714 290.698 L 774.771 290.837 L 774.828 290.977 L 774.886 291.117 L 774.943 291.256 L 774.943 291.465 L 775 291.674 L 775 291.884 L 775 292.093 L 775 292.303 L 775 292.581 L 775 292.86 L 775 293.209 L 775 293.488 L 775 335.558 L 775 335.907 L 775 336.256 L 775 336.604 L 775 336.884 L 775 337.163 L 775 337.442 L 775 337.72 L 775 337.93 L 774.943 338.14 L 774.943 338.349 L 774.943 338.488 L 774.886 338.628 L 774.886 338.767 L 774.828 338.907 L 774.771 339.047 L 774.714 339.117 L 774.657 339.256 L 774.599 339.326 L 774.485 339.396 L 774.427 339.465 L 774.313 339.465 L 774.198 339.535 L 774.084 339.535 L 773.969 339.604 L 773.798 339.604 L 773.683 339.604 L 773.511 339.604 L 773.339 339.674 L 773.168 339.674 L 772.939 339.674 L 772.71 339.674 L 772.481 339.674 L 721.579 339.674 L 721.235 339.674 L 720.892 339.674 L 720.663 339.674 L 720.377 339.674 L 720.09 339.604 L 719.861 339.604 L 719.69 339.604 L 719.46 339.604 L 719.289 339.535 L 719.117 339.535 L 718.945 339.465 L 718.831 339.465 L 718.659 339.396 L 718.544 339.326 L 718.43 339.256 L 718.372 339.117 L 718.258 339.047 L 718.201 338.907 L 718.143 338.767 L 718.086 338.628 L 718.029 338.488 L 717.972 338.349 L 717.914 338.14 L 717.914 337.93 L 717.914 337.72 L 717.857 337.442 L 717.857 337.163 L 717.857 336.884 L 717.857 336.604 L 717.857 336.256 L 717.857 335.907 L 717.857 335.558 L 717.857 292.93 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 732.114 339.674 L 760.743 339.674 L 760.743 342.884 L 732.114 342.884 L 732.114 339.674 Z"/>
<path style="fill: rgb(67, 67, 67); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 722.151 299.419 L 722.151 299.07 L 722.151 298.791 L 722.151 298.442 L 722.151 298.163 L 722.151 297.954 L 722.151 297.675 L 722.209 297.465 L 722.209 297.256 L 722.209 297.046 L 722.266 296.837 L 722.323 296.698 L 722.323 296.558 L 722.381 296.419 L 722.495 296.279 L 722.553 296.209 L 722.61 296.07 L 722.725 296 L 722.838 295.93 L 722.953 295.86 L 723.067 295.791 L 723.239 295.791 L 723.354 295.721 L 723.526 295.721 L 723.754 295.651 L 723.926 295.651 L 724.155 295.651 L 724.385 295.651 L 724.671 295.651 L 724.9 295.651 L 725.243 295.651 L 725.53 295.651 L 725.873 295.651 L 767.557 295.651 L 767.843 295.651 L 768.129 295.651 L 768.358 295.651 L 768.587 295.651 L 768.816 295.651 L 769.045 295.721 L 769.217 295.721 L 769.389 295.791 L 769.561 295.791 L 769.675 295.86 L 769.79 295.86 L 769.962 295.93 L 770.076 296 L 770.133 296.07 L 770.247 296.139 L 770.305 296.209 L 770.362 296.349 L 770.419 296.419 L 770.477 296.558 L 770.534 296.698 L 770.591 296.837 L 770.591 296.907 L 770.649 297.046 L 770.649 297.256 L 770.649 297.396 L 770.706 297.604 L 770.706 297.744 L 770.706 297.954 L 770.706 298.163 L 770.706 298.442 L 770.706 298.651 L 770.706 298.861 L 770.706 327.744 L 770.706 328.093 L 770.706 328.372 L 770.706 328.581 L 770.706 328.861 L 770.763 329.07 L 770.763 329.279 L 770.763 329.488 L 770.763 329.628 L 770.763 329.838 L 770.763 329.977 L 770.706 330.117 L 770.706 330.186 L 770.649 330.326 L 770.649 330.465 L 770.591 330.535 L 770.534 330.604 L 770.477 330.674 L 770.419 330.744 L 770.305 330.814 L 770.19 330.883 L 770.076 330.883 L 769.962 330.883 L 769.79 330.953 L 769.618 330.953 L 769.446 330.953 L 769.274 331.023 L 769.045 331.023 L 768.759 331.023 L 768.53 331.023 L 768.244 331.023 L 767.9 331.023 L 767.557 331.023 L 725.873 331.023 L 725.53 331.023 L 725.243 331.023 L 724.9 331.023 L 724.671 331.023 L 724.385 331.023 L 724.155 331.023 L 723.926 331.023 L 723.754 331.023 L 723.526 331.023 L 723.354 331.023 L 723.239 331.023 L 723.067 330.953 L 722.953 330.953 L 722.838 330.883 L 722.725 330.814 L 722.61 330.744 L 722.553 330.674 L 722.495 330.604 L 722.381 330.465 L 722.323 330.326 L 722.323 330.186 L 722.266 330.046 L 722.209 329.838 L 722.209 329.628 L 722.209 329.419 L 722.151 329.209 L 722.151 328.93 L 722.151 328.651 L 722.151 328.302 L 722.151 327.954 L 722.151 327.604 L 722.151 327.186 L 722.151 299.419 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 350 L 775 350 L 775 347.558 L 760.743 342.884 L 732.114 342.884 L 717.857 347.558 L 717.857 350 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 770.133 334.023 L 770.133 337.512 L 756.391 337.512 L 756.391 334.023 L 770.133 334.023 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 347.558 L 775 347.558"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 675 285.361 L 703.571 285.361 L 703.571 350 L 675 350 L 675 285.361 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 287.463 L 701.169 287.463 L 701.169 350 L 677.317 350 L 677.317 287.463 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 290.616 L 701.169 290.616 L 701.169 297.898 L 677.317 297.898 L 677.317 290.616 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 297.898 L 701.169 297.898 L 701.169 305.181 L 677.317 305.181 L 677.317 297.898 Z"/>
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 293.694 L 698.767 293.694 L 698.767 294.745 L 679.72 294.745 L 679.72 293.694 Z"/>
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 300 L 698.767 300 L 698.767 303.078 L 679.72 303.078 L 679.72 300 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 275 L 698.767 275 L 703.571 285.361 L 675 285.361 L 679.72 275 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 695.163 300 L 701.169 300 L 701.169 303.078 L 695.163 303.078 L 695.163 300 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 300 L 698.767 300 L 698.767 303.078 L 697.566 303.078 L 697.566 300 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 291.592 L 692.847 291.592 L 692.847 292.643 L 697.566 292.643 L 697.566 291.592 Z"/>
<circle style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="890.673" cy="114.148" r="1.051" transform="matrix(1.142857, 0, 0, 1.000013, -320.346635, 178.493448)"/>
</g>
<rect x="724.832" y="299.864" width="125.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
<rect x="725" y="325" width="125" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="760.352" y="317.628">PC Server</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="751.1" y="317.975" transform="matrix(1, 0, 0, 1, -3.000031, 24.863983)">IP : xxx.xxx.xx.xx<tspan x="751.0999755859375" dy="1em"></tspan></text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 30px; white-space: pre; stroke-width: 1;" x="316.458" y="50.984">OVERVIEW AIR DRYER</text>
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 0, 0);" d="M 625 75 L 625 114.125 L 625 264.125 L 625 475"/>
<g transform="matrix(0.999999, 0, 0, 0.888921, -1058.006891, 44.212168)" style="">
<g id="Group_Base" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M4.392,105.741h62.834v2.253H4.392V105.741z"/>
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M0,112.498l1.014-1.126H2.14v-11.261H1.014L0,98.984h4.392v13.514H0z "/>
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M71.957,98.984l-1.127,1.127h-1.125v11.261h1.125l1.127,1.126h-4.504 V98.984H71.957z"/>
</g>
<g id="Group_Supports" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M11.148,89.977h49.547v13.514H11.148V89.977z"/>
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M4.392,76.463h4.504v27.026H4.392V76.463z"/>
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M62.948,76.463h4.278v27.026h-4.278V76.463z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,102.363h5.631v-6.758h-5.631h-0.337l-0.451,0.338 l-0.338,0.339l-0.338,0.563l-0.338,0.563l-0.225,0.563l-0.112,0.563l-0.113,0.45l0.113,0.451l0.112,0.563l0.225,0.563l0.338,0.563 l0.338,0.449l0.338,0.338l0.451,0.338L15.652,102.363z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,102.363h-5.631v-6.758h5.631h0.451l0.338,0.338l0.449,0.339 l0.338,0.563l0.226,0.563l0.226,0.563l0.225,0.563v0.448v0.451l-0.225,0.563l-0.226,0.563l-0.226,0.563l-0.338,0.449l-0.449,0.338 l-0.338,0.338L56.191,102.363z"/>
</g>
<g id="Group_Pipes" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="341.6875" y1="-275.623" x2="346.1914" y2="-275.623" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#SVGID_1_)" d="M56.191,6.871h-4.504v4.504h4.504V6.871z"/>
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,11.375V6.871h-4.504v4.504H56.191z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="305.6523" y1="-275.623" x2="310.1572" y2="-275.623" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#SVGID_2_)" d="M20.157,6.871h-4.504v4.504h4.504V6.871z"/>
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M20.157,11.375V6.871h-4.504v4.504H20.157z"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="323.6699" y1="-283.5059" x2="328.1738" y2="-283.5059" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#SVGID_3_)" stroke="#4C4C4C" stroke-width="0.25" d="M38.174,6.871H33.67v20.27h4.504V6.871z"/>
<path fill="#999999" d="M58.443,6.871V4.619H13.4v2.252H58.443z"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="325.9219" y1="-271.0625" x2="325.9219" y2="-273.2832" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#SVGID_4_)" stroke="#4C4C4C" stroke-width="0.25" d="M13.4,6.871h45.043V4.619H13.4V6.871"/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="325.9219" y1="-362.064" x2="325.9219" y2="-368.8364" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#SVGID_5_)" stroke="#4C4C4C" stroke-width="0.25" d="M21.283,95.605h29.277v6.758H21.283V95.605z"/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="322.5439" y1="-355.3496" x2="329.3008" y2="-355.3496" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#SVGID_6_)" stroke="#4C4C4C" stroke-width="0.25" d="M39.301,95.605V78.715h-6.757v16.893l0.112,0.676l0.226,0.563 l0.225,0.563l0.45,0.45l0.563,0.449l0.563,0.338l0.563,0.227l0.675,0.111l0.677-0.111l0.676-0.227l0.563-0.338l0.451-0.449 l0.449-0.45l0.339-0.563l0.112-0.563L39.301,95.605z"/>
</g>
<g id="Group_Column2" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="298.8965" y1="-318.8652" x2="316.9141" y2="-318.8652" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#B2B2B2"/>
<stop offset="0.5" style="stop-color:#E5E5E5"/>
<stop offset="1" style="stop-color:#B2B2B2"/>
</linearGradient>
<path fill="url(#SVGID_7_)" d="M26.914,13.628V94.48H8.896V13.628c0,0,2.204-3.378,9.009-3.378 C25.176,10.25,26.914,13.628,26.914,13.628"/>
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,13.628V94.48H8.896V13.628c0,0,1.972-3.378,9.043-3.378 C25.442,10.25,26.914,13.628,26.914,13.628z"/>
</g>
<g id="Group_Column1" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="334.9316" y1="-318.7939" x2="352.9482" y2="-318.7939" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#B2B2B2"/>
<stop offset="0.5" style="stop-color:#E5E5E5"/>
<stop offset="1" style="stop-color:#B2B2B2"/>
</linearGradient>
<path fill="url(#SVGID_8_)" d="M62.948,13.628V94.48H44.932V13.628c0,0,1.692-3.52,8.67-3.52 C61.661,10.108,62.948,13.628,62.948,13.628z"/>
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M62.948,13.628V94.48H44.932V13.628c0,0,1.668-3.52,8.67-3.52 C61.589,10.108,62.948,13.628,62.948,13.628z"/>
</g>
<g id="Group_Boards" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<circle fill="#666666" stroke="#4C4C4C" stroke-width="0.25" cx="31.981" cy="24.325" r="3.941"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M22.409,27.141h27.026v33.783H22.409V27.141z"/>
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M25.787,31.645h20.27v21.396h-20.27V31.645z"/>
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M25.787,57.545h20.27v21.17h-20.27V57.545z"/>
<path fill="#7F7F7F" stroke="#4C4C4C" stroke-width="0.25" d="M37.049,60.924h6.756v7.656h-6.756V60.924z"/>
</g>
<g id="Group_Points" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,55.293h3.378v4.504h-3.378V55.293z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,58.671h3.378v4.505h-3.378V58.671z"/>
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,58.671h3.378v1.126h-3.378V58.671z"/>
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,55.293h3.378v1.126h-3.378V55.293z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,47.41h3.378v4.504h-3.378V47.41z"/>
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,47.41h3.378v1.126h-3.378V47.41z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M52.813,55.293h3.379v4.504h-3.379V55.293z"/>
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M52.813,55.293h3.379v1.126h-3.379V55.293z"/>
</g>
<g id="Group_Connectors" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,1.917h0.676v43.692h-0.676V1.917z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,45.608h-7.207v-0.45h7.207V45.608z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,44.032h-7.207v-0.676h7.207V44.032z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,41.78h-7.207v-0.676h7.207V41.78z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,2.367H23.085v-0.45h30.179V2.367z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M23.085,2.367h0.45v2.252h-0.45V2.367z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,22.636h-40.54v-0.675h40.54V22.636z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M55.516,22.636h0.676v6.757h-0.676V22.636z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,22.636h0.676v6.757h-0.676V22.636z"/>
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M27.364,0.002l1.577,1.464l-2.478,2.478l-1.576-1.577L27.364,0.002z"/>
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M31.418,1.466l1.576-1.464l2.252,2.365L33.67,3.943L31.418,1.466z"/>
</g>
</g>
<g transform="matrix(0.999999, 0, 0, 0.888921, -1061.049982, 169.212228)" style="">
<g id="group-1" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M4.392,105.741h62.834v2.253H4.392V105.741z"/>
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M0,112.498l1.014-1.126H2.14v-11.261H1.014L0,98.984h4.392v13.514H0z "/>
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M71.957,98.984l-1.127,1.127h-1.125v11.261h1.125l1.127,1.126h-4.504 V98.984H71.957z"/>
</g>
<g id="group-2" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M11.148,89.977h49.547v13.514H11.148V89.977z"/>
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M4.392,76.463h4.504v27.026H4.392V76.463z"/>
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M62.948,76.463h4.278v27.026h-4.278V76.463z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,102.363h5.631v-6.758h-5.631h-0.337l-0.451,0.338 l-0.338,0.339l-0.338,0.563l-0.338,0.563l-0.225,0.563l-0.112,0.563l-0.113,0.45l0.113,0.451l0.112,0.563l0.225,0.563l0.338,0.563 l0.338,0.449l0.338,0.338l0.451,0.338L15.652,102.363z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,102.363h-5.631v-6.758h5.631h0.451l0.338,0.338l0.449,0.339 l0.338,0.563l0.226,0.563l0.226,0.563l0.225,0.563v0.448v0.451l-0.225,0.563l-0.226,0.563l-0.226,0.563l-0.338,0.449l-0.449,0.338 l-0.338,0.338L56.191,102.363z"/>
</g>
<g id="group-3" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<linearGradient id="gradient-1" gradientUnits="userSpaceOnUse" x1="341.6875" y1="-275.623" x2="346.1914" y2="-275.623" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#gradient-1)" d="M56.191,6.871h-4.504v4.504h4.504V6.871z"/>
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,11.375V6.871h-4.504v4.504H56.191z"/>
<linearGradient id="gradient-2" gradientUnits="userSpaceOnUse" x1="305.6523" y1="-275.623" x2="310.1572" y2="-275.623" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#gradient-2)" d="M20.157,6.871h-4.504v4.504h4.504V6.871z"/>
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M20.157,11.375V6.871h-4.504v4.504H20.157z"/>
<linearGradient id="gradient-3" gradientUnits="userSpaceOnUse" x1="323.6699" y1="-283.5059" x2="328.1738" y2="-283.5059" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#gradient-3)" stroke="#4C4C4C" stroke-width="0.25" d="M38.174,6.871H33.67v20.27h4.504V6.871z"/>
<path fill="#999999" d="M58.443,6.871V4.619H13.4v2.252H58.443z"/>
<linearGradient id="gradient-4" gradientUnits="userSpaceOnUse" x1="325.9219" y1="-271.0625" x2="325.9219" y2="-273.2832" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#gradient-4)" stroke="#4C4C4C" stroke-width="0.25" d="M13.4,6.871h45.043V4.619H13.4V6.871"/>
<linearGradient id="gradient-5" gradientUnits="userSpaceOnUse" x1="325.9219" y1="-362.064" x2="325.9219" y2="-368.8364" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#gradient-5)" stroke="#4C4C4C" stroke-width="0.25" d="M21.283,95.605h29.277v6.758H21.283V95.605z"/>
<linearGradient id="gradient-6" gradientUnits="userSpaceOnUse" x1="322.5439" y1="-355.3496" x2="329.3008" y2="-355.3496" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#gradient-6)" stroke="#4C4C4C" stroke-width="0.25" d="M39.301,95.605V78.715h-6.757v16.893l0.112,0.676l0.226,0.563 l0.225,0.563l0.45,0.45l0.563,0.449l0.563,0.338l0.563,0.227l0.675,0.111l0.677-0.111l0.676-0.227l0.563-0.338l0.451-0.449 l0.449-0.45l0.339-0.563l0.112-0.563L39.301,95.605z"/>
</g>
<g id="group-4" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<linearGradient id="gradient-7" gradientUnits="userSpaceOnUse" x1="298.8965" y1="-318.8652" x2="316.9141" y2="-318.8652" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#B2B2B2"/>
<stop offset="0.5" style="stop-color:#E5E5E5"/>
<stop offset="1" style="stop-color:#B2B2B2"/>
</linearGradient>
<path fill="url(#gradient-7)" d="M26.914,13.628V94.48H8.896V13.628c0,0,2.204-3.378,9.009-3.378 C25.176,10.25,26.914,13.628,26.914,13.628"/>
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,13.628V94.48H8.896V13.628c0,0,1.972-3.378,9.043-3.378 C25.442,10.25,26.914,13.628,26.914,13.628z"/>
</g>
<g id="group-5" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<linearGradient id="gradient-8" gradientUnits="userSpaceOnUse" x1="334.9316" y1="-318.7939" x2="352.9482" y2="-318.7939" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#B2B2B2"/>
<stop offset="0.5" style="stop-color:#E5E5E5"/>
<stop offset="1" style="stop-color:#B2B2B2"/>
</linearGradient>
<path fill="url(#gradient-8)" d="M62.948,13.628V94.48H44.932V13.628c0,0,1.692-3.52,8.67-3.52 C61.661,10.108,62.948,13.628,62.948,13.628z"/>
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M62.948,13.628V94.48H44.932V13.628c0,0,1.668-3.52,8.67-3.52 C61.589,10.108,62.948,13.628,62.948,13.628z"/>
</g>
<g id="group-6" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<circle fill="#666666" stroke="#4C4C4C" stroke-width="0.25" cx="31.981" cy="24.325" r="3.941"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M22.409,27.141h27.026v33.783H22.409V27.141z"/>
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M25.787,31.645h20.27v21.396h-20.27V31.645z"/>
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M25.787,57.545h20.27v21.17h-20.27V57.545z"/>
<path fill="#7F7F7F" stroke="#4C4C4C" stroke-width="0.25" d="M37.049,60.924h6.756v7.656h-6.756V60.924z"/>
</g>
<g id="group-7" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,55.293h3.378v4.504h-3.378V55.293z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,58.671h3.378v4.505h-3.378V58.671z"/>
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,58.671h3.378v1.126h-3.378V58.671z"/>
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,55.293h3.378v1.126h-3.378V55.293z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,47.41h3.378v4.504h-3.378V47.41z"/>
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,47.41h3.378v1.126h-3.378V47.41z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M52.813,55.293h3.379v4.504h-3.379V55.293z"/>
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M52.813,55.293h3.379v1.126h-3.379V55.293z"/>
</g>
<g id="group-8" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,1.917h0.676v43.692h-0.676V1.917z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,45.608h-7.207v-0.45h7.207V45.608z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,44.032h-7.207v-0.676h7.207V44.032z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,41.78h-7.207v-0.676h7.207V41.78z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,2.367H23.085v-0.45h30.179V2.367z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M23.085,2.367h0.45v2.252h-0.45V2.367z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,22.636h-40.54v-0.675h40.54V22.636z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M55.516,22.636h0.676v6.757h-0.676V22.636z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,22.636h0.676v6.757h-0.676V22.636z"/>
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M27.364,0.002l1.577,1.464l-2.478,2.478l-1.576-1.577L27.364,0.002z"/>
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M31.418,1.466l1.576-1.464l2.252,2.365L33.67,3.943L31.418,1.466z"/>
</g>
</g>
<g transform="matrix(0.999999, 0, 0, 0.888921, -1061.04986, 294.212174)" style="">
<g id="group-9" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M4.392,105.741h62.834v2.253H4.392V105.741z"/>
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M0,112.498l1.014-1.126H2.14v-11.261H1.014L0,98.984h4.392v13.514H0z "/>
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M71.957,98.984l-1.127,1.127h-1.125v11.261h1.125l1.127,1.126h-4.504 V98.984H71.957z"/>
</g>
<g id="group-10" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M11.148,89.977h49.547v13.514H11.148V89.977z"/>
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M4.392,76.463h4.504v27.026H4.392V76.463z"/>
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M62.948,76.463h4.278v27.026h-4.278V76.463z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,102.363h5.631v-6.758h-5.631h-0.337l-0.451,0.338 l-0.338,0.339l-0.338,0.563l-0.338,0.563l-0.225,0.563l-0.112,0.563l-0.113,0.45l0.113,0.451l0.112,0.563l0.225,0.563l0.338,0.563 l0.338,0.449l0.338,0.338l0.451,0.338L15.652,102.363z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,102.363h-5.631v-6.758h5.631h0.451l0.338,0.338l0.449,0.339 l0.338,0.563l0.226,0.563l0.226,0.563l0.225,0.563v0.448v0.451l-0.225,0.563l-0.226,0.563l-0.226,0.563l-0.338,0.449l-0.449,0.338 l-0.338,0.338L56.191,102.363z"/>
</g>
<g id="group-11" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<linearGradient id="gradient-9" gradientUnits="userSpaceOnUse" x1="341.6875" y1="-275.623" x2="346.1914" y2="-275.623" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#gradient-9)" d="M56.191,6.871h-4.504v4.504h4.504V6.871z"/>
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,11.375V6.871h-4.504v4.504H56.191z"/>
<linearGradient id="gradient-10" gradientUnits="userSpaceOnUse" x1="305.6523" y1="-275.623" x2="310.1572" y2="-275.623" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#gradient-10)" d="M20.157,6.871h-4.504v4.504h4.504V6.871z"/>
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M20.157,11.375V6.871h-4.504v4.504H20.157z"/>
<linearGradient id="gradient-11" gradientUnits="userSpaceOnUse" x1="323.6699" y1="-283.5059" x2="328.1738" y2="-283.5059" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#gradient-11)" stroke="#4C4C4C" stroke-width="0.25" d="M38.174,6.871H33.67v20.27h4.504V6.871z"/>
<path fill="#999999" d="M58.443,6.871V4.619H13.4v2.252H58.443z"/>
<linearGradient id="gradient-12" gradientUnits="userSpaceOnUse" x1="325.9219" y1="-271.0625" x2="325.9219" y2="-273.2832" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#gradient-12)" stroke="#4C4C4C" stroke-width="0.25" d="M13.4,6.871h45.043V4.619H13.4V6.871"/>
<linearGradient id="gradient-13" gradientUnits="userSpaceOnUse" x1="325.9219" y1="-362.064" x2="325.9219" y2="-368.8364" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#gradient-13)" stroke="#4C4C4C" stroke-width="0.25" d="M21.283,95.605h29.277v6.758H21.283V95.605z"/>
<linearGradient id="gradient-14" gradientUnits="userSpaceOnUse" x1="322.5439" y1="-355.3496" x2="329.3008" y2="-355.3496" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#999999"/>
<stop offset="0.5" style="stop-color:#CCCCCC"/>
<stop offset="1" style="stop-color:#999999"/>
</linearGradient>
<path fill="url(#gradient-14)" stroke="#4C4C4C" stroke-width="0.25" d="M39.301,95.605V78.715h-6.757v16.893l0.112,0.676l0.226,0.563 l0.225,0.563l0.45,0.45l0.563,0.449l0.563,0.338l0.563,0.227l0.675,0.111l0.677-0.111l0.676-0.227l0.563-0.338l0.451-0.449 l0.449-0.45l0.339-0.563l0.112-0.563L39.301,95.605z"/>
</g>
<g id="group-12" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<linearGradient id="gradient-15" gradientUnits="userSpaceOnUse" x1="298.8965" y1="-318.8652" x2="316.9141" y2="-318.8652" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#B2B2B2"/>
<stop offset="0.5" style="stop-color:#E5E5E5"/>
<stop offset="1" style="stop-color:#B2B2B2"/>
</linearGradient>
<path fill="url(#gradient-15)" d="M26.914,13.628V94.48H8.896V13.628c0,0,2.204-3.378,9.009-3.378 C25.176,10.25,26.914,13.628,26.914,13.628"/>
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,13.628V94.48H8.896V13.628c0,0,1.972-3.378,9.043-3.378 C25.442,10.25,26.914,13.628,26.914,13.628z"/>
</g>
<g id="group-13" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<linearGradient id="gradient-16" gradientUnits="userSpaceOnUse" x1="334.9316" y1="-318.7939" x2="352.9482" y2="-318.7939" gradientTransform="matrix(1 0 0 -1 -290 -266.5)">
<stop offset="0" style="stop-color:#B2B2B2"/>
<stop offset="0.5" style="stop-color:#E5E5E5"/>
<stop offset="1" style="stop-color:#B2B2B2"/>
</linearGradient>
<path fill="url(#gradient-16)" d="M62.948,13.628V94.48H44.932V13.628c0,0,1.692-3.52,8.67-3.52 C61.661,10.108,62.948,13.628,62.948,13.628z"/>
<path fill="none" stroke="#4C4C4C" stroke-width="0.25" d="M62.948,13.628V94.48H44.932V13.628c0,0,1.668-3.52,8.67-3.52 C61.589,10.108,62.948,13.628,62.948,13.628z"/>
</g>
<g id="group-14" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<circle fill="#666666" stroke="#4C4C4C" stroke-width="0.25" cx="31.981" cy="24.325" r="3.941"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M22.409,27.141h27.026v33.783H22.409V27.141z"/>
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M25.787,31.645h20.27v21.396h-20.27V31.645z"/>
<path fill="#4C4C4C" stroke="#4C4C4C" stroke-width="0.25" d="M25.787,57.545h20.27v21.17h-20.27V57.545z"/>
<path fill="#7F7F7F" stroke="#4C4C4C" stroke-width="0.25" d="M37.049,60.924h6.756v7.656h-6.756V60.924z"/>
</g>
<g id="group-15" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,55.293h3.378v4.504h-3.378V55.293z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,58.671h3.378v4.505h-3.378V58.671z"/>
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,58.671h3.378v1.126h-3.378V58.671z"/>
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,55.293h3.378v1.126h-3.378V55.293z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,47.41h3.378v4.504h-3.378V47.41z"/>
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M26.914,47.41h3.378v1.126h-3.378V47.41z"/>
<path fill="#FFFFFF" stroke="#4C4C4C" stroke-width="0.25" d="M52.813,55.293h3.379v4.504h-3.379V55.293z"/>
<path fill="#7F0000" stroke="#4C4C4C" stroke-width="0.25" d="M52.813,55.293h3.379v1.126h-3.379V55.293z"/>
</g>
<g id="group-16" transform="matrix(1, 0, 0, 1, 1236.05109, 62.756969)">
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,1.917h0.676v43.692h-0.676V1.917z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,45.608h-7.207v-0.45h7.207V45.608z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,44.032h-7.207v-0.676h7.207V44.032z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,41.78h-7.207v-0.676h7.207V41.78z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M53.264,2.367H23.085v-0.45h30.179V2.367z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M23.085,2.367h0.45v2.252h-0.45V2.367z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M56.191,22.636h-40.54v-0.675h40.54V22.636z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M55.516,22.636h0.676v6.757h-0.676V22.636z"/>
<path fill="#666666" stroke="#4C4C4C" stroke-width="0.25" d="M15.652,22.636h0.676v6.757h-0.676V22.636z"/>
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M27.364,0.002l1.577,1.464l-2.478,2.478l-1.576-1.577L27.364,0.002z"/>
<path fill="#B2B2B2" stroke="#4C4C4C" stroke-width="0.25" d="M31.418,1.466l1.576-1.464l2.252,2.365L33.67,3.943L31.418,1.466z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -0,0 +1,251 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com" viewBox="0 0 950 500">
<defs>
<bx:grid x="0" y="0" width="25" height="25"/>
</defs>
<rect x="12.226" y="12.005" width="924.818" height="462.995" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0);"/>
<rect x="25" y="75" width="900" height="375" style="stroke: rgb(0, 0, 0); fill: rgb(255, 255, 255);"/>
<rect x="50" y="100.548" width="100.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
<rect x="50" y="125" width="100.168" height="50" style="stroke: rgb(0, 0, 0); stroke-width: 0.693; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.693;" cx="125.124" cy="137.355" rx="11.269" ry="10.987"/>
<g transform="matrix(1.116151, 0, 0, 1.116686, -10.678418, 46.611243)" style="">
<path style="fill: rgb(204, 204, 204); stroke-width: 1;" d="M 166.356 47.81 L 278.348 47.81 L 278.348 114.973 L 166.356 114.973 L 166.356 47.81 Z"/>
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 166.356 95.477 L 278.348 67.396 L 278.348 114.973 L 166.356 114.973 L 166.356 95.477 Z"/>
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 166.356 98.16 L 278.348 70.258 L 278.348 114.973 L 166.356 114.973 L 166.356 98.16 Z"/>
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 166.356 102.453 L 278.348 74.371 L 278.348 114.973 L 166.356 114.973 L 166.356 102.453 Z"/>
<path style="fill: rgb(204, 204, 204); stroke-width: 1;" d="M 168.486 49.688 L 276.106 49.688 L 276.106 109.249 L 168.486 109.249 L 168.486 49.688 Z"/>
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 222.296 91.9 L 276.106 67.038 L 276.106 109.249 L 222.296 109.249 L 222.296 91.9 Z"/>
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 168.486 92.079 L 222.296 67.038 L 222.296 109.249 L 168.486 109.249 L 168.486 92.079 Z"/>
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 222.296 94.583 L 276.106 69.542 L 276.106 109.249 L 222.296 109.249 L 222.296 94.583 Z"/>
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 168.486 94.583 L 222.296 69.542 L 222.296 109.249 L 168.486 109.249 L 168.486 94.583 Z"/>
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 222.296 98.16 L 276.106 73.298 L 276.106 109.249 L 222.296 109.249 L 222.296 98.16 Z"/>
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 168.486 98.16 L 222.296 73.298 L 222.296 109.249 L 168.486 109.249 L 168.486 98.16 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 166.356 47.81 L 278.348 47.81 L 278.348 114.973 L 166.356 114.973 L 166.356 47.81"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 222.296 49.688 L 276.106 49.688 L 276.106 109.249 L 222.296 109.249 L 222.296 49.688"/>
<path style="fill: rgb(153, 153, 153); stroke-width: 1;" d="M 240.906 110.323 L 259.739 110.323 L 259.739 113.9 L 240.906 113.9 L 240.906 110.323 Z"/>
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 169.607 108.713 L 169.159 108.176 L 169.607 107.64 L 170.504 107.64 L 170.952 108.176 L 170.504 108.713 L 169.607 108.713 Z"/>
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 169.607 68.111 L 169.159 67.575 L 169.607 66.859 L 170.504 66.859 L 170.952 67.575 L 170.504 68.111 L 169.607 68.111 Z"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 168.486 49.688 L 222.296 49.688 L 222.296 109.249 L 168.486 109.249 L 168.486 49.688"/>
<path style="fill: rgb(229, 229, 229); stroke-width: 1;" d="M 229.247 51.119 L 235.749 51.119 L 235.749 59.973 L 229.247 59.973 L 229.247 51.119 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 224.762 51.119 L 226.332 51.119 L 226.332 55.233 L 224.762 55.233 L 224.762 51.119 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 272.519 51.119 L 274.313 51.119 L 274.313 55.233 L 272.519 55.233 L 272.519 51.119 Z"/>
<path style="fill: rgb(51, 51, 51); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 168.486 49.688 L 222.296 49.688 L 222.296 66.501 L 168.486 66.501 L 168.486 49.688 Z"/>
<path style="fill: rgb(229, 229, 229); stroke-width: 1;" d="M 185.526 60.778 L 192.028 60.778 L 192.028 63.103 L 185.526 63.103 L 185.526 60.778 Z"/>
<path style="fill: rgb(178, 178, 178); stroke-width: 1;" d="M 200.324 62.745 L 198.978 61.672 L 198.978 60.152 L 200.324 59.079 L 202.117 59.079 L 203.687 60.152 L 203.687 61.672 L 202.117 62.745 L 200.324 62.745 Z"/>
<path style="fill: rgb(178, 178, 178); stroke-width: 1;" d="M 207.947 62.745 L 206.602 61.672 L 206.602 60.152 L 207.947 59.079 L 209.74 59.079 L 211.086 60.152 L 211.086 61.672 L 209.74 62.745 L 207.947 62.745 Z"/>
<path style="fill: rgb(102, 102, 102); stroke-width: 1;" d="M 176.109 60.778 L 182.611 60.778 L 182.611 63.103 L 176.109 63.103 L 176.109 60.778 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 179.024 54.339 L 179.024 59.079 L 176.782 59.079"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 183.06 54.339 L 183.06 59.079 L 180.93 59.079"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 187.32 54.339 L 187.32 59.079 L 184.853 59.079"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 191.355 54.339 L 191.355 59.079 L 188.889 59.079"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 188.889 59.079 L 188.889 54.339 L 191.355 54.339"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 184.853 59.079 L 184.853 54.339 L 187.32 54.339"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 180.93 59.079 L 180.93 54.339 L 183.06 54.339"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 176.782 59.079 L 176.782 54.339 L 179.024 54.339"/>
<path style="fill: rgb(127, 127, 127); stroke-width: 1;" d="M 197.857 66.859 L 222.296 104.241 L 222.296 94.404 L 204.135 66.859 L 197.857 66.859 Z"/>
<path style="fill: rgb(102, 102, 102); stroke-width: 1;" d="M 204.135 66.859 L 222.296 94.404 L 222.296 84.566 L 210.637 66.859 L 204.135 66.859 Z"/>
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 210.637 66.859 L 222.296 84.566 L 222.296 74.729 L 217.139 66.859 L 210.637 66.859 Z"/>
<path style="fill: none; stroke: rgb(255, 255, 255); stroke-width: 2;" d="M 222.969 50.046 L 275.434 50.046"/>
</g>
<g transform="matrix(1.13391, 0, 0, 1.234446, -9.410634, 162.99009)" style="">
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 363.181 109.151 L 363.181 110.989 L 460.721 110.989 L 469.183 107.313 L 469.183 105.958 L 460.721 109.151 L 363.181 109.151 Z"/>
<path style="fill: rgb(115, 135, 166); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 361.061 109.151 L 361.061 53.909 L 462.836 53.909 L 462.836 109.151 L 361.061 109.151 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 445.801 105.475 L 445.801 57.586 L 454.374 57.586 L 454.374 105.475 L 445.801 105.475 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 433.101 105.475 L 433.101 57.586 L 441.571 57.586 L 441.571 105.475 L 433.101 105.475 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 420.411 105.475 L 420.411 57.586 L 428.871 57.586 L 428.871 105.475 L 420.411 105.475 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 407.721 105.475 L 407.721 57.586 L 416.181 57.586 L 416.181 105.475 L 407.721 105.475 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 395.021 105.475 L 395.021 57.586 L 403.481 57.586 L 403.481 105.475 L 395.021 105.475 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 382.221 105.475 L 382.221 57.586 L 390.791 57.586 L 390.791 105.475 L 382.221 105.475 Z"/>
<path style="fill: rgb(166, 186, 217); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 369.521 105.475 L 369.521 57.586 L 377.991 57.586 L 377.991 105.475 L 369.521 105.475 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.641 81.579 L 371.641 76.064 L 375.871 76.064 L 375.871 81.579 L 371.641 81.579 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.531 94.446 L 371.531 88.931 L 375.871 88.931 L 375.871 94.446 L 371.531 94.446 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.641 87.093 L 371.641 83.417 L 375.981 83.417 L 375.981 87.093 L 371.641 87.093 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 371.641 99.96 L 371.641 96.284 L 375.871 96.284 L 375.871 99.96 L 371.641 99.96 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 81.579 L 384.331 76.064 L 388.561 76.064 L 388.561 81.579 L 384.331 81.579 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 94.446 L 384.331 88.931 L 388.561 88.931 L 388.561 94.446 L 384.331 94.446 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 87.093 L 384.331 83.417 L 388.671 83.417 L 388.671 87.093 L 384.331 87.093 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 384.331 99.96 L 384.331 96.284 L 388.561 96.284 L 388.561 99.96 L 384.331 99.96 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.031 81.579 L 397.031 76.064 L 401.371 76.064 L 401.371 81.579 L 397.031 81.579 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.031 94.446 L 397.031 88.931 L 401.261 88.931 L 401.261 94.446 L 397.031 94.446 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.141 87.093 L 397.141 83.417 L 401.371 83.417 L 401.371 87.093 L 397.141 87.093 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 397.031 99.96 L 397.031 96.284 L 401.371 96.284 L 401.371 99.96 L 397.031 99.96 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 385.451 105.475 L 385.451 101.798 L 387.561 101.798 L 387.561 105.475 L 385.451 105.475 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 372.751 105.475 L 372.751 101.798 L 374.871 101.798 L 374.871 105.475 L 372.751 105.475 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 372.751 61.262 L 372.751 57.586 L 374.871 57.586 L 374.871 61.262 L 372.751 61.262 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 385.451 61.262 L 385.451 57.586 L 387.561 57.586 L 387.561 61.262 L 385.451 61.262 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 398.141 61.262 L 398.141 57.586 L 400.261 57.586 L 400.261 61.262 L 398.141 61.262 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 398.141 105.475 L 398.141 101.798 L 400.261 101.798 L 400.261 105.475 L 398.141 105.475 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 80.611 L 451.141 80.611 L 451.141 82.449 L 449.031 82.449 L 449.031 80.611 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 76.935 L 451.141 76.935 L 451.141 78.87 L 449.031 78.87 L 449.031 76.935 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 84.287 L 451.141 84.287 L 451.141 86.126 L 449.031 86.126 L 449.031 84.287 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 73.258 L 451.141 73.258 L 451.141 75.097 L 449.031 75.097 L 449.031 73.258 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 87.964 L 451.141 87.964 L 451.141 89.802 L 449.031 89.802 L 449.031 87.964 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 69.582 L 451.141 69.582 L 451.141 71.42 L 449.031 71.42 L 449.031 69.582 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 449.031 91.64 L 451.141 91.64 L 451.141 93.478 L 449.031 93.478 L 449.031 91.64 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 80.611 L 438.451 80.611 L 438.451 82.449 L 436.331 82.449 L 436.331 80.611 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 76.935 L 438.451 76.935 L 438.451 78.87 L 436.331 78.87 L 436.331 76.935 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 84.287 L 438.451 84.287 L 438.451 86.126 L 436.331 86.126 L 436.331 84.287 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 73.258 L 438.451 73.258 L 438.451 75.097 L 436.331 75.097 L 436.331 73.258 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 87.964 L 438.451 87.964 L 438.451 89.802 L 436.331 89.802 L 436.331 87.964 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 69.582 L 438.451 69.582 L 438.451 71.42 L 436.331 71.42 L 436.331 69.582 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 436.331 91.64 L 438.451 91.64 L 438.451 93.478 L 436.331 93.478 L 436.331 91.64 Z"/>
<path style="fill: rgb(89, 109, 140); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 462.836 53.909 L 471.299 50.233 L 471.299 105.475 L 462.836 109.151 L 462.836 53.909 Z"/>
<path style="fill: rgb(191, 211, 242); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 462.836 53.909 L 471.299 50.233 L 369.521 50.233 L 361.061 53.909 L 462.836 53.909 Z"/>
</g>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre;" x="56" y="116.982">COMPRESSOR A</text>
<rect x="50" y="150" width="100" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0);"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.368" y="141.716">On/Off</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="120.949" y="168.123">H</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.007" y="167.208">####</text>
<rect x="49.832" y="225.548" width="100.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
<rect x="49.832" y="250" width="100.168" height="50" style="stroke: rgb(0, 0, 0); stroke-width: 0.693; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.693;" cx="124.956" cy="262.355" rx="11.269" ry="10.987"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="55.832" y="241.982">COMPRESSOR B</text>
<rect x="49.832" y="275" width="100" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.2" y="266.716">On/Off</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="120.781" y="293.123">H</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="55.839" y="292.208">####</text>
<rect x="49.832" y="350.548" width="100.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
<rect x="49.832" y="375" width="100.168" height="50" style="stroke: rgb(0, 0, 0); stroke-width: 0.693; fill: rgb(244, 248, 248);"/>
<ellipse style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 0.693;" cx="124.956" cy="387.355" rx="11.269" ry="10.987"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="55.832" y="366.982">COMPRESSOR C</text>
<rect x="49.832" y="400" width="100" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="56.2" y="391.716">On/Off</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="120.781" y="418.123">H</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 15px; white-space: pre; stroke-width: 1;" x="55.839" y="417.208">####</text>
<g transform="matrix(1.116151, 0, 0, 1.116686, -10.678391, 171.611261)" style="">
<path style="fill: rgb(204, 204, 204); stroke-width: 1;" d="M 166.356 47.81 L 278.348 47.81 L 278.348 114.973 L 166.356 114.973 L 166.356 47.81 Z"/>
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 166.356 95.477 L 278.348 67.396 L 278.348 114.973 L 166.356 114.973 L 166.356 95.477 Z"/>
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 166.356 98.16 L 278.348 70.258 L 278.348 114.973 L 166.356 114.973 L 166.356 98.16 Z"/>
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 166.356 102.453 L 278.348 74.371 L 278.348 114.973 L 166.356 114.973 L 166.356 102.453 Z"/>
<path style="fill: rgb(204, 204, 204); stroke-width: 1;" d="M 168.486 49.688 L 276.106 49.688 L 276.106 109.249 L 168.486 109.249 L 168.486 49.688 Z"/>
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 222.296 91.9 L 276.106 67.038 L 276.106 109.249 L 222.296 109.249 L 222.296 91.9 Z"/>
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 168.486 92.079 L 222.296 67.038 L 222.296 109.249 L 168.486 109.249 L 168.486 92.079 Z"/>
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 222.296 94.583 L 276.106 69.542 L 276.106 109.249 L 222.296 109.249 L 222.296 94.583 Z"/>
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 168.486 94.583 L 222.296 69.542 L 222.296 109.249 L 168.486 109.249 L 168.486 94.583 Z"/>
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 222.296 98.16 L 276.106 73.298 L 276.106 109.249 L 222.296 109.249 L 222.296 98.16 Z"/>
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 168.486 98.16 L 222.296 73.298 L 222.296 109.249 L 168.486 109.249 L 168.486 98.16 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 166.356 47.81 L 278.348 47.81 L 278.348 114.973 L 166.356 114.973 L 166.356 47.81"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 222.296 49.688 L 276.106 49.688 L 276.106 109.249 L 222.296 109.249 L 222.296 49.688"/>
<path style="fill: rgb(153, 153, 153); stroke-width: 1;" d="M 240.906 110.323 L 259.739 110.323 L 259.739 113.9 L 240.906 113.9 L 240.906 110.323 Z"/>
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 169.607 108.713 L 169.159 108.176 L 169.607 107.64 L 170.504 107.64 L 170.952 108.176 L 170.504 108.713 L 169.607 108.713 Z"/>
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 169.607 68.111 L 169.159 67.575 L 169.607 66.859 L 170.504 66.859 L 170.952 67.575 L 170.504 68.111 L 169.607 68.111 Z"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 168.486 49.688 L 222.296 49.688 L 222.296 109.249 L 168.486 109.249 L 168.486 49.688"/>
<path style="fill: rgb(229, 229, 229); stroke-width: 1;" d="M 229.247 51.119 L 235.749 51.119 L 235.749 59.973 L 229.247 59.973 L 229.247 51.119 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 224.762 51.119 L 226.332 51.119 L 226.332 55.233 L 224.762 55.233 L 224.762 51.119 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 272.519 51.119 L 274.313 51.119 L 274.313 55.233 L 272.519 55.233 L 272.519 51.119 Z"/>
<path style="fill: rgb(51, 51, 51); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 168.486 49.688 L 222.296 49.688 L 222.296 66.501 L 168.486 66.501 L 168.486 49.688 Z"/>
<path style="fill: rgb(229, 229, 229); stroke-width: 1;" d="M 185.526 60.778 L 192.028 60.778 L 192.028 63.103 L 185.526 63.103 L 185.526 60.778 Z"/>
<path style="fill: rgb(178, 178, 178); stroke-width: 1;" d="M 200.324 62.745 L 198.978 61.672 L 198.978 60.152 L 200.324 59.079 L 202.117 59.079 L 203.687 60.152 L 203.687 61.672 L 202.117 62.745 L 200.324 62.745 Z"/>
<path style="fill: rgb(178, 178, 178); stroke-width: 1;" d="M 207.947 62.745 L 206.602 61.672 L 206.602 60.152 L 207.947 59.079 L 209.74 59.079 L 211.086 60.152 L 211.086 61.672 L 209.74 62.745 L 207.947 62.745 Z"/>
<path style="fill: rgb(102, 102, 102); stroke-width: 1;" d="M 176.109 60.778 L 182.611 60.778 L 182.611 63.103 L 176.109 63.103 L 176.109 60.778 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 179.024 54.339 L 179.024 59.079 L 176.782 59.079"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 183.06 54.339 L 183.06 59.079 L 180.93 59.079"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 187.32 54.339 L 187.32 59.079 L 184.853 59.079"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 191.355 54.339 L 191.355 59.079 L 188.889 59.079"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 188.889 59.079 L 188.889 54.339 L 191.355 54.339"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 184.853 59.079 L 184.853 54.339 L 187.32 54.339"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 180.93 59.079 L 180.93 54.339 L 183.06 54.339"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 176.782 59.079 L 176.782 54.339 L 179.024 54.339"/>
<path style="fill: rgb(127, 127, 127); stroke-width: 1;" d="M 197.857 66.859 L 222.296 104.241 L 222.296 94.404 L 204.135 66.859 L 197.857 66.859 Z"/>
<path style="fill: rgb(102, 102, 102); stroke-width: 1;" d="M 204.135 66.859 L 222.296 94.404 L 222.296 84.566 L 210.637 66.859 L 204.135 66.859 Z"/>
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 210.637 66.859 L 222.296 84.566 L 222.296 74.729 L 217.139 66.859 L 210.637 66.859 Z"/>
<path style="fill: none; stroke: rgb(255, 255, 255); stroke-width: 2;" d="M 222.969 50.046 L 275.434 50.046"/>
</g>
<g transform="matrix(1.116151, 0, 0, 1.116686, -10.678387, 296.611283)" style="">
<path style="fill: rgb(204, 204, 204); stroke-width: 1;" d="M 166.356 47.81 L 278.348 47.81 L 278.348 114.973 L 166.356 114.973 L 166.356 47.81 Z"/>
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 166.356 95.477 L 278.348 67.396 L 278.348 114.973 L 166.356 114.973 L 166.356 95.477 Z"/>
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 166.356 98.16 L 278.348 70.258 L 278.348 114.973 L 166.356 114.973 L 166.356 98.16 Z"/>
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 166.356 102.453 L 278.348 74.371 L 278.348 114.973 L 166.356 114.973 L 166.356 102.453 Z"/>
<path style="fill: rgb(204, 204, 204); stroke-width: 1;" d="M 168.486 49.688 L 276.106 49.688 L 276.106 109.249 L 168.486 109.249 L 168.486 49.688 Z"/>
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 222.296 91.9 L 276.106 67.038 L 276.106 109.249 L 222.296 109.249 L 222.296 91.9 Z"/>
<path style="fill: rgb(199, 199, 199); stroke-width: 1;" d="M 168.486 92.079 L 222.296 67.038 L 222.296 109.249 L 168.486 109.249 L 168.486 92.079 Z"/>
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 222.296 94.583 L 276.106 69.542 L 276.106 109.249 L 222.296 109.249 L 222.296 94.583 Z"/>
<path style="fill: rgb(194, 194, 194); stroke-width: 1;" d="M 168.486 94.583 L 222.296 69.542 L 222.296 109.249 L 168.486 109.249 L 168.486 94.583 Z"/>
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 222.296 98.16 L 276.106 73.298 L 276.106 109.249 L 222.296 109.249 L 222.296 98.16 Z"/>
<path style="fill: rgb(189, 189, 189); stroke-width: 1;" d="M 168.486 98.16 L 222.296 73.298 L 222.296 109.249 L 168.486 109.249 L 168.486 98.16 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 166.356 47.81 L 278.348 47.81 L 278.348 114.973 L 166.356 114.973 L 166.356 47.81"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 222.296 49.688 L 276.106 49.688 L 276.106 109.249 L 222.296 109.249 L 222.296 49.688"/>
<path style="fill: rgb(153, 153, 153); stroke-width: 1;" d="M 240.906 110.323 L 259.739 110.323 L 259.739 113.9 L 240.906 113.9 L 240.906 110.323 Z"/>
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 169.607 108.713 L 169.159 108.176 L 169.607 107.64 L 170.504 107.64 L 170.952 108.176 L 170.504 108.713 L 169.607 108.713 Z"/>
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 169.607 68.111 L 169.159 67.575 L 169.607 66.859 L 170.504 66.859 L 170.952 67.575 L 170.504 68.111 L 169.607 68.111 Z"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 168.486 49.688 L 222.296 49.688 L 222.296 109.249 L 168.486 109.249 L 168.486 49.688"/>
<path style="fill: rgb(229, 229, 229); stroke-width: 1;" d="M 229.247 51.119 L 235.749 51.119 L 235.749 59.973 L 229.247 59.973 L 229.247 51.119 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 224.762 51.119 L 226.332 51.119 L 226.332 55.233 L 224.762 55.233 L 224.762 51.119 Z"/>
<path style="fill: rgb(0, 0, 0); stroke-width: 1;" d="M 272.519 51.119 L 274.313 51.119 L 274.313 55.233 L 272.519 55.233 L 272.519 51.119 Z"/>
<path style="fill: rgb(51, 51, 51); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 168.486 49.688 L 222.296 49.688 L 222.296 66.501 L 168.486 66.501 L 168.486 49.688 Z"/>
<path style="fill: rgb(229, 229, 229); stroke-width: 1;" d="M 185.526 60.778 L 192.028 60.778 L 192.028 63.103 L 185.526 63.103 L 185.526 60.778 Z"/>
<path style="fill: rgb(178, 178, 178); stroke-width: 1;" d="M 200.324 62.745 L 198.978 61.672 L 198.978 60.152 L 200.324 59.079 L 202.117 59.079 L 203.687 60.152 L 203.687 61.672 L 202.117 62.745 L 200.324 62.745 Z"/>
<path style="fill: rgb(178, 178, 178); stroke-width: 1;" d="M 207.947 62.745 L 206.602 61.672 L 206.602 60.152 L 207.947 59.079 L 209.74 59.079 L 211.086 60.152 L 211.086 61.672 L 209.74 62.745 L 207.947 62.745 Z"/>
<path style="fill: rgb(102, 102, 102); stroke-width: 1;" d="M 176.109 60.778 L 182.611 60.778 L 182.611 63.103 L 176.109 63.103 L 176.109 60.778 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 179.024 54.339 L 179.024 59.079 L 176.782 59.079"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 183.06 54.339 L 183.06 59.079 L 180.93 59.079"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 187.32 54.339 L 187.32 59.079 L 184.853 59.079"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 191.355 54.339 L 191.355 59.079 L 188.889 59.079"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 188.889 59.079 L 188.889 54.339 L 191.355 54.339"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 184.853 59.079 L 184.853 54.339 L 187.32 54.339"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 180.93 59.079 L 180.93 54.339 L 183.06 54.339"/>
<path style="fill: none; stroke: rgb(127, 127, 127); stroke-width: 2;" d="M 176.782 59.079 L 176.782 54.339 L 179.024 54.339"/>
<path style="fill: rgb(127, 127, 127); stroke-width: 1;" d="M 197.857 66.859 L 222.296 104.241 L 222.296 94.404 L 204.135 66.859 L 197.857 66.859 Z"/>
<path style="fill: rgb(102, 102, 102); stroke-width: 1;" d="M 204.135 66.859 L 222.296 94.404 L 222.296 84.566 L 210.637 66.859 L 204.135 66.859 Z"/>
<path style="fill: rgb(76, 76, 76); stroke-width: 1;" d="M 210.637 66.859 L 222.296 84.566 L 222.296 74.729 L 217.139 66.859 L 210.637 66.859 Z"/>
<path style="fill: none; stroke: rgb(255, 255, 255); stroke-width: 2;" d="M 222.969 50.046 L 275.434 50.046"/>
</g>
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 4, 255);" d="M 300 125 L 350 125 L 350 275 L 400 275"/>
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 4, 255);" d="M 300 250 L 350 250 L 350 275 L 400 275"/>
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 4, 255);" d="M 300 375 L 350 375 L 350 275 L 400 275"/>
<g transform="matrix(1, 0, 0, 1, 49.999999, -99.999998)" style="">
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 292.93 L 717.857 292.651 L 717.857 292.442 L 717.857 292.163 L 717.857 291.954 L 717.857 291.744 L 717.857 291.535 L 717.857 291.396 L 717.914 291.186 L 717.914 291.046 L 717.972 290.907 L 718.029 290.837 L 718.086 290.698 L 718.143 290.558 L 718.201 290.488 L 718.258 290.419 L 718.372 290.349 L 718.487 290.279 L 718.602 290.209 L 718.716 290.14 L 718.888 290.14 L 719.06 290.07 L 719.231 290.07 L 719.46 290 L 719.632 290 L 719.919 290 L 720.147 290 L 720.434 290 L 720.72 290 L 721.063 290 L 721.407 290 L 721.751 290 L 722.151 290 L 770.706 290 L 771.106 290 L 771.45 290 L 771.794 290 L 772.137 290 L 772.423 290 L 772.71 290 L 772.939 290 L 773.225 290 L 773.397 290.07 L 773.626 290.07 L 773.798 290.14 L 773.969 290.14 L 774.141 290.209 L 774.256 290.279 L 774.37 290.349 L 774.485 290.419 L 774.599 290.488 L 774.657 290.558 L 774.714 290.698 L 774.771 290.837 L 774.828 290.977 L 774.886 291.117 L 774.943 291.256 L 774.943 291.465 L 775 291.674 L 775 291.884 L 775 292.093 L 775 292.303 L 775 292.581 L 775 292.86 L 775 293.209 L 775 293.488 L 775 335.558 L 775 335.907 L 775 336.256 L 775 336.604 L 775 336.884 L 775 337.163 L 775 337.442 L 775 337.72 L 775 337.93 L 774.943 338.14 L 774.943 338.349 L 774.943 338.488 L 774.886 338.628 L 774.886 338.767 L 774.828 338.907 L 774.771 339.047 L 774.714 339.117 L 774.657 339.256 L 774.599 339.326 L 774.485 339.396 L 774.427 339.465 L 774.313 339.465 L 774.198 339.535 L 774.084 339.535 L 773.969 339.604 L 773.798 339.604 L 773.683 339.604 L 773.511 339.604 L 773.339 339.674 L 773.168 339.674 L 772.939 339.674 L 772.71 339.674 L 772.481 339.674 L 721.579 339.674 L 721.235 339.674 L 720.892 339.674 L 720.663 339.674 L 720.377 339.674 L 720.09 339.604 L 719.861 339.604 L 719.69 339.604 L 719.46 339.604 L 719.289 339.535 L 719.117 339.535 L 718.945 339.465 L 718.831 339.465 L 718.659 339.396 L 718.544 339.326 L 718.43 339.256 L 718.372 339.117 L 718.258 339.047 L 718.201 338.907 L 718.143 338.767 L 718.086 338.628 L 718.029 338.488 L 717.972 338.349 L 717.914 338.14 L 717.914 337.93 L 717.914 337.72 L 717.857 337.442 L 717.857 337.163 L 717.857 336.884 L 717.857 336.604 L 717.857 336.256 L 717.857 335.907 L 717.857 335.558 L 717.857 292.93 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 732.114 339.674 L 760.743 339.674 L 760.743 342.884 L 732.114 342.884 L 732.114 339.674 Z"/>
<path style="fill: rgb(67, 67, 67); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 722.151 299.419 L 722.151 299.07 L 722.151 298.791 L 722.151 298.442 L 722.151 298.163 L 722.151 297.954 L 722.151 297.675 L 722.209 297.465 L 722.209 297.256 L 722.209 297.046 L 722.266 296.837 L 722.323 296.698 L 722.323 296.558 L 722.381 296.419 L 722.495 296.279 L 722.553 296.209 L 722.61 296.07 L 722.725 296 L 722.838 295.93 L 722.953 295.86 L 723.067 295.791 L 723.239 295.791 L 723.354 295.721 L 723.526 295.721 L 723.754 295.651 L 723.926 295.651 L 724.155 295.651 L 724.385 295.651 L 724.671 295.651 L 724.9 295.651 L 725.243 295.651 L 725.53 295.651 L 725.873 295.651 L 767.557 295.651 L 767.843 295.651 L 768.129 295.651 L 768.358 295.651 L 768.587 295.651 L 768.816 295.651 L 769.045 295.721 L 769.217 295.721 L 769.389 295.791 L 769.561 295.791 L 769.675 295.86 L 769.79 295.86 L 769.962 295.93 L 770.076 296 L 770.133 296.07 L 770.247 296.139 L 770.305 296.209 L 770.362 296.349 L 770.419 296.419 L 770.477 296.558 L 770.534 296.698 L 770.591 296.837 L 770.591 296.907 L 770.649 297.046 L 770.649 297.256 L 770.649 297.396 L 770.706 297.604 L 770.706 297.744 L 770.706 297.954 L 770.706 298.163 L 770.706 298.442 L 770.706 298.651 L 770.706 298.861 L 770.706 327.744 L 770.706 328.093 L 770.706 328.372 L 770.706 328.581 L 770.706 328.861 L 770.763 329.07 L 770.763 329.279 L 770.763 329.488 L 770.763 329.628 L 770.763 329.838 L 770.763 329.977 L 770.706 330.117 L 770.706 330.186 L 770.649 330.326 L 770.649 330.465 L 770.591 330.535 L 770.534 330.604 L 770.477 330.674 L 770.419 330.744 L 770.305 330.814 L 770.19 330.883 L 770.076 330.883 L 769.962 330.883 L 769.79 330.953 L 769.618 330.953 L 769.446 330.953 L 769.274 331.023 L 769.045 331.023 L 768.759 331.023 L 768.53 331.023 L 768.244 331.023 L 767.9 331.023 L 767.557 331.023 L 725.873 331.023 L 725.53 331.023 L 725.243 331.023 L 724.9 331.023 L 724.671 331.023 L 724.385 331.023 L 724.155 331.023 L 723.926 331.023 L 723.754 331.023 L 723.526 331.023 L 723.354 331.023 L 723.239 331.023 L 723.067 330.953 L 722.953 330.953 L 722.838 330.883 L 722.725 330.814 L 722.61 330.744 L 722.553 330.674 L 722.495 330.604 L 722.381 330.465 L 722.323 330.326 L 722.323 330.186 L 722.266 330.046 L 722.209 329.838 L 722.209 329.628 L 722.209 329.419 L 722.151 329.209 L 722.151 328.93 L 722.151 328.651 L 722.151 328.302 L 722.151 327.954 L 722.151 327.604 L 722.151 327.186 L 722.151 299.419 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 350 L 775 350 L 775 347.558 L 760.743 342.884 L 732.114 342.884 L 717.857 347.558 L 717.857 350 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 770.133 334.023 L 770.133 337.512 L 756.391 337.512 L 756.391 334.023 L 770.133 334.023 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 347.558 L 775 347.558"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 675 285.361 L 703.571 285.361 L 703.571 350 L 675 350 L 675 285.361 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 287.463 L 701.169 287.463 L 701.169 350 L 677.317 350 L 677.317 287.463 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 290.616 L 701.169 290.616 L 701.169 297.898 L 677.317 297.898 L 677.317 290.616 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 297.898 L 701.169 297.898 L 701.169 305.181 L 677.317 305.181 L 677.317 297.898 Z"/>
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 293.694 L 698.767 293.694 L 698.767 294.745 L 679.72 294.745 L 679.72 293.694 Z"/>
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 300 L 698.767 300 L 698.767 303.078 L 679.72 303.078 L 679.72 300 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 275 L 698.767 275 L 703.571 285.361 L 675 285.361 L 679.72 275 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 695.163 300 L 701.169 300 L 701.169 303.078 L 695.163 303.078 L 695.163 300 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 300 L 698.767 300 L 698.767 303.078 L 697.566 303.078 L 697.566 300 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 291.592 L 692.847 291.592 L 692.847 292.643 L 697.566 292.643 L 697.566 291.592 Z"/>
<circle style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="890.673" cy="114.148" r="1.051" transform="matrix(1.142857, 0, 0, 1.000013, -320.346635, 178.493448)"/>
</g>
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(6, 255, 0);" d="M 525 250 L 575 250 L 575 200 L 725 200"/>
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(6, 255, 0);" d="M 525 250 L 575 250 L 575 375 L 725 375"/>
<rect x="399.832" y="149.864" width="125.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
<rect x="400" y="175" width="125" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="417.352" y="167.628">PLC Compressor</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="426.101" y="192.839">IP : 192.168.0.1</text>
<rect x="725" y="99.864" width="125.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
<rect x="725.168" y="125" width="125" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="759.52" y="117.628">PC Station</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="751.269" y="142.839">IP : 192.168.0.2</text>
<g transform="matrix(1, 0, 0, 1, 49.999999, 74.999996)" style="">
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 292.93 L 717.857 292.651 L 717.857 292.442 L 717.857 292.163 L 717.857 291.954 L 717.857 291.744 L 717.857 291.535 L 717.857 291.396 L 717.914 291.186 L 717.914 291.046 L 717.972 290.907 L 718.029 290.837 L 718.086 290.698 L 718.143 290.558 L 718.201 290.488 L 718.258 290.419 L 718.372 290.349 L 718.487 290.279 L 718.602 290.209 L 718.716 290.14 L 718.888 290.14 L 719.06 290.07 L 719.231 290.07 L 719.46 290 L 719.632 290 L 719.919 290 L 720.147 290 L 720.434 290 L 720.72 290 L 721.063 290 L 721.407 290 L 721.751 290 L 722.151 290 L 770.706 290 L 771.106 290 L 771.45 290 L 771.794 290 L 772.137 290 L 772.423 290 L 772.71 290 L 772.939 290 L 773.225 290 L 773.397 290.07 L 773.626 290.07 L 773.798 290.14 L 773.969 290.14 L 774.141 290.209 L 774.256 290.279 L 774.37 290.349 L 774.485 290.419 L 774.599 290.488 L 774.657 290.558 L 774.714 290.698 L 774.771 290.837 L 774.828 290.977 L 774.886 291.117 L 774.943 291.256 L 774.943 291.465 L 775 291.674 L 775 291.884 L 775 292.093 L 775 292.303 L 775 292.581 L 775 292.86 L 775 293.209 L 775 293.488 L 775 335.558 L 775 335.907 L 775 336.256 L 775 336.604 L 775 336.884 L 775 337.163 L 775 337.442 L 775 337.72 L 775 337.93 L 774.943 338.14 L 774.943 338.349 L 774.943 338.488 L 774.886 338.628 L 774.886 338.767 L 774.828 338.907 L 774.771 339.047 L 774.714 339.117 L 774.657 339.256 L 774.599 339.326 L 774.485 339.396 L 774.427 339.465 L 774.313 339.465 L 774.198 339.535 L 774.084 339.535 L 773.969 339.604 L 773.798 339.604 L 773.683 339.604 L 773.511 339.604 L 773.339 339.674 L 773.168 339.674 L 772.939 339.674 L 772.71 339.674 L 772.481 339.674 L 721.579 339.674 L 721.235 339.674 L 720.892 339.674 L 720.663 339.674 L 720.377 339.674 L 720.09 339.604 L 719.861 339.604 L 719.69 339.604 L 719.46 339.604 L 719.289 339.535 L 719.117 339.535 L 718.945 339.465 L 718.831 339.465 L 718.659 339.396 L 718.544 339.326 L 718.43 339.256 L 718.372 339.117 L 718.258 339.047 L 718.201 338.907 L 718.143 338.767 L 718.086 338.628 L 718.029 338.488 L 717.972 338.349 L 717.914 338.14 L 717.914 337.93 L 717.914 337.72 L 717.857 337.442 L 717.857 337.163 L 717.857 336.884 L 717.857 336.604 L 717.857 336.256 L 717.857 335.907 L 717.857 335.558 L 717.857 292.93 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 732.114 339.674 L 760.743 339.674 L 760.743 342.884 L 732.114 342.884 L 732.114 339.674 Z"/>
<path style="fill: rgb(67, 67, 67); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 722.151 299.419 L 722.151 299.07 L 722.151 298.791 L 722.151 298.442 L 722.151 298.163 L 722.151 297.954 L 722.151 297.675 L 722.209 297.465 L 722.209 297.256 L 722.209 297.046 L 722.266 296.837 L 722.323 296.698 L 722.323 296.558 L 722.381 296.419 L 722.495 296.279 L 722.553 296.209 L 722.61 296.07 L 722.725 296 L 722.838 295.93 L 722.953 295.86 L 723.067 295.791 L 723.239 295.791 L 723.354 295.721 L 723.526 295.721 L 723.754 295.651 L 723.926 295.651 L 724.155 295.651 L 724.385 295.651 L 724.671 295.651 L 724.9 295.651 L 725.243 295.651 L 725.53 295.651 L 725.873 295.651 L 767.557 295.651 L 767.843 295.651 L 768.129 295.651 L 768.358 295.651 L 768.587 295.651 L 768.816 295.651 L 769.045 295.721 L 769.217 295.721 L 769.389 295.791 L 769.561 295.791 L 769.675 295.86 L 769.79 295.86 L 769.962 295.93 L 770.076 296 L 770.133 296.07 L 770.247 296.139 L 770.305 296.209 L 770.362 296.349 L 770.419 296.419 L 770.477 296.558 L 770.534 296.698 L 770.591 296.837 L 770.591 296.907 L 770.649 297.046 L 770.649 297.256 L 770.649 297.396 L 770.706 297.604 L 770.706 297.744 L 770.706 297.954 L 770.706 298.163 L 770.706 298.442 L 770.706 298.651 L 770.706 298.861 L 770.706 327.744 L 770.706 328.093 L 770.706 328.372 L 770.706 328.581 L 770.706 328.861 L 770.763 329.07 L 770.763 329.279 L 770.763 329.488 L 770.763 329.628 L 770.763 329.838 L 770.763 329.977 L 770.706 330.117 L 770.706 330.186 L 770.649 330.326 L 770.649 330.465 L 770.591 330.535 L 770.534 330.604 L 770.477 330.674 L 770.419 330.744 L 770.305 330.814 L 770.19 330.883 L 770.076 330.883 L 769.962 330.883 L 769.79 330.953 L 769.618 330.953 L 769.446 330.953 L 769.274 331.023 L 769.045 331.023 L 768.759 331.023 L 768.53 331.023 L 768.244 331.023 L 767.9 331.023 L 767.557 331.023 L 725.873 331.023 L 725.53 331.023 L 725.243 331.023 L 724.9 331.023 L 724.671 331.023 L 724.385 331.023 L 724.155 331.023 L 723.926 331.023 L 723.754 331.023 L 723.526 331.023 L 723.354 331.023 L 723.239 331.023 L 723.067 330.953 L 722.953 330.953 L 722.838 330.883 L 722.725 330.814 L 722.61 330.744 L 722.553 330.674 L 722.495 330.604 L 722.381 330.465 L 722.323 330.326 L 722.323 330.186 L 722.266 330.046 L 722.209 329.838 L 722.209 329.628 L 722.209 329.419 L 722.151 329.209 L 722.151 328.93 L 722.151 328.651 L 722.151 328.302 L 722.151 327.954 L 722.151 327.604 L 722.151 327.186 L 722.151 299.419 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 350 L 775 350 L 775 347.558 L 760.743 342.884 L 732.114 342.884 L 717.857 347.558 L 717.857 350 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 770.133 334.023 L 770.133 337.512 L 756.391 337.512 L 756.391 334.023 L 770.133 334.023 Z"/>
<path style="fill: none; stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 717.857 347.558 L 775 347.558"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 675 285.361 L 703.571 285.361 L 703.571 350 L 675 350 L 675 285.361 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 287.463 L 701.169 287.463 L 701.169 350 L 677.317 350 L 677.317 287.463 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 290.616 L 701.169 290.616 L 701.169 297.898 L 677.317 297.898 L 677.317 290.616 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 677.317 297.898 L 701.169 297.898 L 701.169 305.181 L 677.317 305.181 L 677.317 297.898 Z"/>
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 293.694 L 698.767 293.694 L 698.767 294.745 L 679.72 294.745 L 679.72 293.694 Z"/>
<path style="fill: rgb(0, 0, 0); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 300 L 698.767 300 L 698.767 303.078 L 679.72 303.078 L 679.72 300 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 679.72 275 L 698.767 275 L 703.571 285.361 L 675 285.361 L 679.72 275 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 695.163 300 L 701.169 300 L 701.169 303.078 L 695.163 303.078 L 695.163 300 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 300 L 698.767 300 L 698.767 303.078 L 697.566 303.078 L 697.566 300 Z"/>
<path style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" d="M 697.566 291.592 L 692.847 291.592 L 692.847 292.643 L 697.566 292.643 L 697.566 291.592 Z"/>
<circle style="fill: rgb(255, 255, 255); stroke: rgb(76, 76, 76); stroke-width: 2;" cx="890.673" cy="114.148" r="1.051" transform="matrix(1.142857, 0, 0, 1.000013, -320.346635, 178.493448)"/>
</g>
<rect x="724.832" y="275" width="125.168" height="25.136" style="stroke: rgb(0, 0, 0); fill: rgb(120, 231, 228); stroke-width: 0.693;"/>
<rect x="725" y="300.136" width="125" height="25" style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0); stroke-width: 1;"/>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="760.352" y="292.764">PC Server</text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 12px; white-space: pre; stroke-width: 1;" x="751.1" y="317.975" transform="matrix(1, 0, 0, 1, -3, 0)">IP : xxx.xxx.xx.xx<tspan x="751.0999755859375" dy="1em"></tspan></text>
<text style="fill: rgb(51, 51, 51); font-family: Bahnschrift; font-size: 30px; white-space: pre; stroke-width: 1;" x="312.458" y="50.984">OVERVIEW COMPRESSOR</text>
<path style="fill: none; stroke-width: 1.386; stroke-dasharray: 6, 4; stroke: rgb(0, 0, 0);" d="M 625 75 L 625 114.125 L 625 264.125 L 625 450"/>
</svg>

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -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 {

View File

@@ -57,29 +57,35 @@ const CardList = ({
} }
style={getCardStyle(fieldColor ? item[fieldColor] : cardColor)} style={getCardStyle(fieldColor ? item[fieldColor] : cardColor)}
actions={[ actions={[
<EyeOutlined showPreviewModal && (
style={{ color: '#1890ff' }} <EyeOutlined
key="preview" style={{ color: '#1890ff' }}
onClick={() => showPreviewModal(item)} key="preview"
/>, onClick={() => showPreviewModal(item)}
<EditOutlined />
style={{ color: '#faad14' }} ),
key="edit" showEditModal && (
onClick={() => showEditModal(item)} <EditOutlined
/>, style={{ color: '#faad14' }}
<DeleteOutlined key="edit"
style={{ color: '#ff1818' }} onClick={() => showEditModal(item)}
key="delete" />
onClick={() => showDeleteDialog(item)} ),
/>, showDeleteDialog && (
]} <DeleteOutlined
style={{ color: '#ff1818' }}
key="delete"
onClick={() => showDeleteDialog(item)}
/>
),
].filter(Boolean)} // <== Hapus elemen yang undefined
> >
<div style={{ textAlign: 'left' }}> <div style={{ textAlign: 'left' }}>
{column.map((itemCard, index) => ( {column.map((itemCard, index) => (
<React.Fragment key={index}> <React.Fragment key={index}>
{!itemCard.hidden && {!itemCard.hidden &&
itemCard.title !== 'No' && itemCard.title !== 'No' &&
itemCard.title !== 'Aksi' && ( itemCard.title !== 'Action' && (
<p style={{ margin: '8px 0' }}> <p style={{ margin: '8px 0' }}>
<Text strong>{itemCard.title}:</Text>{' '} <Text strong>{itemCard.title}:</Text>{' '}
{itemCard.render {itemCard.render

View File

@@ -0,0 +1,48 @@
import { useState, useEffect } from 'react';
import { Typography } from 'antd';
const { Title, Text } = Typography;
const JamRealtimeAntd = () => {
const [waktu, setWaktu] = useState(new Date());
useEffect(() => {
const interval = setInterval(() => {
setWaktu(new Date());
}, 1000);
return () => clearInterval(interval);
}, []);
// Format custom manual untuk konsistensi
const formatWaktuLengkap = (date) => {
const hari = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
const bulan = [
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
];
const namaHari = hari[date.getDay()];
const tanggal = date.getDate().toString().padStart(2, '0');
const namaBulan = bulan[date.getMonth()];
const tahun = date.getFullYear();
const jam = date.getHours().toString().padStart(2, '0');
const menit = date.getMinutes().toString().padStart(2, '0');
const detik = date.getSeconds().toString().padStart(2, '0');
return `${namaHari}, ${tanggal} ${namaBulan} ${tahun} ${jam}:${menit}:${detik}`;
};
return (
<Text style={{
fontSize: '25px',
// fontWeight: 'bold',
color: '#1BAA56'
}}>
{formatWaktuLengkap(waktu)}
</Text>
);
};
export default JamRealtimeAntd;

View File

@@ -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 = ['PIU_GGCP/Devices/PB']; const topics = [
'PIU_COD/AIR_DRYER/OVERVIEW',
'PIU_COD/AIR_DRYER/AIR_DRYER_A',
'PIU_COD/AIR_DRYER/AIR_DRYER_B',
'PIU_COD/AIR_DRYER/AIR_DRYER_C',
'PIU_COD/COMPRESSOR/OVERVIEW',
'PIU_COD/COMPRESSOR/COMPRESSOR_A',
'PIU_COD/COMPRESSOR/COMPRESSOR_B',
'PIU_COD/COMPRESSOR/COMPRESSOR_C'
];
const options = { 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),
@@ -66,7 +75,8 @@ const listenMessage = (callback) => {
const setValSvg = (listenTopic, svg) => { const setValSvg = (listenTopic, svg) => {
client.on('message', (topic, message) => { client.on('message', (topic, message) => {
if (topic == listenTopic) { // console.log(topic ,' = ', listenTopic);
if (topic === listenTopic) {
const objChanel = JSON.parse(message); const objChanel = JSON.parse(message);
Object.entries(objChanel).forEach(([key, value]) => { Object.entries(objChanel).forEach(([key, value]) => {
@@ -78,7 +88,7 @@ const setValSvg = (listenTopic, svg) => {
} else if (value === false) { } else if (value === false) {
el.style.display = 'none'; el.style.display = 'none';
} else if (!isNaN(value)) { } else if (!isNaN(value)) {
el.textContent = Number(value ?? 0.0); el.textContent = Number(value ?? 0.0).toFixed(2);
} else { } else {
el.textContent = value; el.textContent = value;
} }

View File

@@ -1,6 +1,6 @@
import React, { memo, useState, useEffect, useRef } from 'react'; import React, { memo, useState, useEffect, useRef } from 'react';
import { Table, Pagination, Row, Col, Card, Grid, Button, Typography, Tag, Segmented } from 'antd'; import { Table, Pagination, Row, Col, Card, Grid, Button, Typography, Tag, Segmented } from 'antd';
import { AppstoreOutlined, TableOutlined } from '@ant-design/icons'; import { MacCommandOutlined, TableOutlined } from '@ant-design/icons';
import CardList from './CardList'; import CardList from './CardList';
const { Text } = Typography; const { Text } = Typography;
@@ -18,6 +18,11 @@ const TableList = memo(function TableList({
showDeleteDialog, showDeleteDialog,
cardColor, cardColor,
fieldColor, fieldColor,
firstLoad = true,
columnDynamic = false,
cardComponent, // New prop for custom card component
onStockUpdate, // Prop to pass to card component
onGetData, // Callback to execute when data is received
}) { }) {
const [gridLoading, setGridLoading] = useState(false); const [gridLoading, setGridLoading] = useState(false);
@@ -30,12 +35,21 @@ const TableList = memo(function TableList({
total_page: 1, total_page: 1,
}); });
const [viewMode, setViewMode] = useState('list'); const [columnsDynamic, setColumnsDynamic] = useState(columns);
const [viewMode, setViewMode] = useState('table');
const { useBreakpoint } = Grid; const { useBreakpoint } = Grid;
const [renderCount, setRenderCount] = useState(firstLoad ? 1 : 0);
useEffect(() => { useEffect(() => {
filter(1, pagination.current_limit); if (renderCount < 1) {
setRenderCount(renderCount + 1);
return;
} else {
filter(1, pagination.current_limit);
}
}, [triger]); }, [triger]);
const filter = async (currentPage, pageSize) => { const filter = async (currentPage, pageSize) => {
@@ -49,7 +63,57 @@ const TableList = memo(function TableList({
const param = new URLSearchParams({ ...paging, ...queryParams }); const param = new URLSearchParams({ ...paging, ...queryParams });
const resData = await getData(param); const resData = await getData(param);
setData(resData?.data ?? []); if (columnDynamic && resData) {
const columnsApi = resData[columnDynamic] ?? '';
// Pisahkan string menjadi array kolom
const colArray = columnsApi.split(',').map((c) => c.trim());
// Kolom default datetime di awal
const defaultColumns = [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{
title: 'Datetime',
dataIndex: 'datetime',
key: 'datetime',
width: '15%',
// render: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
},
];
// Buat kolom numerik dengan format 4 angka di belakang koma
const numericColumns = colArray.map((colName) => ({
title: colName,
dataIndex: colName,
key: colName,
align: 'right',
width: 'auto',
render: (value) => {
if (typeof value === 'number') {
return value.toFixed(4);
}
return value ?? '-';
},
}));
// Gabungkan default + API columns
setColumnsDynamic([...defaultColumns, ...numericColumns]);
}
const fetchedData = resData?.data ?? [];
// Panggil callback jika disediakan
if (onGetData && typeof onGetData === 'function') {
onGetData(fetchedData);
}
setData(fetchedData);
const pagingData = resData?.paging; const pagingData = resData?.paging;
@@ -63,6 +127,8 @@ const TableList = memo(function TableList({
})); }));
} }
setGridLoading(false);
if (resData) { if (resData) {
setTimeout(() => { setTimeout(() => {
setGridLoading(false); setGridLoading(false);
@@ -86,36 +152,41 @@ const TableList = memo(function TableList({
const isMobile = !screens.md; // kalau kurang dari md (768px) dianggap mobile const isMobile = !screens.md; // kalau kurang dari md (768px) dianggap mobile
// Use the custom card component if provided, otherwise default to CardList
const CardViewComponent = cardComponent || CardList;
return ( return (
<div> <div>
<Segmented <Segmented
options={[ options={[
{ value: 'card', icon: <AppstoreOutlined /> },
{ value: 'table', icon: <TableOutlined /> }, { value: 'table', icon: <TableOutlined /> },
{ value: 'card', icon: <MacCommandOutlined /> },
]} ]}
value={viewMode} value={viewMode}
onChange={setViewMode} onChange={setViewMode}
/> />
{(isMobile && mobile) || viewMode === 'card' ? ( {(isMobile && mobile) || viewMode === 'card' ? (
<CardList <CardViewComponent
cardColor={cardColor} cardColor={cardColor}
fieldColor={fieldColor} fieldColor={fieldColor}
data={data} data={data}
column={columns} column={columnsDynamic}
header={header} header={header}
showPreviewModal={showPreviewModal} showPreviewModal={showPreviewModal}
showEditModal={showEditModal} showEditModal={showEditModal}
showDeleteDialog={showDeleteDialog} showDeleteDialog={showDeleteDialog}
onStockUpdate={onStockUpdate}
/> />
) : ( ) : (
<Row gutter={24} style={{ marginTop: '16px' }}> <Row gutter={24} style={{ marginTop: '16px' }}>
<Table <Table
rowSelection={rowSelection || null} rowSelection={rowSelection || null}
columns={columns} columns={columnsDynamic}
dataSource={data.map((item, index) => ({ ...item, key: index }))} dataSource={data.map((item, index) => ({ ...item, key: index }))}
pagination={false} pagination={false}
loading={gridLoading} loading={gridLoading}
scroll={{ y: 520 }} scroll={{ y: 520 }}
size="small"
/> />
</Row> </Row>
)} )}
@@ -143,3 +214,4 @@ const TableList = memo(function TableList({
}); });
export default TableList; export default TableList;

View File

@@ -20,36 +20,65 @@ html body {
height: 100vh; height: 100vh;
} }
/* Custom Orange Sidebar Menu Styles */ /* Custom green Sidebar Menu Styles */
.custom-orange-menu.ant-menu-dark .ant-menu-item-selected { .custom-green-menu.ant-menu-dark .ant-menu-item-selected {
background-color: rgba(255, 255, 255, 0.2) !important; background-color: rgba(255, 255, 255, 0.2) !important;
color: white !important; color: white !important;
} }
.custom-orange-menu.ant-menu-dark .ant-menu-item-selected::after { .custom-green-menu.ant-menu-dark .ant-menu-item-selected::after {
border-right-color: white !important; border-right-color: white !important;
} }
.custom-orange-menu.ant-menu-dark .ant-menu-item:hover, .custom-green-menu.ant-menu-dark .ant-menu-item:hover,
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-title:hover { .custom-green-menu.ant-menu-dark .ant-menu-submenu-title:hover {
background-color: rgba(255, 255, 255, 0.15) !important; background-color: rgba(255, 255, 255, 0.15) !important;
color: white !important; color: white !important;
} }
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title { .custom-green-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title {
color: white !important; color: white !important;
} }
.custom-orange-menu.ant-menu-dark.ant-menu-inline .ant-menu-sub { .custom-green-menu.ant-menu-dark.ant-menu-inline .ant-menu-sub {
background: rgba(0, 0, 0, 0.2) !important; background: rgba(0, 0, 0, 0.2) !important;
} }
.custom-orange-menu.ant-menu-dark .ant-menu-item, .custom-green-menu.ant-menu-dark .ant-menu-item,
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-title { .custom-green-menu.ant-menu-dark .ant-menu-submenu-title {
color: rgba(255, 255, 255, 0.9) !important; color: rgba(255, 255, 255, 0.9) !important;
} }
.custom-orange-menu.ant-menu-dark .ant-menu-item-active, .custom-green-menu.ant-menu-dark .ant-menu-item-active,
.custom-orange-menu.ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title { .custom-green-menu.ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title {
color: white !important; color: white !important;
} }
/*start styling for scrollbar menu */
.custom-menu-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-menu-scrollbar::-webkit-scrollbar-track {
background: transparent;
border-radius: 10px;
margin: 5px 0;
}
.custom-menu-scrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, #1BAA56 0%, rgb(5, 75, 34) 100%);
border-radius: 10px;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.custom-menu-scrollbar::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, #2bc56d 0%, rgb(8, 94, 43) 100%);
}
.custom-menu-scrollbar {
scrollbar-width: thin;
scrollbar-color: #1BAA56 transparent;
}
/* Hilangkan panah atas/bawah dengan important */
.custom-menu-scrollbar::-webkit-scrollbar-button {
display: none !important;
width: 0 !important;
height: 0 !important;
}
/*end styling for scrollbar menu */

View File

@@ -5,6 +5,7 @@ import handleLogOut from '../Utils/Auth/Logout';
import { useBreadcrumb } from './LayoutBreadcrumb'; import { useBreadcrumb } from './LayoutBreadcrumb';
import { decryptData } from '../components/Global/Formatter'; import { decryptData } from '../components/Global/Formatter';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import DateRealTime from '../components/Global/DateRealTime';
const { Link, Text } = Typography; const { Link, Text } = Typography;
const { Header } = Layout; const { Header } = Layout;
@@ -17,7 +18,7 @@ const LayoutHeader = () => {
const { token } = theme.useToken() || {}; const { token } = theme.useToken() || {};
const colorBgContainer = token?.colorBgContainer || '#fff'; const colorBgContainer = token?.colorBgContainer || '#fff';
const colorBorder = token?.colorBorder || '#d9d9d9'; const colorBorder = token?.colorBorder || '#d9d9d9';
const colorText = token?.colorText || '#000'; const colorText = token?.colorText || '#1BAA56';
// Ambil data user dari localStorage // Ambil data user dari localStorage
let userData = null; let userData = null;
@@ -40,15 +41,8 @@ const LayoutHeader = () => {
// console.log('User data di header:', userData?.user); // console.log('User data di header:', userData?.user);
// Role handling // Role handling
const roleNameDefault = let roleName = userData?.user?.role_name || 'Guest';
userData?.user?.approval || const userName = userData?.user?.name || userData?.user?.username || userData?.user?.user_name || 'User';
userData?.user?.partner_name ||
userData?.user?.role_name ||
'Guest';
let roleName = roleNameDefault;
const userName =
userData?.user?.name || userData?.user?.username || userData?.user?.user_name || 'User';
// Override jika Super Admin // Override jika Super Admin
if ( if (
@@ -73,8 +67,9 @@ const LayoutHeader = () => {
paddingBottom: 20, paddingBottom: 20,
paddingLeft: 24, paddingLeft: 24,
paddingRight: 24, paddingRight: 24,
minHeight: 100, // minHeight: 100,
boxSizing: 'border-box', boxSizing: 'border-box',
boxShadow: '5px 0 10px rgba(0, 0, 0, 0.4)'
}} }}
> >
<div <div
@@ -87,16 +82,39 @@ const LayoutHeader = () => {
> >
<Text <Text
style={{ style={{
color: colorText, color: '#1BAA56',
fontSize: 16, fontSize: 26,
fontWeight: 'bold', fontWeight: 'bold',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}} }}
> >
Login AS {roleName} {/* Login AS {roleName} */}
CALL OF DUTY
</Text> </Text>
</div> </div>
<div
style={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
gap: 12,
}}
>
{/* <Text
style={{
color: '#000000',
fontSize: 20,
fontWeight: 'bold',
whiteSpace: 'nowrap',
}}
> */}
{/* Login AS {roleName} */}
{/* Kamis, 04 November 2025 16:35:00 */}
{/* </Text> */}
<DateRealTime/>
</div>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@@ -117,7 +135,7 @@ const LayoutHeader = () => {
> >
<UserOutlined style={{ fontSize: 16, color: colorText }} /> <UserOutlined style={{ fontSize: 16, color: colorText }} />
<Text style={{ marginLeft: 8, color: colorText }} strong> <Text style={{ marginLeft: 8, color: colorText }} strong>
{userName} {userName} @ {roleName}
</Text> </Text>
</Button> </Button>
<Link <Link

View File

@@ -31,6 +31,8 @@ import {
GroupOutlined, GroupOutlined,
SlidersOutlined, SlidersOutlined,
SnippetsOutlined, SnippetsOutlined,
ContactsOutlined,
ToolOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
const { Text } = Typography; const { Text } = Typography;
@@ -51,39 +53,58 @@ const allItems = [
label: 'Dashboard', label: 'Dashboard',
children: [ children: [
{ {
key: 'dashboard-svg-overview', key: 'dashboard-svg-compressor',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />, icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/overview">Overview</Link>, label: 'Compressor',
children: [
{
key: 'dashboard-svg-compressor-overview',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/overview-compressor">Overview</Link>,
},
{
key: 'dashboard-svg-compressor-compressor-a',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/compressor-a">Compressor A</Link>,
},
{
key: 'dashboard-svg-compressor-compressor-b',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/compressor-b">Compressor B</Link>,
},
{
key: 'dashboard-svg-compressor-compressor-c',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/compressor-c">Compressor C</Link>,
},
],
}, },
{ {
key: 'dashboard-svg-compressor-a', key: 'dashboard-svg-airdryer',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />, icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/compressor-a">Compressor A</Link>, label: 'Air Dryer',
}, children: [
{ {
key: 'dashboard-svg-compressor-b', key: 'dashboard-svg-airdryer-overview',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />, icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/compressor-b">Compressor B</Link>, label: <Link to="/dashboard-svg/overview-airdryer">Overview</Link>,
}, },
{ {
key: 'dashboard-svg-compressor-c', key: 'dashboard-svg-airdryer-airdryer-a',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />, icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/compressor-c">Compressor C</Link>, label: <Link to="/dashboard-svg/airdryer-a">Air Dryer A</Link>,
}, },
{ {
key: 'dashboard-svg-airdryer-a', key: 'dashboard-svg-airdryer-airdryer-b',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />, icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/airdryer-a">Air Dryer A</Link>, label: <Link to="/dashboard-svg/airdryer-b">Air Dryer B</Link>,
}, },
{ {
key: 'dashboard-svg-airdryer-b', key: 'dashboard-svg-airdryer-airdryer-c',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />, icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/airdryer-b">Air Dryer B</Link>, label: <Link to="/dashboard-svg/airdryer-c">Air Dryer C</Link>,
}, },
{ ],
key: 'dashboard-svg-airdryer-c',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/airdryer-c">Air Dryer C</Link>,
}, },
], ],
}, },
@@ -123,10 +144,15 @@ const allItems = [
label: <Link to="/master/status">Status</Link>, label: <Link to="/master/status">Status</Link>,
}, },
{ {
key: 'master-shift', key: 'master-sparepart',
icon: <ClockCircleOutlined style={{ fontSize: '19px' }} />, icon: <ToolOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/master/shift">Shift</Link>, label: <Link to="/master/sparepart">Sparepart</Link>,
}, },
// {
// key: 'master-shift',
// icon: <ClockCircleOutlined style={{ fontSize: '19px' }} />,
// label: <Link to="/master/shift">Shift</Link>,
// },
], ],
}, },
{ {
@@ -163,6 +189,15 @@ const allItems = [
}, },
], ],
}, },
{
key: 'contact',
icon: <ContactsOutlined style={{ fontSize: '19px' }} />,
label: (
<Link to="/contact" className="fontMenus">
Contact
</Link>
),
},
{ {
key: 'notification', key: 'notification',
icon: <BellOutlined style={{ fontSize: '19px' }} />, icon: <BellOutlined style={{ fontSize: '19px' }} />,
@@ -190,15 +225,15 @@ const allItems = [
</Link> </Link>
), ),
}, },
{ // {
key: 'jadwal-shift', // key: 'jadwal-shift',
icon: <CalendarOutlined style={{ fontSize: '19px' }} />, // icon: <CalendarOutlined style={{ fontSize: '19px' }} />,
label: ( // label: (
<Link to="/jadwal-shift" className="fontMenus"> // <Link to="/jadwal-shift" className="fontMenus">
Jadwal Shift // Jadwal Shift
</Link> // </Link>
), // ),
}, // },
]; ];
const LayoutMenu = () => { const LayoutMenu = () => {
@@ -217,41 +252,83 @@ const LayoutMenu = () => {
if (pathname === '/role') return 'role'; if (pathname === '/role') return 'role';
if (pathname === '/notification') return 'notification'; if (pathname === '/notification') return 'notification';
if (pathname === '/jadwal-shift') return 'jadwal-shift'; if (pathname === '/jadwal-shift') return 'jadwal-shift';
if (pathname === '/contact') return 'contact';
// Handle master routes // Handle master routes
if (pathname.startsWith('/master/')) { if (pathname.startsWith('/master/')) {
const subPath = pathParts[1]; const subPath = pathParts[1];
return `master-${subPath}`; // Convert kebab-case to the actual menu keys
const masterKeyMap = {
'plant-sub-section': 'master-plant-sub-section',
'brand-device': 'master-brand-device',
device: 'master-device',
unit: 'master-unit',
tag: 'master-tag',
status: 'master-status',
sparepart: 'master-sparepart',
shift: 'master-shift',
};
return masterKeyMap[subPath] || `master-${subPath}`;
} }
// Handle dashboard svg routes // Handle dashboard svg routes
if (pathname.startsWith('/dashboard-svg/')) { if (pathname.startsWith('/dashboard-svg/')) {
const subPath = pathParts[1]; const subPath = pathParts[1];
// Map specific routes to their menu keys
if (subPath === 'overview-compressor') return 'dashboard-svg-compressor-overview';
if (subPath === 'compressor-a') return 'dashboard-svg-compressor-compressor-a';
if (subPath === 'compressor-b') return 'dashboard-svg-compressor-compressor-b';
if (subPath === 'compressor-c') return 'dashboard-svg-compressor-compressor-c';
if (subPath === 'overview-airdryer') return 'dashboard-svg-airdryer-overview';
if (subPath === 'airdryer-a') return 'dashboard-svg-airdryer-airdryer-a';
if (subPath === 'airdryer-b') return 'dashboard-svg-airdryer-airdryer-b';
if (subPath === 'airdryer-c') return 'dashboard-svg-airdryer-airdryer-c';
return `dashboard-svg-${subPath}`; return `dashboard-svg-${subPath}`;
} }
// Handle report routes // Handle report routes
if (pathname.startsWith('/report/')) { if (pathname.startsWith('/report/')) {
const subPath = pathParts[1]; const subPath = pathParts[1];
return `report-${subPath}`; const reportKeyMap = {
trending: 'report-trending',
report: 'report-report',
};
return reportKeyMap[subPath] || `report-${subPath}`;
} }
// Handle history routes // Handle history routes
if (pathname.startsWith('/history/')) { if (pathname.startsWith('/history/')) {
const subPath = pathParts[1]; const subPath = pathParts[1];
return `history-${subPath}`; const historyKeyMap = {
alarm: 'history-alarm',
event: 'history-event',
};
return historyKeyMap[subPath] || `history-${subPath}`;
} }
return 'home'; // default return 'home'; // default
}; };
// Function to get parent key from menu key // Function to get parent keys from menu key
const getParentKey = (key) => { const getParentKeys = (key) => {
if (key.startsWith('master-')) return 'master'; const parentKeys = [];
if (key.startsWith('dashboard-svg-')) return 'dashboard-svg';
if (key.startsWith('report-')) return 'report'; if (key.startsWith('dashboard-svg-compressor-')) {
if (key.startsWith('history-')) return 'history'; parentKeys.push('dashboard-svg', 'dashboard-svg-compressor');
return null; } else if (key.startsWith('dashboard-svg-airdryer-')) {
parentKeys.push('dashboard-svg', 'dashboard-svg-airdryer');
} else if (key.startsWith('dashboard-svg-')) {
parentKeys.push('dashboard-svg');
} else if (key.startsWith('master-')) {
parentKeys.push('master');
} else if (key.startsWith('report-')) {
parentKeys.push('report');
} else if (key.startsWith('history-')) {
parentKeys.push('history');
}
return parentKeys;
}; };
// Update selected and open keys when route changes // Update selected and open keys when route changes
@@ -259,11 +336,11 @@ const LayoutMenu = () => {
const currentKey = getMenuKeyFromPath(location.pathname); const currentKey = getMenuKeyFromPath(location.pathname);
setSelectedKeys([currentKey]); setSelectedKeys([currentKey]);
const parentKey = getParentKey(currentKey); const parentKeys = getParentKeys(currentKey);
// If current menu has parent, open it. Otherwise, close all dropdowns // Always keep the parent menus open when a child is selected
if (parentKey) { if (parentKeys.length > 0) {
setStateOpenKeys([parentKey]); setStateOpenKeys(parentKeys);
} else { } else {
setStateOpenKeys([]); setStateOpenKeys([]);
} }
@@ -289,17 +366,28 @@ const LayoutMenu = () => {
const onOpenChange = (openKeys) => { const onOpenChange = (openKeys) => {
const currentOpenKey = openKeys.find((key) => stateOpenKeys.indexOf(key) === -1); const currentOpenKey = openKeys.find((key) => stateOpenKeys.indexOf(key) === -1);
// If clicking on a menu that was previously closed
if (currentOpenKey !== undefined) { if (currentOpenKey !== undefined) {
const repeatIndex = openKeys const repeatIndex = openKeys
.filter((key) => key !== currentOpenKey) .filter((key) => key !== currentOpenKey)
.findIndex((key) => levelKeys[key] === levelKeys[currentOpenKey]); .findIndex((key) => levelKeys[key] === levelKeys[currentOpenKey]);
setStateOpenKeys( setStateOpenKeys(
openKeys openKeys
.filter((_, index) => index !== repeatIndex) .filter((_, index) => index !== repeatIndex)
.filter((key) => levelKeys[key] <= levelKeys[currentOpenKey]) .filter((key) => levelKeys[key] <= levelKeys[currentOpenKey])
); );
} else { } else {
setStateOpenKeys(openKeys); // If clicking on a menu that was previously open, close only that menu
// but keep other parent menus open if they have active children
const currentKey = getMenuKeyFromPath(location.pathname);
const necessaryParentKeys = getParentKeys(currentKey);
// Filter out only the menus that are necessary to keep open
const filteredOpenKeys = openKeys.filter((key) => necessaryParentKeys.includes(key));
setStateOpenKeys(filteredOpenKeys);
} }
}; };
@@ -308,27 +396,17 @@ const LayoutMenu = () => {
const karyawan = () => { const karyawan = () => {
return allItems return allItems
.filter( .filter((item) => item.key !== 'setting')
(item) => item.key !== 'setting'
// tambahkan menu jika terdapat menu yang di sembunyikan dari user karyawan
// && item.key !== 'master'
// && item.key !== 'master'
)
.map((item) => { .map((item) => {
if (item.key === 'master') { if (item.key === 'master') {
return { return {
...item, ...item,
// buka command dibawah jika terdapat sub menu yang di sembunyikan
// children: item.children.filter(
// child => child.key !== 'master-product'
// tambahkan menu jika terdapat menu yang di sembunyikan dari user karyawan
// && child.key !== 'master-service'
// )
}; };
} }
return item; return item;
}); });
}; };
const items = isAdmin === 1 ? allItems : karyawan(); const items = isAdmin === 1 ? allItems : karyawan();
return ( return (
@@ -344,7 +422,7 @@ const LayoutMenu = () => {
border: 'none', border: 'none',
}} }}
theme="dark" theme="dark"
className="custom-orange-menu" className="custom-green-menu"
/> />
); );
}; };

View File

@@ -17,17 +17,37 @@ const LayoutSidebar = () => {
// console.log(collapsed, type); // console.log(collapsed, type);
}} }}
style={{ style={{
background: 'linear-gradient(180deg, #FF8C42 0%, #FF6B35 100%)', background: 'linear-gradient(180deg, #1BAA56 0%,rgb(5, 75, 34) 100%)',
overflow: 'auto', // overflow: 'auto',
height: '100vh', height: '100vh',
position: 'fixed', position: 'fixed',
left: 0, left: 0,
top: 0, top: 0,
bottom: 0, bottom: 0,
borderTopRightRadius: '30px',
borderBottomRightRadius: '30px',
boxShadow: '5px 0 10px rgba(0, 0, 0, 0.4)',
zIndex: 9999
}} }}
> >
<LayoutLogo /> <div style={{
<LayoutMenu /> display: 'flex',
flexDirection: 'column',
height: '100vh',
overflow: 'hidden'
}}>
{/* Logo section - fixed height */}
<div style={{flexShrink: 0,minHeight: '64px'}}>
<LayoutLogo />
</div>
{/* Menu section - scrollable */}
<div style={{flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column'}}>
<div className="custom-menu-scrollbar" style={{flex: 1, overflowY: 'auto', overflowX: 'hidden', backgroundColor: 'transparent'}}>
<LayoutMenu />
</div>
</div>
</div>
</Sider> </Sider>
); );
}; };

View File

@@ -3,7 +3,7 @@ import { Flex, Input, Form, Button, Card, Space, Image } from 'antd';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { NotifAlert } from '../../components/Global/ToastNotif'; import { NotifAlert } from '../../components/Global/ToastNotif';
import { SendRequest } from '../../components/Global/ApiRequest'; import { SendRequest } from '../../components/Global/ApiRequest';
import bg_cod from 'assets/bg_cod.jpg'; import bg_cod from 'assets/bg-cod-1.jpg';
import logo from 'assets/freepik/LOGOPIU.png'; import logo from 'assets/freepik/LOGOPIU.png';
const SignIn = () => { const SignIn = () => {

View 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} />;
}

View File

@@ -0,0 +1,72 @@
import React, { memo, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import ListContact from './component/ListContact';
import DetailContact from './component/DetailContact';
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
import { Typography } from 'antd';
const { Text } = Typography;
const IndexContact = memo(function IndexContact() {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [actionMode, setActionMode] = useState('list');
const [selectedData, setSelectedData] = useState(null);
const [readOnly, setReadOnly] = useState(false);
const [showModal, setShowModal] = useState(false);
const [contactType, setContactType] = useState('operator');
const setMode = (param) => {
setShowModal(param !== 'list');
setReadOnly(param === 'preview');
setActionMode(param);
};
const handleContactSaved = (contactData, actionMode) => {
setLastSavedContact({ contactData, actionMode });
// Clear after processing
setTimeout(() => setLastSavedContact(null), 100);
};
const [lastSavedContact, setLastSavedContact] = useState(null);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
setBreadcrumbItems([
{ title: <Text strong style={{ fontSize: '14px' }}> Contact</Text> },
]);
} else {
navigate('/signin');
}
}, [navigate, setBreadcrumbItems]);
return (
<React.Fragment>
<ListContact
actionMode={actionMode}
setActionMode={setMode}
selectedData={selectedData}
setSelectedData={setSelectedData}
readOnly={readOnly}
lastSavedContact={lastSavedContact}
setContactType={setContactType}
/>
<DetailContact
setActionMode={setMode}
selectedData={selectedData}
setSelectedData={setSelectedData}
readOnly={readOnly}
showModal={showModal}
actionMode={actionMode}
onContactSaved={handleContactSaved}
contactType={contactType}
/>
</React.Fragment>
);
});
export default IndexContact;

View File

@@ -0,0 +1,272 @@
import React, { memo, useEffect, useState } from 'react';
import { Modal, Input, Button, Switch, ConfigProvider, Typography, Divider, Select } from 'antd';
import { NotifAlert, NotifOk } from '../../../components/Global/ToastNotif';
import { validateRun } from '../../../Utils/validate';
import { createContact, updateContact } from '../../../api/contact';
const { Text } = Typography;
const DetailContact = memo(function DetailContact(props) {
const [confirmLoading, setConfirmLoading] = useState(false);
const defaultData = {
id: '',
name: '',
phone: '',
is_active: true,
};
const [formData, setFormData] = useState(defaultData);
const handleInputChange = (e) => {
const { name, value } = e.target;
// Validasi untuk field phone - hanya angka yang diperbolehkan
if (name === 'phone') {
const cleanedValue = value.replace(/[^0-9+\-\s()]/g, '');
setFormData((prev) => ({
...prev,
[name]: cleanedValue,
}));
} else {
setFormData((prev) => ({
...prev,
[name]: value,
}));
}
};
const handleStatusToggle = (checked) => {
setFormData({
...formData,
is_active: checked,
});
};
const handleSave = async () => {
setConfirmLoading(true);
// Validation rules
const validationRules = [
{ field: 'name', label: 'Contact Name', required: true },
{ field: 'phone', label: 'Phone', required: true },
];
if (
validateRun(formData, validationRules, (errorMessages) => {
NotifOk({ icon: 'warning', title: 'Peringatan', message: errorMessages });
setConfirmLoading(false);
})
)
return;
// Custom validation untuk name - minimal 3 karakter
if (formData.name && formData.name.length < 3) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Nama contact minimal 3 karakter',
});
setConfirmLoading(false);
return;
}
// Custom validation untuk phone - Indonesian phone format
const phoneRegex = /^(?:\+62|0)8\d{7,10}$/;
if (formData.phone && !phoneRegex.test(formData.phone.replace(/[\s\-\(\)]/g, ''))) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Nomor telepon harus format Indonesia (+628XXXXXXXXX atau 08XXXXXXXXX)',
});
setConfirmLoading(false);
return;
}
try {
const contactData = {
contact_name: formData.name,
contact_phone: formData.phone.replace(/[\s\-\(\)]/g, ''), // Clean phone number
is_active: formData.is_active,
};
let response;
if (props.actionMode === 'edit') {
response = await updateContact(
props.selectedData.contact_id || props.selectedData.id,
contactData
);
} else {
response = await createContact(contactData);
}
NotifAlert({
icon: 'success',
title: 'Berhasil',
message: `Data Contact "${formData.name}" berhasil ${
props.actionMode === 'add' ? 'ditambahkan' : 'diperbarui'
}.`,
});
props.onContactSaved?.(response.data, props.actionMode);
handleCancel();
} catch (error) {
console.error('Save failed:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: error.response?.data?.message || 'Terjadi kesalahan saat menyimpan data.',
});
} finally {
setConfirmLoading(false);
}
};
const handleCancel = () => {
props.setActionMode('list');
props.setSelectedData(null);
};
useEffect(() => {
if (props.showModal) {
if (props.actionMode === 'edit' && props.selectedData) {
setFormData({
name: props.selectedData.contact_name || props.selectedData.name,
phone: props.selectedData.contact_phone || props.selectedData.phone,
is_active:
props.selectedData.is_active || props.selectedData.status === 'active',
});
} else if (props.actionMode === 'add') {
setFormData({
name: '',
phone: '',
is_active: true,
});
}
}
}, [props.showModal, props.actionMode, props.selectedData]);
return (
<Modal
title={`${
props.actionMode === 'add'
? 'Tambah'
: props.actionMode === 'edit'
? 'Edit'
: 'Detail'
} Kontak`}
open={props.showModal}
onCancel={handleCancel}
footer={[
<React.Fragment key="modal-footer">
<ConfigProvider
theme={{
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
},
},
}}
>
<Button onClick={handleCancel}>{props.readOnly ? 'Tutup' : 'Batal'}</Button>
</ConfigProvider>
<ConfigProvider
theme={{
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
},
},
}}
>
{!props.readOnly && (
<Button loading={confirmLoading} onClick={handleSave}>
Simpan
</Button>
)}
</ConfigProvider>
</React.Fragment>,
]}
>
<div style={{ padding: '8px 0' }}>
{/* Status field only show in add mode*/}
{props.actionMode === 'add' && (
<>
<div>
<div>
<Text strong>Status</Text>
</div>
<div
style={{ display: 'flex', alignItems: 'center', marginTop: '8px' }}
>
<div style={{ marginRight: '8px' }}>
<Switch
disabled={props.readOnly}
style={{
backgroundColor: formData.is_active
? '#23A55A'
: '#bfbfbf',
}}
checked={formData.is_active}
onChange={handleStatusToggle}
/>
</div>
<div>
<Text>{formData.is_active ? 'Active' : 'Inactive'}</Text>
</div>
</div>
</div>
<Divider style={{ margin: '12px 0' }} />
</>
)}
<div style={{ marginBottom: 12 }}>
<Text strong>Name</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="name"
value={formData.name}
onChange={handleInputChange}
placeholder="Enter Name"
readOnly={props.readOnly}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Phone</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="phone"
value={formData.phone}
onChange={handleInputChange}
placeholder="Enter Phone Number"
readOnly={props.readOnly}
maxLength={15}
style={{ color: formData.is_active ? '#000000' : '#ff4d4f' }}
/>
</div>
{/* Contact Type */}
{/* <div style={{ marginBottom: 12 }}>
<Text strong>Contact Type</Text>
<Text style={{ color: 'red' }}> *</Text>
<Select
value={formData.contact_type || undefined}
onChange={handleContactTypeChange}
placeholder="Select Contact Type"
disabled={props.readOnly}
style={{ width: '100%' }}
>
<Select.Option value="operator">Operator</Select.Option>
<Select.Option value="gudang">Gudang</Select.Option>
</Select>
</div> */}
</div>
</Modal>
);
});
export default DetailContact;

View File

@@ -0,0 +1,487 @@
import React, { memo, useState, useEffect } from 'react';
import { Button, Row, Col, Input, Tabs, Space, ConfigProvider, Card, Tag, Switch } from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
SearchOutlined,
UserOutlined,
PhoneOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { NotifAlert, NotifConfirmDialog } from '../../../components/Global/ToastNotif';
import { getAllContact, deleteContact, updateContact } from '../../../api/contact';
const ContactCard = memo(function ContactCard({
contact,
showEditModal,
showDeleteModal,
onStatusToggle,
}) {
const handleStatusToggle = async (checked) => {
try {
const updatedContact = {
contact_name: contact.contact_name || contact.name,
contact_phone: contact.contact_phone || contact.phone,
is_active: checked,
contact_type: contact.contact_type,
};
await updateContact(contact.contact_id || contact.id, updatedContact);
NotifAlert({
icon: 'success',
title: 'Berhasil',
message: `Status "${contact.contact_name || contact.name}" berhasil diperbarui.`,
});
// Refresh contacts list
onStatusToggle && onStatusToggle();
} catch (error) {
console.error('Error updating contact status:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Gagal memperbarui status kontak',
});
}
};
return (
<Col xs={24} sm={12} md={8} lg={6}>
<div
className="contact-card"
style={{
marginBottom: 16,
borderRadius: 8,
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
height: '100%',
padding: '16px',
backgroundColor: '#f5f5f5',
border: '1px solid #e8e8e8',
transition: 'all 0.3s ease',
cursor: 'pointer',
}}
onMouseEnter={(e) => {
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
}}
>
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
position: 'relative',
}}
>
{/* Type Badge - Top Left */}
{/* <div style={{ position: 'absolute', top: 0, left: 0, zIndex: 1 }}>
<Tag
color={
contact.contact_type === 'operator'
? 'blue'
: contact.contact_type === 'gudang'
? 'orange'
: 'default'
}
style={{ fontSize: '11px' }}
>
{contact.contact_type === 'operator' ? 'Operator' : contact.contact_type === 'gudang' ? 'Gudang' : 'Unknown'}
</Tag>
</div> */}
{/* Status Slider - Top Right */}
<div
style={{
position: 'absolute',
top: 0,
right: 0,
zIndex: 1,
padding: '4px 8px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<Switch
checked={contact.status === 'active'}
onChange={handleStatusToggle}
style={{
backgroundColor:
contact.status === 'active' ? '#52c41a' : '#d9d9d9',
}}
/>
<span
style={{
fontSize: '12px',
color: contact.status === 'active' ? '#52c41a' : '#ff4d4f',
fontWeight: 500,
}}
>
{contact.status === 'active' ? 'Active' : 'Inactive'}
</span>
</div>
</div>
{/* Main Content */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
flex: 1,
paddingTop: '28px',
}}
>
<div
className="avatar"
style={{
width: 55,
height: 55,
borderRadius: '50%',
backgroundColor:
contact.status === 'active' ? '#52c41a' : '#ff4d4f',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
<UserOutlined style={{ color: 'white', fontSize: '25px' }} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontWeight: 600,
fontSize: '16px',
marginBottom: '4px',
color: '#262626',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{contact.contact_name || contact.name}
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
fontSize: '14px',
}}
>
<PhoneOutlined style={{ marginRight: 6, color: '#1890ff' }} />
<span
style={{
color: contact.status === 'active' ? '#262626' : '#262626',
}}
>
{contact.contact_phone || contact.phone}
</span>
</div>
</div>
</div>
{/* Edit and Delete Buttons - Bottom Right */}
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
gap: '8px',
marginTop: '8px',
}}
>
<Space>
<Button
type="default"
size="small"
style={{
backgroundColor: '#fff7e6',
borderColor: '#faad14',
color: '#faad14',
padding: '2px 6px',
fontSize: '11px',
height: '24px',
}}
icon={
<EditOutlined style={{ color: '#faad14', fontSize: '11px' }} />
}
onClick={(e) => {
e.stopPropagation();
showEditModal(contact);
}}
>
Edit info
</Button>
<Button
type="default"
danger
size="small"
style={{
backgroundColor: '#fff1f0',
borderColor: 'red',
padding: '2px 6px',
fontSize: '11px',
height: '24px',
}}
icon={<DeleteOutlined style={{ fontSize: '11px' }} />}
onClick={(e) => {
e.stopPropagation();
showDeleteModal(contact);
}}
>
Delete
</Button>
</Space>
</div>
</div>
</div>
</Col>
);
});
const ListContact = memo(function ListContact(props) {
const [activeTab, setActiveTab] = useState('all');
const [filteredContacts, setFilteredContacts] = useState([]);
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
// Default filter object matching plantSection pattern
const defaultFilter = { criteria: '' };
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
// Fetch contacts from API
const fetchContacts = async () => {
setLoading(true);
try {
// Build search parameters matching database pattern
const searchParams = { ...formDataFilter };
// Add specific filters if not "all"
if (activeTab !== 'all') {
if (activeTab === 'operator') {
searchParams.code = 'operator';
} else if (activeTab === 'gudang') {
searchParams.code = 'gudang';
}
}
const queryParams = new URLSearchParams();
Object.entries(searchParams).forEach(([key, value]) => {
if (value !== '' && value !== null && value !== undefined) {
queryParams.append(key, value);
}
});
const response = await getAllContact(queryParams);
setFilteredContacts(response.data || []);
} catch (error) {
console.error('Error fetching contacts:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Gagal memuat data kontak',
});
} finally {
setLoading(false);
}
};
// Fetch contacts on component mount
useEffect(() => {
const token = localStorage.getItem('token');
if (!token) {
navigate('/signin');
return;
}
fetchContacts();
}, []);
// Refetch when filters change
useEffect(() => {
fetchContacts();
}, [formDataFilter, activeTab]);
// Listen for saved contact data
useEffect(() => {
if (props.lastSavedContact) {
fetchContacts();
}
}, [props.lastSavedContact]);
const getFilteredContacts = () => {
return filteredContacts;
};
const showEditModal = (param) => {
props.setSelectedData(param);
props.setActionMode('edit');
};
const showAddModal = () => {
props.setSelectedData(null);
props.setActionMode('add');
props.setContactType?.(activeTab);
};
const showDeleteModal = (contact) => {
NotifConfirmDialog({
icon: 'question',
title: 'Konfirmasi Hapus',
message: `Kontak "${contact.contact_name || contact.name}" akan dihapus?`,
onConfirm: () => handleDelete(contact),
onCancel: () => props.setSelectedData(null),
});
};
const handleDelete = async (contact) => {
try {
await deleteContact(contact.contact_id || contact.id);
NotifAlert({
icon: 'success',
title: 'Berhasil',
message: `Kontak "${contact.contact_name || contact.name}" berhasil dihapus.`,
});
// Refetch contacts after deletion
fetchContacts();
} catch (error) {
console.error('Error deleting contact:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Gagal menghapus kontak',
});
}
};
return (
<React.Fragment>
<Card>
<Row>
<Col xs={24}>
<Row justify="space-between" align="middle" gutter={[8, 8]}>
<Col xs={24} sm={24} md={12} lg={12}>
<Input.Search
placeholder="Search by name..."
value={formDataFilter.criteria}
onChange={(e) => {
const value = e.target.value;
setFormDataFilter({ criteria: value });
if (value === '') {
setFormDataFilter(defaultFilter);
}
}}
onSearch={(value) => setFormDataFilter({ criteria: value })}
allowClear={{
clearIcon: (
<span onClick={() => setFormDataFilter(defaultFilter)}>
</span>
),
}}
enterButton={
<Button
type="primary"
icon={<SearchOutlined />}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
}}
>
Search
</Button>
}
size="large"
/>
</Col>
<Col>
<Space wrap size="small">
<ConfigProvider
theme={{
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
},
},
}}
>
<Button
icon={<PlusOutlined />}
onClick={() => showAddModal()}
size="large"
>
Add Contact
</Button>
</ConfigProvider>
</Space>
</Col>
</Row>
</Col>
<Col xs={24} style={{ marginTop: '16px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}
>
{/* Tabs */}
{/* <Tabs
activeKey={activeTab}
onChange={setActiveTab}
size="large"
items={[
{
key: 'all',
label: 'All',
},
{
key: 'operator',
label: 'Operator',
},
{
key: 'gudang',
label: 'Gudang',
},
]}
/> */}
</div>
{getFilteredContacts().length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px' }}>
<span style={{ color: '#8c8c8c' }}>
{loading ? 'Loading contacts...' : 'No contacts found'}
</span>
</div>
) : (
<Row gutter={[16, 16]}>
{getFilteredContacts().map((contact) => (
<ContactCard
key={contact.contact_id || contact.id}
contact={{
...contact,
id: contact.contact_id || contact.id,
name: contact.contact_name || contact.name,
phone: contact.contact_phone || contact.phone,
status: contact.is_active ? 'active' : 'inactive',
}}
showEditModal={showEditModal}
showDeleteModal={showDeleteModal}
onStatusToggle={fetchContacts}
/>
))}
</Row>
)}
</Col>
</Row>
</Card>
</React.Fragment>
);
});
export default ListContact;

View File

@@ -2,6 +2,7 @@ import React, { memo, useState, useEffect } from 'react';
import { Button, Row, Col, Card, Input } from 'antd'; import { Button, Row, Col, Card, Input } from 'antd';
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from '@ant-design/icons';
import TableList from '../../../../components/Global/TableList'; import TableList from '../../../../components/Global/TableList';
import { getAllHistoryAlarm } from '../../../../api/history-value';
const ListHistoryAlarm = memo(function ListHistoryAlarm(props) { const ListHistoryAlarm = memo(function ListHistoryAlarm(props) {
const columns = [ const columns = [
@@ -16,7 +17,8 @@ const ListHistoryAlarm = memo(function ListHistoryAlarm(props) {
title: 'Datetime', title: 'Datetime',
dataIndex: 'datetime', dataIndex: 'datetime',
key: 'datetime', key: 'datetime',
width: '10%', width: '15%',
// render: (_, record) => toAppDateTimezoneFormatter(record.datetime),
}, },
{ {
title: 'Tag Name', title: 'Tag Name',
@@ -26,50 +28,78 @@ const ListHistoryAlarm = memo(function ListHistoryAlarm(props) {
}, },
{ {
title: 'Value', title: 'Value',
dataIndex: 'stat', dataIndex: 'new_val',
key: 'stat', key: 'new_val',
width: '10%', width: '10%',
render: (_, record) => Number(record.new_val).toFixed(4),
}, },
{ {
title: 'Threshold', title: 'Threshold',
dataIndex: 'threshold', dataIndex: 'threshold',
key: 'threshold', key: 'threshold',
width: '10%', width: '10%',
render: (_, record) => {
switch (record.status) {
case 1:
return (
<span>
{record.lim_low} : {record.lim_high}
</span>
);
case 2:
return <span>{`< ${record.lim_low_crash}`}</span>;
case 3:
return (
<span>
{record.lim_low_crash} : {record.lim_low}
</span>
);
case 4:
return (
<span>
{record.lim_high} : {record.lim_high_crash}
</span>
);
case 5:
return <span>{`> ${record.lim_high_crash}`}</span>;
default:
return <span>Undefined</span>;
}
},
}, },
{ {
title: 'Condition', title: 'Condition',
dataIndex: 'condition', dataIndex: 'condition',
key: 'condition', key: 'condition',
width: '20%', width: '20%',
render: (_, record) => (
<Button type="text" style={{ backgroundColor: record.status_color, width: '100%' }}>
{record.condition}
</Button>
),
}, },
{ {
title: 'Stat', title: 'Stat',
dataIndex: 'stat', dataIndex: 'status',
key: 'stat', key: 'status',
width: '5%', width: '5%',
}, },
]; ];
const [trigerFilter, setTrigerFilter] = useState(false); const [trigerFilter, setTrigerFilter] = useState(false);
const defaultFilter = { search: '' }; const defaultFilter = { criteria: '' };
const [formDataFilter, setFormDataFilter] = useState(defaultFilter); const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const getAllEventAlarm = async (params) => {
return {
data: [],
};
};
const handleSearch = () => { const handleSearch = () => {
setFormDataFilter({ search: searchValue }); setFormDataFilter({ criteria: searchValue });
setTrigerFilter((prev) => !prev); setTrigerFilter((prev) => !prev);
}; };
const handleSearchClear = () => { const handleSearchClear = () => {
setSearchValue(''); setSearchValue('');
setFormDataFilter({ search: '' }); setFormDataFilter({ criteria: '' });
setTrigerFilter((prev) => !prev); setTrigerFilter((prev) => !prev);
}; };
@@ -113,7 +143,7 @@ const ListHistoryAlarm = memo(function ListHistoryAlarm(props) {
</Col> </Col>
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}> <Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
<TableList <TableList
getData={getAllEventAlarm} getData={getAllHistoryAlarm}
queryParams={formDataFilter} queryParams={formDataFilter}
columns={columns} columns={columns}
triger={trigerFilter} triger={trigerFilter}

View File

@@ -2,6 +2,7 @@ import React, { memo, useState, useEffect } from 'react';
import { Button, Row, Col, Card, Input } from 'antd'; import { Button, Row, Col, Card, Input } from 'antd';
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from '@ant-design/icons';
import TableList from '../../../../components/Global/TableList'; import TableList from '../../../../components/Global/TableList';
import { getAllHistoryEvent } from '../../../../api/history-value';
const ListHistoryEvent = memo(function ListHistoryEvent(props) { const ListHistoryEvent = memo(function ListHistoryEvent(props) {
const columns = [ const columns = [
@@ -16,48 +17,48 @@ const ListHistoryEvent = memo(function ListHistoryEvent(props) {
title: 'Datetime', title: 'Datetime',
dataIndex: 'datetime', dataIndex: 'datetime',
key: 'datetime', key: 'datetime',
width: '10%', width: '15%',
// render: (_, record) => toAppDateTimezoneFormatter(record.datetime),
}, },
{ {
title: 'Tag Name', title: 'Tag Name',
dataIndex: 'tag_name', dataIndex: 'tagname',
key: 'tag_name', key: 'tagname',
width: '40%', width: '40%',
}, },
{
title: 'Stat',
dataIndex: 'stat',
key: 'stat',
width: '5%',
},
{ {
title: 'Description', title: 'Description',
dataIndex: 'description', dataIndex: 'description',
key: 'description', key: 'description',
width: '15%', width: '20%',
render: (_, record) => (
<Button type="text" style={{ backgroundColor: record.status_color, width: '100%' }}>
{record.description}
</Button>
),
},
{
title: 'Stat',
dataIndex: 'status',
key: 'status',
width: '5%',
}, },
]; ];
const [trigerFilter, setTrigerFilter] = useState(false); const [trigerFilter, setTrigerFilter] = useState(false);
const defaultFilter = { search: '' }; const defaultFilter = { criteria: '' };
const [formDataFilter, setFormDataFilter] = useState(defaultFilter); const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const getAllEventAlarm = async (params) => {
return {
data: [],
};
};
const handleSearch = () => { const handleSearch = () => {
setFormDataFilter({ search: searchValue }); setFormDataFilter({ criteria: searchValue });
setTrigerFilter((prev) => !prev); setTrigerFilter((prev) => !prev);
}; };
const handleSearchClear = () => { const handleSearchClear = () => {
setSearchValue(''); setSearchValue('');
setFormDataFilter({ search: '' }); setFormDataFilter({ criteria: '' });
setTrigerFilter((prev) => !prev); setTrigerFilter((prev) => !prev);
}; };
@@ -101,7 +102,7 @@ const ListHistoryEvent = memo(function ListHistoryEvent(props) {
</Col> </Col>
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}> <Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
<TableList <TableList
getData={getAllEventAlarm} getData={getAllHistoryEvent}
queryParams={formDataFilter} queryParams={formDataFilter}
columns={columns} columns={columns}
triger={trigerFilter} triger={trigerFilter}

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -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 (

View File

@@ -3,12 +3,12 @@ import { Card, Typography, Flex } from 'antd';
import { setValSvg } from '../../components/Global/MqttConnection'; import { setValSvg } from '../../components/Global/MqttConnection';
import SvgTemplate from './SvgTemplate'; import SvgTemplate from './SvgTemplate';
import SvgViewer from './SvgViewer'; import SvgViewer from './SvgViewer';
import filePathSvg from '../../assets/svg/test-new.svg'; 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 (

View File

@@ -3,12 +3,10 @@ import { Card, Typography, Flex } from 'antd';
import { setValSvg } from '../../components/Global/MqttConnection'; import { setValSvg } from '../../components/Global/MqttConnection';
import SvgTemplate from './SvgTemplate'; import SvgTemplate from './SvgTemplate';
import SvgViewer from './SvgViewer'; import SvgViewer from './SvgViewer';
import filePathSvg from '../../assets/svg/test-new.svg'; import filePathSvg from '../../assets/svg/compressorB_rev.svg';
const { Text } = Typography; const { Text } = Typography;
const topicMqtt = 'PIU_COD/COMPRESSOR/COMPRESSOR_B';
// const filePathSvg = '/src/assets/svg/test-new.svg';
const topicMqtt = 'PIU_GGCP/Devices/PB';
const SvgCompressorB = () => { const SvgCompressorB = () => {
return ( return (

View File

@@ -3,12 +3,12 @@ import { Card, Typography, Flex } from 'antd';
import { setValSvg } from '../../components/Global/MqttConnection'; import { setValSvg } from '../../components/Global/MqttConnection';
import SvgTemplate from './SvgTemplate'; import SvgTemplate from './SvgTemplate';
import SvgViewer from './SvgViewer'; import SvgViewer from './SvgViewer';
import filePathSvg from '../../assets/svg/test-new.svg'; 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 (

View File

@@ -3,14 +3,14 @@ import { Card, Typography, Flex } from 'antd';
import { setValSvg } from '../../components/Global/MqttConnection'; import { setValSvg } from '../../components/Global/MqttConnection';
import SvgTemplate from './SvgTemplate'; import SvgTemplate from './SvgTemplate';
import SvgViewer from './SvgViewer'; import SvgViewer from './SvgViewer';
import filePathSvg from '../../assets/svg/test-new.svg'; 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 SvgOverview = () => { const SvgOverviewAirDryer = () => {
return ( return (
<SvgTemplate> <SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} /> <SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
@@ -18,4 +18,4 @@ const SvgOverview = () => {
); );
}; };
export default SvgOverview; export default SvgOverviewAirDryer;

View File

@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
import { Card, Typography, Flex } from 'antd';
import { setValSvg } from '../../components/Global/MqttConnection';
import SvgTemplate from './SvgTemplate';
import SvgViewer from './SvgViewer';
import filePathSvg from '../../assets/svg/overview-compressor.svg';
const { Text } = Typography;
// const filePathSvg = '/src/assets/svg/test-new.svg';
const topicMqtt = 'PIU_COD/COMPRESSOR/OVERVIEW';
const SvgOverviewCompressor = () => {
return (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgOverviewCompressor;

View File

@@ -1,173 +1,278 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import { Modal, Select, Typography, Button, ConfigProvider } from 'antd';
Modal, import { NotifOk } from '../../../components/Global/ToastNotif';
Typography, import { createJadwalShift, updateJadwalShift } from '../../../api/jadwal-shift';
Button, import { getAllUser } from '../../../api/user';
ConfigProvider, import { getAllShift } from '../../../api/master-shift';
Form, import { validateRun } from '../../../Utils/validate';
Select,
Spin,
Input
} from 'antd';
import { NotifOk, NotifAlert } from '../../../components/Global/ToastNotif';
import { updateJadwalShift, createJadwalShift } from '../../../api/jadwal-shift.jsx';
const { Text } = Typography; const { Text } = Typography;
const { Option } = Select; const { Option } = Select;
const DetailJadwalShift = (props) => { const DetailJadwalShift = (props) => {
const [form] = Form.useForm();
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const [employees, setEmployees] = useState([]); const [employees, setEmployees] = useState([]);
const [loadingEmployees, setLoadingEmployees] = useState(false); const [shifts, setShifts] = useState([]);
const [loadingData, setLoadingData] = useState(false);
const isReadOnly = props.actionMode === 'preview'; const defaultData = {
id: '',
user_id: null,
shift_id: null,
schedule_id: '',
user_phone: null,
};
const [formData, setFormData] = useState(defaultData);
const handleSelectChange = (name, value) => {
const updates = { [name]: value };
if (name === 'user_id') {
const selectedEmployee = employees.find((emp) => emp.user_id === value);
updates.user_phone = selectedEmployee?.user_phone || '-';
}
setFormData({
...formData,
...updates,
});
};
const handleCancel = () => { const handleCancel = () => {
props.setSelectedData(null);
props.setActionMode('list'); props.setActionMode('list');
}; };
const fetchEmployees = async () => { const fetchData = async () => {
setLoadingEmployees(true); setLoadingData(true);
try { try {
// Data dummy untuk dropdown karyawan const params = new URLSearchParams({
const dummyEmployees = [ page: 1,
{ employee_id: '101', nama_employee: 'Andi Pratama' }, limit: 100,
{ employee_id: '102', nama_employee: 'Budi Santoso' }, });
{ employee_id: '103', nama_employee: 'Citra Lestari' },
{ employee_id: '104', nama_employee: 'Dewi Anggraini' }, const [usersResponse, shiftsResponse] = await Promise.all([
{ employee_id: '105', nama_employee: 'Eko Wahyudi' }, getAllUser(params),
{ employee_id: '106', nama_employee: 'Fitriani' }, getAllShift(params),
]; ]);
setEmployees(dummyEmployees);
const userData = usersResponse?.data || usersResponse || [];
const shiftData = shiftsResponse?.data || shiftsResponse || [];
setEmployees(Array.isArray(userData) ? userData : []);
setShifts(Array.isArray(shiftData) ? shiftData : []);
} catch (error) { } catch (error) {
NotifAlert({ icon: 'error', title: 'Gagal', message: 'Gagal memuat daftar karyawan.' }); NotifOk({
icon: 'error',
title: 'Gagal',
message: 'Gagal memuat data karyawan atau shift.',
});
} finally { } finally {
setLoadingEmployees(false); setLoadingData(false);
} }
}; };
const handleSave = async () => { const handleSave = async () => {
setConfirmLoading(true);
// Daftar aturan validasi
const validationRules = [
{ field: 'user_id', label: 'Nama Karyawan', required: true },
{ field: 'shift_id', label: 'Shift', required: true },
];
if (
validateRun(formData, validationRules, (errorMessages) => {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: errorMessages,
});
setConfirmLoading(false);
})
)
return;
try { try {
const values = await form.validateFields(); const payload = {
let payload; user_id: formData.user_id,
let responseMessage; shift_id: formData.shift_id,
};
setConfirmLoading(true); // Add schedule_id only if editing and it exists
if (props.actionMode === 'edit') { if (props.actionMode === 'edit' && formData.schedule_id) {
payload = { ...props.selectedData, ...values }; payload.schedule_id = formData.schedule_id;
// await updateJadwalShift(payload.schedule_id, payload);
console.log("Updating schedule with payload:", payload);
responseMessage = 'Jadwal berhasil diperbarui.';
} else { // 'add' mode
payload = {
employee_id: values.employee_id,
shift_name: props.selectedData.shift_name,
schedule_date: new Date().toISOString().split('T')[0], // Example date
};
// await createJadwalShift(payload);
console.log("Creating schedule with payload:", payload);
responseMessage = 'User berhasil ditambahkan ke jadwal.';
} }
await new Promise(resolve => setTimeout(resolve, 500)); // Simulasi API call
NotifOk({ icon: 'success', title: 'Berhasil', message: responseMessage }); const response =
props.setActionMode('list'); // Menutup modal dan memicu refresh di parent props.actionMode === 'edit'
? await updateJadwalShift(formData.id, payload)
: await createJadwalShift(payload);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
const action = props.actionMode === 'edit' ? 'diubah' : 'ditambahkan';
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Jadwal berhasil ${action}.`,
});
props.setActionMode('list');
} else {
NotifOk({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Terjadi kesalahan saat menyimpan data.',
});
}
} catch (error) { } catch (error) {
const message = error.response?.data?.message || 'Gagal memperbarui jadwal.'; NotifOk({
NotifAlert({ icon: 'error', title: 'Gagal', message }); icon: 'error',
title: 'Error',
message: error.message || 'Terjadi kesalahan pada server.',
});
} finally { } finally {
setConfirmLoading(false); setConfirmLoading(false);
} }
}; };
useEffect(() => { useEffect(() => {
// Hanya jalankan jika modal untuk 'edit' atau 'preview' terbuka
if (props.showModal) { if (props.showModal) {
fetchEmployees(); fetchData();
if (props.actionMode === 'edit' || props.actionMode === 'preview') {
form.setFieldsValue({
employee_id: props.selectedData.employee_id,
shift_name: props.selectedData.shift_name,
});
} else if (props.actionMode === 'add') {
form.setFieldsValue({
shift_name: props.selectedData.shift_name,
employee_id: null, // Reset employee selection
});
}
} }
}, [props.actionMode, props.showModal, props.selectedData, form]);
if (props.selectedData) {
setFormData({
id: props.selectedData.id || '',
user_id: props.selectedData.user_id || null,
shift_id: props.selectedData.shift_id || null,
schedule_id: props.selectedData.schedule_id || '',
user_phone: props.selectedData.whatsapp || props.selectedData.user_phone || null,
});
} else {
setFormData(defaultData);
}
}, [props.showModal, props.selectedData, props.actionMode]);
return ( return (
<Modal <Modal
title={isReadOnly ? 'Preview Jadwal' : (props.actionMode === 'edit' ? 'Edit Jadwal' : 'Tambah User')} title={`${
props.actionMode === 'add'
? 'Tambah'
: props.actionMode === 'preview'
? 'Preview'
: 'Edit'
} Jadwal Shift`}
open={props.showModal} open={props.showModal}
onCancel={handleCancel} onCancel={handleCancel}
width={600}
footer={[ footer={[
<React.Fragment key="modal-footer"> <React.Fragment key="modal-footer">
<Button key="back" onClick={handleCancel}> <ConfigProvider
{isReadOnly ? 'Tutup' : 'Batal'} theme={{
</Button> components: {
{!isReadOnly && ( Button: {
<Button key="submit" type="primary" loading={confirmLoading} onClick={handleSave} style={{ backgroundColor: '#23A55A' }}> defaultBg: 'white',
Simpan defaultColor: '#23A55A',
</Button> defaultBorderColor: '#23A55A',
)} },
},
}}
>
<Button onClick={handleCancel}>{props.readOnly ? 'Tutup' : 'Batal'}</Button>
</ConfigProvider>
<ConfigProvider
theme={{
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
},
},
}}
>
{!props.readOnly && (
<Button loading={confirmLoading} onClick={handleSave}>
Simpan
</Button>
)}
</ConfigProvider>
</React.Fragment>, </React.Fragment>,
]} ]}
> >
<Spin spinning={loadingEmployees} tip="Memuat data..."> {formData && (
<Form form={form} layout="vertical" name="shift_form"> <div>
{props.actionMode === 'add' ? ( <div style={{ marginBottom: 12 }}>
<> <Text strong>Nama Karyawan</Text>
<Form.Item <Text style={{ color: 'red' }}> *</Text>
name="shift_name" <Select
label="Shift" value={formData.user_id}
> onChange={(value) => handleSelectChange('user_id', value)}
<Input disabled /> placeholder="Pilih karyawan"
</Form.Item> disabled={props.readOnly || loadingData}
<Form.Item loading={loadingData}
name="employee_id" showSearch
label="Nama Karyawan" optionFilterProp="children"
rules={[{ required: true, message: 'Nama karyawan wajib dipilih!' }]} filterOption={(input, option) =>
> option?.children?.toLowerCase().indexOf(input.toLowerCase()) >= 0
<Select }
placeholder="Pilih karyawan" style={{ width: '100%' }}
showSearch >
optionFilterProp="children" {employees
> .filter((emp) => emp.user_id != null)
{employees.map(emp => ( .map((emp) => (
<Option key={emp.employee_id} value={emp.employee_id}>{emp.nama_employee}</Option> <Option key={`emp-${emp.user_id}`} value={emp.user_id}>
))} {emp.user_fullname || emp.user_name}
</Select> </Option>
</Form.Item> ))}
</> </Select>
) : ( </div>
<>
<Form.Item <div style={{ marginBottom: 12 }}>
name="employee_id" <Text strong>No. Telepon</Text>
label="Nama Karyawan" <div
rules={[{ required: true, message: 'Nama karyawan wajib dipilih!' }]} style={{
> padding: '8px 12px',
<Select placeholder="Pilih karyawan" disabled={isReadOnly} showSearch optionFilterProp="children"> backgroundColor: '#f5f5f5',
{employees.map(emp => ( borderRadius: '6px',
<Option key={emp.employee_id} value={emp.employee_id}>{emp.nama_employee}</Option>
))} marginTop: '4px',
</Select> color: formData.user_phone ? '#000' : '#999',
</Form.Item> }}
<Form.Item name="shift_name" label="Shift" rules={[{ required: true, message: 'Shift wajib dipilih!' }]}> >
<Select placeholder="Pilih shift" disabled={isReadOnly}> {formData.user_phone || 'Pilih karyawan terlebih dahulu'}
<Option value="PAGI">PAGI</Option> </div>
<Option value="SIANG">SIANG</Option> </div>
<Option value="MALAM">MALAM</Option>
</Select> <div style={{ marginBottom: 12 }}>
</Form.Item> <Text strong>Shift</Text>
</> <Text style={{ color: 'red' }}> *</Text>
)} <Select
</Form> value={formData.shift_id}
</Spin> onChange={(value) => handleSelectChange('shift_id', value)}
placeholder="Pilih shift"
disabled={props.readOnly || loadingData}
loading={loadingData}
showSearch
optionFilterProp="children"
filterOption={(input, option) =>
option?.children?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
style={{ width: '100%' }}
>
{shifts
.filter((shift) => shift.shift_id != null)
.map((shift) => (
<Option key={`shift-${shift.shift_id}`} value={shift.shift_id}>
{shift.shift_name}
</Option>
))}
</Select>
</div>
</div>
)}
</Modal> </Modal>
); );
}; };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,766 @@
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import {
Divider,
Typography,
Button,
Steps,
Form,
Row,
Col,
Card,
Spin,
Tag,
Space,
ConfigProvider,
Empty
} from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { NotifAlert } from '../../../components/Global/ToastNotif';
import { getBrandById, getErrorCodesByBrandId } from '../../../api/master-brand';
import { getFileUrl, getFolderFromFileType } from '../../../api/file-uploads';
import { SendRequest } from '../../../components/Global/ApiRequest';
import ListErrorCode from './component/ListErrorCode';
import BrandForm from './component/BrandForm';
import ErrorCodeForm from './component/ErrorCodeForm';
import SolutionForm from './component/SolutionForm';
import SparepartSelect from './component/SparepartSelect';
const { Title, Text } = Typography;
const { Step } = Steps;
const ViewBrandDevice = () => {
const navigate = useNavigate();
const { id } = useParams();
const location = useLocation();
const { setBreadcrumbItems } = useBreadcrumb();
const [brandForm] = Form.useForm();
const [errorCodeForm] = Form.useForm();
const [solutionForm] = Form.useForm();
const [brandData, setBrandData] = useState(null);
const [errorCodes, setErrorCodes] = useState([]);
const [loading, setLoading] = useState(true);
const [currentStep, setCurrentStep] = useState(0);
const [selectedErrorCode, setSelectedErrorCode] = useState(null);
const [selectedSparepartIds, setSelectedSparepartIds] = useState([]);
const [errorCodeIcon, setErrorCodeIcon] = useState(null);
const [trigerFilter, setTrigerFilter] = useState(false);
const [searchText, setSearchText] = useState('');
const [solutionFields, setSolutionFields] = useState([0]);
const [solutionTypes, setSolutionTypes] = useState({ 0: 'text' });
const [solutionStatuses, setSolutionStatuses] = useState({ 0: true });
const [currentSolutionData, setCurrentSolutionData] = useState([]);
const [brandInfo, setBrandInfo] = useState({});
const resetSolutionFields = () => {
if (solutionForm && solutionForm.resetFields) {
solutionForm.resetFields();
solutionForm.setFieldsValue({
solution_items: {
0: {
name: '',
type: 'text',
text: '',
status: true
}
}
});
}
setCurrentSolutionData([]);
};
const getSolutionData = () => {
if (!solutionForm) return [];
try {
const values = solutionForm.getFieldsValue(true);
let solutions = [];
if (values.solution_items) {
if (Array.isArray(values.solution_items)) {
solutions = values.solution_items.filter(Boolean);
} else if (typeof values.solution_items === 'object') {
solutions = Object.values(values.solution_items).filter(Boolean);
}
}
return solutions;
} catch (error) {
return [];
}
};
useEffect(() => {
const savedPhase = location.state?.phase || localStorage.getItem(`brand_device_${id}_last_phase`);
if (savedPhase) {
setCurrentStep(parseInt(savedPhase));
localStorage.removeItem(`brand_device_${id}_last_phase`);
}
}, [location.state, id]);
useEffect(() => {
setBreadcrumbItems([
{
title: <span style={{ fontSize: '14px', fontWeight: 'bold' }}> Master</span>
},
{
title: (
<span
style={{ fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}
onClick={() => navigate('/master/brand-device')}
>
Brand Device
</span>
),
},
{
title: (
<span style={{ fontSize: '14px', fontWeight: 'bold' }}>
View Brand Device
</span>
),
},
]);
}, [setBreadcrumbItems, navigate]);
useEffect(() => {
const fetchBrandData = async () => {
const token = localStorage.getItem('token');
if (!token) {
navigate('/signin');
return;
}
try {
setLoading(true);
const response = await getBrandById(id);
if (response && response.statusCode === 200) {
const brandData = response.data;
const brandInfoData = {
brand_code: brandData.brand_code,
brand_name: brandData.brand_name,
brand_type: brandData.brand_type || '',
brand_manufacture: brandData.brand_manufacture || '',
brand_model: brandData.brand_model || '',
is_active: brandData.is_active
};
setBrandInfo(brandInfoData);
setBrandData(brandData);
brandForm.setFieldsValue(brandInfoData);
if (brandData.brand_id) {
try {
const errorCodesResponse = await getErrorCodesByBrandId(id || brandData.brand_id);
if (errorCodesResponse && errorCodesResponse.statusCode === 200) {
const apiErrorData = errorCodesResponse.data || [];
const existingCodes = apiErrorData.map(ec => ({
...ec,
tempId: `existing_${ec.error_code_id}`,
status: 'existing',
solution: ec.solution || [],
spareparts: ec.spareparts || []
}));
setErrorCodes(existingCodes);
}
} catch (error) {
}
}
} else {
NotifAlert({
icon: 'error',
title: 'Error',
message: response?.message || 'Failed to fetch brand device data',
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'Failed to fetch brand device data',
});
} finally {
setLoading(false);
}
};
fetchBrandData();
}, [id, navigate, brandForm]);
useEffect(() => {
if (currentStep === 1 && id) {
setTrigerFilter(prev => !prev);
}
}, [currentStep, id]);
useEffect(() => {
if (currentStep === 1 && errorCodes.length > 0 && !selectedErrorCode) {
handleErrorCodeSelect(errorCodes[0]);
}
}, [currentStep, errorCodes]);
const setSolutionsForExistingRecord = (solutions, targetForm) => {
if (!targetForm || !solutions || solutions.length === 0) {
return;
}
targetForm.resetFields();
const solutionItems = {};
const newSolutionFields = [];
const newSolutionTypes = {};
const newSolutionStatuses = {};
solutions.forEach((solution, index) => {
const fieldKey = index;
newSolutionFields.push(fieldKey);
const isFileType = solution.type_solution && solution.type_solution !== 'text';
newSolutionTypes[fieldKey] = isFileType ? 'file' : 'text';
newSolutionStatuses[fieldKey] = solution.is_active;
let fileObject = null;
if (isFileType && (solution.path_solution || solution.path_document)) {
fileObject = {
uploadPath: solution.path_solution || solution.path_document,
path_solution: solution.path_solution || solution.path_document,
name: solution.file_upload_name || (solution.path_solution || solution.path_document).split('/').pop() || 'File',
type_solution: solution.type_solution,
isExisting: true,
size: 0,
url: solution.path_solution || solution.path_document
};
}
solutionItems[fieldKey] = {
brand_code_solution_id: solution.brand_code_solution_id,
name: solution.solution_name || '',
type: isFileType ? 'file' : 'text',
text: solution.text_solution || '',
status: solution.is_active,
file: fileObject,
fileUpload: fileObject,
path_solution: solution.path_solution || solution.path_document || null,
fileName: solution.file_upload_name || null
};
});
setSolutionFields(newSolutionFields);
setSolutionTypes(newSolutionTypes);
setSolutionStatuses(newSolutionStatuses);
targetForm.resetFields();
setTimeout(() => {
targetForm.setFieldsValue({
solution_items: solutionItems
});
setTimeout(() => {
Object.keys(solutionItems).forEach(key => {
const solution = solutionItems[key];
targetForm.setFieldValue(['solution_items', key, 'name'], solution.name);
targetForm.setFieldValue(['solution_items', key, 'type'], solution.type);
targetForm.setFieldValue(['solution_items', key, 'text'], solution.text);
targetForm.setFieldValue(['solution_items', key, 'file'], solution.file);
targetForm.setFieldValue(['solution_items', key, 'fileUpload'], solution.fileUpload);
targetForm.setFieldValue(['solution_items', key, 'status'], solution.status);
targetForm.setFieldValue(['solution_items', key, 'path_solution'], solution.path_solution);
targetForm.setFieldValue(['solution_items', key, 'fileName'], solution.fileName);
});
const finalValues = targetForm.getFieldsValue();
}, 100);
}, 100);
};
const handleErrorCodeSelect = async (errorCode) => {
setSelectedErrorCode(errorCode);
try {
const directResponse = await SendRequest({
method: 'get',
prefix: `error-code/${errorCode.error_code_id}`,
});
const apiResponse = directResponse.data;
if (apiResponse && apiResponse.statusCode === 200 && apiResponse.data) {
const fullErrorCodeData = {
...apiResponse.data,
tempId: `existing_${apiResponse.data.error_code_id}`
};
const formValues = {
error_code: fullErrorCodeData.error_code,
error_code_name: fullErrorCodeData.error_code_name,
error_code_description: fullErrorCodeData.error_code_description || '',
error_code_color: fullErrorCodeData.error_code_color && fullErrorCodeData.error_code_color !== '' ? fullErrorCodeData.error_code_color : '#000000',
status: fullErrorCodeData.is_active,
};
errorCodeForm.setFieldsValue(formValues);
if (fullErrorCodeData.path_icon && fullErrorCodeData.path_icon !== '') {
const iconData = {
name: fullErrorCodeData.path_icon.split('/').pop(),
uploadPath: fullErrorCodeData.path_icon,
};
setErrorCodeIcon(iconData);
} else {
setErrorCodeIcon(null);
}
if (apiResponse.data.solution && apiResponse.data.solution.length > 0) {
setCurrentSolutionData(apiResponse.data.solution);
setSolutionsForExistingRecord(apiResponse.data.solution, solutionForm);
}
if (apiResponse.data.spareparts && apiResponse.data.spareparts.length > 0) {
setSelectedSparepartIds(apiResponse.data.spareparts.map(sp => sp.sparepart_id));
} else {
setSelectedSparepartIds([]);
}
} else {
const basicErrorCodeData = {
...errorCode,
tempId: `existing_${errorCode.error_code_id}`
};
const formValues = {
error_code: basicErrorCodeData.error_code,
error_code_name: basicErrorCodeData.error_code_name,
error_code_description: basicErrorCodeData.error_code_description || '',
error_code_color: basicErrorCodeData.error_code_color && basicErrorCodeData.error_code_color !== '' ? basicErrorCodeData.error_code_color : '#000000',
status: basicErrorCodeData.is_active,
};
errorCodeForm.setFieldsValue(formValues);
if (basicErrorCodeData.path_icon && basicErrorCodeData.path_icon !== '') {
const iconData = {
name: basicErrorCodeData.path_icon.split('/').pop(),
uploadPath: basicErrorCodeData.path_icon,
};
setErrorCodeIcon(iconData);
} else {
setErrorCodeIcon(null);
}
resetSolutionFields();
setSelectedSparepartIds([]);
}
} catch (error) {
const basicErrorCodeData = {
...errorCode,
tempId: `existing_${errorCode.error_code_id}`
};
const formValues = {
error_code: basicErrorCodeData.error_code,
error_code_name: basicErrorCodeData.error_code_name,
error_code_description: basicErrorCodeData.error_code_description || '',
error_code_color: basicErrorCodeData.error_code_color && basicErrorCodeData.error_code_color !== '' ? basicErrorCodeData.error_code_color : '#000000',
status: basicErrorCodeData.is_active,
};
errorCodeForm.setFieldsValue(formValues);
if (basicErrorCodeData.path_icon && basicErrorCodeData.path_icon !== '') {
const iconData = {
name: basicErrorCodeData.path_icon.split('/').pop(),
uploadPath: basicErrorCodeData.path_icon,
};
setErrorCodeIcon(iconData);
} else {
setErrorCodeIcon(null);
}
resetSolutionFields();
setSelectedSparepartIds([]);
}
};
const handleBrandFormValuesChange = useCallback((changedValues, allValues) => {
setBrandInfo(allValues);
}, [setBrandInfo]);
const handleSearch = () => {
setTrigerFilter((prev) => !prev);
};
const handleSearchClear = () => {
setSearchText('');
setTrigerFilter((prev) => !prev);
};
const handleFileView = (fileName) => {
try {
let fileUrl = '';
let actualFileName = '';
const filePath = fileName || '';
if (filePath) {
actualFileName = filePath.split('/').pop();
}
if (actualFileName) {
const fileExtension = actualFileName.split('.').pop()?.toLowerCase();
const folder = getFolderFromFileType(fileExtension);
fileUrl = getFileUrl(folder, actualFileName);
}
if (!fileUrl && filePath) {
fileUrl = filePath.startsWith('http') ? filePath : `${import.meta.env.VITE_API_SERVER}/${filePath}`;
}
if (fileUrl && actualFileName) {
const fileExtension = actualFileName.split('.').pop()?.toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
const pdfExtensions = ['pdf'];
if (imageExtensions.includes(fileExtension) || pdfExtensions.includes(fileExtension)) {
const viewerUrl = `/image-viewer/${encodeURIComponent(actualFileName)}`;
window.open(viewerUrl, '_blank', 'noopener,noreferrer');
} else {
window.open(fileUrl, '_blank', 'noopener,noreferrer');
}
} else {
NotifAlert({
icon: 'error',
title: 'Error',
message: 'File URL not found'
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Failed to open file preview'
});
}
};
const handleNextStep = () => {
setCurrentStep(1);
};
const renderStepContent = () => {
if (currentStep === 0) {
return (
<div style={{ position: 'relative' }}>
{loading && (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.7)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
borderRadius: '8px',
}}
>
<Spin size="large" />
</div>
)}
<BrandForm
form={brandForm}
onValuesChange={handleBrandFormValuesChange}
isEdit={false}
readOnly={true}
/>
</div>
);
}
if (currentStep === 1) {
return (
<Row gutter={[16, 8]} style={{ minHeight: '70vh' }}>
<Col xs={24} md={8} lg={8}>
<ListErrorCode
brandId={id}
selectedErrorCode={selectedErrorCode}
onErrorCodeSelect={handleErrorCodeSelect}
tempErrorCodes={[]}
trigerFilter={trigerFilter}
searchText={searchText}
onSearchChange={(value) => {
setSearchText(value);
if (value === '') {
setTrigerFilter((prev) => !prev);
}
}}
onSearch={handleSearch}
onSearchClear={handleSearchClear}
isReadOnly={true}
/>
</Col>
<Col xs={24} md={16} lg={16}>
<div style={{
paddingLeft: '12px'
}}>
{selectedErrorCode ? (
<Card
title={
<span style={{
fontSize: '16px',
fontWeight: '600',
color: '#262626',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<span style={{
width: '4px',
height: '20px',
backgroundColor: '#23A55A',
borderRadius: '2px'
}}></span>
Error Code Form
</span>
}
style={{
width: '100%',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
borderRadius: '12px'
}}
styles={{
body: { padding: '16px 24px 12px 24px' },
header: {
padding: '16px 24px',
borderBottom: '1px solid #f0f0f0',
backgroundColor: '#fafafa'
}
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{
padding: '16px',
border: '1px solid #f0f0f0',
borderRadius: '10px',
backgroundColor: '#ffffff',
marginBottom: '0',
transition: 'all 0.3s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
paddingBottom: '8px',
borderBottom: '1px solid #f5f5f5'
}}>
<div style={{
width: '3px',
height: '16px',
backgroundColor: '#23A55A',
borderRadius: '2px'
}}></div>
<h4 style={{ margin: 0, color: '#262626', fontSize: '14px', fontWeight: '600' }}>
Error Code Details
</h4>
</div>
<ErrorCodeForm
errorCodeForm={errorCodeForm}
isErrorCodeFormReadOnly={true}
errorCodeIcon={errorCodeIcon}
onErrorCodeIconUpload={() => { }}
onErrorCodeIconRemove={() => { }}
isEdit={true}
/>
</div>
<Row gutter={[20, 0]} style={{ marginTop: '0' }}>
<Col xs={24} md={12} lg={12}>
<div style={{
padding: '16px',
border: '1px solid #f0f0f0',
borderRadius: '10px',
backgroundColor: '#ffffff',
transition: 'all 0.3s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
paddingBottom: '8px',
borderBottom: '1px solid #f5f5f5'
}}>
<div style={{
width: '3px',
height: '16px',
backgroundColor: '#1890ff',
borderRadius: '2px'
}}></div>
<h4 style={{ margin: 0, color: '#262626', fontSize: '14px', fontWeight: '600' }}>
Solution
</h4>
</div>
<SolutionForm
solutionForm={solutionForm}
solutionFields={solutionFields}
solutionTypes={solutionTypes}
solutionStatuses={solutionStatuses}
onAddSolutionField={() => { }}
onRemoveSolutionField={() => { }}
onSolutionTypeChange={() => { }}
onSolutionStatusChange={() => { }}
onSolutionFileUpload={() => { }}
onFileView={(fileData) => {
if (fileData && (fileData.url || fileData.uploadPath)) {
window.open(fileData.url || fileData.uploadPath, '_blank');
}
}}
isReadOnly={true}
solutionData={currentSolutionData}
/>
</div>
</Col>
<Col xs={24} md={12} lg={12}>
<div style={{
padding: '16px',
border: '1px solid #f0f0f0',
borderRadius: '10px',
backgroundColor: '#ffffff',
transition: 'all 0.3s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
paddingBottom: '8px',
borderBottom: '1px solid #f5f5f5'
}}>
<div style={{
width: '3px',
height: '16px',
backgroundColor: '#faad14',
borderRadius: '2px'
}}></div>
<h4 style={{ margin: 0, color: '#262626', fontSize: '14px', fontWeight: '600' }}>
Sparepart Selection
</h4>
</div>
<div style={{
maxHeight: '45vh',
overflow: 'auto',
border: '1px solid #e8e8e8',
borderRadius: '8px',
padding: '12px',
backgroundColor: '#fafafa'
}}>
<SparepartSelect
selectedSparepartIds={selectedSparepartIds}
onSparepartChange={() => { }}
isReadOnly={true}
/>
</div>
</div>
</Col>
</Row>
</div>
</Card>
) : (
<div style={{
height: '100%', display: 'flex', flexDirection: 'column',
justifyContent: 'center', alignItems: 'center',
backgroundColor: '#ffffff', borderRadius: '12px',
border: '1px dashed #d9d9d9', color: '#8c8c8c', padding: '48px'
}}>
<Empty description="Select an error code to view details" />
</div>
)}
</div>
</Col>
</Row>
);
}
return null;
};
return (
<ConfigProvider
theme={{
components: {
Switch: {
colorPrimary: '#23A55A',
colorPrimaryHover: '#23A55A',
},
},
}}
>
<Card>
<Steps current={currentStep} style={{ marginBottom: 24 }}>
<Step title="Brand Device Details" />
<Step title="Error Codes & Solutions" />
</Steps>
{renderStepContent()}
<Divider />
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>
{currentStep === 1 && (
<Button
onClick={() => setCurrentStep(0)}
>
Back to Brand Info
</Button>
)}
</div>
<div>
{currentStep === 0 && (
<Button
type="primary"
onClick={handleNextStep}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
}}
>
Error Code
</Button>
)}
{currentStep === 1 && (
<Button
type="primary"
onClick={() => navigate('/master/brand-device')}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
}}
>
Selesai
</Button>
)}
</div>
</div>
</Card>
</ConfigProvider>
);
};
export default ViewBrandDevice;

View File

@@ -0,0 +1,461 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Card, Button, Typography, Spin, Alert, Space } from 'antd';
import { NotifAlert } from '../../../components/Global/ToastNotif';
import { ArrowLeftOutlined, FilePdfOutlined, FileImageOutlined, DownloadOutlined } from '@ant-design/icons';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { getBrandById } from '../../../api/master-brand';
import {
downloadFile,
getFile,
getFileUrl,
getFolderFromFileType,
} from '../../../api/file-uploads';
const { Title } = Typography;
const ViewFilePage = () => {
const params = useParams();
const { id, fileType, fileName } = params;
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [brandData, setBrandData] = useState(null);
const [actualFileName, setActualFileName] = useState('');
const [pdfBlobUrl, setPdfBlobUrl] = useState(null);
const [pdfLoading, setPdfLoading] = useState(false);
const isFromEdit = window.location.pathname.includes('/edit/');
let fallbackId = id;
let fallbackFileType = fileType;
let fallbackFileName = fileName;
if (!fileName || !fileType || !id) {
const urlParts = window.location.pathname.split('/');
const viewIndex = urlParts.indexOf('view');
const editIndex = urlParts.indexOf('edit');
const actionIndex = viewIndex !== -1 ? viewIndex : editIndex;
if (actionIndex !== -1 && urlParts.length > actionIndex + 4) {
fallbackId = urlParts[actionIndex + 1];
fallbackFileType = urlParts[actionIndex + 3];
fallbackFileName = decodeURIComponent(urlParts[actionIndex + 4]);
}
}
useEffect(() => {
setPdfBlobUrl(null);
setPdfLoading(false);
setError(null);
const fetchData = async () => {
const token = localStorage.getItem('token');
if (!token) {
navigate('/signin');
return;
}
try {
const actualId = fallbackId || id;
const actualFileName = fallbackFileName || fileName;
const brandResponse = await getBrandById(actualId);
if (brandResponse && brandResponse.statusCode === 200) {
setBrandData(brandResponse.data);
}
const decodedFileName = decodeURIComponent(actualFileName);
setActualFileName(decodedFileName);
const fileExtension = decodedFileName.split('.').pop().toLowerCase();
if (fileExtension === 'pdf') {
setPdfLoading(true);
const folder = getFolderFromFileType('pdf');
try {
const blobData = await getFile(folder, decodedFileName);
const blobUrl = window.URL.createObjectURL(blobData);
setPdfBlobUrl(blobUrl);
} catch (pdfError) {
setError('Failed to load PDF file: ' + (pdfError.message || pdfError));
setPdfBlobUrl(null);
} finally {
setPdfLoading(false);
}
}
setLoading(false);
} catch (error) {
setError('Failed to load data');
setLoading(false);
}
};
fetchData();
return () => {
if (pdfBlobUrl) {
window.URL.revokeObjectURL(pdfBlobUrl);
}
};
}, [id, fileName, fileType, navigate]);
useEffect(() => {
if (brandData) {
const breadcrumbItems = [
{ title: <strong style={{ fontSize: '14px' }}> Master</strong> },
{
title: <strong style={{ fontSize: '14px' }} onClick={() => navigate('/master/brand-device')}>Brand Device</strong>
}
];
if (isFromEdit) {
breadcrumbItems.push({
title: <strong style={{ fontSize: '14px' }} onClick={() => navigate(`/master/brand-device/edit/${fallbackId || id}`)}>Edit Brand Device</strong>
});
} else {
breadcrumbItems.push({
title: <strong style={{ fontSize: '14px' }} onClick={() => navigate(`/master/brand-device/view/${fallbackId || id}`)}>View Brand Device</strong>
});
}
breadcrumbItems.push({ title: <strong style={{ fontSize: '14px' }}>View Document</strong> });
setBreadcrumbItems(breadcrumbItems);
}
}, [brandData, id, isFromEdit, fallbackId, navigate, setBreadcrumbItems]);
const handleBack = () => {
if (isFromEdit) {
const savedPhase = localStorage.getItem(`brand_device_edit_${fallbackId || id}_last_phase`);
if (savedPhase) {
localStorage.removeItem(`brand_device_edit_${fallbackId || id}_last_phase`);
}
const targetPhase = savedPhase ? parseInt(savedPhase) : 1;
navigate(`/master/brand-device/edit/${fallbackId || id}`, {
state: { phase: targetPhase, fromFileViewer: true },
replace: true
});
} else {
navigate(`/master/brand-device/view/${fallbackId || id}`, {
state: { phase: 1 },
replace: true
});
}
};
const renderContent = () => {
if (error) {
return (
<Alert
message="Error Loading File"
description={error}
type="error"
showIcon
style={{ margin: '20px 0' }}
/>
);
}
const displayFileName = actualFileName || 'Loading...';
const fileExtension = displayFileName.split('.').pop().toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
const isPdf = fileExtension === 'pdf';
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
{isImage ? (
<div style={{
width: '100%',
height: '300px',
backgroundColor: '#f5f5f5',
border: '1px solid #d9d9d9',
borderRadius: '8px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#999'
}}>
<div>
<FileImageOutlined style={{ fontSize: '48px', marginBottom: '16px' }} />
<div>Loading image...</div>
</div>
</div>
) : isPdf ? (
<div style={{
width: '100%',
height: '400px',
backgroundColor: '#f5f5f5',
border: '1px solid #d9d9d9',
borderRadius: '8px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#999'
}}>
<div>
<FilePdfOutlined style={{ fontSize: '48px', marginBottom: '16px' }} />
<div>Loading PDF...</div>
</div>
</div>
) : (
<div style={{
width: '100%',
height: '200px',
backgroundColor: '#f5f5f5',
border: '1px solid #d9d9d9',
borderRadius: '8px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#999'
}}>
<div>
<FilePdfOutlined style={{ fontSize: '48px', marginBottom: '16px' }} />
<div>Loading file...</div>
</div>
</div>
)}
</div>
);
}
if (isImage) {
return (
<div style={{ textAlign: 'center', padding: '20px' }}>
<img
src={getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName)}
alt={actualFileName}
style={{
maxWidth: '100%',
maxHeight: '70vh',
objectFit: 'contain',
border: '1px solid #d9d9d9',
borderRadius: '8px'
}}
onError={() => setError('Failed to load image')}
/>
</div>
);
}
if (isPdf) {
const displayUrl = pdfBlobUrl || getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName);
return (
<div style={{ height: '75vh', width: '100%', border: '1px solid #d9d9d9', borderRadius: '8px', overflow: 'hidden' }}>
{pdfBlobUrl ? (
<iframe
src={pdfBlobUrl}
title={actualFileName}
style={{
width: '100%',
height: '100%',
border: 'none',
borderRadius: '8px'
}}
onError={() => {
setError('Failed to load PDF. Please try downloading the file.');
}}
/>
) : pdfLoading ? (
<div style={{
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
gap: '16px',
backgroundColor: '#f5f5f5'
}}>
<Spin size="large" />
<div style={{ fontSize: '16px', color: '#666', textAlign: 'center' }}>
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>Memuat PDF...</div>
<div>Silakan tunggu sebentar</div>
</div>
</div>
) : (
<div style={{
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
gap: '16px',
backgroundColor: '#f5f5f5'
}}>
<FilePdfOutlined style={{ fontSize: '48px', color: '#ff4d4f' }} />
<div style={{ fontSize: '16px', color: '#666', textAlign: 'center' }}>
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>PDF tidak dapat dimuat</div>
<div>Silakan download file untuk melihat kontennya</div>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<Button
type="primary"
onClick={() => {
const folder = getFolderFromFileType(fallbackFileType || fileType);
downloadFile(folder, actualFileName);
}}
icon={<DownloadOutlined />}
>
Download PDF
</Button>
<Button
onClick={() => {
setPdfLoading(true);
const folder = getFolderFromFileType('pdf');
getFile(folder, actualFileName)
.then(blobData => {
const blobUrl = window.URL.createObjectURL(blobData);
setPdfBlobUrl(blobUrl);
})
.catch(error => {
setError('Failed to load PDF file: ' + (error.message || error));
setPdfBlobUrl(null);
})
.finally(() => {
setPdfLoading(false);
});
}}
>
Coba Lagi
</Button>
</div>
</div>
)}
</div>
);
}
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<FilePdfOutlined style={{ fontSize: '48px', color: '#ff4d4f', marginBottom: '16px' }} />
<div style={{ fontSize: '16px', marginBottom: '8px' }}>Preview tidak tersedia untuk jenis file ini</div>
<div style={{ color: '#666', marginBottom: '16px' }}>{actualFileName}</div>
<div style={{ marginTop: '16px' }}>
<Button type="primary" href={getFileUrl(getFolderFromFileType(fallbackFileType || fileType), actualFileName)} target="_blank" rel="noopener noreferrer">
Buka di Tab Baru
</Button>
</div>
</div>
);
};
const getFileIcon = () => {
const displayFileName = actualFileName || 'Loading...';
const fileExtension = displayFileName.split('.').pop().toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
const isPdf = fileExtension === 'pdf';
if (isImage) return <FileImageOutlined style={{ color: '#1890ff', fontSize: '20px' }} />;
if (isPdf) return <FilePdfOutlined style={{ color: '#ff4d4f', fontSize: '20px' }} />;
return <FilePdfOutlined style={{ color: '#ff4d4f', fontSize: '20px' }} />;
};
const getFileTypeColor = () => {
const displayFileName = actualFileName || 'Loading...';
const fileExtension = displayFileName.split('.').pop().toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif'].includes(fileExtension);
const isPdf = fileExtension === 'pdf';
if (isImage) return '#1890ff';
if (isPdf) return '#ff4d4f';
return '#ff4d4f';
};
return (
<div style={{ padding: '24px', minHeight: '100vh', backgroundColor: '#f5f5f5' }}>
<Card>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{getFileIcon()}
<div>
<Title level={4} style={{ margin: 0 }}>
{actualFileName || 'Loading...'}
</Title>
{brandData ? (
<div style={{ color: '#666', fontSize: '14px' }}>
Brand: {brandData.brand_name} | ID: {brandData.brand_id}
</div>
) : (
<div style={{ color: '#666', fontSize: '14px' }}>
Loading brand information...
</div>
)}
</div>
</div>
<Space>
<Button
icon={<ArrowLeftOutlined />}
onClick={handleBack}
>
Kembali
</Button>
<Button
type="primary"
onClick={() => {
const folder = getFolderFromFileType(fallbackFileType || fileType);
downloadFile(folder, actualFileName);
}}
disabled={loading}
>
Download File
</Button>
</Space>
</div>
<div style={{ marginBottom: '16px' }}>
<div style={{
display: 'inline-block',
padding: '4px 12px',
backgroundColor: getFileTypeColor() + '15',
border: `1px solid ${getFileTypeColor()}30`,
borderRadius: '16px',
fontSize: '12px',
fontWeight: 'bold',
color: getFileTypeColor()
}}>
{(fallbackFileType || fileType || 'FILE')?.toUpperCase()}
</div>
</div>
<div style={{ position: 'relative' }}>
{loading && (
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.6)',
backdropFilter: 'blur(0.8px)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 5,
borderRadius: '8px'
}}>
<Spin size="large" />
</div>
)}
<div style={{ filter: loading ? 'blur(0.5px)' : 'none', transition: 'filter 0.3s ease' }}>
{renderContent()}
</div>
</div>
</Card>
</div>
);
};
export default ViewFilePage;

View File

@@ -0,0 +1,99 @@
import React from 'react';
import { Form, Input, Row, Col, Typography, Switch } from 'antd';
const { Text } = Typography;
const BrandForm = ({
form,
onValuesChange,
isEdit = false,
brandInfo = null,
readOnly = false,
}) => {
const isActive = Form.useWatch('is_active', form) ?? true;
React.useEffect(() => {
if (brandInfo && brandInfo.brand_code) {
form.setFieldsValue({
brand_code: brandInfo.brand_code
});
}
}, [brandInfo, form]);
return (
<div>
<Form
layout="vertical"
form={form}
onValuesChange={onValuesChange}
initialValues={{
brand_name: '',
brand_type: '',
brand_model: '',
brand_manufacture: '',
is_active: true,
}}
>
<Form.Item label="Status">
<div style={{ display: 'flex', alignItems: 'center' }}>
<Form.Item name="is_active" valuePropName="checked" noStyle>
<Switch
style={{ backgroundColor: isActive ? '#23A55A' : '#bfbfbf' }}
disabled={readOnly}
/>
</Form.Item>
<Text style={{ marginLeft: 8 }}>
{isActive ? 'Running' : 'Offline'}
</Text>
</div>
</Form.Item>
<Form.Item label="Brand Code" name="brand_code">
<Input
disabled={true}
style={{
backgroundColor: '#f5f5f5',
cursor: 'not-allowed'
}}
/>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Brand Name"
name="brand_name"
rules={[{ required: !readOnly, message: 'Brand Name wajib diisi!' }]}
>
<Input disabled={readOnly} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Manufacturer"
name="brand_manufacture"
rules={[{ required: !readOnly, message: 'Manufacturer wajib diisi!' }]}
>
<Input placeholder="Enter Manufacturer" disabled={readOnly} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="Brand Type" name="brand_type">
<Input placeholder="Enter Brand Type (Optional)" disabled={readOnly} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="Model" name="brand_model">
<Input placeholder="Enter Model (Optional)" disabled={readOnly} />
</Form.Item>
</Col>
</Row>
</Form>
</div>
);
};
export default BrandForm;

View File

@@ -0,0 +1,397 @@
import React, { useState } from 'react';
import { Card, Typography, Tag, Button, Modal, Row, Col, Space } from 'antd';
import { EyeOutlined, DeleteOutlined, CheckOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
const { Text, Title } = Typography;
const CustomSparepartCard = ({
sparepart,
isSelected = false,
isReadOnly = false,
showPreview = true,
showDelete = false,
onPreview,
onDelete,
onCardClick,
loading = false,
size = 'small',
style = {},
}) => {
const [previewModalVisible, setPreviewModalVisible] = useState(false);
const getImageSrc = () => {
if (sparepart.sparepart_foto) {
if (sparepart.sparepart_foto.startsWith('http')) {
return sparepart.sparepart_foto;
} else {
const fileName = sparepart.sparepart_foto.split('/').pop();
if (fileName === 'defaultSparepartImg.jpg') {
return `/assets/defaultSparepartImg.jpg`;
} else {
const token = localStorage.getItem('token');
const baseURL = import.meta.env.VITE_API_SERVER || '';
return `${baseURL}/file-uploads/images/${encodeURIComponent(fileName)}${token ? `?token=${encodeURIComponent(token)}` : ''}`;
}
}
}
return 'https://via.placeholder.com/150';
};
const handlePreview = () => {
if (onPreview) {
onPreview(sparepart);
} else {
setPreviewModalVisible(true);
}
};
const truncateText = (text, maxLength = 15) => {
if (!text) return 'Unnamed';
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
};
const handleCardClick = () => {
if (!isReadOnly && onCardClick) {
onCardClick(sparepart);
}
};
const getCardActions = () => {
const actions = [];
if (showPreview) {
actions.push(
<Button
key="preview"
type="text"
icon={<EyeOutlined />}
title="Lihat Detail"
style={{ color: '#1890ff' }}
onClick={(e) => {
e.stopPropagation();
handlePreview();
}}
/>
);
}
if (showDelete && !isReadOnly) {
actions.push(
<Button
key="delete"
type="text"
icon={<DeleteOutlined />}
title="Hapus"
style={{ color: '#ff4d4f' }}
onClick={(e) => {
e.stopPropagation();
onDelete?.(sparepart);
}}
/>
);
}
return actions;
};
const getCardStyle = () => {
const baseStyle = {
borderRadius: '12px',
overflow: 'hidden',
border: isSelected ? '2px solid #1890ff' : '1px solid #E0E0E0',
cursor: isReadOnly ? 'default' : 'pointer',
position: 'relative',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
transition: 'all 0.3s ease'
};
switch (size) {
case 'small':
return {
...baseStyle,
height: '180px',
minHeight: '180px'
};
case 'large':
return {
...baseStyle,
height: '280px',
minHeight: '280px'
};
default:
return {
...baseStyle,
height: '220px',
minHeight: '220px'
};
}
};
return (
<>
<div
style={{
border: '1px solid #f0f0f0',
borderRadius: '6px',
padding: '12px 16px',
marginBottom: '8px',
backgroundColor: 'white',
cursor: onCardClick && !isReadOnly ? 'pointer' : 'default',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
onClick={handleCardClick}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '4px' }}>
<Text
strong
style={{
fontSize: '14px',
color: '#262626',
marginRight: '12px'
}}
title={sparepart.sparepart_name || sparepart.name || 'Unnamed'}
>
{truncateText(sparepart.sparepart_name || sparepart.name || 'Unnamed')}
</Text>
<Tag
color={sparepart.sparepart_stok === 'Available' ? 'green' : 'red'}
style={{ fontSize: '11px', margin: 0 }}
>
{sparepart.sparepart_stok || 'Not Available'}
</Tag>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Text style={{ fontSize: '12px', color: '#666', marginRight: '4px' }}>
qty:
</Text>
<Text
style={{
fontSize: '12px',
fontWeight: 600,
color: '#262626'
}}
>
{sparepart.sparepart_qty || 0}
</Text>
</div>
</div>
</div>
<Space size="small">
{showPreview && (
<Button
type="text"
icon={<EyeOutlined />}
size="small"
onClick={(e) => {
e.stopPropagation();
handlePreview();
}}
title="Preview"
/>
)}
{showDelete && !isReadOnly && (
<Button
type="text"
icon={<DeleteOutlined />}
size="small"
danger
onClick={(e) => {
e.stopPropagation();
onDelete?.(sparepart);
}}
title="Remove"
/>
)}
</Space>
</div>
<Modal
title="Sparepart Details"
open={previewModalVisible}
onCancel={() => setPreviewModalVisible(false)}
footer={[
<Button key="close" onClick={() => setPreviewModalVisible(false)}>
Close
</Button>
]}
width={800}
centered
styles={{ body: { padding: '24px' } }}
>
<Row gutter={[24, 24]}>
<Col span={10}>
<div style={{ textAlign: 'center' }}>
<div
style={{
backgroundColor: '#f0f0f0',
width: '220px',
height: '220px',
margin: '0 auto 16px',
position: 'relative',
borderRadius: '12px',
overflow: 'hidden',
border: '1px solid #E0E0E0',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)'
}}
>
<img
src={getImageSrc()}
alt={sparepart.sparepart_name || 'Sparepart'}
style={{
width: '100%',
height: '100%',
objectFit: 'cover'
}}
onError={(e) => {
e.target.src = 'https://via.placeholder.com/220x220/d9d9d9/666666?text=No+Image';
}}
/>
</div>
{sparepart.sparepart_item_type && (
<div style={{ marginBottom: '12px' }}>
<Tag color="blue" style={{ fontSize: '14px', padding: '4px 12px' }}>
{sparepart.sparepart_item_type}
</Tag>
</div>
)}
<div style={{
textAlign: 'left',
background: '#f8f9fa',
padding: '12px',
borderRadius: '8px',
marginTop: '25px'
}}>
<div style={{ marginBottom: '8px' }}>
<Text strong style={{ fontSize: '14px', color: '#262626' }}>Stock Status:</Text>
<Tag
color={sparepart.sparepart_stok === 'Available' ? 'green' : 'red'}
style={{ marginLeft: '8px', fontSize: '12px' }}
>
{sparepart.sparepart_stok || 'Not Available'}
</Tag>
</div>
<div>
<Text strong style={{ fontSize: '14px', color: '#262626' }}>Quantity:</Text>
<Text style={{ fontSize: '14px', marginLeft: '8px', fontWeight: 600 }}>
{sparepart.sparepart_qty || 0} {sparepart.sparepart_unit || ''}
</Text>
</div>
</div>
</div>
</Col>
<Col span={14}>
<div>
<Title level={3} style={{ marginBottom: '20px', color: '#262626' }}>
{sparepart.sparepart_name || 'Unnamed'}
</Title>
<div style={{ marginBottom: '24px' }}>
<Row gutter={[16, 12]}>
<Col span={24}>
<div style={{
padding: '12px',
backgroundColor: '#fafafa',
borderRadius: '8px',
border: '1px solid #f0f0f0'
}}>
<Row gutter={16}>
<Col span={8}>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}>Code</Text>
<div style={{ fontSize: '15px', fontWeight: 500, marginTop: '2px' }}>
{sparepart.sparepart_code || 'N/A'}
</div>
</div>
</Col>
<Col span={8}>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}>Brand</Text>
<div style={{ fontSize: '15px', fontWeight: 500, marginTop: '2px' }}>
{sparepart.sparepart_merk || '-'}
</div>
</div>
</Col>
<Col span={8}>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}>Unit</Text>
<div style={{ fontSize: '15px', fontWeight: 500, marginTop: '2px' }}>
{sparepart.sparepart_unit || '-'}
</div>
</div>
</Col>
</Row>
</div>
</Col>
{sparepart.sparepart_model && (
<Col span={24}>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}>Model</Text>
<div style={{ fontSize: '15px', fontWeight: 500, marginTop: '2px' }}>
{sparepart.sparepart_model}
</div>
</div>
</Col>
)}
{sparepart.sparepart_description && (
<Col span={24}>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}>Description</Text>
<div style={{ fontSize: '15px', marginTop: '2px', lineHeight: '1.5' }}>
{sparepart.sparepart_description}
</div>
</div>
</Col>
)}
</Row>
</div>
{sparepart.created_at && (
<div>
<Row gutter={16}>
<Col span={12}>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}>Created</Text>
<div style={{ fontSize: '13px', marginTop: '2px' }}>
{dayjs(sparepart.created_at).format('DD MMM YYYY, HH:mm')}
</div>
</div>
</Col>
<Col span={12}>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}>Last Updated</Text>
<div style={{ fontSize: '13px', marginTop: '2px' }}>
{dayjs(sparepart.updated_at).format('DD MMM YYYY, HH:mm')}
</div>
</div>
</Col>
</Row>
</div>
)}
</div>
</Col>
</Row>
</Modal>
</>
);
};
export default CustomSparepartCard;

View File

@@ -0,0 +1,288 @@
import React, { useState, useEffect } from 'react';
import { Form, Input, Switch, Typography, ConfigProvider, Card, Button } from 'antd';
import { FileOutlined, EyeOutlined, DeleteOutlined } from '@ant-design/icons';
import FileUploadHandler from './FileUploadHandler';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { getFileUrl, getFolderFromFileType } from '../../../../api/file-uploads';
const { Text } = Typography;
const ErrorCodeForm = ({
errorCodeForm,
isErrorCodeFormReadOnly = false,
errorCodeIcon,
onErrorCodeIconUpload,
onErrorCodeIconRemove,
isEdit = false,
}) => {
const [currentIcon, setCurrentIcon] = useState(null);
const statusWatch = Form.useWatch('status', errorCodeForm) ?? true;
useEffect(() => {
if (errorCodeIcon && typeof errorCodeIcon === 'object' && Object.keys(errorCodeIcon).length > 0) {
setCurrentIcon(errorCodeIcon);
} else {
setCurrentIcon(null);
}
}, [errorCodeIcon]);
const handleIconRemove = () => {
setCurrentIcon(null);
onErrorCodeIconRemove();
};
const renderIconUpload = () => {
if (currentIcon) {
const displayFileName = currentIcon.name || currentIcon.uploadPath?.split('/').pop() || currentIcon.url?.split('/').pop() || 'Icon File';
return (
<Card
style={{
marginTop: 8,
borderRadius: 8,
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
border: '1px solid #e8e8e8'
}}
styles={{ body: { padding: '16px' } }}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 48,
height: 48,
borderRadius: 8,
backgroundColor: '#f0f5ff',
flexShrink: 0
}}>
<FileOutlined style={{ fontSize: 24, color: '#1890ff' }} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontSize: 13,
fontWeight: 600,
color: '#262626',
marginBottom: 4,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{displayFileName}
</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>
{currentIcon.size ? `${(currentIcon.size / 1024).toFixed(1)} KB` : 'Icon uploaded'}
</div>
</div>
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
<Button
type="primary"
size="middle"
icon={<EyeOutlined />}
style={{
fontSize: 12,
display: 'flex',
alignItems: 'center',
gap: 4
}}
onClick={() => {
try {
let iconUrl = '';
let actualFileName = '';
const filePath = currentIcon.uploadPath || currentIcon.url || currentIcon.path || '';
const iconDisplayName = currentIcon.name || '';
if (iconDisplayName) {
actualFileName = iconDisplayName;
} else if (filePath) {
actualFileName = filePath.split('/').pop();
}
if (actualFileName) {
const fileExtension = actualFileName.split('.').pop()?.toLowerCase();
const folder = getFolderFromFileType(fileExtension);
iconUrl = getFileUrl(folder, actualFileName);
}
if (!iconUrl && filePath) {
iconUrl = filePath.startsWith('http') ? filePath : `${import.meta.env.VITE_API_SERVER}/${filePath}`;
}
if (iconUrl && actualFileName) {
const fileExtension = actualFileName.split('.').pop()?.toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
const pdfExtensions = ['pdf'];
if (imageExtensions.includes(fileExtension) || pdfExtensions.includes(fileExtension)) {
const viewerUrl = `/image-viewer/${encodeURIComponent(actualFileName)}`;
window.open(viewerUrl, '_blank', 'noopener,noreferrer');
} else {
window.open(iconUrl, '_blank', 'noopener,noreferrer');
}
} else {
NotifAlert({
icon: 'error',
title: 'Error',
message: `File URL not found. FileName: ${actualFileName}, FilePath: ${filePath}`
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: `Failed to open file preview: ${error.message}`
});
}
}}
/>
<Button
danger
size="middle"
icon={<DeleteOutlined />}
style={{
fontSize: 12,
display: 'flex',
alignItems: 'center',
}}
onClick={handleIconRemove}
disabled={isErrorCodeFormReadOnly}
/>
</div>
</div>
</Card>
);
} else {
return (
<FileUploadHandler
type="error_code"
existingFile={null}
accept="image/*"
onFileUpload={(fileData) => {
setCurrentIcon(fileData);
onErrorCodeIconUpload(fileData);
}}
onFileRemove={handleIconRemove}
buttonText="Upload Icon"
buttonStyle={{
width: '100%',
borderColor: '#23A55A',
color: '#23A55A',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px'
}}
uploadText="Upload error code icon"
disabled={isErrorCodeFormReadOnly}
/>
);
}
};
return (
<ConfigProvider
theme={{
components: {
Switch: {
colorPrimary: '#23A55A',
colorPrimaryHover: '#23A55A',
},
},
}}
>
<Form
form={errorCodeForm}
layout="vertical"
initialValues={{
status: true,
error_code_color: '#000000'
}}
>
{/* Header bar with color picker, icon upload, and status toggle */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: '16px',
gap: '16px'
}}>
{/* Color picker on left */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Form.Item
name="error_code_color"
noStyle
getValueFromEvent={(e) => e.target.value}
getValueProps={(value) => ({ value: value || '#000000' })}
>
<input
type="color"
style={{
width: '120px',
height: '40px',
border: '1px solid #d9d9d9',
borderRadius: 4,
cursor: isErrorCodeFormReadOnly ? 'not-allowed' : 'pointer',
}}
disabled={isErrorCodeFormReadOnly}
/>
</Form.Item>
{/* Icon upload beside color picker */}
<div style={{ flex: 1, maxWidth: '300px' }}>
{renderIconUpload()}
</div>
</div>
{/* Status toggle on right */}
<div style={{ display: 'flex', alignItems: 'center' }}>
<Form.Item name="status" valuePropName="checked" noStyle>
<Switch
disabled={isErrorCodeFormReadOnly}
/>
</Form.Item>
<Text style={{ marginLeft: 8 }}>
{statusWatch ? 'Active' : 'Inactive'}
</Text>
</div>
</div>
{/* Error Code and Error Name in one row with 1/3 and 2/3 ratio */}
<div style={{ display: 'flex', gap: '12px', marginBottom: '16px' }}>
<Form.Item
label="Error Code"
name="error_code"
rules={[{ required: true, message: 'Error code wajib diisi!' }]}
style={{ flex: 1, marginBottom: 0, maxWidth: '33.33%' }}
>
<Input
placeholder="Enter error code"
disabled={isErrorCodeFormReadOnly}
/>
</Form.Item>
<Form.Item
label="Error Name"
name="error_code_name"
rules={[{ required: !isErrorCodeFormReadOnly, message: 'Error name wajib diisi!' }]}
style={{ flex: 2, marginBottom: 0, maxWidth: '66.67%' }}
>
<Input placeholder="Enter error name" disabled={isErrorCodeFormReadOnly} />
</Form.Item>
</div>
<Form.Item label="Description" name="error_code_description">
<Input.TextArea
placeholder="Enter error description"
rows={3}
disabled={isErrorCodeFormReadOnly}
/>
</Form.Item>
</Form>
</ConfigProvider>
);
};
export default ErrorCodeForm;

View File

@@ -0,0 +1,421 @@
import React, { useState } from 'react';
import { Upload, Modal, Button, Typography, Space, Image } from 'antd';
import { UploadOutlined, EyeOutlined, DeleteOutlined, FileOutlined } from '@ant-design/icons';
import { NotifOk, NotifAlert } from '../../../../components/Global/ToastNotif';
import { uploadFile, getFolderFromFileType, getFileUrl, getFileType } from '../../../../api/file-uploads';
const { Text } = Typography;
const FileUploadHandler = ({
type = 'solution',
maxCount = 1,
accept = '.pdf,.jpg,.jpeg,.png,.gif',
disabled = false,
fileList = [],
onFileUpload,
onFileRemove,
existingFile = null,
clearSignal = null,
debugProps = {},
uploadText = 'Click or drag file to this area to upload',
uploadHint = 'Support for PDF and image files only',
buttonText = 'Upload File',
buttonType = 'default',
containerStyle = {},
buttonStyle = {},
showPreview = true
}) => {
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState('');
const [previewTitle, setPreviewTitle] = useState('');
const [isUploading, setIsUploading] = useState(false);
const [uploadedFile, setUploadedFile] = useState(null);
React.useEffect(() => {
if (clearSignal !== null && clearSignal > 0) {
setUploadedFile(null);
}
}, [clearSignal, debugProps]);
const getBase64 = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
const handlePreview = async (file) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
setPreviewImage(file.url || file.preview);
setPreviewOpen(true);
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
};
const validateFile = (file) => {
const isAllowedType = [
'application/pdf',
'image/jpeg',
'image/png',
'image/gif',
].includes(file.type);
if (!isAllowedType) {
NotifAlert({
icon: 'error',
title: 'Error',
message: `${file.name} bukan file PDF atau gambar yang diizinkan.`,
});
return false;
}
return true;
};
const handleFileUpload = async (file) => {
if (isUploading) {
return false;
}
if (!validateFile(file)) {
return false;
}
try {
setIsUploading(true);
const fileExtension = file.name.split('.').pop().toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(fileExtension);
const fileType = isImage ? 'image' : 'pdf';
const folder = getFolderFromFileType(fileType);
const uploadResponse = await uploadFile(file, folder);
const isSuccess = uploadResponse && (
uploadResponse.statusCode === 200 ||
uploadResponse.statusCode === 201
);
if (!isSuccess) {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: uploadResponse?.message || `Gagal mengupload ${file.name}`,
});
setIsUploading(false);
return false;
}
let actualPath = '';
if (uploadResponse && typeof uploadResponse === 'object') {
if (uploadResponse.data && uploadResponse.data.path_document) {
actualPath = uploadResponse.data.path_document;
}
else if (uploadResponse.path_document) {
actualPath = uploadResponse.path_document;
}
else if (uploadResponse.data && uploadResponse.data.path_solution) {
actualPath = uploadResponse.data.path_solution;
}
else if (uploadResponse.data && typeof uploadResponse.data === 'object') {
if (uploadResponse.data.file_url) {
actualPath = uploadResponse.data.file_url;
} else if (uploadResponse.data.url) {
actualPath = uploadResponse.data.url;
} else if (uploadResponse.data.path) {
actualPath = uploadResponse.data.path;
} else if (uploadResponse.data.location) {
actualPath = uploadResponse.data.location;
} else if (uploadResponse.data.filePath) {
actualPath = uploadResponse.data.filePath;
} else if (uploadResponse.data.file_path) {
actualPath = uploadResponse.data.file_path;
} else if (uploadResponse.data.publicUrl) {
actualPath = uploadResponse.data.publicUrl;
} else if (uploadResponse.data.public_url) {
actualPath = uploadResponse.data.public_url;
}
}
else if (uploadResponse && typeof uploadResponse === 'string') {
actualPath = uploadResponse;
}
}
if (actualPath) {
let fileObject;
if (type === 'error_code') {
fileObject = {
name: file.name,
path_icon: actualPath,
uploadPath: actualPath,
url: actualPath,
size: file.size,
type: file.type,
fileExtension
};
} else {
fileObject = {
name: file.name,
path_solution: actualPath,
uploadPath: actualPath,
type_solution: fileType,
size: file.size,
type: file.type
};
}
onFileUpload(fileObject);
setUploadedFile(fileObject);
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `${file.name} berhasil diupload!`
});
setIsUploading(false);
return false;
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: `Gagal mengupload ${file.name}. Tidak dapat menemukan path file dalam response.`,
});
setIsUploading(false);
return false;
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: `Gagal mengupload ${file.name}. Silakan coba lagi.`,
});
setIsUploading(false);
return false;
}
};
const handleFileChange = ({ fileList }) => {
if (fileList && fileList.length > 0 && fileList[0] && fileList[0].originFileObj) {
handleFileUpload(fileList[0].originFileObj);
}
};
const handleRemove = () => {
if (existingFile && onFileRemove) {
onFileRemove(existingFile);
} else if (onFileRemove) {
onFileRemove(null);
}
};
const renderExistingFile = () => {
const fileToShow = existingFile || uploadedFile;
if (!fileToShow) {
return null;
}
const filePath = fileToShow.uploadPath || fileToShow.url || fileToShow.path_icon || fileToShow.path_solution;
const fileName = fileToShow.name || filePath?.split('/').pop() || 'Unknown file';
const fileType = getFileType(fileName);
const isImage = fileType === 'image';
const handlePreview = () => {
if (!showPreview || !filePath) return;
if (isImage) {
const folder = fileToShow.type_solution === 'pdf' ? 'pdf' : 'images';
const filename = filePath.split('/').pop();
const imageUrl = getFileUrl(folder, filename);
if (imageUrl) {
setPreviewImage(imageUrl);
setPreviewOpen(true);
setPreviewTitle(fileName);
} else {
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Cannot generate image preview URL',
});
}
} else {
const folder = fileToShow.type_solution === 'pdf' ? 'pdf' : 'images';
const filename = filePath.split('/').pop();
const fileUrl = getFileUrl(folder, filename);
if (fileUrl) {
window.open(fileUrl, '_blank', 'noopener,noreferrer');
} else {
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Cannot generate file preview URL',
});
}
}
};
const getThumbnailUrl = () => {
if (!isImage || !filePath) return null;
const folder = fileToShow.type_solution === 'pdf' ? 'pdf' : 'images';
const filename = filePath.split('/').pop();
return getFileUrl(folder, filename);
};
const thumbnailUrl = getThumbnailUrl();
return (
<div style={{ marginTop: 12 }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '8px',
border: '1px solid #d9d9d9',
borderRadius: 4,
backgroundColor: '#fafafa'
}}>
{isImage ? (
<img
src={thumbnailUrl || filePath}
alt={fileName}
style={{
width: 50,
height: 50,
objectFit: 'cover',
border: '1px solid #d9d9d9',
borderRadius: 4,
cursor: showPreview ? 'pointer' : 'default'
}}
onClick={handlePreview}
onError={(e) => {
e.target.src = filePath;
}}
/>
) : (
<div
style={{
width: 50,
height: 50,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid #d9d9d9',
borderRadius: 4,
backgroundColor: '#f5f5f5',
cursor: showPreview ? 'pointer' : 'default'
}}
onClick={handlePreview}
>
<FileOutlined style={{ fontSize: 24, color: '#666' }} />
</div>
)}
<div style={{ flex: 1 }}>
<Text style={{ fontSize: 12, fontWeight: 500 }}>
{fileName}
</Text>
<br />
<Text type="secondary" style={{ fontSize: 10 }}>
{fileType === 'image' ? 'Image' : fileType === 'pdf' ? 'PDF' : 'File'}
{fileToShow.size && `${(fileToShow.size / 1024).toFixed(1)} KB`}
</Text>
</div>
<div style={{ display: 'flex', gap: 4 }}>
{showPreview && (
<Button
type="text"
icon={<EyeOutlined />}
size="small"
onClick={handlePreview}
title={isImage ? "Preview Image" : "Open File"}
/>
)}
<Button
type="text"
danger
icon={<DeleteOutlined />}
size="small"
onClick={handleRemove}
title="Remove File"
/>
</div>
</div>
</div>
);
};
const uploadProps = {
name: 'file',
multiple: false,
accept,
disabled: disabled || isUploading,
fileList: [],
beforeUpload: () => false,
onChange: handleFileChange,
onPreview: handlePreview,
maxCount,
};
return (
<div style={{ ...containerStyle }}>
{!existingFile && (
<Upload {...uploadProps}>
{type === 'drag' ? (
<Upload.Dragger>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">{uploadText}</p>
<p className="ant-upload-hint">{uploadHint}</p>
</Upload.Dragger>
) : (
<Button
type={buttonType}
icon={<UploadOutlined />}
loading={isUploading}
style={{ ...buttonStyle }}
>
{isUploading ? 'Uploading...' : buttonText}
</Button>
)}
</Upload>
)}
{showPreview && (
<Modal
open={previewOpen}
title={previewTitle}
footer={null}
onCancel={() => setPreviewOpen(false)}
width={600}
style={{ top: 100 }}
>
{previewImage && (
<img
alt={previewTitle}
style={{ width: '100%' }}
src={previewImage}
/>
)}
</Modal>
)}
</div>
);
};
export default FileUploadHandler;

View File

@@ -1,5 +1,5 @@
import React, { memo, useState, useEffect } from 'react'; import React, { memo, useState, useEffect } from 'react';
import { Button, Col, Row, Space, Input, ConfigProvider, Card, Tag } from 'antd'; import { Button, Col, Row, Space, Input, ConfigProvider, Card, Tag, Spin } from 'antd';
import { import {
PlusOutlined, PlusOutlined,
EditOutlined, EditOutlined,
@@ -7,46 +7,10 @@ import {
SearchOutlined, SearchOutlined,
EyeOutlined, EyeOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { NotifAlert, NotifConfirmDialog } from '../../../../components/Global/ToastNotif'; import { NotifAlert, NotifConfirmDialog, NotifOk } from '../../../../components/Global/ToastNotif';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import TableList from '../../../../components/Global/TableList'; import TableList from '../../../../components/Global/TableList';
import { getAllBrands } from '../../../../api/master-brand'; import { getAllBrands, deleteBrand } from '../../../../api/master-brand';
// Dummy data
const initialBrandDeviceData = [
{
brand_id: 1,
brandName: 'Siemens S7-1200',
brandType: 'PLC',
manufacturer: 'Siemens',
model: 'S7-1200',
status: 'Active',
},
{
brand_id: 2,
brandName: 'Allen Bradley CompactLogix',
brandType: 'PLC',
manufacturer: 'Rockwell Automation',
model: 'CompactLogix 5370',
status: 'Active',
},
{
brand_id: 3,
brandName: 'Schneider Modicon M580',
brandType: 'PLC',
manufacturer: 'Schneider Electric',
model: 'M580',
status: 'Active',
},
{
brand_id: 4,
brandName: 'Mitsubishi FX5U',
brandType: 'PLC',
manufacturer: 'Mitsubishi',
model: 'FX5U',
status: 'Inactive',
},
];
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{ {
@@ -58,50 +22,38 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
}, },
{ {
title: 'Brand Device ', title: 'Brand Device ',
dataIndex: 'brandName', dataIndex: 'brand_name',
key: 'brandName', key: 'brand_name',
width: '20%', width: '20%',
}, },
{
title: 'Type',
dataIndex: 'brandType',
key: 'brandType',
width: '15%',
},
{ {
title: 'Manufacturer', title: 'Manufacturer',
dataIndex: 'manufacturer', dataIndex: 'brand_manufacture',
key: 'manufacturer', key: 'brand_manufacture',
width: '20%', width: '20%',
}, },
{
title: 'Model',
dataIndex: 'model',
key: 'model',
width: '15%',
},
{ {
title: 'Status', title: 'Status',
dataIndex: 'status', dataIndex: 'is_active',
key: 'status', key: 'is_active',
width: '10%', width: '10%',
align: 'center', align: 'center',
render: (_, { status }) => ( render: (_, { is_active }) => (
<> <>
{status === 'Active' ? ( {is_active === true ? (
<Tag color={'green'} key={'status'}> <Tag color={'green'} key={'status'}>
Active Running
</Tag> </Tag>
) : ( ) : (
<Tag color={'red'} key={'status'}> <Tag color={'red'} key={'status'}>
Inactive Offline
</Tag> </Tag>
)} )}
</> </>
), ),
}, },
{ {
title: 'Aksi', title: 'Action',
key: 'action', key: 'action',
align: 'center', align: 'center',
width: '15%', width: '15%',
@@ -138,77 +90,87 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
const ListBrandDevice = memo(function ListBrandDevice(props) { const ListBrandDevice = memo(function ListBrandDevice(props) {
const [trigerFilter, setTrigerFilter] = useState(false); const [trigerFilter, setTrigerFilter] = useState(false);
const [brandDeviceData, setBrandDeviceData] = useState(initialBrandDeviceData);
const defaultFilter = { search: '' }; const defaultFilter = { criteria: '' };
const [formDataFilter, setFormDataFilter] = useState(defaultFilter); const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
const [searchValue, setSearchValue] = useState(''); const [searchText, setSearchText] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (token) { if (token) {
if (props.actionMode == 'list') { if (props.actionMode === 'list') {
setFormDataFilter(defaultFilter); setFormDataFilter(defaultFilter);
doFilter(); doFilter();
} }
} else { } else {
navigate('/signin'); navigate('/signin');
} }
}, [props.actionMode, brandDeviceData]); }, [props.actionMode, navigate]);
const doFilter = () => { const doFilter = () => {
setTrigerFilter((prev) => !prev); setTrigerFilter((prev) => !prev);
}; };
const handleSearch = () => { const handleSearch = () => {
setFormDataFilter({ search: searchValue }); setFormDataFilter({ criteria: searchText });
setTrigerFilter((prev) => !prev); setTrigerFilter((prev) => !prev);
}; };
const handleSearchClear = () => { const handleSearchClear = () => {
setSearchValue(''); setSearchText('');
setFormDataFilter({ search: '' }); setFormDataFilter({ criteria: '' });
setTrigerFilter((prev) => !prev); setTrigerFilter((prev) => !prev);
}; };
const showPreviewModal = (param) => { const showPreviewModal = (param) => {
props.setSelectedData(param); navigate(`/master/brand-device/view/${param.brand_id}`);
props.setActionMode('preview');
}; };
const showEditModal = (param = null) => { const showEditModal = (param = null) => {
props.setSelectedData(param); if (param) {
props.setActionMode('edit'); navigate(`/master/brand-device/edit/${param.brand_id}`);
} else {
navigate('/master/brand-device/add');
}
}; };
const showDeleteDialog = (param) => { const showDeleteDialog = (param) => {
NotifConfirmDialog({ NotifConfirmDialog({
icon: 'question', icon: 'question',
title: 'Konfirmasi', title: 'Konfirmasi',
message: 'Apakah anda yakin hapus data "' + param.brandName + '" ?', message: 'Apakah anda yakin hapus data "' + param.brand_name + '" ?',
onConfirm: () => handleDelete(param.brand_id), onConfirm: () => handleDelete(param.brand_id, param.brand_name),
onCancel: () => props.setSelectedData(null), onCancel: () => { },
}); });
}; };
const handleDelete = async (brand_id) => { const handleDelete = async (brand_id, brand_name) => {
// Find brand name before deleting try {
const brandToDelete = brandDeviceData.find((brand) => brand.brand_id === brand_id); const response = await deleteBrand(brand_id);
// Simulate delete API call if (response && response.statusCode === 200) {
await new Promise((resolve) => setTimeout(resolve, 300)); NotifOk({
icon: 'success',
// Remove from state title: 'Berhasil',
const updatedBrands = brandDeviceData.filter((brand) => brand.brand_id !== brand_id); message: `Brand ${brand_name} deleted successfully.`,
setBrandDeviceData(updatedBrands); });
doFilter();
NotifAlert({ } else {
icon: 'success', NotifAlert({
title: 'Berhasil', icon: 'error',
message: `Data Brand Device "${brandToDelete?.brandName || ''}" berhasil dihapus.`, title: 'Gagal',
}); message: response?.message || 'Gagal menghapus Data Brand Device',
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'Gagal menghapus Data Brand Device',
});
}
}; };
return ( return (
@@ -220,13 +182,12 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
<Col xs={24} sm={24} md={12} lg={12}> <Col xs={24} sm={24} md={12} lg={12}>
<Input.Search <Input.Search
placeholder="Search brand device..." placeholder="Search brand device..."
value={searchValue} value={searchText}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
setSearchValue(value); setSearchText(value);
// Auto search when clearing by backspace/delete
if (value === '') { if (value === '') {
setFormDataFilter({ search: '' }); setFormDataFilter({ criteria: '' });
setTrigerFilter((prev) => !prev); setTrigerFilter((prev) => !prev);
} }
}} }}
@@ -267,10 +228,12 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
> >
<Button <Button
icon={<PlusOutlined />} icon={<PlusOutlined />}
onClick={() => navigate('/master/brand-device/add')} onClick={() => {
navigate('/master/brand-device/add');
}}
size="large" size="large"
> >
Tambah Brand Device Add data
</Button> </Button>
</ConfigProvider> </ConfigProvider>
</Space> </Space>
@@ -281,7 +244,7 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
<TableList <TableList
mobile mobile
cardColor={'#42AAFF'} cardColor={'#42AAFF'}
header={'tag_name'} header={'brand_name'}
showPreviewModal={showPreviewModal} showPreviewModal={showPreviewModal}
showEditModal={showEditModal} showEditModal={showEditModal}
showDeleteDialog={showDeleteDialog} showDeleteDialog={showDeleteDialog}
@@ -297,4 +260,4 @@ const ListBrandDevice = memo(function ListBrandDevice(props) {
); );
}); });
export default ListBrandDevice; export default ListBrandDevice;

View File

@@ -0,0 +1,315 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Card, Input, Button, Row, Col, Empty } from 'antd';
import { PlusOutlined, SearchOutlined, DeleteOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
import { getErrorCodesByBrandId, deleteErrorCode } from '../../../../api/master-brand';
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
const ListErrorCode = ({
brandId,
selectedErrorCode,
onErrorCodeSelect,
onAddNew,
tempErrorCodes = [],
trigerFilter,
searchText,
onSearchChange,
onSearch,
onSearchClear,
isReadOnly = false,
errorCodes: propErrorCodes = null
}) => {
const [errorCodes, setErrorCodes] = useState([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({
current_page: 1,
current_limit: 15,
total_limit: 0,
total_page: 0,
});
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 15;
const queryParams = useMemo(() => {
const params = new URLSearchParams();
params.set('page', currentPage.toString());
params.set('limit', pageSize.toString());
if (searchText) {
params.set('criteria', searchText);
}
return params;
}, [searchText, currentPage, pageSize]);
const fetchErrorCodes = async () => {
if (!brandId) {
setErrorCodes([]);
return;
}
setLoading(true);
try {
const response = await getErrorCodesByBrandId(brandId, queryParams);
if (response && response.statusCode === 200) {
const apiErrorData = response.data || [];
const allErrorCodes = [
...apiErrorData.map(ec => ({
...ec,
tempId: `existing_${ec.error_code_id}`,
status: 'existing'
})),
...tempErrorCodes.filter(ec => ec.status !== 'deleted')
];
setErrorCodes(allErrorCodes);
if (response.paging) {
setPagination({
current_page: response.paging.current_page || 1,
current_limit: response.paging.current_limit || 15,
total_limit: response.paging.total_limit || 0,
total_page: response.paging.total_page || 0,
});
}
} else {
setErrorCodes([]);
}
} catch (error) {
setErrorCodes([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isReadOnly && propErrorCodes) {
setErrorCodes(propErrorCodes);
setLoading(false);
} else {
fetchErrorCodes();
}
}, [brandId, queryParams, tempErrorCodes, trigerFilter, isReadOnly, propErrorCodes]);
const handlePrevious = () => {
if (pagination.current_page > 1) {
setCurrentPage(pagination.current_page - 1);
}
};
const handleNext = () => {
if (pagination.current_page < pagination.total_page) {
setCurrentPage(pagination.current_page + 1);
}
};
const handleSearch = () => {
setCurrentPage(1);
if (onSearch) {
onSearch();
}
};
const handleSearchClear = () => {
setCurrentPage(1);
if (onSearchClear) {
onSearchClear();
}
};
const handleDelete = async (item, e) => {
e.stopPropagation();
if (item.status === 'existing' && item.error_code_id) {
NotifConfirmDialog({
icon: 'warning',
title: 'Hapus Error Code',
message: `Apakah Anda yakin ingin menghapus error code ${item.error_code}?`,
onConfirm: () => performDelete(item),
onCancel: () => { },
confirmButtonText: 'Hapus'
});
}
};
const performDelete = async (item) => {
try {
if (!item.error_code_id || item.error_code_id === 'undefined') {
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Error code ID tidak valid'
});
return;
}
if (!item.brand_id || item.brand_id === 'undefined') {
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Brand ID tidak valid'
});
return;
}
const response = await deleteErrorCode(item.brand_id, item.error_code_id);
if (response && response.statusCode === 200) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: 'Error code berhasil dihapus'
});
fetchErrorCodes();
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: 'Gagal menghapus error code'
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Terjadi kesalahan saat menghapus error code'
});
}
};
return (
<Card
title="Daftar Error Code"
style={{ width: '100%', minWidth: '472px' }}
styles={{ body: { padding: '12px' } }}
>
<Input.Search
placeholder="Cari error code..."
value={searchText}
onChange={(e) => {
const value = e.target.value;
if (onSearchChange) {
onSearchChange(value);
}
}}
onSearch={handleSearch}
allowClear
enterButton={
<Button
type="primary"
icon={<SearchOutlined />}
onClick={handleSearch}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
height: '32px'
}}
>
Search
</Button>
}
size="default"
style={{
marginBottom: 12,
height: '32px',
width: '100%',
}}
/>
<div style={{
height: '90vh',
border: '1px solid #d9d9d9',
borderRadius: '6px',
overflow: 'auto',
marginBottom: 12,
backgroundColor: '#fafafa'
}}>
{errorCodes.length === 0 ? (
<Empty
description="Belum ada error code"
style={{ marginTop: 50 }}
/>
) : (
<div style={{ padding: '8px' }}>
{errorCodes.map((item) => (
<div
key={item.tempId || item.error_code_id}
style={{
cursor: 'pointer',
padding: '8px 12px',
borderRadius: '6px',
marginBottom: '4px',
border: selectedErrorCode?.tempId === item.tempId ? '2px solid #23A55A' : '1px solid #d9d9d9',
backgroundColor: selectedErrorCode?.tempId === item.tempId ? '#f6ffed' : '#fff',
transition: 'all 0.2s ease'
}}
onClick={() => onErrorCodeSelect(item)}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 'bold', fontSize: '12px' }}>
{item.error_code}
</div>
<div style={{ fontSize: '11px', color: '#666' }}>
{item.error_code_name}
</div>
</div>
{item.status === 'existing' && (
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={(e) => handleDelete(item, e)}
style={{
padding: '2px 6px',
height: '24px',
fontSize: '11px',
border: '1px solid #ff4d4f'
}}
/>
)}
</div>
</div>
))}
</div>
)}
</div>
{pagination.total_limit > 0 && (
<Row justify="space-between" align="middle" gutter={16}>
<Col flex="auto">
<span style={{ fontSize: '12px', color: '#666' }}>
Menampilkan {pagination.current_limit} data halaman{' '}
{pagination.current_page} dari total {pagination.total_limit} data
</span>
</Col>
<Col flex="none">
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<Button
icon={<LeftOutlined />}
onClick={handlePrevious}
disabled={pagination.current_page <= 1}
size="small"
>
</Button>
<span style={{ fontSize: '12px', color: '#666', minWidth: '60px', textAlign: 'center' }}>
{pagination.current_page} / {pagination.total_page}
</span>
<Button
icon={<RightOutlined />}
onClick={handleNext}
disabled={pagination.current_page >= pagination.total_page}
size="small"
>
</Button>
</div>
</Col>
</Row>
)}
</Card>
);
};
export default ListErrorCode;

View File

@@ -1,33 +0,0 @@
import React, { memo } from 'react';
import { Row, Col } from 'antd';
const ListErrorMaster = memo(function ListErrorMaster(props) {
return (
<React.Fragment>
<Row>
<Col xs={24}>
<div
style={{
textAlign: 'center',
padding: '100px 20px',
backgroundColor: '#f5f5f5',
borderRadius: '8px',
}}
>
<h2
style={{
fontSize: '24px',
color: '#595959',
marginBottom: '16px',
}}
>
Cooming soon
</h2>
</div>
</Col>
</Row>
</React.Fragment>
);
});
export default ListErrorMaster;

View File

@@ -0,0 +1,496 @@
import React, { useState } from 'react';
import { Form, Input, Button, Switch, Radio, Typography, Space, Card, ConfigProvider } from 'antd';
import { DeleteOutlined, EyeOutlined, FileOutlined } from '@ant-design/icons';
import FileUploadHandler from './FileUploadHandler';
import { NotifAlert } from '../../../../components/Global/ToastNotif';
import { getFileUrl, getFolderFromFileType } from '../../../../api/file-uploads';
const { Text } = Typography;
const { TextArea } = Input;
const SolutionFieldNew = ({
fieldKey,
fieldName,
index,
solutionType,
solutionStatus,
isReadOnly = false,
canRemove = true,
onTypeChange,
onStatusChange,
onRemove,
onFileUpload,
onFileView,
fileList = [],
originalSolutionData = null
}) => {
const form = Form.useFormInstance();
const [currentFile, setCurrentFile] = useState(null);
const [isDeleted, setIsDeleted] = useState(false);
const fileUpload = Form.useWatch(['solution_items', fieldKey, 'fileUpload'], form);
const file = Form.useWatch(['solution_items', fieldKey, 'file'], form);
const nameValue = Form.useWatch(['solution_items', fieldKey, 'name'], form);
const fileNameValue = Form.useWatch(['solution_items', fieldKey, 'fileName'], form);
const statusValue = Form.useWatch(['solution_items', fieldKey, 'status'], form) ?? true;
const pathSolution = Form.useWatch(['solution_items', fieldKey, 'path_solution'], form);
const [deleteCounter, setDeleteCounter] = useState(0);
React.useEffect(() => {
if (!nameValue || nameValue === '') {
setCurrentFile(null);
setIsDeleted(false);
setDeleteCounter(prev => prev + 1);
}
}, [nameValue]);
React.useEffect(() => {
const getFileFromFormValues = () => {
const hasValidFileUpload = fileUpload && typeof fileUpload === 'object' && Object.keys(fileUpload).length > 0;
const hasValidFile = file && typeof file === 'object' && Object.keys(file).length > 0;
const hasValidPath = pathSolution && pathSolution.trim() !== '';
const wasExplicitlyDeleted =
(fileUpload === null || file === null || pathSolution === null) &&
!hasValidFileUpload &&
!hasValidFile &&
!hasValidPath;
if (wasExplicitlyDeleted) {
return null;
}
if (solutionType === 'text') {
return null;
}
if (hasValidFileUpload) {
return fileUpload;
}
if (hasValidFile) {
return file;
}
if (hasValidPath) {
return {
name: fileNameValue || pathSolution.split('/').pop() || 'File',
uploadPath: pathSolution,
url: pathSolution,
path: pathSolution
};
}
return null;
};
const fileFromForm = getFileFromFormValues();
if (JSON.stringify(currentFile) !== JSON.stringify(fileFromForm)) {
setCurrentFile(fileFromForm);
}
}, [fileUpload, file, pathSolution, solutionType, deleteCounter, fileNameValue, fieldKey]);
const renderSolutionContent = () => {
if (solutionType === 'text') {
return (
<Form.Item
name={['solution_items', fieldKey, 'text']}
rules={[{ required: true, message: 'Text solution wajib diisi!' }]}
>
<TextArea
placeholder="Enter solution text"
rows={3}
disabled={isReadOnly}
style={{ fontSize: 12 }}
/>
</Form.Item>
);
}
if (solutionType === 'file') {
const hasOriginalFile = originalSolutionData && (
originalSolutionData.path_solution ||
originalSolutionData.path_document
);
let displayFile = null;
if (currentFile && Object.keys(currentFile).length > 0) {
displayFile = currentFile;
}
else if (hasOriginalFile && !isDeleted) {
displayFile = {
name: originalSolutionData.file_upload_name ||
(originalSolutionData.path_solution || originalSolutionData.path_document)?.split('/').pop() ||
'File',
uploadPath: originalSolutionData.path_solution || originalSolutionData.path_document,
url: originalSolutionData.path_solution || originalSolutionData.path_document,
path: originalSolutionData.path_solution || originalSolutionData.path_document,
isExisting: true
};
}
else if (fileUpload && typeof fileUpload === 'object' && Object.keys(fileUpload).length > 0) {
displayFile = fileUpload;
}
else if (file && typeof file === 'object' && Object.keys(file).length > 0) {
displayFile = file;
}
else if (pathSolution && pathSolution.trim() !== '') {
displayFile = {
name: pathSolution.split('/').pop() || 'File',
uploadPath: pathSolution,
url: pathSolution,
path: pathSolution
};
}
if (displayFile) {
const getFileNameFromPath = () => {
const filePath = displayFile.uploadPath || displayFile.url || displayFile.path || '';
if (filePath) {
const fileName = filePath.split('/').pop();
return fileName || 'Uploaded File';
}
return displayFile.name || 'Uploaded File';
};
const displayFileName = getFileNameFromPath();
return (
<Card
style={{
marginBottom: 8,
borderRadius: 8,
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
border: '1px solid #e8e8e8'
}}
styles={{ body: { padding: '16px' } }}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 48,
height: 48,
borderRadius: 8,
backgroundColor: '#f0f5ff',
flexShrink: 0
}}>
<FileOutlined style={{ fontSize: 24, color: '#1890ff' }} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontSize: 13,
fontWeight: 600,
color: '#262626',
marginBottom: 4,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{displayFileName}
</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>
{displayFile.size ? `${(displayFile.size / 1024).toFixed(1)} KB` : 'File uploaded'}
</div>
</div>
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
<Button
type="primary"
size="middle"
icon={<EyeOutlined />}
style={{
fontSize: 12,
display: 'flex',
alignItems: 'center',
gap: 4
}}
onClick={() => {
try {
let fileUrl = '';
let actualFileName = '';
const filePath = displayFile.uploadPath || displayFile.url || displayFile.path || '';
if (filePath) {
actualFileName = filePath.split('/').pop();
if (actualFileName) {
const fileExtension = actualFileName.split('.').pop()?.toLowerCase();
const folder = getFolderFromFileType(fileExtension);
fileUrl = getFileUrl(folder, actualFileName);
}
}
if (!fileUrl && filePath) {
fileUrl = filePath.startsWith('http') ? filePath : `${import.meta.env.VITE_API_SERVER}/${filePath}`;
}
if (fileUrl && actualFileName) {
const fileExtension = actualFileName.split('.').pop()?.toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
if (imageExtensions.includes(fileExtension)) {
const viewerUrl = `/image-viewer/${encodeURIComponent(actualFileName)}`;
window.open(viewerUrl, '_blank', 'noopener,noreferrer');
} else {
window.open(fileUrl, '_blank', 'noopener,noreferrer');
}
} else {
NotifAlert({
icon: 'error',
title: 'Error',
message: 'File URL not found'
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: 'Failed to open file preview'
});
}
}}
/>
<Button
danger
size="middle"
icon={<DeleteOutlined />}
style={{
fontSize: 12,
display: 'flex',
alignItems: 'center',
}}
onClick={() => {
setIsDeleted(true);
form.setFieldValue(['solution_items', fieldKey, 'fileUpload'], null);
form.setFieldValue(['solution_items', fieldKey, 'file'], null);
form.setFieldValue(['solution_items', fieldKey, 'path_solution'], null);
form.setFieldValue(['solution_items', fieldKey, 'fileName'], null);
setCurrentFile(null);
if (onFileUpload && typeof onFileUpload === 'function') {
onFileUpload(null);
}
setDeleteCounter(prev => prev + 1);
setTimeout(() => {
form.validateFields(['solution_items', fieldKey]);
}, 50);
}}
/>
</div>
</div>
</Card>
);
} else {
return (
<FileUploadHandler
type="solution"
existingFile={null}
clearSignal={deleteCounter}
debugProps={{
currentFile: !!currentFile,
deleteCounter,
shouldClear: !currentFile && deleteCounter > 0
}}
onFileUpload={(fileObject) => {
setIsDeleted(false);
const filePath = fileObject.path_solution || fileObject.uploadPath || fileObject.path || fileObject.url;
const fileWithKey = {
...fileObject,
solutionId: fieldKey,
path_solution: filePath,
uploadPath: filePath
};
if (onFileUpload && typeof onFileUpload === 'function') {
onFileUpload(fileWithKey);
}
form.setFieldValue(['solution_items', fieldKey, 'fileUpload'], fileWithKey);
form.setFieldValue(['solution_items', fieldKey, 'file'], fileWithKey);
form.setFieldValue(['solution_items', fieldKey, 'type'], 'file');
form.setFieldValue(['solution_items', fieldKey, 'path_solution'], filePath);
form.setFieldValue(['solution_items', fieldKey, 'fileName'], fileObject.name);
setTimeout(() => {
const values = form.getFieldValue(['solution_items', fieldKey]);
const pathSolutionValue = form.getFieldValue(['solution_items', fieldKey, 'path_solution']);
}, 100);
setCurrentFile(fileWithKey);
}}
onFileRemove={() => {
form.setFieldValue(['solution_items', fieldKey, 'fileUpload'], null);
form.setFieldValue(['solution_items', fieldKey, 'file'], null);
form.setFieldValue(['solution_items', fieldKey, 'path_solution'], null);
setCurrentFile(null);
if (onFileUpload && typeof onFileUpload === 'function') {
onFileUpload(null);
}
setDeleteCounter(prev => prev + 1);
}}
disabled={isReadOnly}
buttonText="Upload File"
buttonStyle={{ width: '100%', fontSize: 12 }}
uploadText="Upload solution file (includes images, PDF, documents)"
acceptFileTypes="*"
/>
);
}
}
return null;
};
return (
<ConfigProvider
theme={{
components: {
Switch: {
colorPrimary: '#23A55A',
colorPrimaryHover: '#23A55A',
},
},
}}
>
<div style={{
border: '1px solid #d9d9d9',
borderRadius: 6,
padding: 12,
marginBottom: 12,
backgroundColor: isReadOnly ? '#f5f5f5' : 'white'
}}>
<div style={{
marginBottom: 8,
gap: 8
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<Text strong style={{
fontSize: 12,
color: '#262626',
display: 'block'
}}>
Solution #{index + 1}
</Text>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Form.Item name={['solution_items', fieldKey, 'status']} valuePropName="checked" noStyle>
<Switch
size="small"
disabled={isReadOnly}
onChange={(checked) => {
onStatusChange(fieldKey, checked);
}}
/>
</Form.Item>
<Text style={{
fontSize: 11,
color: '#666',
whiteSpace: 'nowrap'
}}>
{statusValue ? 'Active' : 'Inactive'}
</Text>
</div>
{canRemove && !isReadOnly && (
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={onRemove}
style={{
fontSize: 12,
padding: '2px 4px',
height: '24px'
}}
/>
)}
</div>
</div>
<Form.Item
name={['solution_items', fieldKey, 'name']}
rules={[{ required: true, message: 'Solution name wajib diisi!' }]}
style={{ margin: 0 }}
>
<Input
placeholder="Solution name"
disabled={isReadOnly}
size="default"
style={{ fontSize: 13 }}
/>
</Form.Item>
</div>
<Form.Item
name={['solution_items', fieldKey, 'type']}
rules={[{ required: true, message: 'Solution type wajib diisi!' }]}
style={{ marginBottom: 8 }}
initialValue={solutionType || 'text'}
>
<Radio.Group
onChange={(e) => {
const newType = e.target.value;
if (newType === 'text') {
form.setFieldValue(['solution_items', fieldKey, 'fileUpload'], null);
form.setFieldValue(['solution_items', fieldKey, 'file'], null);
form.setFieldValue(['solution_items', fieldKey, 'path_solution'], null);
form.setFieldValue(['solution_items', fieldKey, 'fileName'], null);
setCurrentFile(null);
setIsDeleted(true);
if (onFileUpload && typeof onFileUpload === 'function') {
onFileUpload(null);
}
} else if (newType === 'file') {
form.setFieldValue(['solution_items', fieldKey, 'text'], null);
setIsDeleted(false);
}
onTypeChange(fieldKey, newType);
}}
disabled={isReadOnly}
size="small"
>
<Radio value="text" style={{ fontSize: 12 }}>Text</Radio>
<Radio value="file" style={{ fontSize: 12 }}>File</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name={['solution_items', fieldKey, 'status']}
initialValue={solutionStatus !== false ? true : false}
noStyle
>
<input type="hidden" />
</Form.Item>
{renderSolutionContent()}
</div>
</ConfigProvider>
);
};
export default SolutionFieldNew;

View File

@@ -0,0 +1,77 @@
import React from 'react';
import { Typography, Divider, Button, Form } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import SolutionFieldNew from './SolutionField';
const { Text } = Typography;
const SolutionForm = ({
solutionForm,
solutionFields,
solutionTypes,
solutionStatuses,
onAddSolutionField,
onRemoveSolutionField,
onSolutionTypeChange,
onSolutionStatusChange,
onSolutionFileUpload,
onFileView,
fileList,
isReadOnly = false,
solutionData = [],
}) => {
return (
<div style={{ marginBottom: 0 }}>
<Form form={solutionForm} layout="vertical">
<div style={{
maxHeight: '400px',
overflowY: 'auto',
paddingRight: '8px'
}}>
{solutionFields.map((field, displayIndex) => (
<SolutionFieldNew
key={field}
fieldKey={field}
fieldName={['solution_items', field]}
index={displayIndex}
solutionType={solutionTypes[field]}
solutionStatus={solutionStatuses[field]}
onTypeChange={onSolutionTypeChange}
onStatusChange={onSolutionStatusChange}
onRemove={() => onRemoveSolutionField(field)}
onFileUpload={onSolutionFileUpload}
onFileView={onFileView}
fileList={fileList}
isReadOnly={isReadOnly}
canRemove={solutionFields.length > 1 && displayIndex > 0}
originalSolutionData={solutionData[displayIndex]}
/>
))}
</div>
{!isReadOnly && (
<div style={{ marginBottom: 8, marginTop: 12 }}>
<Button
type="dashed"
onClick={onAddSolutionField}
icon={<PlusOutlined />}
style={{
width: '100%',
borderColor: '#23A55A',
color: '#23A55A',
height: '32px',
fontSize: '12px'
}}
>
Add sollution
</Button>
</div>
)}
</Form>
</div>
);
};
export default SolutionForm;

View File

@@ -0,0 +1,178 @@
import React, { useState, useEffect } from 'react';
import { Select, Typography, Tag, Spin, Empty, Button } from 'antd';
import { PlusOutlined, DeleteOutlined, CheckOutlined, EyeOutlined, InfoCircleOutlined } from '@ant-design/icons';
import { getAllSparepart } from '../../../../api/sparepart';
import CustomSparepartCard from './CustomSparepartCard';
const { Text, Title } = Typography;
const { Option } = Select;
const SparepartSelect = ({
selectedSparepartIds = [],
onSparepartChange,
isReadOnly = false
}) => {
const [spareparts, setSpareparts] = useState([]);
const [loading, setLoading] = useState(false);
const [selectedSpareparts, setSelectedSpareparts] = useState([]);
const [dropdownOpen, setDropdownOpen] = useState(false);
useEffect(() => {
fetchSpareparts();
}, []);
useEffect(() => {
if (selectedSparepartIds && selectedSparepartIds.length > 0) {
const fullSelectedSpareparts = spareparts.filter(sp =>
selectedSparepartIds.includes(sp.sparepart_id)
);
setSelectedSpareparts(fullSelectedSpareparts);
} else {
setSelectedSpareparts([]);
}
}, [selectedSparepartIds, spareparts]);
const fetchSpareparts = async (searchQuery = '') => {
setLoading(true);
try {
const params = new URLSearchParams();
params.set('limit', '10');
if (searchQuery && searchQuery.trim() !== '') {
params.set('criteria', searchQuery.trim());
}
const response = await getAllSparepart(params);
if (response && (response.statusCode === 200 || response.data)) {
const sparepartData = response.data?.data || response.data || [];
setSpareparts(sparepartData);
} else {
setSpareparts([]);
}
} catch (error) {
setSpareparts([]);
} finally {
setLoading(false);
}
};
const handleSparepartSelect = (sparepartId) => {
const selectedSparepart = spareparts.find(sp => sp.sparepart_id === sparepartId);
if (selectedSparepart) {
const isAlreadySelected = selectedSpareparts.some(sp => sp.sparepart_id === sparepartId);
if (!isAlreadySelected) {
const newSelectedSpareparts = [...selectedSpareparts, selectedSparepart];
setSelectedSpareparts(newSelectedSpareparts);
const newSelectedIds = newSelectedSpareparts.map(sp => sp.sparepart_id);
onSparepartChange(newSelectedIds);
}
}
setDropdownOpen(false);
};
const handleSearch = (value) => {
fetchSpareparts(value);
};
const onDropdownOpenChange = (open) => {
setDropdownOpen(open);
if (open) {
fetchSpareparts();
}
};
const handleRemoveSparepart = (sparepartId) => {
const newSelectedSpareparts = selectedSpareparts.filter(sp => sp.sparepart_id !== sparepartId);
setSelectedSpareparts(newSelectedSpareparts);
const newSelectedIds = newSelectedSpareparts.map(sp => sp.sparepart_id);
onSparepartChange(newSelectedIds);
};
const renderSparepartCard = (sparepart, isSelected = false) => {
const isAlreadySelected = selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id);
return (
<CustomSparepartCard
key={sparepart.sparepart_id}
sparepart={sparepart}
isSelected={isSelected}
isReadOnly={isReadOnly}
showPreview={true}
showDelete={isAlreadySelected && !isReadOnly}
onCardClick={!isAlreadySelected && !isReadOnly ? () => handleSparepartSelect(sparepart.sparepart_id) : undefined}
onDelete={() => handleRemoveSparepart(sparepart.sparepart_id)}
/>
);
};
return (
<>
{!isReadOnly && (
<div style={{
marginBottom: 16,
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: 'white',
padding: '8px 0',
borderBottom: '1px solid #f0f0f0'
}}>
<Select
placeholder="search and select sparepart"
style={{ width: '100%' }}
loading={loading}
onSelect={handleSparepartSelect}
value={null}
showSearch
onSearch={handleSearch}
filterOption={false}
open={dropdownOpen}
onOpenChange={onDropdownOpenChange}
suffixIcon={<PlusOutlined />}
>
{spareparts
.filter(sparepart => !selectedSpareparts.some(sp => sp.sparepart_id === sparepart.sparepart_id))
.slice(0, 10)
.map((sparepart) => (
<Option key={sparepart.sparepart_id} value={sparepart.sparepart_id}>
<div>
<Text strong>{sparepart.sparepart_name || sparepart.name || 'Unnamed'}</Text>
<Text type="secondary" style={{ marginLeft: 8 }}>
({sparepart.sparepart_code || 'No code'})
</Text>
</div>
</Option>
))}
</Select>
</div>
)}
<div>
{selectedSpareparts.length > 0 ? (
<div>
<Title level={5} style={{ marginBottom: 16 }}>
Selected Spareparts ({selectedSpareparts.length})
</Title>
<div>
{selectedSpareparts.map(sparepart => renderSparepartCard(sparepart, true))}
</div>
</div>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="No spareparts selected"
style={{ margin: '20px 0' }}
/>
)}
</div>
</>
);
};
export default SparepartSelect;

View File

@@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider } from 'antd'; import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, Select } from 'antd';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { createDevice, updateDevice } from '../../../../api/master-device'; import { createDevice, updateDevice } from '../../../../api/master-device';
import { getAllBrands } from '../../../../api/master-brand';
import { validateRun } from '../../../../Utils/validate'; import { validateRun } from '../../../../Utils/validate';
const { Text } = Typography; const { Text } = Typography;
@@ -9,16 +10,20 @@ const { TextArea } = Input;
const DetailDevice = (props) => { const DetailDevice = (props) => {
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const [brands, setBrands] = useState([]);
const [loadingBrands, setLoadingBrands] = useState(false);
const defaultData = { const defaultData = {
device_id: '', device_id: '',
device_code: '', device_code: '',
device_name: '', device_name: '',
brand_device: '', brand_id: '',
brand_code: '',
is_active: true, is_active: true,
device_location: '', device_location: '',
device_description: '', device_description: '',
ip_address: '', ip_address: '',
listen_channel: '',
}; };
const [formData, setFormData] = useState(defaultData); const [formData, setFormData] = useState(defaultData);
@@ -35,6 +40,7 @@ const DetailDevice = (props) => {
const validationRules = [ const validationRules = [
{ field: 'device_name', label: 'Device Name', required: true }, { field: 'device_name', label: 'Device Name', required: true },
{ field: 'ip_address', label: 'Ip Address', required: true, ip: true }, { field: 'ip_address', label: 'Ip Address', required: true, ip: true },
{ field: 'brand_id', label: 'Brand Device', required: true },
]; ];
if ( if (
@@ -54,8 +60,13 @@ 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, 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,
listen_channel: formData.listen_channel,
}; };
const response = formData.device_id const response = formData.device_id
@@ -102,6 +113,13 @@ const DetailDevice = (props) => {
}); });
}; };
const handleSelectChange = (name, value) => {
setFormData({
...formData,
[name]: value,
});
};
const handleStatusToggle = (event) => { const handleStatusToggle = (event) => {
const isChecked = event; const isChecked = event;
setFormData({ setFormData({
@@ -110,6 +128,32 @@ const DetailDevice = (props) => {
}); });
}; };
// Fungsi untuk mengambil daftar brand
const fetchBrands = async () => {
setLoadingBrands(true);
try {
const response = await getAllBrands(new URLSearchParams());
if (response && response.data) {
setBrands(response.data || []);
}
} catch (error) {
console.error('Error fetching brands:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'Gagal mengambil data brand',
});
} finally {
setLoadingBrands(false);
}
};
useEffect(() => {
if (props.showModal && (props.actionMode === 'add' || props.actionMode === 'edit')) {
fetchBrands();
}
}, [props.showModal, props.actionMode]);
useEffect(() => { useEffect(() => {
if (props.selectedData) { if (props.selectedData) {
setFormData(props.selectedData); setFormData(props.selectedData);
@@ -143,7 +187,6 @@ const DetailDevice = (props) => {
defaultBorderColor: '#23A55A', defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A', defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A', defaultHoverBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
}, },
}, },
}} }}
@@ -244,19 +287,26 @@ const DetailDevice = (props) => {
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text strong>Brand Device</Text> <Text strong>Brand Device</Text>
<Text style={{ color: 'red' }}> *</Text> <Text style={{ color: 'red' }}> *</Text>
<Input <Select
name="brand_device" name="brand_id"
value={formData.brand_device} value={formData.brand_id}
onChange={handleInputChange} onChange={(value) => handleSelectChange('brand_id', value)}
placeholder="Enter Brand Device" placeholder="Select Brand Device"
readOnly={props.readOnly} disabled={props.readOnly}
disabled loading={loadingBrands}
style={{ style={{ width: '100%' }}
backgroundColor: '#f5f5f5', allowClear
cursor: 'not-allowed', showSearch
color: formData.brand_device ? '#000000' : '#bfbfbf', filterOption={(input, option) =>
}} option.children.toLowerCase().includes(input.toLowerCase())
/> }
>
{brands.map((brand) => (
<Select.Option key={brand.brand_id} value={brand.brand_id}>
{`${brand.brand_code} - ${brand.brand_name} `}
</Select.Option>
))}
</Select>
</div> </div>
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text strong>Device Location</Text> <Text strong>Device Location</Text>
@@ -280,6 +330,16 @@ const DetailDevice = (props) => {
readOnly={props.readOnly} readOnly={props.readOnly}
/> />
</div> </div>
<div style={{ marginBottom: 12 }}>
<Text strong>Listen Channel</Text>
<Input
name="listen_channel"
value={formData.listen_channel}
onChange={handleInputChange}
placeholder="Enter Listen Channel"
readOnly={props.readOnly}
/>
</div>
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text strong>Device Description</Text> <Text strong>Device Description</Text>
<TextArea <TextArea

View File

@@ -1,4 +1,4 @@
import React, {useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, Button, ConfigProvider } from 'antd'; import { Modal, Button, ConfigProvider } from 'antd';
import { jsPDF } from 'jspdf'; import { jsPDF } from 'jspdf';
import logoPiEnergi from '../../../../assets/images/logo/pi-energi.png'; import logoPiEnergi from '../../../../assets/images/logo/pi-energi.png';
@@ -22,12 +22,12 @@ const GeneratePdf = (props) => {
}; };
const generatePdf = async () => { const generatePdf = async () => {
const {images, title} = await kopReportPdf(logoPiEnergi, 'COLD WORK PERMIT'); const { images, title } = await kopReportPdf(logoPiEnergi, 'COLD WORK PERMIT');
const doc = new jsPDF({ const doc = new jsPDF({
orientation: "portrait", orientation: 'portrait',
unit: "mm", unit: 'mm',
format: "a4" format: 'a4',
}); });
const width = 45; const width = 45;
@@ -45,32 +45,32 @@ const GeneratePdf = (props) => {
doc.setFontSize(11); doc.setFontSize(11);
doc.setFont('helvetica', 'normal'); doc.setFont('helvetica', 'normal');
doc.setLineWidth(0.2); doc.setLineWidth(0.2);
doc.line(10, 32, 200, 32); doc.line(10, 32, 200, 32);
doc.setLineWidth(0.6); doc.setLineWidth(0.6);
doc.line(10, 32.8, 200, 32.8); doc.line(10, 32.8, 200, 32.8);
doc.text("Tanggal Pengajuan", 10, 42); doc.text('Tanggal Pengajuan', 10, 42);
doc.text(":", 59, 42); doc.text(':', 59, 42);
doc.text("Deskripsi Pekerjaan", 10, 48); doc.text('Deskripsi Pekerjaan', 10, 48);
doc.text(":", 59, 48); doc.text(':', 59, 48);
doc.text("No. Permit", 10, 54);
doc.text(":", 59, 54);
doc.text("Spesifik Lokasi", 120, 54);
doc.text(":", 160, 54);
doc.text("No. Order", 10, 60); doc.text('No. Permit', 10, 54);
doc.text(":", 59, 60); doc.text(':', 59, 54);
doc.text("Jum. Personil Terlihat", 120, 60); doc.text('Spesifik Lokasi', 120, 54);
doc.text(":", 160, 60); doc.text(':', 160, 54);
doc.text("Peralatan yang digunakan", 10, 66); doc.text('No. Order', 10, 60);
doc.text(":", 59, 66); doc.text(':', 59, 60);
doc.text('Jum. Personil Terlihat', 120, 60);
doc.text(':', 160, 60);
doc.text("Jenis APD yang digunakan", 10, 72); doc.text('Peralatan yang digunakan', 10, 66);
doc.text(":", 59, 72); doc.text(':', 59, 66);
doc.text('Jenis APD yang digunakan', 10, 72);
doc.text(':', 59, 72);
const blob = doc.output('blob'); const blob = doc.output('blob');
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@@ -84,7 +84,7 @@ const GeneratePdf = (props) => {
return ( return (
<Modal <Modal
width='60%' width="60%"
title="Preview PDF" title="Preview PDF"
open={props.showPdf} open={props.showPdf}
// open={true} // open={true}
@@ -101,7 +101,6 @@ const GeneratePdf = (props) => {
defaultBorderColor: '#23A55A', defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A', defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A', defaultHoverBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
}, },
}, },
}} }}

View File

@@ -13,6 +13,7 @@ import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../../components/
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { deleteDevice, getAllDevice } from '../../../../api/master-device'; import { deleteDevice, getAllDevice } from '../../../../api/master-device';
import TableList from '../../../../components/Global/TableList'; import TableList from '../../../../components/Global/TableList';
import { getAllBrands } from '../../../../api/master-brand';
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{ {
@@ -44,9 +45,10 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
}, },
{ {
title: 'Brand Device', title: 'Brand Device',
dataIndex: 'brand_device', dataIndex: 'brand_name',
key: 'brand_device', key: 'brand_name',
width: '20%', width: '20%',
render: (brand_name) => brand_name || '-'
}, },
{ {
title: 'Location', title: 'Location',
@@ -60,6 +62,13 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
key: 'ip_address', key: 'ip_address',
width: '10%', width: '10%',
}, },
{
title: 'Listen Channel',
dataIndex: 'listen_channel',
key: 'listen_channel',
width: '10%',
render: (listen_channel) => listen_channel || '-'
},
{ {
title: 'Status', title: 'Status',
dataIndex: 'is_active', dataIndex: 'is_active',
@@ -81,7 +90,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
), ),
}, },
{ {
title: 'Aksi', title: 'Action',
key: 'aksi', key: 'aksi',
align: 'center', align: 'center',
width: '15%', width: '15%',
@@ -249,7 +258,7 @@ const ListDevice = memo(function ListDevice(props) {
onClick={() => showAddModal()} onClick={() => showAddModal()}
size="large" size="large"
> >
Tambah Data Add data
</Button> </Button>
</ConfigProvider> </ConfigProvider>
</Space> </Space>

View File

@@ -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, 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]);

View File

@@ -62,7 +62,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
), ),
}, },
{ {
title: 'Aksi', title: 'Action',
key: 'aksi', key: 'aksi',
align: 'center', align: 'center',
width: '15%', width: '15%',
@@ -226,7 +226,7 @@ const ListPlantSubSection = memo(function ListPlantSubSection(props) {
onClick={() => showAddModal()} onClick={() => showAddModal()}
size="large" size="large"
> >
Tambah Data Add data
</Button> </Button>
</ConfigProvider> </ConfigProvider>
</Space> </Space>
@@ -237,7 +237,7 @@ const ListPlantSubSection = memo(function ListPlantSubSection(props) {
<TableList <TableList
mobile mobile
cardColor={'#42AAFF'} cardColor={'#42AAFF'}
header={'sub_section_name'} header={'plant_sub_section_name'}
showPreviewModal={showPreviewModal} showPreviewModal={showPreviewModal}
showEditModal={showEditModal} showEditModal={showEditModal}
showDeleteDialog={showDeleteDialog} showDeleteDialog={showDeleteDialog}

View File

@@ -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'

View File

@@ -78,7 +78,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
), ),
}, },
{ {
title: 'Aksi', title: 'Action',
key: 'aksi', key: 'aksi',
align: 'center', align: 'center',
width: '25%', width: '25%',
@@ -239,7 +239,7 @@ const ListShift = memo(function ListShift(props) {
onClick={() => showAddModal()} onClick={() => showAddModal()}
size="large" size="large"
> >
Tambah Data Add data
</Button> </Button>
</ConfigProvider> </ConfigProvider>
</Space> </Space>

View File

@@ -0,0 +1,75 @@
import React, { memo, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { Typography } from 'antd';
import ListSparepart from './component/ListSparepart';
import DetailSparepart from './component/DetailSparepart';
const { Text } = Typography;
const IndexSparepart = memo(function IndexSparepart() {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [actionMode, setActionMode] = useState('list');
const [selectedData, setSelectedData] = useState(null);
const [readOnly, setReadOnly] = useState(false);
const [showModal, setShowmodal] = useState(false);
const setMode = (param) => {
setShowmodal(true);
switch (param) {
case 'add':
setReadOnly(false);
break;
case 'edit':
setReadOnly(false);
break;
case 'preview':
setReadOnly(true);
break;
default:
setShowmodal(false);
break;
}
setActionMode(param);
};
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
setBreadcrumbItems([
{ title: <Text strong style={{ fontSize: '14px' }}> Master</Text> },
{ title: <Text strong style={{ fontSize: '14px' }}>Sparepart</Text> }
]);
} else {
navigate('/signin');
}
}, []);
return (
<React.Fragment>
<ListSparepart
actionMode={actionMode}
setActionMode={setMode}
selectedData={selectedData}
setSelectedData={setSelectedData}
readOnly={readOnly}
/>
<DetailSparepart
setActionMode={setMode}
selectedData={selectedData}
setSelectedData={setSelectedData}
readOnly={readOnly}
showModal={showModal}
permitDefault={false}
actionMode={actionMode}
/>
</React.Fragment>
);
});
export default IndexSparepart;

View File

@@ -0,0 +1,591 @@
import React, { useState, useEffect } from 'react';
import {
Modal,
Input,
Select,
Divider,
Typography,
Button,
ConfigProvider,
Upload,
Row,
Col,
Image,
} from 'antd';
import { PlusOutlined, EyeOutlined, DeleteOutlined } from '@ant-design/icons';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { createSparepart, updateSparepart } from '../../../../api/sparepart';
import { uploadFile } from '../../../../api/file-uploads';
import { validateRun } from '../../../../Utils/validate';
const { Text } = Typography;
const { TextArea } = Input;
const getBase64 = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
const DetailSparepart = (props) => {
const [confirmLoading, setConfirmLoading] = useState(false);
const [fileList, setFileList] = useState([]);
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState('');
const [previewTitle, setPreviewTitle] = useState('');
const [isHovering, setIsHovering] = useState(false);
const defaultData = {
sparepart_id: '',
sparepart_name: '',
sparepart_description: '',
sparepart_model: '',
sparepart_item_type: null,
sparepart_qty: 0,
sparepart_unit: '',
sparepart_merk: '',
sparepart_stok: 'Not Available',
sparepart_foto: '',
};
const [formData, setFormData] = useState(defaultData);
const handleCancel = () => {
props.setSelectedData(null);
props.setActionMode('list');
setFileList([]);
};
const handlePreviewCancel = () => setPreviewOpen(false);
const handlePreview = async (file) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
setPreviewImage(file.url || file.preview);
setPreviewOpen(true);
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
};
const handleChange = ({ fileList: newFileList }) => setFileList(newFileList);
const handleRemove = () => {
setFileList([]);
};
const handleSave = async () => {
setConfirmLoading(true);
const validationRules = [
{ field: 'sparepart_name', label: 'Sparepart Name', required: true },
];
if (
validateRun(formData, validationRules, (errorMessages) => {
NotifOk({ icon: 'warning', title: 'Peringatan', message: errorMessages });
setConfirmLoading(false);
})
)
return;
try {
let imageUrl = formData.sparepart_foto;
const newFile = fileList.length > 0 ? fileList[0] : null;
if (newFile && newFile.originFileObj) {
// console.log('Uploading file:', newFile.originFileObj);
const uploadResponse = await uploadFile(newFile.originFileObj, 'images');
// Log untuk debugging
// console.log('Upload response:', uploadResponse);
// Cek berbagai kemungkinan struktur respons dari API
let uploadedUrl = null;
// Cek berbagai kemungkinan struktur respons dari API
// Cek langsung properti file_url atau url
if (uploadResponse && typeof uploadResponse === 'object') {
// Cek jika uploadResponse langsung memiliki file_url
if (uploadResponse.file_url) {
uploadedUrl = uploadResponse.file_url;
}
// Cek jika uploadResponse memiliki data yang berisi file_url
else if (uploadResponse.data && uploadResponse.data.file_url) {
uploadedUrl = uploadResponse.data.file_url;
}
// Cek jika uploadResponse memiliki data yang berisi url
else if (uploadResponse.data && uploadResponse.data.url) {
uploadedUrl = uploadResponse.data.url;
}
// Cek jika uploadResponse langsung memiliki url
else if (uploadResponse.url) {
uploadedUrl = uploadResponse.url;
}
// Cek jika uploadResponse.data adalah string URL
else if (uploadResponse.data && typeof uploadResponse.data === 'string') {
uploadedUrl = uploadResponse.data;
}
// Cek jika uploadResponse.data adalah objek yang berisi file URL dalam format berbeda
else if (uploadResponse.data && typeof uploadResponse.data === 'object') {
// Cek kemungkinan nama field lain
if (uploadResponse.data.file) {
uploadedUrl = uploadResponse.data.file;
} else if (uploadResponse.data.filename) {
// Jika hanya nama file dikembalikan, bangun URL
const baseUrl = import.meta.env.VITE_API_SERVER || '';
uploadedUrl = `${baseUrl}/uploads/images/${uploadResponse.data.filename}`;
} else if (uploadResponse.data.path) {
uploadedUrl = uploadResponse.data.path;
} else if (uploadResponse.data.location) {
uploadedUrl = uploadResponse.data.location;
}
// Tambahkan kemungkinan lain berdasarkan struktur respons umum
else if (uploadResponse.data.filePath) {
uploadedUrl = uploadResponse.data.filePath;
} else if (uploadResponse.data.file_path) {
uploadedUrl = uploadResponse.data.file_path;
} else if (uploadResponse.data.publicUrl) {
uploadedUrl = uploadResponse.data.publicUrl;
} else if (uploadResponse.data.public_url) {
uploadedUrl = uploadResponse.data.public_url;
}
// Berdasarkan log yang ditampilkan, API mengembalikan path_document atau path_solution
else if (uploadResponse.data.path_document) {
uploadedUrl = uploadResponse.data.path_document;
} else if (uploadResponse.data.path_solution) {
uploadedUrl = uploadResponse.data.path_solution;
} else if (uploadResponse.data.file_upload_name) {
// Jika hanya nama file dikembalikan, bangun URL
const baseUrl = import.meta.env.VITE_API_SERVER || '';
uploadedUrl = `${baseUrl}/uploads/images/${uploadResponse.data.file_upload_name}`;
}
}
}
// Jika respons adalah string, mungkin itu adalah URL
else if (uploadResponse && typeof uploadResponse === 'string') {
uploadedUrl = uploadResponse;
}
if (uploadedUrl) {
// console.log('Successfully extracted image URL:', uploadedUrl);
imageUrl = uploadedUrl;
} else {
console.error('Upload response structure:', uploadResponse);
console.error('Available properties:', Object.keys(uploadResponse || {}));
console.error('Response type:', typeof uploadResponse);
console.error(
'Is response an object?',
uploadResponse && typeof uploadResponse === 'object'
);
if (uploadResponse && typeof uploadResponse === 'object') {
console.error('Response keys:', Object.keys(uploadResponse));
console.error(
'Response data keys:',
uploadResponse.data
? Object.keys(uploadResponse.data)
: 'No data property'
);
}
// Tampilkan notifikasi bahwa upload gagal tapi lanjutkan penyimpanan
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Upload gambar gagal. Data akan disimpan tanpa gambar.',
});
// Gunakan URL gambar yang sebelumnya jika ada, atau kosongkan
imageUrl = formData.sparepart_foto || '';
}
} else if (fileList.length === 0) {
// Jika tidak ada file di fileList (termasuk saat user menghapus file), gunakan gambar default
imageUrl = '/assets/defaultSparepartImg.jpg';
}
// Payload hanya berisi field yang tidak kosong untuk menghindari error validasi
const payload = {
sparepart_name: formData.sparepart_name, // Wajib
};
payload.sparepart_description =
formData.sparepart_description && formData.sparepart_description.trim() !== ''
? formData.sparepart_description
: ' ';
if (formData.sparepart_model && formData.sparepart_model.trim() !== '') {
payload.sparepart_model = formData.sparepart_model;
}
if (formData.sparepart_item_type && formData.sparepart_item_type.trim() !== '') {
payload.sparepart_item_type = formData.sparepart_item_type;
}
if (formData.sparepart_unit && formData.sparepart_unit.trim() !== '') {
payload.sparepart_unit = formData.sparepart_unit;
}
if (formData.sparepart_merk && formData.sparepart_merk.trim() !== '') {
payload.sparepart_merk = formData.sparepart_merk;
}
// sparepart_qty disimpan sebagai angka kuantitas
const qty = parseInt(formData.sparepart_qty) || 0;
payload.sparepart_qty = qty;
// sparepart_stok ditentukan otomatis berdasarkan qty sebenarnya
payload.sparepart_stok = qty > 0 ? 'Available' : 'Not Available';
// Sertakan sparepart_foto hanya jika nilainya tidak kosong, agar tidak memicu validasi
if (imageUrl && imageUrl.trim() !== '') {
payload.sparepart_foto = imageUrl;
}
// console.log('Sending payload:', payload);
const response = formData.sparepart_id
? await updateSparepart(formData.sparepart_id, payload)
: await createSparepart(payload);
// console.log('API response:', response);
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
NotifOk({
icon: 'success',
title: 'Berhasil',
message: `Data Sparepart berhasil ${
formData.sparepart_id ? 'diubah' : 'ditambahkan'
}.`,
});
props.setActionMode('list');
setFileList([]);
} else {
NotifAlert({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Terjadi kesalahan saat menyimpan data.',
});
}
} catch (error) {
console.error('Save Sparepart Error:', error);
NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'Terjadi kesalahan pada server. Coba lagi nanti.',
});
}
setConfirmLoading(false);
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handleSelectChange = (name, value) => {
setFormData({ ...formData, [name]: value });
};
useEffect(() => {
if (props.selectedData) {
setFormData(props.selectedData);
if (props.selectedData.sparepart_foto) {
let displayUrl = props.selectedData.sparepart_foto;
// Jika URL bukan full URL (tidak mengandung http/https), bangun URL lokal
if (!props.selectedData.sparepart_foto.startsWith('http')) {
const fileName = props.selectedData.sparepart_foto.split('/').pop();
// Cek apakah ini file default
if (fileName === 'defaultSparepartImg.jpg') {
displayUrl = '/assets/defaultSparepartImg.jpg';
} else {
// Gunakan format file URL seperti di brandDevice
const token = localStorage.getItem('token');
const baseURL = import.meta.env.VITE_API_SERVER || '';
displayUrl = `${baseURL}/file-uploads/images/${encodeURIComponent(
fileName
)}${token ? `?token=${encodeURIComponent(token)}` : ''}`;
}
}
const fileName = props.selectedData.sparepart_foto.split('/').pop();
setFileList([
{
uid: '-1',
name: fileName,
status: 'done',
url: displayUrl,
},
]);
} else {
setFileList([]);
}
} else {
setFormData(defaultData);
setFileList([]);
}
}, [props.showModal, props.selectedData, props.actionMode]);
const uploadButton = (
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</div>
);
return (
<Modal
title={`${
props.actionMode === 'add'
? 'Tambah'
: props.actionMode === 'preview'
? 'Preview'
: 'Edit'
} Sparepart`}
open={props.showModal}
onCancel={handleCancel}
footer={[
<React.Fragment key="modal-footer">
<ConfigProvider
theme={{
token: { colorBgContainer: '#E9F6EF' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button onClick={handleCancel}>Batal</Button>
</ConfigProvider>
<ConfigProvider
theme={{
token: { colorBgContainer: '#209652' },
components: {
Button: {
defaultBg: '#23a55a',
defaultColor: '#FFFFFF',
defaultBorderColor: '#23a55a',
defaultHoverColor: '#FFFFFF',
defaultHoverBorderColor: '#23a55a',
},
},
}}
>
{!props.readOnly && (
<Button loading={confirmLoading} onClick={handleSave}>
Simpan
</Button>
)}
</ConfigProvider>
</React.Fragment>,
]}
>
{formData && (
<div>
<Row gutter={[16, 16]}>
{/* Kolom untuk foto */}
<Col span={10} style={{ display: 'flex', flexDirection: 'column' }}>
<Text strong>Foto</Text>
<div
style={{
flexGrow: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
}}
>
{fileList.length > 0 ? (
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
style={{
position: 'relative',
width: '180px', // Fixed width for square
height: '180px', // Fixed height
border: '1px solid #d9d9d9',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Image
src={fileList[0].url || fileList[0].thumbUrl}
alt="preview"
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
}}
preview={false} // Disable default preview
/>
{isHovering && !props.readOnly && (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: 'white',
gap: '16px',
fontSize: '20px',
borderRadius: '8px',
cursor: 'pointer',
}}
>
<EyeOutlined
onClick={() => handlePreview(fileList[0])}
/>
<DeleteOutlined onClick={handleRemove} />
</div>
)}
</div>
) : (
<Upload
name="file"
multiple={false}
fileList={fileList}
onChange={handleChange}
beforeUpload={() => false}
maxCount={1}
disabled={props.readOnly}
showUploadList={false}
>
<div
style={{
width: '180px', // Fixed width for square
height: '180px',
border: '1px dashed #d9d9d9',
borderRadius: '8px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
gap: '8px',
}}
>
<PlusOutlined />
<div>Upload</div>
</div>
</Upload>
)}
</div>
</Col>
{/* Kolom untuk field lainnya */}
<Col span={14}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Text strong>Sparepart Name</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="sparepart_name"
value={formData.sparepart_name}
onChange={handleInputChange}
placeholder="Enter Sparepart Name"
readOnly={props.readOnly}
/>
</Col>
<Col span={24}>
<Text strong>Item Type</Text>
<Select
name="sparepart_item_type"
value={formData.sparepart_item_type}
onChange={(value) =>
handleSelectChange('sparepart_item_type', value)
}
placeholder="Enter Item Type"
disabled={props.readOnly}
style={{ width: '100%' }}
>
<Select.Option value="Air Dryer">Air Dryer</Select.Option>
<Select.Option value="Compressor">Compressor</Select.Option>
</Select>
</Col>
<Col span={12}>
<Text strong>Qty</Text>
<Input
name="sparepart_qty"
value={formData.sparepart_qty}
onChange={handleInputChange}
placeholder="Enter quantity"
readOnly={props.readOnly}
type="number"
min="0"
/>
</Col>
<Col span={12}>
<Text strong>Unit</Text>
<Input
name="sparepart_unit"
value={formData.sparepart_unit}
onChange={handleInputChange}
placeholder="e.g., pcs"
readOnly={props.readOnly}
/>
</Col>
</Row>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={12}>
<Text strong>Brand</Text>
<Input
name="sparepart_merk"
value={formData.sparepart_merk}
onChange={handleInputChange}
placeholder="Enter Brand (Optional)"
readOnly={props.readOnly}
/>
</Col>
<Col span={12}>
<Text strong>Model</Text>
<Input
name="sparepart_model"
value={formData.sparepart_model}
onChange={handleInputChange}
placeholder="Enter Model (Optional)"
readOnly={props.readOnly}
/>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={24}>
<Text strong>Description</Text>
<TextArea
name="sparepart_description"
value={formData.sparepart_description}
onChange={handleInputChange}
placeholder="Enter Description (Optional)"
readOnly={props.readOnly}
rows={3}
/>
</Col>
</Row>
</div>
)}
<Modal
open={previewOpen}
title={previewTitle}
footer={null}
onCancel={handlePreviewCancel}
>
<img alt="preview" style={{ width: '100%' }} src={previewImage} />
</Modal>
</Modal>
);
};
export default DetailSparepart;

View File

@@ -0,0 +1,292 @@
import React, { memo, useState, useEffect } from 'react';
import { Space, Tag, ConfigProvider, Button, Row, Col, Card, Input, Segmented } from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined,
SearchOutlined,
AppstoreOutlined,
TableOutlined,
} from '@ant-design/icons';
import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../../components/Global/ToastNotif';
import { useNavigate } from 'react-router-dom';
import { deleteSparepart, getAllSparepart } from '../../../../api/sparepart';
import TableList from '../../../../components/Global/TableList';
import SparepartCardList from './SparepartCardList'; // Import the new custom card component
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{
title: 'ID',
dataIndex: 'sparepart_id',
key: 'sparepart_id',
width: '5%',
hidden: 'true',
},
{
title: 'Sparepart Name',
dataIndex: 'sparepart_name',
key: 'sparepart_name',
width: '20%',
},
{
title: 'Description',
dataIndex: 'sparepart_description',
key: 'sparepart_description',
width: '20%',
render: (sparepart_description) => sparepart_description || '-'
},
{
title: 'Model',
dataIndex: 'sparepart_model',
key: 'sparepart_model',
width: '10%',
render: (sparepart_model) => sparepart_model || '-'
},
{
title: 'Item Type',
dataIndex: 'sparepart_item_type',
key: 'sparepart_item_type',
width: '10%',
render: (sparepart_item_type) => sparepart_item_type || '-'
},
{
title: 'Unit',
dataIndex: 'sparepart_unit',
key: 'sparepart_unit',
width: '8%',
render: (sparepart_unit) => sparepart_unit || '-'
},
{
title: 'Merk',
dataIndex: 'sparepart_merk',
key: 'sparepart_merk',
width: '12%',
render: (sparepart_merk) => sparepart_merk || '-'
},
{
title: 'Qty',
dataIndex: 'sparepart_qty',
key: 'sparepart_qty',
width: '8%',
render: (sparepart_qty) => sparepart_qty || '0'
},
{
title: 'Status',
dataIndex: 'sparepart_stok',
key: 'sparepart_stok',
width: '8%',
render: (sparepart_stok) => sparepart_stok || 'Not Available'
},
{
title: 'Action',
key: 'aksi',
align: 'center',
width: '12%',
render: (_, record) => (
<Space>
<Button
type="text"
style={{ borderColor: '#1890ff' }}
icon={<EyeOutlined style={{ color: '#1890ff' }} />}
onClick={() => showPreviewModal(record)}
/>
<Button
type="text"
style={{ borderColor: '#faad14' }}
icon={<EditOutlined style={{ color: '#faad14' }} />}
onClick={() => showEditModal(record)}
/>
<Button
type="text"
danger
style={{ borderColor: 'red' }}
icon={<DeleteOutlined />}
onClick={() => showDeleteDialog(record)}
/>
</Space>
),
},
];
const ListSparepart = memo(function ListSparepart(props) {
const [trigerFilter, setTrigerFilter] = useState(false);
const defaultFilter = { criteria: '' };
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
const [searchValue, setSearchValue] = useState('');
const navigate = useNavigate();
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
if (props.actionMode === 'list') {
setFormDataFilter(defaultFilter);
doFilter();
}
} else {
navigate('/signin');
}
}, [props.actionMode]);
const doFilter = () => {
setTrigerFilter((prev) => !prev);
};
const handleSearch = () => {
setFormDataFilter({ criteria: searchValue });
setTrigerFilter((prev) => !prev);
};
const handleSearchClear = () => {
setSearchValue('');
setFormDataFilter({ criteria: '' });
setTrigerFilter((prev) => !prev);
};
const showPreviewModal = (param) => {
props.setSelectedData(param);
props.setActionMode('preview');
};
const showEditModal = (param = null) => {
props.setSelectedData(param);
props.setActionMode('edit');
};
const showAddModal = (param = null) => {
props.setSelectedData(param);
props.setActionMode('add');
};
const showDeleteDialog = (param) => {
NotifConfirmDialog({
icon: 'question',
title: 'Konfirmasi Hapus',
message: 'Sparepart "' + param.sparepart_name + '" akan dihapus?',
onConfirm: () => handleDelete(param.sparepart_id),
onCancel: () => props.setSelectedData(null),
});
};
const handleDelete = async (sparepart_id) => {
const response = await deleteSparepart(sparepart_id);
if (response.statusCode === 200 && response.data === true) {
NotifAlert({
icon: 'success',
title: 'Berhasil',
message: response.message || 'Data Sparepart berhasil dihapus.',
});
doFilter();
} else {
NotifOk({
icon: 'error',
title: 'Gagal',
message: response?.message || 'Gagal Menghapus Data Sparepart',
});
}
};
return (
<React.Fragment>
<Card>
<Row>
<Col xs={24}>
<Row justify="space-between" align="middle" gutter={[8, 8]}>
<Col xs={24} sm={24} md={12} lg={12}>
<Input.Search
placeholder="Search sparepart by name, model, or merk..."
value={searchValue}
onChange={(e) => {
const value = e.target.value;
setSearchValue(value);
if (value === '') {
setFormDataFilter({ criteria: '' });
setTrigerFilter((prev) => !prev);
}
}}
onSearch={handleSearch}
allowClear={{
clearIcon: <span onClick={handleSearchClear}></span>,
}}
enterButton={
<Button
type="primary"
icon={<SearchOutlined />}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
}}
>
Search
</Button>
}
size="large"
/>
</Col>
<Col>
<Space wrap size="small">
<ConfigProvider
theme={{
token: { colorBgContainer: '#E9F6EF' },
components: {
Button: {
defaultBg: 'white',
defaultColor: '#23A55A',
defaultBorderColor: '#23A55A',
defaultHoverColor: '#23A55A',
defaultHoverBorderColor: '#23A55A',
},
},
}}
>
<Button
icon={<PlusOutlined />}
onClick={() => showAddModal()}
size="large"
>
Add data
</Button>
</ConfigProvider>
</Space>
</Col>
</Row>
</Col>
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
<TableList
mobile
cardColor={'#42AAFF'}
header={'sparepart_name'}
showPreviewModal={showPreviewModal}
showEditModal={showEditModal}
showDeleteDialog={showDeleteDialog}
getData={getAllSparepart}
queryParams={formDataFilter}
columns={columns(showPreviewModal, showEditModal, showDeleteDialog)}
triger={trigerFilter}
cardComponent={SparepartCardList} // Pass the custom component here
onStockUpdate={doFilter}
onGetData={(data) => {
if(data && data.length > 0) {
console.log('Sample sparepart data from API:', data[0]);
console.log('Available fields:', Object.keys(data[0] || {}));
}
}} // Log untuk debugging field-field yang tersedia
/>
</Col>
</Row>
</Card>
</React.Fragment>
);
});
export default ListSparepart;

View File

@@ -0,0 +1,395 @@
import React, { useState } from 'react';
import dayjs from 'dayjs';
import { Card, Button, Row, Col, Typography, Divider, Tag, Space, InputNumber, Input } from 'antd';
import { EditOutlined, DeleteOutlined, PlusOutlined, MinusOutlined } from '@ant-design/icons';
import { updateSparepart } from '../../../../api/sparepart';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
const { Text, Title } = Typography;
const SparepartCardList = ({
data,
header,
showPreviewModal,
showEditModal,
showDeleteDialog,
fieldColor,
cardColor,
onStockUpdate, // Prop to refresh the list
}) => {
const [updateQuantities, setUpdateQuantities] = useState({});
const [loadingQuantities, setLoadingQuantities] = useState({});
const handleQuantityChange = (id, value) => {
// Prevent the adjustment from going below the negative value of the original quantity
// This ensures the final quantity (original + adjustment) never goes below 0
const originalQty = data.find((item) => item.sparepart_id === id)?.sparepart_qty || 0;
const maxNegativeAdjustment = -originalQty;
const clampedValue = Math.max(value, maxNegativeAdjustment);
const newQuantities = { ...updateQuantities };
newQuantities[id] = clampedValue;
setUpdateQuantities(newQuantities);
};
const handleUpdateStock = async (item) => {
const quantityToAdd = updateQuantities[item.sparepart_id] || 0;
if (quantityToAdd === 0) {
NotifAlert({
icon: 'info',
title: 'Info',
message: 'Please change the quantity first.',
});
return;
}
const currentQty = Number(item.sparepart_qty) || 0;
const newQty = currentQty + quantityToAdd;
if (newQty < 0) {
NotifAlert({ icon: 'error', title: 'Error', message: 'Quantity cannot be negative.' });
return;
}
setLoadingQuantities((prev) => ({ ...prev, [item.sparepart_id]: true }));
// sparepart_qty disimpan sebagai angka kuantitas (update boleh 0 sesuai validasi update schema)
const payload = {
sparepart_qty: newQty,
sparepart_stok: newQty > 0 ? 'Available' : 'Not Available', // Otomatis tentukan status
};
// Hanya tambahkan field jika nilainya tidak kosong untuk menghindari validasi error
if (item.sparepart_unit && item.sparepart_unit.trim() !== '') {
payload.sparepart_unit = item.sparepart_unit;
}
if (item.sparepart_merk && item.sparepart_merk.trim() !== '') {
payload.sparepart_merk = item.sparepart_merk;
}
if (item.sparepart_model && item.sparepart_model.trim() !== '') {
payload.sparepart_model = item.sparepart_model;
}
if (item.sparepart_description && item.sparepart_description.trim() !== '') {
payload.sparepart_description = item.sparepart_description;
}
if (item.sparepart_item_type && item.sparepart_item_type !== null) {
payload.sparepart_item_type = item.sparepart_item_type;
}
if (item.sparepart_foto && item.sparepart_foto.trim() !== '') {
payload.sparepart_foto = item.sparepart_foto;
}
try {
const response = await updateSparepart(item.sparepart_id, payload);
// Periksa apakah response valid sebelum mengakses propertinya
if (response && response.statusCode === 200) {
NotifOk({
icon: 'success',
title: 'Success',
message: 'Stock updated successfully.',
});
// Cek apakah qty baru kurang dari 1, tampilkan alert
if (newQty < 1) {
NotifAlert({
icon: 'warning',
title: 'Low Stock',
message: `Warning: Sparepart "${item.sparepart_name}" is out of stock. Please restock immediately.`,
});
}
if (onStockUpdate) {
onStockUpdate();
}
handleQuantityChange(item.sparepart_id, 0); // Reset quantity
} else {
NotifAlert({
icon: 'error',
title: 'Failed',
message: response?.message || 'Failed to update stock.',
});
}
} catch (error) {
NotifAlert({
icon: 'error',
title: 'Error',
message: error.message || 'An error occurred.',
});
} finally {
setLoadingQuantities((prev) => ({ ...prev, [item.sparepart_id]: false }));
}
};
return (
<Row gutter={[16, 16]} style={{ marginTop: '16px' }}>
{data.map((item) => {
const quantity = updateQuantities[item.sparepart_id] || 0;
const isLoading = loadingQuantities[item.sparepart_id] || false;
return (
<Col xs={24} sm={12} md={8} lg={6} key={item.sparepart_id || item.key}>
<Card
style={{
borderRadius: '8px',
overflow: 'hidden',
border: `1px solid ${
fieldColor ? item[fieldColor] : cardColor || '#E0E0E0'
}`,
}}
bodyStyle={{ padding: 0 }}
>
<Row>
<Col span={8}>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
padding: '16px 8px',
height: '100%',
}}
>
{item.sparepart_item_type && (
<Tag
color="blue"
style={{
marginBottom: '8px',
}}
>
{item.sparepart_item_type}
</Tag>
)}
<div
style={{
backgroundColor: '#f0f0f0',
width: '100%',
paddingTop:
'100%' /* Ini membuat tinggi sama dengan lebar (aspect ratio 1:1) */,
position: 'relative',
borderRadius: '4px',
overflow: 'hidden',
}}
>
{(() => {
// Debug: log the image path construction
let imgSrc;
if (item.sparepart_foto) {
if (item.sparepart_foto.startsWith('http')) {
imgSrc = item.sparepart_foto;
} else {
// Gunakan format file URL seperti di brandDevice
const fileName = item.sparepart_foto
.split('/')
.pop();
// Jika filename adalah default file, gunakan dari public assets
if (
fileName === 'defaultSparepartImg.jpg'
) {
imgSrc = `/assets/defaultSparepartImg.jpg`;
} else {
// Gunakan API getFileUrl untuk mendapatkan URL yang benar untuk file upload
const token =
localStorage.getItem('token');
const baseURL =
import.meta.env.VITE_API_SERVER ||
'';
imgSrc = `${baseURL}/file-uploads/images/${encodeURIComponent(
fileName
)}${
token
? `?token=${encodeURIComponent(
token
)}`
: ''
}`;
}
}
console.log(
'Image path being constructed:',
imgSrc
);
} else {
imgSrc = 'https://via.placeholder.com/150';
}
return (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}
>
<img
src={imgSrc}
alt={item[header]}
style={{
width: '100%',
height: '100%',
objectFit: 'cover', // Mengisi container dan crop sisi berlebih
}}
onError={(e) => {
console.error(
'Image failed to load:',
imgSrc
);
e.target.src =
'https://via.placeholder.com/150';
}}
onLoad={() =>
console.log(
'Image loaded successfully:',
imgSrc
)
}
/>
</div>
);
})()}
</div>
</div>
</Col>
<Col span={16}>
<div
style={{
padding: '16px',
position: 'relative',
height: '100%',
}}
>
<div
style={{
position: 'absolute',
top: 8,
right: 8,
display: 'flex',
gap: '8px',
}}
>
{showEditModal && (
<Button
style={{
color: '#faad14',
borderColor: '#faad14',
}}
icon={<EditOutlined />}
key="edit"
onClick={() => showEditModal(item)}
size="small"
/>
)}
{showDeleteDialog && (
<Button
icon={<DeleteOutlined />}
key="delete"
onClick={() => showDeleteDialog(item)}
size="small"
danger
/>
)}
</div>
<Title
level={5}
style={{
margin: 0,
marginBottom: '8px',
paddingRight: '60px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{item[header]}
</Title>
<Text type="secondary" style={{ display: 'block' }}>
Stok: {item.sparepart_stok || 'Not Available'}
</Text>
<Divider style={{ margin: '8px 0' }} />
<Space
align="center"
style={{
marginBottom: '8px',
display: 'flex',
}}
>
<Text type="secondary">Qty</Text>
<Button
icon={<MinusOutlined />}
onClick={() =>
handleQuantityChange(
item.sparepart_id,
quantity - 1
)
}
disabled={
isLoading || item.sparepart_qty + quantity <= 0
}
style={{ width: 28, height: 28 }}
/>
<Text
strong
style={{ padding: '0 8px', fontSize: '16px' }}
>
{item.sparepart_qty + (quantity || 0)}
</Text>
<Button
icon={<PlusOutlined />}
onClick={() =>
handleQuantityChange(
item.sparepart_id,
quantity + 1
)
}
disabled={isLoading}
style={{ width: 28, height: 28 }}
/>
<Text type="secondary">
{item.sparepart_unit
? ` / ${item.sparepart_unit}`
: ' / pcs'}
</Text>
</Space>
{quantity !== 0 && (
<Button
type={quantity === 0 ? 'default' : 'primary'}
size="small"
style={{ width: '100%' }}
onClick={() => handleUpdateStock(item)}
loading={isLoading}
>
Update Stock
</Button>
)}
<br />
<Text
type="secondary"
style={{
fontSize: '12px',
marginTop: '8px',
display: 'inline-block',
}}
>
Last updated:{' '}
{item.updated_at
? dayjs(item.updated_at).format('DD MMM YYYY')
: 'N/A'}
</Text>
</div>
</Col>
</Row>
</Card>
</Col>
);
})}
</Row>
);
};
export default SparepartCardList;

View File

@@ -81,7 +81,7 @@ const DetailStatus = (props) => {
status_number: formData.status_number, status_number: formData.status_number,
status_name: formData.status_name, status_name: formData.status_name,
status_color: formData.status_color, status_color: formData.status_color,
status_description: formData.status_description, status_description: (formData.status_description && formData.status_description.trim() !== '') ? formData.status_description : ' ',
is_active: formData.is_active, is_active: formData.is_active,
}; };
@@ -128,7 +128,7 @@ const DetailStatus = (props) => {
title={ title={
<Text style={{ fontSize: '18px' }}> <Text style={{ fontSize: '18px' }}>
{props.actionMode === 'add' {props.actionMode === 'add'
? 'Tambah Data' ? 'Add data'
: props.actionMode === 'preview' : props.actionMode === 'preview'
? 'Preview Status' ? 'Preview Status'
: 'Edit Status'} : 'Edit Status'}

View File

@@ -43,7 +43,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
width: '40%', width: '40%',
}, },
{ {
title: 'Aksi', title: 'Action',
key: 'aksi', key: 'aksi',
align: 'center', align: 'center',
width: '20%', width: '20%',
@@ -198,7 +198,7 @@ const ListStatus = memo(function ListStatus(props) {
}} }}
> >
<Button icon={<PlusOutlined />} onClick={showAddModal} size="large"> <Button icon={<PlusOutlined />} onClick={showAddModal} size="large">
Tambah Data Add data
</Button> </Button>
</ConfigProvider> </ConfigProvider>
</Col> </Col>

View File

@@ -84,12 +84,32 @@ const DetailTag = (props) => {
const params = new URLSearchParams({ limit: 10000 }); const params = new URLSearchParams({ limit: 10000 });
const response = await getAllTag(params); const response = await getAllTag(params);
if (response && response.data && response.data.data) { // Handle different response structures
const existingTags = response.data.data; let existingTags = [];
if (response) {
if (Array.isArray(response)) {
existingTags = response;
} else if (response.data && Array.isArray(response.data)) {
existingTags = response.data;
} else if (response.data && response.data.data && Array.isArray(response.data.data)) {
existingTags = response.data.data;
}
}
if (existingTags.length > 0) {
const isDuplicate = existingTags.some((tag) => { const isDuplicate = existingTags.some((tag) => {
const isSameNumber = Number(tag.tag_number) === tagNumberInt; // Handle both string and number tag_number
const isDifferentTag = formData.tag_id ? tag.tag_id !== formData.tag_id : true; const existingTagNumber = Number(tag.tag_number);
const currentTagNumber = Number(formData.tag_number);
// Check if numbers are valid and equal
const isSameNumber = !isNaN(existingTagNumber) && !isNaN(currentTagNumber) &&
existingTagNumber === currentTagNumber;
// For edit mode, exclude the current tag from duplicate check
const isDifferentTag = formData.tag_id ?
String(tag.tag_id) !== String(formData.tag_id) : true;
return isSameNumber && isDifferentTag; return isSameNumber && isDifferentTag;
}); });
@@ -97,7 +117,7 @@ const DetailTag = (props) => {
NotifOk({ NotifOk({
icon: 'warning', icon: 'warning',
title: 'Peringatan', title: 'Peringatan',
message: `Tag Number ${tagNumberInt} sudah digunakan. Silakan gunakan nomor yang berbeda.`, message: `Tag Number ${formData.tag_number} sudah digunakan. Silakan gunakan nomor yang berbeda.`,
}); });
setConfirmLoading(false); setConfirmLoading(false);
return; return;
@@ -148,10 +168,7 @@ const DetailTag = (props) => {
payload.unit = formData.unit.trim(); payload.unit = formData.unit.trim();
} }
// Add tag_description only if it has a value payload.tag_description = (formData.tag_description && formData.tag_description.trim() !== '') ? formData.tag_description.trim() : ' ';
if (formData.tag_description && formData.tag_description.trim() !== '') {
payload.tag_description = formData.tag_description.trim();
}
// Add device_id only if it has a value // Add device_id only if it has a value
if (formData.device_id) { if (formData.device_id) {

View File

@@ -63,7 +63,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
render: (text) => text || '-', render: (text) => text || '-',
}, },
{ {
title: 'Sub Section', title: 'Plant Sub Section',
dataIndex: 'plant_sub_section_name', dataIndex: 'plant_sub_section_name',
key: 'plant_sub_section_name', key: 'plant_sub_section_name',
width: '10%', width: '10%',
@@ -98,7 +98,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
), ),
}, },
{ {
title: 'Aksi', title: 'Action',
key: 'aksi', key: 'aksi',
align: 'center', align: 'center',
width: '15%', width: '15%',
@@ -289,7 +289,7 @@ const ListTag = memo(function ListTag(props) {
onClick={() => showAddModal()} onClick={() => showAddModal()}
size="large" size="large"
> >
Tambah Data Add data
</Button> </Button>
</ConfigProvider> </ConfigProvider>
</Space> </Space>

View File

@@ -181,7 +181,7 @@ const DetailUnit = (props) => {
<Input <Input
name="unit_code" name="unit_code"
value={formData.unit_code || ''} value={formData.unit_code || ''}
placeholder="Dibuat otomatis oleh sistem" placeholder="Unit Code Auto Fill"
disabled disabled
style={{ style={{
backgroundColor: '#f5f5f5', backgroundColor: '#f5f5f5',

View File

@@ -61,7 +61,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
), ),
}, },
{ {
title: 'Aksi', title: 'Action',
key: 'aksi', key: 'aksi',
align: 'center', align: 'center',
width: '20%', width: '20%',
@@ -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({
@@ -258,7 +258,7 @@ const ListUnit = memo(function ListUnit(props) {
onClick={() => showAddModal()} onClick={() => showAddModal()}
size="large" size="large"
> >
Tambah Data Add Data
</Button> </Button>
</ConfigProvider> </ConfigProvider>
</Space> </Space>

View File

@@ -1,7 +1,7 @@
import React, { memo, useState, useEffect } from 'react'; import React, { memo, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb'; import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
import { Form, Typography } from 'antd'; import { Typography, Row, Col } from 'antd';
import ListNotification from './component/ListNotification'; import ListNotification from './component/ListNotification';
import DetailNotification from './component/DetailNotification'; import DetailNotification from './component/DetailNotification';
@@ -10,11 +10,7 @@ const { Text } = Typography;
const IndexNotification = memo(function IndexNotification() { const IndexNotification = memo(function IndexNotification() {
const navigate = useNavigate(); const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb(); const { setBreadcrumbItems } = useBreadcrumb();
const [form] = Form.useForm();
const [actionMode, setActionMode] = useState('list');
const [selectedData, setSelectedData] = useState(null); const [selectedData, setSelectedData] = useState(null);
const [isModalVisible, setIsModalVisible] = useState(false);
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
@@ -23,7 +19,7 @@ const IndexNotification = memo(function IndexNotification() {
{ {
title: ( title: (
<Text strong style={{ fontSize: '14px' }}> <Text strong style={{ fontSize: '14px' }}>
Notifikasi Notification
</Text> </Text>
), ),
}, },
@@ -33,39 +29,34 @@ const IndexNotification = memo(function IndexNotification() {
} }
}, [navigate, setBreadcrumbItems]); }, [navigate, setBreadcrumbItems]);
useEffect(() => { const handleCloseDetail = () => {
if (actionMode === 'preview') {
setIsModalVisible(true);
if (selectedData) {
form.setFieldsValue(selectedData);
}
} else {
setIsModalVisible(false);
form.resetFields();
}
}, [actionMode, selectedData, form]);
const handleCancel = () => {
setActionMode('list');
setSelectedData(null); setSelectedData(null);
form.resetFields(); };
// This handler will be passed to ListNotification to update the selected item
const handleSelectNotification = (data) => {
setSelectedData(data);
}; };
return ( return (
<React.Fragment> <Row gutter={16}>
<ListNotification <Col span={selectedData ? 16 : 24}>
actionMode={actionMode} <ListNotification
setActionMode={setActionMode} // The setActionMode is likely not needed anymore,
selectedData={selectedData} // but we pass the selection handler
setSelectedData={setSelectedData} setActionMode={() => {}} // Keep prop for safety, but can be empty
/> setSelectedData={handleSelectNotification}
<DetailNotification />
visible={isModalVisible} </Col>
onCancel={handleCancel} {selectedData && (
form={form} <Col span={8}>
selectedData={selectedData} <DetailNotification
/> selectedData={selectedData}
</React.Fragment> onClose={handleCloseDetail}
/>
</Col>
)}
</Row>
); );
}); });

View File

@@ -1,8 +1,30 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import { Modal, Row, Col, Tag, Divider } from 'antd'; import { Row, Col, Tag, Card, Button } from 'antd';
import { CloseCircleFilled, WarningFilled, CheckCircleFilled, InfoCircleFilled } from '@ant-design/icons'; import {
CloseCircleFilled,
WarningFilled,
CheckCircleFilled,
InfoCircleFilled,
} from '@ant-design/icons';
const DetailNotification = memo(function DetailNotification({ selectedData, onClose }) {
if (!selectedData) {
return null;
}
// Get error code data from the nested structure
const errorCodeData = selectedData.error_code;
// Get active solution (is_active: true) or first solution
const activeSolution = errorCodeData?.solution?.find(sol => sol.is_active) || errorCodeData?.solution?.[0] || {};
const sparepartsData = selectedData.spareparts || errorCodeData?.spareparts || [];
// Determine notification type based on is_read status
const getTypeFromStatus = () => {
if (selectedData.is_read === false) return 'critical'; // Not read yet
if (selectedData.is_delivered === false) return 'warning'; // Not delivered
return 'resolved'; // Read and delivered
};
const DetailNotification = memo(function DetailNotification({ visible, onCancel, form, selectedData }) {
const getIconAndColor = (type) => { const getIconAndColor = (type) => {
switch (type) { switch (type) {
case 'critical': case 'critical':
@@ -36,133 +58,194 @@ const DetailNotification = memo(function DetailNotification({ visible, onCancel,
} }
}; };
const { IconComponent, color, bgColor, tagColor } = selectedData ? getIconAndColor(selectedData.type) : {}; const notificationType = getTypeFromStatus();
const { IconComponent, color, bgColor, tagColor } = getIconAndColor(notificationType);
return ( return (
<Modal <Card
title="Detail Notifikasi" title="Detail Notifikasi"
open={visible} extra={<Button onClick={onClose}>Tutup</Button>}
onCancel={onCancel} style={{ height: '100%' }}
onOk={onCancel} bodyStyle={{ padding: '0 24px' }}
okText="Tutup"
cancelButtonProps={{ style: { display: 'none' } }}
width={700}
> >
{selectedData && ( <div>
<div> {/* Header with Icon and Status */}
{/* Header with Icon and Status */} <div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '0',
padding: '2px 0',
backgroundColor: '#fafafa',
borderRadius: '8px',
}}
>
<div <div
style={{ style={{
width: '32px',
height: '32px',
borderRadius: '50%',
backgroundColor: bgColor,
color: color,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '16px', justifyContent: 'center',
marginBottom: '24px', fontSize: '18px',
padding: '16px', flexShrink: 0,
backgroundColor: '#fafafa',
borderRadius: '8px',
}} }}
> >
<div {IconComponent && <IconComponent style={{ fontSize: '18px' }} />}
style={{
width: '64px',
height: '64px',
borderRadius: '50%',
backgroundColor: bgColor,
color: color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '32px',
flexShrink: 0,
}}
>
{IconComponent && <IconComponent style={{ fontSize: '32px' }} />}
</div>
<div style={{ flex: 1 }}>
<Tag color={tagColor} style={{ marginBottom: '8px', fontSize: '12px' }}>
{selectedData.type.toUpperCase()}
</Tag>
<div style={{ fontSize: '16px', fontWeight: 600, color: '#262626' }}>
{selectedData.title}
</div>
</div>
</div> </div>
<div style={{ flex: 1 }}>
<Divider style={{ margin: '16px 0' }} /> <Tag color={tagColor} style={{ marginBottom: '2px', fontSize: '11px' }}>
{notificationType.toUpperCase()}
{/* Information Grid */}
<Row gutter={[16, 16]}>
<Col span={12}>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
PLC
</div>
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
{selectedData.plc}
</div>
</div>
</Col>
<Col span={12}>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>Tag</div>
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
{selectedData.tag}
</div>
</div>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={12}>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
Engineer
</div>
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
{selectedData.engineer}
</div>
</div>
</Col>
<Col span={12}>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
Waktu
</div>
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
{selectedData.time}
</div>
</div>
</Col>
</Row>
<Divider style={{ margin: '16px 0' }} />
{/* Status */}
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '8px' }}>Status</div>
<Tag color={selectedData.isRead ? 'default' : 'blue'}>
{selectedData.isRead ? 'Sudah Dibaca' : 'Belum Dibaca'}
</Tag> </Tag>
</div> <div style={{ fontSize: '14px', fontWeight: 600, color: '#262626' }}>
{errorCodeData?.error_code_name || 'N/A'}
{/* Additional Info */}
<div
style={{
marginTop: '16px',
padding: '12px',
backgroundColor: '#f6f9ff',
borderRadius: '6px',
border: '1px solid #d6e4ff',
}}
>
<div style={{ fontSize: '12px', color: '#595959' }}>
<strong>Catatan:</strong> Notifikasi ini telah dikirim ke engineer yang bersangkutan
untuk ditindaklanjuti sesuai dengan prosedur yang berlaku.
</div> </div>
</div> </div>
</div> </div>
)}
</Modal> {/* Information Grid */}
<Row gutter={[16, 0]}>
<Col span={12}>
<div style={{ marginBottom: '2px' }}>
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
Kode Error
</div>
<div style={{ fontSize: '13px', color: '#262626', fontWeight: 500 }}>
{errorCodeData?.error_code || 'N/A'}
</div>
</div>
</Col>
<Col span={12}>
<div style={{ marginBottom: '2px' }}>
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
ID Notifikasi
</div>
<div style={{ fontSize: '13px', color: '#262626', fontWeight: 500 }}>
{selectedData.notification_error_id || 'N/A'}
</div>
</div>
</Col>
</Row>
<Row gutter={[16, 0]}>
<Col span={12}>
<div style={{ marginBottom: '2px' }}>
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
Solusi
</div>
<div style={{ fontSize: '13px', color: '#262626', fontWeight: 500 }}>
{activeSolution?.solution_name || 'N/A'}
</div>
</div>
</Col>
<Col span={12}>
<div style={{ marginBottom: '2px' }}>
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
Waktu Dibuat
</div>
<div style={{ fontSize: '13px', color: '#262626', fontWeight: 500 }}>
{selectedData.created_at
? new Date(selectedData.created_at).toLocaleString('id-ID', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
}) + ' WIB'
: 'N/A'}
</div>
</div>
</Col>
</Row>
{/* Status Information */}
<Row gutter={[16, 0]}>
<Col span={8}>
<div style={{ marginBottom: '2px' }}>
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
Status Kirim
</div>
<Tag color={selectedData.is_send ? 'success' : 'error'}>
{selectedData.is_send ? 'Terkirim' : 'Belum Terkirim'}
</Tag>
</div>
</Col>
<Col span={8}>
<div style={{ marginBottom: '2px' }}>
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
Status Terkirim
</div>
<Tag color={selectedData.is_delivered ? 'success' : 'warning'}>
{selectedData.is_delivered ? 'Terkirim' : 'Belum Terkirim'}
</Tag>
</div>
</Col>
<Col span={8}>
<div style={{ marginBottom: '2px' }}>
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '0' }}>
Status Baca
</div>
<Tag color={selectedData.is_read ? 'success' : 'processing'}>
{selectedData.is_read ? 'Dibaca' : 'Belum Dibaca'}
</Tag>
</div>
</Col>
</Row>
{/* Description */}
<div style={{ marginTop: '16px', marginBottom: '8px' }}>
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '4px' }}>
Deskripsi Error
</div>
<div
style={{
fontSize: '13px',
color: '#262626',
fontWeight: 500,
padding: '8px',
backgroundColor: '#fafafa',
borderRadius: '4px',
border: '1px solid #f0f0f0',
}}
>
{selectedData.message_error_issue || 'N/A'}
</div>
</div>
{/* Spareparts Information */}
{sparepartsData.length > 0 && (
<div style={{ marginTop: '16px' }}>
<div style={{ fontSize: '11px', color: '#8c8c8c', marginBottom: '4px' }}>
Spareparts Terkait
</div>
{sparepartsData.map((sparepart, index) => (
<div
key={index}
style={{
padding: '8px',
marginBottom: '4px',
backgroundColor: '#fafafa',
borderRadius: '4px',
border: '1px solid #f0f0f0',
}}
>
<div style={{ fontWeight: 600, marginBottom: '4px' }}>
{sparepart.sparepart_name}
</div>
<div style={{ fontSize: '12px' }}>
Kode: {sparepart.sparepart_code} | Stok:{' '}
{sparepart.sparepart_stok}
</div>
</div>
))}
</div>
)}
</div>
</Card>
); );
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { Card, Table, Tag, Typography } from 'antd';
import { ClockCircleOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
const { Text } = Typography;
const getDummyLogHistory = (notification) => {
if (!notification) return [];
return [
{
key: '1',
timestamp: dayjs().subtract(2, 'hour').format('DD-MM-YYYY HH:mm:ss'),
activity: 'Notification Created',
details: `System generated a ${notification.type} notification for: ${notification.issue}`,
},
{
key: '2',
timestamp: dayjs().subtract(1, 'hour').format('DD-MM-YYYY HH:mm:ss'),
activity: 'Notification Sent',
details: 'Sent to 2 engineers',
},
{
key: '3',
timestamp: dayjs().subtract(30, 'minute').format('DD-MM-YYYY HH:mm:ss'),
activity: 'Notification Read',
details: 'Read by Engineer A',
},
{
key: '4',
timestamp: dayjs().subtract(5, 'minute').format('DD-MM-YYYY HH:mm:ss'),
activity: 'Resend Triggered',
details: 'Notification resent by Admin',
},
];
};
const columns = [
{
title: 'Timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
render: (text) => (
<span>
<ClockCircleOutlined style={{ marginRight: 8 }} />
{text}
</span>
),
},
{
title: 'Activity',
dataIndex: 'activity',
key: 'activity',
render: (text) => {
let color = 'blue';
if (text.includes('Created')) {
color = 'geekblue';
} else if (text.includes('Sent')) {
color = 'purple';
} else if (text.includes('Read')) {
color = 'green';
} else if (text.includes('Triggered')) {
color = 'orange';
}
return <Tag color={color}>{text.toUpperCase()}</Tag>;
},
},
{
title: 'Details',
dataIndex: 'details',
key: 'details',
},
];
const LogHistoryCard = ({ notificationData }) => {
const logHistoryData = getDummyLogHistory(notificationData);
return (
<Card
title="Log History"
size="small"
style={{ height: '100%' }}
>
<Table
columns={columns}
dataSource={logHistoryData}
pagination={false} // Remove pagination entirely
size="small"
scroll={{ y: 200 }} // Use scroll for overflow, adjust height as needed
/>
</Card>
);
};
export default LogHistoryCard;

View File

@@ -0,0 +1,102 @@
import React from 'react';
import { Modal, Table, Tag, Typography } from 'antd';
import { ClockCircleOutlined, UserOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
const { Text } = Typography;
// Dummy data untuk log history
const getDummyLogHistory = (notification) => {
if (!notification) return [];
return [
{
key: '1',
timestamp: dayjs().subtract(2, 'hour').format('DD-MM-YYYY HH:mm:ss'),
activity: 'Notification Created',
details: `System generated a ${notification.type} notification for: ${notification.issue}`,
},
{
key: '2',
timestamp: dayjs().subtract(1, 'hour').format('DD-MM-YYYY HH:mm:ss'),
activity: 'Notification Sent',
details: 'Sent to 2 engineers',
},
{
key: '3',
timestamp: dayjs().subtract(30, 'minute').format('DD-MM-YYYY HH:mm:ss'),
activity: 'Notification Read',
details: 'Read by Engineer A',
},
{
key: '4',
timestamp: dayjs().subtract(5, 'minute').format('DD-MM-YYYY HH:mm:ss'),
activity: 'Resend Triggered',
details: 'Notification resent by Admin',
},
];
};
const columns = [
{
title: 'Timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
render: (text) => (
<span>
<ClockCircleOutlined style={{ marginRight: 8 }} />
{text}
</span>
),
},
{
title: 'Activity',
dataIndex: 'activity',
key: 'activity',
render: (text) => {
let color = 'blue';
if (text.includes('Created')) {
color = 'geekblue';
} else if (text.includes('Sent')) {
color = 'purple';
} else if (text.includes('Read')) {
color = 'green';
} else if (text.includes('Triggered')) {
color = 'orange';
}
return <Tag color={color}>{text.toUpperCase()}</Tag>;
},
},
{
title: 'Details',
dataIndex: 'details',
key: 'details',
},
];
const LogHistoryModal = ({ visible, onCancel, notificationData }) => {
const logHistoryData = getDummyLogHistory(notificationData);
return (
<Modal
title={
<Text strong>
Log History: <Text type="secondary">{notificationData?.title}</Text>
</Text>
}
open={visible}
onCancel={onCancel}
footer={null}
width={800}
destroyOnClose
>
<Table
columns={columns}
dataSource={logHistoryData}
pagination={{ pageSize: 5 }}
style={{ marginTop: 24 }}
/>
</Modal>
);
};
export default LogHistoryModal;

View File

@@ -0,0 +1,132 @@
import React from 'react';
import { Modal, Typography, Card, Row, Col, Avatar, Tag, Button, Space } from 'antd';
import {
UserOutlined,
PhoneOutlined,
CheckCircleOutlined,
SyncOutlined,
SendOutlined,
} from '@ant-design/icons';
const { Text } = Typography;
// Dummy data baru untuk user history
const getDummyUsers = (notification) => {
if (!notification) return [];
return [
{
id: '1',
name: 'Budi Santoso',
phone: '081234567890',
status: 'delivered',
},
{
id: '2',
name: 'Citra Lestari',
phone: '082345678901',
status: 'sent',
},
{
id: '3',
name: 'Agus Wijaya',
phone: '083456789012',
status: 'failed',
},
{
id: '4',
name: 'Dewi Anggraini',
phone: '084567890123',
status: 'delivered',
},
];
};
const UserHistoryModal = ({ visible, onCancel, notificationData }) => {
const userData = getDummyUsers(notificationData);
const getStatusTag = (status) => {
switch (status) {
case 'delivered':
return (
<Tag icon={<CheckCircleOutlined />} color="success">
Delivered
</Tag>
);
case 'sent':
return (
<Tag icon={<SyncOutlined spin />} color="processing">
Sent
</Tag>
);
case 'failed':
return <Tag color="error">Failed</Tag>;
default:
return <Tag color="default">{status}</Tag>;
}
};
return (
<Modal
title={
<Text strong style={{ fontSize: '18px' }}>
History User Notification
</Text>
}
open={visible}
onCancel={onCancel}
footer={[
<Button key="back" onClick={onCancel}>
Close
</Button>,
]}
width={600}
destroyOnClose
>
<div style={{ maxHeight: '60vh', overflowY: 'auto', padding: '8px' }}>
<Space direction="vertical" style={{ width: '100%' }}>
{userData.map((user) => (
<Card key={user.id} size="small" style={{ width: '100%' }}>
<Row align="middle" justify="space-between">
<Col>
<Space align="center">
<Avatar size="large" icon={<UserOutlined />} />
<div>
<Text strong>{user.name}</Text>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
>
<PhoneOutlined style={{ color: '#8c8c8c' }} />
<Text type="secondary">{user.phone}</Text>
</div>
</div>
</Space>
</Col>
<Col>
<Space align="center" size="large">
{getStatusTag(user.status)}
<Button
type="primary"
icon={<SendOutlined />}
onClick={(e) => {
e.stopPropagation();
console.log(`Resend to ${user.name}`);
}}
>
Resend
</Button>
</Space>
</Col>
</Row>
</Card>
))}
</Space>
</div>
</Modal>
);
};
export default UserHistoryModal;

View File

@@ -0,0 +1,92 @@
import React from 'react';
import { Button, Row, Col, Card, Badge, Typography, Space, Divider } from 'antd';
import {
SendOutlined,
MobileOutlined,
CheckCircleFilled,
ArrowLeftOutlined,
} from '@ant-design/icons';
const { Text } = Typography;
// Dummy data for user history
const userHistoryData = [
{
id: 1,
name: 'John Doe',
phone: '081234567890',
status: 'Delivered',
timestamp: '04-11-2025 11:40 WIB',
},
{
id: 2,
name: 'Jane Smith',
phone: '087654321098',
status: 'Delivered',
timestamp: '04-11-2025 11:41 WIB',
},
{
id: 3,
name: 'Peter Jones',
phone: '082345678901',
status: 'Delivered',
timestamp: '04-11-2025 11:42 WIB',
},
];
const UserHistory = ({ notification, onBack }) => {
return (
<Card>
<Row justify="space-between" align="middle" style={{ marginBottom: '20px' }}>
<Col>
<Space align="center">
<Button type="text" icon={<ArrowLeftOutlined />} onClick={onBack} />
<Typography.Title level={4} style={{ margin: 0 }}>
History User Notification
</Typography.Title>
</Space>
<Text type="secondary" style={{ marginLeft: '40px' }}>
{notification.title} - {notification.issue}
</Text>
</Col>
</Row>
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
{userHistoryData.map((user) => (
<Card
key={user.id}
style={{ backgroundColor: '#e6f7ff', borderColor: '#91d5ff' }}
>
<Row align="middle" justify="space-between">
<Col>
<Space align="center">
<Text strong>{user.name}</Text>
<Text>|</Text>
<Text>
<MobileOutlined /> {user.phone}
</Text>
<Text>|</Text>
<Badge status="success" text={user.status} />
</Space>
<Divider style={{ margin: '8px 0' }} />
<Space align="center">
<CheckCircleFilled style={{ color: '#52c41a' }} />
<Text type="secondary">
Success Delivered at {user.timestamp}
</Text>
</Space>
</Col>
<Col>
<Button type="primary" ghost icon={<SendOutlined />}>
Resend
</Button>
</Col>
</Row>
</Card>
))}
</Space>
</Card>
);
};
export default UserHistory;

View 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;

View File

@@ -1,71 +1,806 @@
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 {
getAllHistoryValueReportPivot,
getAllHistoryValueReport,
} from '../../../../api/history-value';
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 = [ const dateNow = dayjs();
{ const dateNowFormated = dateNow.format('YYYY-MM-DD');
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{
title: 'Datetime',
dataIndex: 'datetime',
key: 'datetime',
width: '10%',
},
{
title: 'Tag Name',
dataIndex: 'tag_name',
key: 'tag_name',
width: '70%',
},
{
title: 'Value',
dataIndex: 'val',
key: 'val',
width: '10%',
},
{
title: 'Stat',
dataIndex: 'stat',
key: 'stat',
width: '10%',
},
];
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 defaultFilter = { search: '' }; const [plantSubSection, setPlantSubSection] = useState(0);
const [formDataFilter, setFormDataFilter] = useState(defaultFilter); const [plantSubSectionList, setPlantSubSectionList] = useState([]);
const [startDate, setStartDate] = useState(dateNow);
const [endDate, setEndDate] = useState(dateNow);
const [periode, setPeriode] = useState(30);
const [plantSubSection, setPlantSubSection] = useState('Semua Plant'); const generateFullDayTimes = (dateString, intervalMinutes) => {
const [startDate, setStartDate] = useState(dayjs('2025-09-30')); const times = [];
const [endDate, setEndDate] = useState(dayjs('2025-10-09')); const startOfDay = dayjs(dateString).startOf('day');
const [periode, setPeriode] = useState('10 Menit'); const endOfDay = dayjs(dateString).endOf('day');
const getAllReport = async (params) => { let currentTime = startOfDay;
return {
data: [], 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 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 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);
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('Semua Plant'); setPlantSubSection(0);
setStartDate(dayjs('2025-09-30')); setStartDate(dateNow);
setEndDate(dayjs('2025-10-09')); setEndDate(dateNow);
setPeriode('10 Menit'); setPeriode(30);
setTableData([]);
setColumns([]);
setPivotData([]);
setValueReportData([]);
setPagination({
current: 1,
pageSize: 10,
total: 0,
});
};
const getPlantSubSection = async () => {
const params = new URLSearchParams({ page: 1 });
const response = await getAllPlantSection(params);
if (response && response.data) {
const activePlantSubSections = response.data.filter(
(section) => section.is_active === true
);
setPlantSubSectionList(activePlantSubSections);
}
};
useEffect(() => {
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}>
@@ -77,14 +812,21 @@ const ListReport = memo(function ListReport(props) {
</Text> </Text>
<Select <Select
value={plantSubSection} value={plantSubSection}
onChange={setPlantSubSection} onChange={(value) => setPlantSubSection(value)}
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
options={[ >
{ value: 'Semua Plant', label: 'Semua Plant' }, <Select.Option key={0} value={0}>
{ value: 'Plant 1', label: 'Plant 1' }, Pilih Plant Sub Section
{ value: 'Plant 2', label: 'Plant 2' }, </Select.Option>
]} {plantSubSectionList.map((item) => (
/> <Select.Option
key={item.plant_sub_section_id}
value={item.plant_sub_section_id}
>
{item.plant_sub_section_name}
</Select.Option>
))}
</Select>
</div> </div>
</Col> </Col>
<Col xs={24} sm={12} md={6}> <Col xs={24} sm={12} md={6}>
@@ -95,7 +837,7 @@ const ListReport = memo(function ListReport(props) {
<DatePicker <DatePicker
value={startDate} value={startDate}
onChange={setStartDate} onChange={setStartDate}
format="DD/MM/YYYY" format="DD-MM-YYYY"
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
/> />
</div> </div>
@@ -108,7 +850,7 @@ const ListReport = memo(function ListReport(props) {
<DatePicker <DatePicker
value={endDate} value={endDate}
onChange={setEndDate} onChange={setEndDate}
format="DD/MM/YYYY" format="DD-MM-YYYY"
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
/> />
</div> </div>
@@ -120,20 +862,43 @@ 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 Menit', label: '5 Menit' },
{ value: '10 Menit', label: '10 Menit' },
{ value: '30 Menit', label: '30 Menit' },
{ value: '1 Jam', label: '1 Jam' },
]}
/> />
</div> </div>
</Col> </Col>
</Row> </Row>
<Row gutter={8} style={{ marginTop: '16px' }}> <Row gutter={8} style={{ marginTop: '16px' }}>
<Col> <Col>
<Button type="primary" danger icon={<FileTextOutlined />}> <Button
Tampilkan type="primary"
danger
icon={<FileTextOutlined />}
onClick={handleSearch}
disabled={false}
>
Show
</Button>
</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> </Button>
</Col> </Col>
<Col> <Col>
@@ -146,13 +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}>
getData={getAllReport} <div style={{ overflowX: 'auto', width: '100%' }}>
queryParams={formDataFilter} <Table
columns={columns} columns={columns}
triger={trigerFilter} dataSource={tableData}
/> 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>
@@ -160,4 +938,4 @@ const ListReport = memo(function ListReport(props) {
); );
}); });
export default ListReport; export default ListReport;

View File

@@ -9,7 +9,6 @@ const { Text } = Typography;
const IndexTrending = memo(function IndexTrending() { const IndexTrending = memo(function IndexTrending() {
const navigate = useNavigate(); const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb(); const { setBreadcrumbItems } = useBreadcrumb();
const [selectedData, setSelectedData] = useState(null);
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');

View File

@@ -1,80 +1,300 @@
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 { getAllHistoryValueTrendingPivot } from '../../../api/history-value';
const { Text } = Typography; const { Text } = Typography;
const tagTrendingData = [
{
id: 'TEMP_SENSOR_1',
color: '#FF6B4A',
data: [
{ y: '08:00', x: 75 },
{ y: '08:05', x: 76 },
{ y: '08:10', x: 75 },
{ y: '08:15', x: 77 },
{ y: '08:20', x: 76 },
{ y: '08:25', x: 78 },
{ y: '08:30', x: 79 },
],
},
{
id: 'GAS_LEAK_SENSOR_1',
color: '#4ECDC4',
data: [
{ y: '08:00', x: 10 },
{ y: '08:05', x: 150 },
{ y: '08:10', x: 40 },
{ y: '08:15', x: 20 },
{ y: '08:20', x: 15 },
{ y: '08:25', x: 18 },
{ y: '08:30', x: 25 },
],
},
{
id: 'PRESSURE_SENSOR_1',
color: '#FFE66D',
data: [
{ y: '08:00', x: 1.2 },
{ y: '08:05', x: 1.3 },
{ y: '08:10', x: 1.2 },
{ y: '08:15', x: 1.4 },
{ y: '08:20', x: 1.5 },
{ y: '08:25', x: 1.3 },
{ y: '08:30', x: 1.2 },
],
},
];
const ReportTrending = memo(function ReportTrending(props) { const ReportTrending = memo(function ReportTrending(props) {
const [trigerFilter, setTrigerFilter] = useState(false); const dateNow = dayjs();
const dateNowFormated = dateNow.format('YYYY-MM-DD');
const defaultFilter = { search: '' }; const [plantSubSection, setPlantSubSection] = useState(0);
const [plantSubSectionList, setPlantSubSectionList] = useState([]);
const [startDate, setStartDate] = useState(dateNow);
const [endDate, setEndDate] = useState(dateNow);
const [periode, setPeriode] = useState(60);
const [isLoading, setIsLoading] = useState(false);
const defaultFilter = {
criteria: '',
plant_sub_section_id: plantSubSection,
from: dateNowFormated,
to: dateNowFormated,
interval: periode,
};
const [formDataFilter, setFormDataFilter] = useState(defaultFilter); const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
const [plantSubSection, setPlantSubSection] = useState('Semua Plant'); const [trendingValue, setTrendingValue] = useState([]);
const [startDate, setStartDate] = useState(dayjs('2025-09-30')); const [chartData, setChartData] = useState([]);
const [endDate, setEndDate] = useState(dayjs('2025-10-09')); const [metrics, setMetrics] = useState([]);
const [periode, setPeriode] = useState('10 Menit');
const getAllReport = async (params) => { // Palet warna
return { const colorPalette = [
data: [], '#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
}; '#ec4899', '#14b8a6', '#f97316', '#6366f1', '#84cc16'
];
const handleSearch = async () => {
setIsLoading(true);
try {
const formattedDateStart = startDate.format('YYYY-MM-DD');
const formattedDateEnd = endDate.format('YYYY-MM-DD');
const newFilter = {
criteria: '',
plant_sub_section_id: plantSubSection,
from: formattedDateStart,
to: formattedDateEnd,
interval: periode,
};
setFormDataFilter(newFilter);
const param = new URLSearchParams(newFilter);
const response = await getAllHistoryValueTrendingPivot(param);
if (response?.data?.length > 0) {
transformDataForRecharts(response.data);
} else {
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('Semua Plant'); setPlantSubSection(0);
setStartDate(dayjs('2025-09-30')); setStartDate(dateNow);
setEndDate(dayjs('2025-10-09')); setEndDate(dateNow);
setPeriode('10 Menit'); setPeriode(60);
setChartData([]);
setMetrics([]);
}; };
const getPlantSubSection = async () => {
const params = new URLSearchParams({ page: 1 });
const response = await getAllPlantSection(params);
if (response && response.data) {
const activePlantSubSections = response.data.filter(
(section) => section.is_active === true
);
setPlantSubSectionList(activePlantSubSections);
}
};
// 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(() => {
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}>
@@ -86,14 +306,21 @@ const ReportTrending = memo(function ReportTrending(props) {
</Text> </Text>
<Select <Select
value={plantSubSection} value={plantSubSection}
onChange={setPlantSubSection} onChange={(value) => setPlantSubSection(value)}
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
options={[ >
{ value: 'Semua Plant', label: 'Semua Plant' }, <Select.Option key={0} value={0}>
{ value: 'Plant 1', label: 'Plant 1' }, Pilih Plant Sub Section
{ value: 'Plant 2', label: 'Plant 2' }, </Select.Option>
]} {plantSubSectionList.map((item) => (
/> <Select.Option
key={item.plant_sub_section_id}
value={item.plant_sub_section_id}
>
{item.plant_sub_section_name}
</Select.Option>
))}
</Select>
</div> </div>
</Col> </Col>
<Col xs={24} sm={12} md={6}> <Col xs={24} sm={12} md={6}>
@@ -104,7 +331,7 @@ const ReportTrending = memo(function ReportTrending(props) {
<DatePicker <DatePicker
value={startDate} value={startDate}
onChange={setStartDate} onChange={setStartDate}
format="DD/MM/YYYY" format="DD-MM-YYYY"
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
/> />
</div> </div>
@@ -117,7 +344,7 @@ const ReportTrending = memo(function ReportTrending(props) {
<DatePicker <DatePicker
value={endDate} value={endDate}
onChange={setEndDate} onChange={setEndDate}
format="DD/MM/YYYY" format="DD-MM-YYYY"
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
/> />
</div> </div>
@@ -130,19 +357,26 @@ const ReportTrending = memo(function ReportTrending(props) {
onChange={setPeriode} onChange={setPeriode}
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
options={[ options={[
{ value: '5 Menit', label: '5 Menit' }, { value: 5, label: '5 Minute' },
{ value: '10 Menit', label: '10 Menit' }, { value: 10, label: '10 Minute' },
{ value: '30 Menit', label: '30 Menit' }, { value: 30, label: '30 Minute' },
{ value: '1 Jam', label: '1 Jam' }, { value: 60, label: '1 Hour' },
{ value: 120, label: '2 Hour' },
]} ]}
/> />
</div> </div>
</Col> </Col>
</Row> </Row>
<Row gutter={8} style={{ marginTop: '16px' }}> <Row gutter={8} style={{ marginTop: '16px' }}>
<Col> <Col>
<Button type="primary" danger icon={<FileTextOutlined />}> <Button
Tampilkan type="primary"
danger
icon={<FileTextOutlined />}
onClick={handleSearch}
>
Show
</Button> </Button>
</Col> </Col>
<Col> <Col>
@@ -155,63 +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' }}>
<ResponsiveLine {renderChart()}
data={tagTrendingData}
margin={{ top: 20, right: 20, bottom: 50, left: 60 }}
xScale={{
type: 'linear',
min: 'auto',
max: 'auto',
stacked: false,
reverse: false,
}}
yScale={{
type: 'point',
}}
curve="natural"
axisBottom={{
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: 'Value',
legendOffset: 40,
legendPosition: 'middle',
}}
axisLeft={{
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: 'Time',
legendOffset: -45,
legendPosition: 'middle',
}}
colors={{ datum: 'color' }}
pointSize={6}
pointColor={{ theme: 'background' }}
pointBorderWidth={2}
pointBorderColor={{ from: 'serieColor' }}
pointLabelYOffset={-12}
useMesh={true}
legends={[
{
anchor: 'bottom-right',
direction: 'column',
justify: false,
translateX: 100,
translateY: 0,
itemsSpacing: 2,
itemDirection: 'left-to-right',
itemWidth: 80,
itemHeight: 20,
itemOpacity: 0.75,
symbolSize: 12,
symbolShape: 'circle',
},
]}
/>
</div>
</Col> </Col>
</Row> </Row>
</Card> </Card>
@@ -219,4 +399,4 @@ const ReportTrending = memo(function ReportTrending(props) {
); );
}); });
export default ReportTrending; export default ReportTrending;

View File

@@ -42,7 +42,7 @@ const DetailRole = (props) => {
setConfirmLoading(true); setConfirmLoading(true);
const validationRules = [ const validationRules = [
{ field: 'role_name', label: 'Nama Role', required: true }, { field: 'role_name', label: 'Role name', required: true },
{ field: 'role_level', label: 'Level', required: true }, { field: 'role_level', label: 'Level', required: true },
]; ];
@@ -169,7 +169,7 @@ const DetailRole = (props) => {
<Row gutter={16}> <Row gutter={16}>
<Col span={12}> <Col span={12}>
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text strong>Nama Role</Text> <Text strong>Role name</Text>
<Text style={{ color: 'red' }}> *</Text> <Text style={{ color: 'red' }}> *</Text>
<Input <Input
name="role_name" name="role_name"
@@ -196,7 +196,7 @@ const DetailRole = (props) => {
</Col> </Col>
</Row> </Row>
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text strong>Deskripsi Role</Text> <Text strong>Role Description</Text>
<TextArea <TextArea
name="role_description" name="role_description"
value={formData.role_description} value={formData.role_description}

View File

@@ -28,7 +28,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
hidden: true, hidden: true,
}, },
{ {
title: 'Nama Role', title: 'Role Name',
dataIndex: 'role_name', dataIndex: 'role_name',
key: 'role_name', key: 'role_name',
width: '25%', width: '25%',
@@ -41,7 +41,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
align: 'center', align: 'center',
}, },
{ {
title: 'Deskripsi', title: 'Description',
dataIndex: 'role_description', dataIndex: 'role_description',
key: 'role_description', key: 'role_description',
width: '35%', width: '35%',
@@ -67,7 +67,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
), ),
}, },
{ {
title: 'Aksi', title: 'Action',
key: 'aksi', key: 'aksi',
align: 'center', align: 'center',
width: '15%', width: '15%',
@@ -229,7 +229,7 @@ const ListRole = memo(function ListRole(props) {
onClick={() => showAddModal()} onClick={() => showAddModal()}
size="large" size="large"
> >
Tambah Data Add Role
</Button> </Button>
</ConfigProvider> </ConfigProvider>
</Space> </Space>

View File

@@ -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({

View File

@@ -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>

View File

@@ -192,7 +192,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog, showApproval
}, },
}, },
{ {
title: 'Aksi', title: 'Action',
key: 'aksi', key: 'aksi',
align: 'center', align: 'center',
width: '12%', width: '12%',

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { Layout, Card, Row, Col, Typography, Button, Input } from 'antd';
import { ArrowLeftOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router-dom';
const { Content } = Layout;
const { Title, Paragraph } = Typography;
const { Search } = Input;
const IndexVerificationSparepart = () => {
const navigate = useNavigate();
const { notification_error_id } = useParams();
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 }}
>
Kembali ke daftar notifikasi
</Button>
</Col>
</Row>
<div style={{ backgroundColor: '#f6ffed', border: '1px solid #b7eb8f', borderRadius: '4px 4px 0 0', padding: '8px 16px', marginTop: '16px' }}>
<Row justify="center" align="middle">
<Col>
<Title level={4} style={{ margin: 0, color: '#262626' }}>
List Available Sparepart
</Title>
</Col>
</Row>
<Paragraph style={{ margin: '4px 0 0', color: '#595959', textAlign: 'center' }}>
Select items from inventory to save changes
</Paragraph>
</div>
<div style={{ border: '1px solid #91d5ff', borderTop: 'none', backgroundColor: '#e6f7ff', padding: '12px 16px', borderRadius: '0 0 4px 4px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<ExclamationCircleOutlined style={{ color: '#1890ff', fontSize: '20px', marginRight: '12px' }} />
<Paragraph style={{ margin: 0 }}>
<strong>Important Notice:</strong> All items listed are currently in stock and available for immediate use. Please verify part numbers before installation. Selected items will be marked for inventory tracking.
</Paragraph>
</div>
</div>
<Row justify="space-between" align="middle" style={{ marginBottom: '24px' }}>
<Col>
<Title level={5} style={{ margin: 0 }}> Inventory</Title>
</Col>
<Col>
<Search
placeholder="Search in inventory"
onSearch={value => console.log(value)}
style={{ width: 200 }}
/>
</Col>
</Row>
{/* Konten untuk verifikasi spare part akan ditambahkan di sini */}
<div style={{ textAlign: 'center', padding: '40px 0' }}>
<Title level={5}>ID Notifikasi: {notification_error_id}</Title>
<p>Halaman ini dalam pengembangan. Di sini akan ditampilkan detail spare part yang perlu diverifikasi.</p>
</div>
</Card>
</Content>
</Layout>
);
};
export default IndexVerificationSparepart;