Slicing menu history alarm and event alarm
This commit is contained in:
11
src/App.jsx
11
src/App.jsx
@@ -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 />}>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
38
src/pages/history/alarm/IndexHistoryAlarm.jsx
Normal file
38
src/pages/history/alarm/IndexHistoryAlarm.jsx
Normal 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;
|
||||||
128
src/pages/history/alarm/component/ListHistoryAlarm.jsx
Normal file
128
src/pages/history/alarm/component/ListHistoryAlarm.jsx
Normal 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;
|
||||||
38
src/pages/history/event/IndexHistoryEvent.jsx
Normal file
38
src/pages/history/event/IndexHistoryEvent.jsx
Normal 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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
38
src/pages/report/report/IndexReport.jsx
Normal file
38
src/pages/report/report/IndexReport.jsx
Normal 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;
|
||||||
163
src/pages/report/report/component/ListReport.jsx
Normal file
163
src/pages/report/report/component/ListReport.jsx
Normal 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;
|
||||||
38
src/pages/report/trending/IndexTrending.jsx
Normal file
38
src/pages/report/trending/IndexTrending.jsx
Normal 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;
|
||||||
222
src/pages/report/trending/ReportTrending.jsx
Normal file
222
src/pages/report/trending/ReportTrending.jsx
Normal 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;
|
||||||
Reference in New Issue
Block a user