Slicing menu history alarm and event alarm

This commit is contained in:
2025-10-23 15:57:26 +07:00
parent bddd249e07
commit 034cf636f9
14 changed files with 692 additions and 816 deletions

View File

@@ -23,12 +23,11 @@ import IndexShift from './pages/master/shift/IndexShift';
import IndexJadwalShift from './pages/jadwalShift/IndexJadwalShift'; import IndexJadwalShift from './pages/jadwalShift/IndexJadwalShift';
// History // History
import IndexTrending from './pages/history/trending/IndexTrending'; import IndexTrending from './pages/report/trending/IndexTrending';
import IndexReport from './pages/history/report/IndexReport'; import IndexReport from './pages/report/report/IndexReport';
// Other Pages // Other Pages
import IndexNotification from './pages/notification/IndexNotification'; import IndexNotification from './pages/notification/IndexNotification';
import IndexEventAlarm from './pages/eventAlarm/IndexEventAlarm';
import IndexRole from './pages/role/IndexRole'; import IndexRole from './pages/role/IndexRole';
import IndexUser from './pages/user/IndexUser'; import IndexUser from './pages/user/IndexUser';
@@ -40,6 +39,8 @@ import SvgCompressorC from './pages/home/SvgCompressorC';
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 IndexHistoryEvent from './pages/history/event/IndexHistoryEvent';
const App = () => { const App = () => {
return ( return (
@@ -84,8 +85,8 @@ const App = () => {
</Route> </Route>
<Route path="/history" element={<ProtectedRoute />}> <Route path="/history" element={<ProtectedRoute />}>
<Route path="alarm" element={<IndexEventAlarm />} /> <Route path="alarm" element={<IndexHistoryAlarm />} />
<Route path="event" element={<IndexEventAlarm />} /> <Route path="event" element={<IndexHistoryEvent />} />
</Route> </Route>
<Route path="/notification" element={<ProtectedRoute />}> <Route path="/notification" element={<ProtectedRoute />}>

View File

@@ -1,72 +0,0 @@
import React, { memo, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
import { Form, Typography } from 'antd';
import ListEventAlarm from './component/ListEventAlarm';
import DetailEventAlarm from './component/DetailEventAlarm';
const { Text } = Typography;
const IndexEventAlarm = memo(function IndexEventAlarm() {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [form] = Form.useForm();
const [actionMode, setActionMode] = useState('list');
const [selectedData, setSelectedData] = useState(null);
const [isModalVisible, setIsModalVisible] = useState(false);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
setBreadcrumbItems([
{
title: (
<Text strong style={{ fontSize: '14px' }}>
Event Alarm
</Text>
),
},
]);
} else {
navigate('/signin');
}
}, [navigate, setBreadcrumbItems]);
useEffect(() => {
if (actionMode === 'preview') {
setIsModalVisible(true);
if (selectedData) {
form.setFieldsValue(selectedData);
}
} else {
setIsModalVisible(false);
form.resetFields();
}
}, [actionMode, selectedData, form]);
const handleCancel = () => {
setActionMode('list');
setSelectedData(null);
form.resetFields();
};
return (
<React.Fragment>
<ListEventAlarm
actionMode={actionMode}
setActionMode={setActionMode}
selectedData={selectedData}
setSelectedData={setSelectedData}
/>
<DetailEventAlarm
visible={isModalVisible}
onCancel={handleCancel}
form={form}
selectedData={selectedData}
/>
</React.Fragment>
);
});
export default IndexEventAlarm;

View File

@@ -1,58 +0,0 @@
import { memo } from 'react';
import { Modal, Divider, Descriptions } from 'antd';
const DetailEventAlarm = memo(function DetailEventAlarm({ visible, onCancel, selectedData }) {
return (
<Modal
title="Detail Event Alarm"
open={visible}
onCancel={onCancel}
onOk={onCancel}
okText="Tutup"
cancelButtonProps={{ style: { display: 'none' } }}
width={700}
>
{selectedData && (
<div>
<Descriptions bordered column={2}>
<Descriptions.Item label="Tanggal" span={2}>
{selectedData.tanggal}
</Descriptions.Item>
<Descriptions.Item label="Plant Sub Section" span={2}>
{selectedData.plant_sub_section}
</Descriptions.Item>
<Descriptions.Item label="Device">
{selectedData.device}
</Descriptions.Item>
<Descriptions.Item label="Tag">
{selectedData.tag}
</Descriptions.Item>
<Descriptions.Item label="Engineer" span={2}>
{selectedData.engineer}
</Descriptions.Item>
</Descriptions>
<Divider style={{ margin: '16px 0' }} />
{/* 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> Event alarm ini telah tercatat dalam sistem untuk
monitoring dan analisis lebih lanjut.
</div>
</div>
</div>
)}
</Modal>
);
});
export default DetailEventAlarm;

View File

@@ -0,0 +1,38 @@
import React, { memo, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { Typography } from 'antd';
import ListHistoryAlarm from './component/ListHistoryAlarm';
const { Text } = Typography;
const IndexHistoryAlarm = memo(function IndexHistoryAlarm() {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [selectedData, setSelectedData] = useState(null);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
setBreadcrumbItems([
{
title: (
<Text strong style={{ fontSize: '14px' }}>
History Event
</Text>
),
},
]);
} else {
navigate('/signin');
}
}, [navigate, setBreadcrumbItems]);
return (
<React.Fragment>
<ListHistoryAlarm selectedData={selectedData} setSelectedData={setSelectedData} />
</React.Fragment>
);
});
export default IndexHistoryAlarm;

View File

@@ -0,0 +1,128 @@
import React, { memo, useState, useEffect } from 'react';
import { Button, Row, Col, Card, Input } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import TableList from '../../../../components/Global/TableList';
const ListHistoryAlarm = memo(function ListHistoryAlarm(props) {
const columns = [
{
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: '40%',
},
{
title: 'Value',
dataIndex: 'stat',
key: 'stat',
width: '10%',
},
{
title: 'Threshold',
dataIndex: 'threshold',
key: 'threshold',
width: '10%',
},
{
title: 'Condition',
dataIndex: 'condition',
key: 'condition',
width: '20%',
},
{
title: 'Stat',
dataIndex: 'stat',
key: 'stat',
width: '5%',
},
];
const [trigerFilter, setTrigerFilter] = useState(false);
const defaultFilter = { search: '' };
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
const [searchValue, setSearchValue] = useState('');
const getAllEventAlarm = async (params) => {
return {
data: [],
};
};
const handleSearch = () => {
setFormDataFilter({ search: searchValue });
setTrigerFilter((prev) => !prev);
};
const handleSearchClear = () => {
setSearchValue('');
setFormDataFilter({ search: '' });
setTrigerFilter((prev) => !prev);
};
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 ..."
value={searchValue}
onChange={(e) => {
const value = e.target.value;
setSearchValue(value);
if (value === '') {
handleSearchClear();
}
}}
onSearch={handleSearch}
allowClear={{
clearIcon: <span onClick={handleSearchClear}></span>,
}}
enterButton={
<Button
type="primary"
icon={<SearchOutlined />}
style={{
backgroundColor: '#23A55A',
borderColor: '#23A55A',
}}
>
Search
</Button>
}
size="large"
/>
</Col>
</Row>
</Col>
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
<TableList
getData={getAllEventAlarm}
queryParams={formDataFilter}
columns={columns}
triger={trigerFilter}
/>
</Col>
</Row>
</Card>
</React.Fragment>
);
});
export default ListHistoryAlarm;

View File

@@ -0,0 +1,38 @@
import React, { memo, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { Typography } from 'antd';
import ListHistoryEvent from './component/ListHistoryEvent';
const { Text } = Typography;
const IndexHistoryEvent = memo(function IndexHistoryEvent() {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [selectedData, setSelectedData] = useState(null);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
setBreadcrumbItems([
{
title: (
<Text strong style={{ fontSize: '14px' }}>
History Event
</Text>
),
},
]);
} else {
navigate('/signin');
}
}, [navigate, setBreadcrumbItems]);
return (
<React.Fragment>
<ListHistoryEvent selectedData={selectedData} setSelectedData={setSelectedData} />
</React.Fragment>
);
});
export default IndexHistoryEvent;

View File

@@ -1,70 +1,9 @@
import React, { memo, useState, useEffect } from 'react'; 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 { useNavigate } from 'react-router-dom'; import TableList from '../../../../components/Global/TableList';
import TableList from '../../../components/Global/TableList';
// Dummy data untuk riwayat alarm const ListHistoryEvent = memo(function ListHistoryEvent(props) {
const initialAlarmsData = [
{
alarm_id: 1,
tanggal: '2025-01-15 08:30:00',
plant_sub_section: 'Plant A - Section 1',
device: 'Device 001',
tag: 'TEMP-001',
engineer: 'Pras',
},
{
alarm_id: 2,
tanggal: '2025-01-15 09:15:00',
plant_sub_section: 'Plant B - Section 2',
device: 'Device 002',
tag: 'PRESS-002',
engineer: 'Bagus',
},
{
alarm_id: 3,
tanggal: '2025-01-15 10:00:00',
plant_sub_section: 'Plant A - Section 3',
device: 'Device 003',
tag: 'FLOW-003',
engineer: 'iqbal',
},
{
alarm_id: 4,
tanggal: '2025-01-15 11:45:00',
plant_sub_section: 'Plant C - Section 1',
device: 'Device 004',
tag: 'LEVEL-004',
engineer: 'riski',
},
{
alarm_id: 5,
tanggal: '2025-01-15 13:20:00',
plant_sub_section: 'Plant B - Section 3',
device: 'Device 005',
tag: 'TEMP-005',
engineer: 'anton',
},
{
alarm_id: 6,
tanggal: '2025-01-15 14:00:00',
plant_sub_section: 'Plant A - Section 2',
device: 'Device 006',
tag: 'PRESS-006',
engineer: 'kurniawan',
},
{
alarm_id: 7,
tanggal: '2025-01-15 15:30:00',
plant_sub_section: 'Plant C - Section 2',
device: 'Device 007',
tag: 'FLOW-007',
engineer: 'wawan',
},
];
const ListEventAlarm = memo(function ListEventAlarm(props) {
const columns = [ const columns = [
{ {
title: 'No', title: 'No',
@@ -74,33 +13,27 @@ const ListEventAlarm = memo(function ListEventAlarm(props) {
render: (_, __, index) => index + 1, render: (_, __, index) => index + 1,
}, },
{ {
title: 'Tanggal', title: 'Datetime',
dataIndex: 'tanggal', dataIndex: 'datetime',
key: 'tanggal', key: 'datetime',
width: '15%', width: '10%',
}, },
{ {
title: 'Plant Sub Section', title: 'Tag Name',
dataIndex: 'plant_sub_section', dataIndex: 'tag_name',
key: 'plant_sub_section', key: 'tag_name',
width: '25%', width: '40%',
}, },
{ {
title: 'Device', title: 'Stat',
dataIndex: 'device', dataIndex: 'stat',
key: 'device', key: 'stat',
width: '15%', width: '5%',
}, },
{ {
title: 'Tag', title: 'Description',
dataIndex: 'tag', dataIndex: 'description',
key: 'tag', key: 'description',
width: '15%',
},
{
title: 'Engineer',
dataIndex: 'engineer',
key: 'engineer',
width: '15%', width: '15%',
}, },
]; ];
@@ -111,31 +44,12 @@ const ListEventAlarm = memo(function ListEventAlarm(props) {
const [formDataFilter, setFormDataFilter] = useState(defaultFilter); const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const navigate = useNavigate();
// Dummy data function to simulate API call
const getAllEventAlarm = async (params) => { const getAllEventAlarm = async (params) => {
return { return {
data: initialAlarmsData, data: [],
}; };
}; };
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 = () => { const handleSearch = () => {
setFormDataFilter({ search: searchValue }); setFormDataFilter({ search: searchValue });
setTrigerFilter((prev) => !prev); setTrigerFilter((prev) => !prev);
@@ -155,15 +69,13 @@ const ListEventAlarm = memo(function ListEventAlarm(props) {
<Row justify="space-between" align="middle" gutter={[8, 8]}> <Row justify="space-between" align="middle" gutter={[8, 8]}>
<Col xs={24} sm={24} md={12} lg={12}> <Col xs={24} sm={24} md={12} lg={12}>
<Input.Search <Input.Search
placeholder="Search alarm by tanggal, plant, device, tag, engineer..." placeholder="Search ..."
value={searchValue} value={searchValue}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
setSearchValue(value); setSearchValue(value);
// Auto search when clearing by backspace/delete
if (value === '') { if (value === '') {
setFormDataFilter({ search: '' }); handleSearchClear();
setTrigerFilter((prev) => !prev);
} }
}} }}
onSearch={handleSearch} onSearch={handleSearch}
@@ -201,4 +113,4 @@ const ListEventAlarm = memo(function ListEventAlarm(props) {
); );
}); });
export default ListEventAlarm; export default ListHistoryEvent;

View File

@@ -1,275 +0,0 @@
import React, { memo, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { Typography, Table, Card, Select, DatePicker, Button, Row, Col } from 'antd';
import { FileTextOutlined } from '@ant-design/icons';
import { decryptData } from '../../../components/Global/Formatter';
import dayjs from 'dayjs';
const { Text } = Typography;
// New data structure for tag history
const tagHistoryData = [
{
tag: 'TEMP_SENSOR_1',
color: '#FF6B4A',
history: [
{ timestamp: '2025-10-09 08:00', value: 75 },
{ timestamp: '2025-10-09 08:05', value: 76 },
{ timestamp: '2025-10-09 08:10', value: 75 },
],
},
{
tag: 'GAS_LEAK_SENSOR_1',
color: '#4ECDC4',
history: [
{ timestamp: '2025-10-09 08:00', value: 10 },
{ timestamp: '2025-10-09 08:05', value: 150 },
{ timestamp: '2025-10-09 08:10', value: 12 },
],
},
{
tag: 'PRESSURE_SENSOR_1',
color: '#FFE66D',
history: [
{ timestamp: '2025-10-09 08:00', value: 1.2 },
{ timestamp: '2025-10-09 08:05', value: 1.3 },
{ timestamp: '2025-10-09 08:10', value: 1.2 },
],
},
];
const IndexReport = memo(function IndexReport() {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [plantSubSection, setPlantSubSection] = useState('Semua Plant');
const [startDate, setStartDate] = useState(dayjs('2025-09-30'));
const [endDate, setEndDate] = useState(dayjs('2025-10-09'));
const [periode, setPeriode] = useState('30 Menit');
const [userRole, setUserRole] = useState(null);
const [roleLevel, setRoleLevel] = useState(null);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
// Get user data and role
let userData = null;
const sessionData = localStorage.getItem('session');
if (sessionData) {
userData = decryptData(sessionData);
} else {
const userRaw = localStorage.getItem('user');
if (userRaw) {
try {
userData = { user: JSON.parse(userRaw) };
} catch (e) {
console.error('Error parsing user data:', e);
}
}
}
if (userData?.user) {
setUserRole(userData.user.role_name);
setRoleLevel(userData.user.role_level);
}
setBreadcrumbItems([
{
title: (
<Text strong style={{ fontSize: '14px' }}>
History
</Text>
),
},
{
title: (
<Text strong style={{ fontSize: '14px' }}>
Report
</Text>
),
},
]);
} else {
navigate('/signin');
}
}, []);
const handleReset = () => {
setPlantSubSection('Semua Plant');
setStartDate(dayjs('2025-09-30'));
setEndDate(dayjs('2025-10-09'));
setPeriode('30 Menit');
};
// Check if user has permission to view data (all except guest)
const canViewData = userRole && userRole !== 'guest';
// Convert tag history data to table format
const convertToTableData = () => {
const timestamps = {}; // Use an object to collect data per timestamp
tagHistoryData.forEach((tagData) => {
tagData.history.forEach((point) => {
if (!timestamps[point.timestamp]) {
timestamps[point.timestamp] = {
key: point.timestamp,
'Date and Time': point.timestamp,
};
}
timestamps[point.timestamp][tagData.tag] = point.value;
});
});
// Convert the object to an array
return Object.values(timestamps);
};
const tableData = convertToTableData();
// Create dynamic columns based on tags
const tags = tagHistoryData.map((tagData) => tagData.tag);
const columns = [
{
title: 'Date and Time',
dataIndex: 'Date and Time',
key: 'Date and Time',
fixed: 'left',
width: 180,
render: (text) => <Text strong>{text}</Text>,
},
...tags.map((tag) => ({
title: tag,
dataIndex: tag,
key: tag,
align: 'center',
width: 150,
render: (value) => <Text>{value !== undefined ? value : '-'}</Text>,
})),
];
return (
<React.Fragment>
<div style={{ minHeight: 360 }}>
{/* Filter Section */}
<Card className="filter-card">
<div className="filter-header">
<Text strong style={{ fontSize: '14px' }}>
Filter Data
</Text>
</div>
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>
Plant Sub Section
</Text>
<Select
value={plantSubSection}
onChange={setPlantSubSection}
style={{ width: '100%', marginTop: '4px' }}
options={[
{ value: 'Semua Plant', label: 'Semua Plant' },
{ value: 'Plant 1', label: 'Plant 1' },
{ value: 'Plant 2', label: 'Plant 2' },
]}
/>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>
Tanggal Mulai
</Text>
<DatePicker
value={startDate}
onChange={setStartDate}
format="DD/MM/YYYY"
style={{ width: '100%', marginTop: '4px' }}
/>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>
Tanggal Akhir
</Text>
<DatePicker
value={endDate}
onChange={setEndDate}
format="DD/MM/YYYY"
style={{ width: '100%', marginTop: '4px' }}
/>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>Periode</Text>
<Select
value={periode}
onChange={setPeriode}
style={{ width: '100%', marginTop: '4px' }}
options={[
{ 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>
</Col>
</Row>
<Row gutter={8} style={{ marginTop: '16px' }}>
<Col>
<Button
type="primary"
danger
icon={<FileTextOutlined />}
disabled={!canViewData}
>
Tampilkan
</Button>
</Col>
<Col>
<Button
onClick={handleReset}
style={{ backgroundColor: '#6c757d', color: 'white' }}
disabled={!canViewData}
>
Reset
</Button>
</Col>
</Row>
</Card>
{/* Table Section */}
{/* {!canViewData ? (
<Card style={{ marginTop: '24px', textAlign: 'center', padding: '40px' }}>
<Text style={{ fontSize: '16px', color: '#999' }}>
Anda tidak memiliki akses untuk melihat data report.
<br />
Silakan hubungi administrator untuk mendapatkan akses.
</Text>
</Card>
) : ( */}
<Card style={{ marginTop: '24px' }}>
<div style={{ marginBottom: '16px' }}>
<Text strong style={{ fontSize: '16px' }}>
History Report
</Text>
</div>
<Table
columns={columns}
dataSource={tableData}
pagination={false}
scroll={{ x: 1000 }}
size="middle"
/>
</Card>
{/* )} */}
</div>
</React.Fragment>
);
});
export default IndexReport;

View File

@@ -1,297 +0,0 @@
import React, { memo, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { Typography, Select, DatePicker, Button, Row, Col, Card } from 'antd';
import { ResponsiveLine } from '@nivo/line';
import { FileTextOutlined } from '@ant-design/icons';
import { decryptData } from '../../../components/Global/Formatter';
import dayjs from 'dayjs';
import './trending.css';
const { Text } = Typography;
const IndexTrending = memo(function IndexTrending() {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [plantSubSection, setPlantSubSection] = useState('Semua Plant');
const [startDate, setStartDate] = useState(dayjs('2025-09-30'));
const [endDate, setEndDate] = useState(dayjs('2025-10-09'));
const [periode, setPeriode] = useState('10 Menit');
const [userRole, setUserRole] = useState(null);
const [roleLevel, setRoleLevel] = useState(null);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
// Get user data and role
let userData = null;
const sessionData = localStorage.getItem('session');
if (sessionData) {
userData = decryptData(sessionData);
} else {
const userRaw = localStorage.getItem('user');
if (userRaw) {
try {
userData = { user: JSON.parse(userRaw) };
} catch (e) {
console.error('Error parsing user data:', e);
}
}
}
if (userData?.user) {
setUserRole(userData.user.role_name);
setRoleLevel(userData.user.role_level);
}
setBreadcrumbItems([
{
title: (
<Text strong style={{ fontSize: '14px' }}>
History
</Text>
),
},
{
title: (
<Text strong style={{ fontSize: '14px' }}>
Trending
</Text>
),
},
]);
} else {
navigate('/signin');
}
}, []);
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 handleReset = () => {
setPlantSubSection('Semua Plant');
setStartDate(dayjs('2025-09-30'));
setEndDate(dayjs('2025-10-09'));
setPeriode('10 Menit');
};
// Check if user has permission to view data (all except guest)
const canViewData = userRole && userRole !== 'guest';
// Check if user can export/filter (administrator, engineer)
const canExportData = userRole && (userRole === 'administrator' || userRole === 'engineer');
return (
<React.Fragment>
{/* Filter Section */}
<Card className="filter-card">
<div className="filter-header">
<Text strong style={{ fontSize: '14px' }}>
Filter Data
</Text>
</div>
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>
Plant Sub Section
</Text>
<Select
value={plantSubSection}
onChange={setPlantSubSection}
style={{ width: '100%', marginTop: '4px' }}
options={[
{ value: 'Semua Plant', label: 'Semua Plant' },
{ value: 'Plant 1', label: 'Plant 1' },
{ value: 'Plant 2', label: 'Plant 2' },
]}
/>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>Tanggal Mulai</Text>
<DatePicker
value={startDate}
onChange={setStartDate}
format="DD/MM/YYYY"
style={{ width: '100%', marginTop: '4px' }}
/>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>Tanggal Akhir</Text>
<DatePicker
value={endDate}
onChange={setEndDate}
format="DD/MM/YYYY"
style={{ width: '100%', marginTop: '4px' }}
/>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>Periode</Text>
<Select
value={periode}
onChange={setPeriode}
style={{ width: '100%', marginTop: '4px' }}
options={[
{ 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>
</Col>
</Row>
<Row gutter={8} style={{ marginTop: '16px' }}>
<Col>
<Button
type="primary"
danger
icon={<FileTextOutlined />}
disabled={!canViewData}
>
Tampilkan
</Button>
</Col>
<Col>
<Button
onClick={handleReset}
style={{ backgroundColor: '#6c757d', color: 'white' }}
disabled={!canViewData}
>
Reset
</Button>
</Col>
</Row>
</Card>
{/* Charts Section */}
{/* {!canViewData ? (
<Card style={{ marginTop: '24px', textAlign: 'center', padding: '40px' }}>
<Text style={{ fontSize: '16px', color: '#999' }}>
Anda tidak memiliki akses untuk melihat data trending.
<br />
Silakan hubungi administrator untuk mendapatkan akses.
</Text>
</Card>
) : ( */}
<>
<Row gutter={16} style={{ marginTop: '24px' }}>
{/* Line Chart */}
<Col xs={24}>
<Card className="chart-card">
<div className="chart-header">
<Text strong style={{ fontSize: '14px' }}>
Tag Value Trending
</Text>
</div>
<div style={{ height: '500px', marginTop: '16px' }}>
<ResponsiveLine
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>
</Card>
</Col>
</Row>
</>
{/* )} */}
</React.Fragment>
);
});
export default IndexTrending;

View File

@@ -0,0 +1,38 @@
import React, { memo, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { Typography } from 'antd';
import ListReport from './component/ListReport';
const { Text } = Typography;
const IndexReport = memo(function IndexReport() {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [selectedData, setSelectedData] = useState(null);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
setBreadcrumbItems([
{
title: (
<Text strong style={{ fontSize: '14px' }}>
Report
</Text>
),
},
]);
} else {
navigate('/signin');
}
}, [navigate, setBreadcrumbItems]);
return (
<React.Fragment>
<ListReport selectedData={selectedData} setSelectedData={setSelectedData} />
</React.Fragment>
);
});
export default IndexReport;

View File

@@ -0,0 +1,163 @@
import React, { memo, useState, useEffect } from 'react';
import { Button, Row, Col, Card, Input, DatePicker, Select, Typography } from 'antd';
import TableList from '../../../../components/Global/TableList';
import dayjs from 'dayjs';
import { FileTextOutlined } from '@ant-design/icons';
const { Text } = Typography;
const ListReport = memo(function ListReport(props) {
const columns = [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{
title: 'Datetime',
dataIndex: 'datetime',
key: 'datetime',
width: '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 defaultFilter = { search: '' };
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
const [plantSubSection, setPlantSubSection] = useState('Semua Plant');
const [startDate, setStartDate] = useState(dayjs('2025-09-30'));
const [endDate, setEndDate] = useState(dayjs('2025-10-09'));
const [periode, setPeriode] = useState('10 Menit');
const getAllReport = async (params) => {
return {
data: [],
};
};
const handleReset = () => {
setPlantSubSection('Semua Plant');
setStartDate(dayjs('2025-09-30'));
setEndDate(dayjs('2025-10-09'));
setPeriode('10 Menit');
};
return (
<React.Fragment>
<Card>
<Row>
<Col xs={24}>
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>
Plant Sub Section
</Text>
<Select
value={plantSubSection}
onChange={setPlantSubSection}
style={{ width: '100%', marginTop: '4px' }}
options={[
{ value: 'Semua Plant', label: 'Semua Plant' },
{ value: 'Plant 1', label: 'Plant 1' },
{ value: 'Plant 2', label: 'Plant 2' },
]}
/>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>
Tanggal Mulai
</Text>
<DatePicker
value={startDate}
onChange={setStartDate}
format="DD/MM/YYYY"
style={{ width: '100%', marginTop: '4px' }}
/>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>
Tanggal Akhir
</Text>
<DatePicker
value={endDate}
onChange={setEndDate}
format="DD/MM/YYYY"
style={{ width: '100%', marginTop: '4px' }}
/>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>Periode</Text>
<Select
value={periode}
onChange={setPeriode}
style={{ width: '100%', marginTop: '4px' }}
options={[
{ 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>
</Col>
</Row>
<Row gutter={8} style={{ marginTop: '16px' }}>
<Col>
<Button type="primary" danger icon={<FileTextOutlined />}>
Tampilkan
</Button>
</Col>
<Col>
<Button
onClick={handleReset}
style={{ backgroundColor: '#6c757d', color: 'white' }}
>
Reset
</Button>
</Col>
</Row>
</Col>
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
<TableList
getData={getAllReport}
queryParams={formDataFilter}
columns={columns}
triger={trigerFilter}
/>
</Col>
</Row>
</Card>
</React.Fragment>
);
});
export default ListReport;

View File

@@ -0,0 +1,38 @@
import React, { memo, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { Typography } from 'antd';
import ReportTrending from './ReportTrending';
const { Text } = Typography;
const IndexTrending = memo(function IndexTrending() {
const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb();
const [selectedData, setSelectedData] = useState(null);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
setBreadcrumbItems([
{
title: (
<Text strong style={{ fontSize: '14px' }}>
Trending
</Text>
),
},
]);
} else {
navigate('/signin');
}
}, [navigate, setBreadcrumbItems]);
return (
<React.Fragment>
<ReportTrending/>
</React.Fragment>
);
});
export default IndexTrending;

View File

@@ -0,0 +1,222 @@
import React, { memo, useState, useEffect } from 'react';
import { Button, Row, Col, Card, Input, DatePicker, Select, Typography } from 'antd';
import dayjs from 'dayjs';
import { FileTextOutlined } from '@ant-design/icons';
import { ResponsiveLine } from '@nivo/line';
import './trending.css';
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 [trigerFilter, setTrigerFilter] = useState(false);
const defaultFilter = { search: '' };
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
const [plantSubSection, setPlantSubSection] = useState('Semua Plant');
const [startDate, setStartDate] = useState(dayjs('2025-09-30'));
const [endDate, setEndDate] = useState(dayjs('2025-10-09'));
const [periode, setPeriode] = useState('10 Menit');
const getAllReport = async (params) => {
return {
data: [],
};
};
const handleReset = () => {
setPlantSubSection('Semua Plant');
setStartDate(dayjs('2025-09-30'));
setEndDate(dayjs('2025-10-09'));
setPeriode('10 Menit');
};
return (
<React.Fragment>
<Card>
<Row>
<Col xs={24}>
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>
Plant Sub Section
</Text>
<Select
value={plantSubSection}
onChange={setPlantSubSection}
style={{ width: '100%', marginTop: '4px' }}
options={[
{ value: 'Semua Plant', label: 'Semua Plant' },
{ value: 'Plant 1', label: 'Plant 1' },
{ value: 'Plant 2', label: 'Plant 2' },
]}
/>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>
Tanggal Mulai
</Text>
<DatePicker
value={startDate}
onChange={setStartDate}
format="DD/MM/YYYY"
style={{ width: '100%', marginTop: '4px' }}
/>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>
Tanggal Akhir
</Text>
<DatePicker
value={endDate}
onChange={setEndDate}
format="DD/MM/YYYY"
style={{ width: '100%', marginTop: '4px' }}
/>
</div>
</Col>
<Col xs={24} sm={12} md={6}>
<div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>Periode</Text>
<Select
value={periode}
onChange={setPeriode}
style={{ width: '100%', marginTop: '4px' }}
options={[
{ 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>
</Col>
</Row>
<Row gutter={8} style={{ marginTop: '16px' }}>
<Col>
<Button type="primary" danger icon={<FileTextOutlined />}>
Tampilkan
</Button>
</Col>
<Col>
<Button
onClick={handleReset}
style={{ backgroundColor: '#6c757d', color: 'white' }}
>
Reset
</Button>
</Col>
</Row>
</Col>
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
<div style={{ height: '500px', marginTop: '16px' }}>
<ResponsiveLine
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>
</Row>
</Card>
</React.Fragment>
);
});
export default ReportTrending;