This commit is contained in:
2025-10-21 17:07:37 +07:00
17 changed files with 1142 additions and 10 deletions

View File

@@ -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 = () => {
<Route path="blank" element={<Blank />} />
</Route>
<Route path="/dashboard-svg" element={<ProtectedRoute />}>
<Route path="overview" element={<SvgOverview />} />
<Route path="compressor-a" element={<SvgCompressorA />} />
<Route path="compressor-b" element={<SvgCompressorB />} />
<Route path="compressor-c" element={<SvgCompressorC />} />
<Route path="airdryer-a" element={<SvgAirDryerA />} />
<Route path="airdryer-b" element={<SvgAirDryerB />} />
<Route path="airdryer-c" element={<SvgAirDryerC />} />
</Route>
<Route path="/master" element={<ProtectedRoute />}>
<Route path="device" element={<IndexDevice />} />
<Route path="tag" element={<IndexTag />} />

168
src/api/master-status.jsx Normal file
View File

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

View File

@@ -26,6 +26,9 @@ import {
TeamOutlined,
ClockCircleOutlined,
CalendarOutlined,
DesktopOutlined,
NodeExpandOutlined,
GroupOutlined,
} from '@ant-design/icons';
const { Text } = Typography;
@@ -40,6 +43,48 @@ const allItems = [
</Link>
),
},
{
key: 'dashboard-svg',
icon: <GroupOutlined style={{ fontSize: '19px' }} />,
label: 'Dashboard',
children: [
{
key: 'dashboard-svg-overview',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/overview">Overview</Link>,
},
{
key: 'dashboard-svg-compressor-a',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/compressor-a">Compressor A</Link>,
},
{
key: 'dashboard-svg-compressor-b',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/compressor-b">Compressor B</Link>,
},
{
key: 'dashboard-svg-compressor-c',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/compressor-c">Compressor C</Link>,
},
{
key: 'dashboard-svg-airdryer-a',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/airdryer-a">Air Dryer A</Link>,
},
{
key: 'dashboard-svg-airdryer-b',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/airdryer-b">Air Dryer B</Link>,
},
{
key: 'dashboard-svg-airdryer-c',
icon: <NodeExpandOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/dashboard-svg/airdryer-c">Air Dryer C</Link>,
},
],
},
{
key: 'master',
icon: <DatabaseOutlined style={{ fontSize: '19px' }} />,
@@ -60,16 +105,16 @@ const allItems = [
icon: <MobileOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/master/device">Device</Link>,
},
{
key: 'master-tag',
icon: <TagOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/master/tag">Tag</Link>,
},
{
key: 'master-unit',
icon: <AppstoreOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/master/unit">Unit</Link>,
},
{
key: 'master-tag',
icon: <TagOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/master/tag">Tag</Link>,
},
{
key: 'master-status',
icon: <SafetyOutlined style={{ fontSize: '19px' }} />,
@@ -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;

View File

@@ -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 (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgAirDryerA;

View File

@@ -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 (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgAirDryerB;

View File

@@ -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 (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgAirDryerC;

View File

@@ -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 (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgCompressorA;

View File

@@ -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 (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgCompressorB;

View File

@@ -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 (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgCompressorC;

View File

@@ -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 (
<SvgTemplate>
<SvgViewer filePathSvg={filePathSvg} topicMqtt={topicMqtt} setValSvg={setValSvg} />
</SvgTemplate>
);
};
export default SvgOverview;

View File

@@ -0,0 +1,19 @@
const SvgTemplate = ({ children }) => {
return (
<div
style={{
height: '80vh', // penuh 1 layar
width: '80vw', // penuh 1 layar lebar
overflow: 'hidden', // hilangkan scroll
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff', // opsional
}}
>
{children}
</div>
);
};
export default SvgTemplate;

View File

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

View File

@@ -0,0 +1,19 @@
// SvgViewer.jsx
import { ReactSVG } from 'react-svg';
const SvgViewer = ({ filePathSvg, topicMqtt, setValSvg }) => {
return (
<ReactSVG
src={filePathSvg}
beforeInjection={(svg) => {
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;

View File

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