diff --git a/src/App.jsx b/src/App.jsx index 9f77ca2..498ca76 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -36,6 +36,13 @@ import IndexUser from './pages/user/IndexUser'; import IndexMember from './pages/shiftManagement/member/IndexMember'; import SvgTest from './pages/home/SvgTest'; +import SvgOverview from './pages/home/SvgOverview'; +import SvgCompressorA from './pages/home/SvgCompressorA'; +import SvgCompressorB from './pages/home/SvgCompressorB'; +import SvgCompressorC from './pages/home/SvgCompressorC'; +import SvgAirDryerA from './pages/home/SvgAirDryerA'; +import SvgAirDryerB from './pages/home/SvgAirDryerB'; +import SvgAirDryerC from './pages/home/SvgAirDryerC'; const App = () => { return ( @@ -53,6 +60,16 @@ const App = () => { } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + }> } /> } /> diff --git a/src/api/master-status.jsx b/src/api/master-status.jsx new file mode 100644 index 0000000..d5ed80d --- /dev/null +++ b/src/api/master-status.jsx @@ -0,0 +1,168 @@ +import { SendRequest } from '../components/Global/ApiRequest'; + +const getAllStatus = async (queryParams) => { + try { + const response = await SendRequest({ + method: 'get', + prefix: `status?${queryParams.toString()}`, + }); + + if (response.error) { + console.error('getAllStatus error response:', response); + return { + status: response.statusCode || 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: response.message + }; + } + + if (response.paging) { + const totalData = response.data?.[0]?.total_data || response.rows || response.data?.length || 0; + + return { + status: response.statusCode || 200, + data: { + data: response.data || [], + paging: { + page: response.paging.current_page || 1, + limit: response.paging.current_limit || 10, + total: totalData, + page_total: response.paging.total_page || Math.ceil(totalData / (response.paging.current_limit || 10)) + }, + total: totalData + } + }; + } + + const params = Object.fromEntries(queryParams); + const currentPage = parseInt(params.page) || 1; + const currentLimit = parseInt(params.limit) || 10; + + const allData = response.data || []; + const totalData = allData.length; + + const startIndex = (currentPage - 1) * currentLimit; + const endIndex = startIndex + currentLimit; + const paginatedData = allData.slice(startIndex, endIndex); + + return { + status: response.statusCode || 200, + data: { + data: paginatedData, + paging: { + page: currentPage, + limit: currentLimit, + total: totalData, + page_total: Math.ceil(totalData / currentLimit) + }, + total: totalData + } + }; + } catch (error) { + console.error('getAllStatus catch error:', error); + return { + status: 500, + data: { + data: [], + paging: { + page: 1, + limit: 10, + total: 0, + page_total: 0 + }, + total: 0 + }, + error: error.message + }; + } +}; + +const getStatusById = async (id) => { + const response = await SendRequest({ + method: 'get', + prefix: `status/${id}`, + }); + return response.data; +}; + +const createStatus = async (queryParams) => { + const response = await SendRequest({ + method: 'post', + prefix: `status`, + params: queryParams, + }); + + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const updateStatus = async (status_id, queryParams) => { + const response = await SendRequest({ + method: 'put', + prefix: `status/${status_id}`, + params: queryParams, + }); + + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + return { + statusCode: response.statusCode || 200, + data: response.data?.[0] || response.data, + message: response.message, + rows: response.rows + }; +}; + +const deleteStatus = async (queryParams) => { + const response = await SendRequest({ + method: 'delete', + prefix: `status/${queryParams}`, + }); + + if (response.error) { + return { + statusCode: response.statusCode || 500, + data: null, + message: response.message || 'Request failed', + rows: 0 + }; + } + + return { + statusCode: response.statusCode || 200, + data: response.data, + message: response.message, + rows: response.rows + }; +}; + +export { getAllStatus, getStatusById, createStatus, updateStatus, deleteStatus }; \ No newline at end of file diff --git a/src/layout/LayoutMenu.jsx b/src/layout/LayoutMenu.jsx index 6e0bc57..e3c9d62 100644 --- a/src/layout/LayoutMenu.jsx +++ b/src/layout/LayoutMenu.jsx @@ -26,6 +26,9 @@ import { TeamOutlined, ClockCircleOutlined, CalendarOutlined, + DesktopOutlined, + NodeExpandOutlined, + GroupOutlined, } from '@ant-design/icons'; const { Text } = Typography; @@ -40,6 +43,48 @@ const allItems = [ ), }, + { + key: 'dashboard-svg', + icon: , + label: 'Dashboard', + children: [ + { + key: 'dashboard-svg-overview', + icon: , + label: Overview, + }, + { + key: 'dashboard-svg-compressor-a', + icon: , + label: Compressor A, + }, + { + key: 'dashboard-svg-compressor-b', + icon: , + label: Compressor B, + }, + { + key: 'dashboard-svg-compressor-c', + icon: , + label: Compressor C, + }, + { + key: 'dashboard-svg-airdryer-a', + icon: , + label: Air Dryer A, + }, + { + key: 'dashboard-svg-airdryer-b', + icon: , + label: Air Dryer B, + }, + { + key: 'dashboard-svg-airdryer-c', + icon: , + label: Air Dryer C, + }, + ], + }, { key: 'master', icon: , @@ -60,16 +105,16 @@ const allItems = [ icon: , label: Device, }, - { - key: 'master-tag', - icon: , - label: Tag, - }, { key: 'master-unit', icon: , label: Unit, }, + { + key: 'master-tag', + icon: , + label: Tag, + }, { key: 'master-status', icon: , @@ -163,6 +208,7 @@ const LayoutMenu = () => { if (pathname === '/notification') return 'notification'; if (pathname === '/event-alarm') return 'event-alarm'; if (pathname === '/jadwal-shift') return 'jadwal-shift'; + if (pathname === '/dashboard-svg') return 'dashboard-svg'; // Handle master routes if (pathname.startsWith('/master/')) { @@ -170,6 +216,12 @@ const LayoutMenu = () => { return `master-${subPath}`; } + // Handle master routes + if (pathname.startsWith('/dashboard-svg/')) { + const subPath = pathParts[1]; + return `dashboard-svg-${subPath}`; + } + // Handle history routes if (pathname.startsWith('/history/')) { const subPath = pathParts[1]; @@ -188,6 +240,7 @@ const LayoutMenu = () => { // Function to get parent key from menu key const getParentKey = (key) => { if (key.startsWith('master-')) return 'master'; + if (key.startsWith('dashboard-svg-')) return 'dashboard-svg'; if (key.startsWith('history-')) return 'history'; if (key.startsWith('shift-')) return 'shift-management'; return null; diff --git a/src/pages/home/SvgAirDryerA.jsx b/src/pages/home/SvgAirDryerA.jsx new file mode 100644 index 0000000..37f07a3 --- /dev/null +++ b/src/pages/home/SvgAirDryerA.jsx @@ -0,0 +1,20 @@ +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'; + +const { Text } = Typography; + +const filePathSvg = '/svg/air_dryer_A_rev.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgAirDryerA = () => { + return ( + + + + ); +}; + +export default SvgAirDryerA; diff --git a/src/pages/home/SvgAirDryerB.jsx b/src/pages/home/SvgAirDryerB.jsx new file mode 100644 index 0000000..6250cea --- /dev/null +++ b/src/pages/home/SvgAirDryerB.jsx @@ -0,0 +1,20 @@ +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'; + +const { Text } = Typography; + +const filePathSvg = '/svg/air_dryer_B_rev.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgAirDryerB = () => { + return ( + + + + ); +}; + +export default SvgAirDryerB; diff --git a/src/pages/home/SvgAirDryerC.jsx b/src/pages/home/SvgAirDryerC.jsx new file mode 100644 index 0000000..cdb61ce --- /dev/null +++ b/src/pages/home/SvgAirDryerC.jsx @@ -0,0 +1,20 @@ +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'; + +const { Text } = Typography; + +const filePathSvg = '/svg/air_dryer_C_rev.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgAirDryerC = () => { + return ( + + + + ); +}; + +export default SvgAirDryerC; diff --git a/src/pages/home/SvgCompressorA.jsx b/src/pages/home/SvgCompressorA.jsx new file mode 100644 index 0000000..bc7ffd2 --- /dev/null +++ b/src/pages/home/SvgCompressorA.jsx @@ -0,0 +1,20 @@ +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'; + +const { Text } = Typography; + +const filePathSvg = '/svg/test-new.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgCompressorA = () => { + return ( + + + + ); +}; + +export default SvgCompressorA; diff --git a/src/pages/home/SvgCompressorB.jsx b/src/pages/home/SvgCompressorB.jsx new file mode 100644 index 0000000..fbf5871 --- /dev/null +++ b/src/pages/home/SvgCompressorB.jsx @@ -0,0 +1,20 @@ +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'; + +const { Text } = Typography; + +const filePathSvg = '/svg/test-new.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgCompressorB = () => { + return ( + + + + ); +}; + +export default SvgCompressorB; diff --git a/src/pages/home/SvgCompressorC.jsx b/src/pages/home/SvgCompressorC.jsx new file mode 100644 index 0000000..0983260 --- /dev/null +++ b/src/pages/home/SvgCompressorC.jsx @@ -0,0 +1,20 @@ +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'; + +const { Text } = Typography; + +const filePathSvg = '/svg/test-new.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgCompressorC = () => { + return ( + + + + ); +}; + +export default SvgCompressorC; diff --git a/src/pages/home/SvgOverview.jsx b/src/pages/home/SvgOverview.jsx new file mode 100644 index 0000000..d5b432d --- /dev/null +++ b/src/pages/home/SvgOverview.jsx @@ -0,0 +1,20 @@ +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'; + +const { Text } = Typography; + +const filePathSvg = '/svg/test-new.svg'; +const topicMqtt = 'PIU_GGCP/Devices/PB'; + +const SvgOverview = () => { + return ( + + + + ); +}; + +export default SvgOverview; diff --git a/src/pages/home/SvgTemplate.jsx b/src/pages/home/SvgTemplate.jsx new file mode 100644 index 0000000..da2a702 --- /dev/null +++ b/src/pages/home/SvgTemplate.jsx @@ -0,0 +1,19 @@ +const SvgTemplate = ({ children }) => { + return ( +
+ {children} +
+ ); +}; + +export default SvgTemplate; diff --git a/src/pages/home/SvgTest.jsx b/src/pages/home/SvgTest.jsx index 1b65a69..87d8beb 100644 --- a/src/pages/home/SvgTest.jsx +++ b/src/pages/home/SvgTest.jsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { Card, Typography, Flex } from 'antd'; // import { ReactSVG } from 'react-svg'; import { setValSvg } from '../../components/Global/MqttConnection'; +import { ReactSVG } from 'react-svg'; const { Text } = Typography; diff --git a/src/pages/home/SvgViewer.jsx b/src/pages/home/SvgViewer.jsx new file mode 100644 index 0000000..242f012 --- /dev/null +++ b/src/pages/home/SvgViewer.jsx @@ -0,0 +1,19 @@ +// SvgViewer.jsx +import { ReactSVG } from 'react-svg'; + +const SvgViewer = ({ filePathSvg, topicMqtt, setValSvg }) => { + return ( + { + svg.setAttribute('width', '100%'); + svg.setAttribute('height', '100%'); + svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); + if (setValSvg) setValSvg(topicMqtt, svg); + }} + style={{ width: '100%', height: '100%' }} + /> + ); +}; + +export default SvgViewer; diff --git a/src/pages/master/shift/component/DetailShift.jsx b/src/pages/master/shift/component/DetailShift.jsx index d8bd410..86d1b8d 100644 --- a/src/pages/master/shift/component/DetailShift.jsx +++ b/src/pages/master/shift/component/DetailShift.jsx @@ -2,6 +2,10 @@ import { useEffect, useState } from 'react'; import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider } from 'antd'; import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; import { createShift, updateShift } from '../../../../api/master-shift'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; + +dayjs.extend(utc); const { Text } = Typography; @@ -198,16 +202,13 @@ const DetailShift = (props) => { }); }; - // Helper function to extract time from ISO timestamp + // Helper function to extract time from ISO timestamp using dayjs const extractTime = (timeString) => { if (!timeString) return ''; // If it's ISO timestamp like "1970-01-01T08:00:00.000Z" if (timeString.includes('T')) { - const date = new Date(timeString); - const hours = String(date.getUTCHours()).padStart(2, '0'); - const minutes = String(date.getUTCMinutes()).padStart(2, '0'); - return `${hours}:${minutes}`; + return dayjs.utc(timeString).format('HH:mm'); } // If it's already in HH:mm:ss format, remove seconds diff --git a/svg/air_dryer_A_rev.svg b/svg/air_dryer_A_rev.svg new file mode 100644 index 0000000..53876ed --- /dev/null +++ b/svg/air_dryer_A_rev.svg @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Heater Temp SP + + ####.## + °F + + Heater Temp + + ####.## + °F + + Heater Temp + + ####.## + °F + HEATER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dew Temp + + ####.## + °C + + Dew Temp + + ####.## + °F + AIROUTLET + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AIRINLET + + + + + + + + + + + + + + + + + + + + + + + + + RUN HOUR + + ####.## + H + + PURGE HOUR + + ####.## + H + + HEATER HOUR + + ####.## + H + + Alarm Info + + ####.## + H + + RT or LT Dry + + LT Dry + RT Dry + + Opmode + + HTD + + Step + ####.## + s + + Cycle Timer + + ####.## + s + Time + ## + + Dryer Status + + + + Shutdown + + + + Alarm Status + + + REGEN + + DRYING + + REGEN + + DRYING + + AIR DRYER UNIT A (01-CL-10532-A) + \ No newline at end of file diff --git a/svg/air_dryer_B_rev.svg b/svg/air_dryer_B_rev.svg new file mode 100644 index 0000000..4dff501 --- /dev/null +++ b/svg/air_dryer_B_rev.svg @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Heater Temp SP + + ####.## + °F + + Heater Temp + + ####.## + °F + + Heater Temp + + ####.## + °F + HEATER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dew Temp + + ####.## + °C + + Dew Temp + + ####.## + °F + AIROUTLET + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AIRINLET + + + + + + + + + + + + + + + + + + + + + + + + + RUN HOUR + + ####.## + H + + PURGE HOUR + + ####.## + H + + HEATER HOUR + + ####.## + H + + Alarm Info + + ####.## + H + + RT or LT Dry + + LT Dry + RT Dry + + Opmode + + HTD + + Step + ####.## + s + + Cycle Timer + + ####.## + s + Time + ## + + Dryer Status + + + + Shutdown + + + + Alarm Status + + + REGEN + + DRYING + + REGEN + + DRYING + + AIR DRYER UNIT B (01-CL-10535-B) + \ No newline at end of file diff --git a/svg/air_dryer_C_rev.svg b/svg/air_dryer_C_rev.svg new file mode 100644 index 0000000..3c13ba1 --- /dev/null +++ b/svg/air_dryer_C_rev.svg @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Heater Temp SP + + ####.## + °F + + Heater Temp + + ####.## + °F + + Heater Temp + + ####.## + °F + HEATER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dew Temp + + ####.## + °C + + Dew Temp + + ####.## + °F + AIROUTLET + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AIRINLET + + + + + + + + + + + + + + + + + + + + + + + + + RUN HOUR + + ####.## + H + + PURGE HOUR + + ####.## + H + + HEATER HOUR + + ####.## + H + + Alarm Info + + ####.## + H + + RT or LT Dry + + LT Dry + RT Dry + + Opmode + + HTD + + Step + ####.## + s + + Cycle Timer + + ####.## + s + Time + ## + + Dryer Status + + + + Shutdown + + + + Alarm Status + + + REGEN + + DRYING + + REGEN + + DRYING + + AIR DRYER UNIT C (01-CL-10539-C) + \ No newline at end of file