feat: convert line chart data to table format and enhance filtering options in IndexReport

This commit is contained in:
2025-10-16 15:36:01 +07:00
parent 61ec188d59
commit 7538c18624
2 changed files with 339 additions and 280 deletions

View File

@@ -1,116 +1,305 @@
import React, { memo, useEffect, useState } from 'react'; import React, { memo, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { Typography, Table } from 'antd'; 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; const { Text } = Typography;
// Mock Data // Line Chart Data (same as IndexTrending) converted to table format
const initialData = [ const lineChartData = [
{ {
key: '1', id: 'Compressor Section 1',
plantSubSection: 'Section A', color: '#FF6B4A',
device: 'Device 1', data: [
errorCode: 'E-101', { x: 'Jan', y: 15 },
status: 'Warning', { x: 'Feb', y: 18 },
{ x: 'Mar', y: 17 },
{ x: 'Apr', y: 24 },
{ x: 'Mei', y: 21 },
{ x: 'Jun', y: 19 },
{ x: 'Jul', y: 28 },
{ x: 'Agu', y: 26 },
{ x: 'Sep', y: 22 },
{ x: 'Okt', y: 25 },
],
}, },
{ {
key: '2', id: 'Compressor Section 2',
plantSubSection: 'Section B', color: '#4ECDC4',
device: 'Device 2', data: [
errorCode: 'E-102', { x: 'Jan', y: 12 },
status: 'Alarm', { x: 'Feb', y: 14 },
{ x: 'Mar', y: 19 },
{ x: 'Apr', y: 21 },
{ x: 'Mei', y: 22 },
{ x: 'Jun', y: 20 },
{ x: 'Jul', y: 19 },
{ x: 'Agu', y: 21 },
{ x: 'Sep', y: 18 },
{ x: 'Okt', y: 20 },
],
}, },
{ {
key: '3', id: 'Compressor Section 3',
plantSubSection: 'Section C', color: '#FFE66D',
device: 'Device 3', data: [
errorCode: 'E-103', { x: 'Jan', y: 8 },
status: 'Done', { x: 'Feb', y: 10 },
}, { x: 'Mar', y: 12 },
{ { x: 'Apr', y: 15 },
key: '4', { x: 'Mei', y: 18 },
plantSubSection: 'Section A', { x: 'Jun', y: 20 },
device: 'Device 4', { x: 'Jul', y: 22 },
errorCode: 'E-104', { x: 'Agu', y: 21 },
status: 'Warning', { x: 'Sep', y: 19 },
{ x: 'Okt', y: 26 },
],
}, },
]; ];
const IndexReport = memo(function IndexReport() { const IndexReport = memo(function IndexReport() {
const navigate = useNavigate(); const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb(); const { setBreadcrumbItems } = useBreadcrumb();
const [data, setData] = useState(initialData);
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('Bulanan');
const [userRole, setUserRole] = useState(null);
const [roleLevel, setRoleLevel] = useState(null);
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (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([ setBreadcrumbItems([
{ title: <Text strong style={{ fontSize: '14px' }}> History</Text> }, {
{ title: <Text strong style={{ fontSize: '14px' }}>Report</Text> } title: (
<Text strong style={{ fontSize: '14px' }}>
History
</Text>
),
},
{
title: (
<Text strong style={{ fontSize: '14px' }}>
Report
</Text>
),
},
]); ]);
} else { } else {
navigate('/signin'); navigate('/signin');
} }
}, []); }, []);
const getTitleStyle = (statusName) => { const handleReset = () => {
let backgroundColor; setPlantSubSection('Semua Plant');
switch (statusName.toLowerCase()) { setStartDate(dayjs('2025-09-30'));
case 'done': setEndDate(dayjs('2025-10-09'));
backgroundColor = '#52c41a'; // green setPeriode('Bulanan');
break;
case 'warning':
backgroundColor = '#faad14'; // orange
break;
case 'alarm':
backgroundColor = '#f5222d'; // red
break;
case 'critical':
backgroundColor = '#000000'; // black
break;
default:
backgroundColor = 'transparent';
}
return {
backgroundColor,
color: '#fff',
padding: '2px 8px',
borderRadius: '4px',
display: 'inline-block'
};
}; };
// Check if user has permission to view data (all except guest)
const canViewData = userRole && userRole !== 'guest';
// Convert chart data to table format
const convertToTableData = () => {
return lineChartData.map((section, index) => {
const rowData = {
key: index,
section: section.id,
color: section.color,
};
// Add each month's data as a column
section.data.forEach((point) => {
rowData[point.x] = point.y;
});
return rowData;
});
};
const tableData = convertToTableData();
// Create dynamic columns based on months
const months = lineChartData[0]?.data.map((point) => point.x) || [];
const columns = [ const columns = [
{ {
title: 'Plant Sub Section', title: 'Plant Sub Section',
dataIndex: 'plantSubSection', dataIndex: 'section',
key: 'plantSubSection', key: 'section',
}, fixed: 'left',
{ width: 200,
title: 'Device', render: (text, record) => (
dataIndex: 'device', <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
key: 'device', <div
}, style={{
{ width: '12px',
title: 'Error Code', height: '12px',
dataIndex: 'errorCode', borderRadius: '50%',
key: 'errorCode', backgroundColor: record.color,
}, }}
{ />
title: 'Status', <Text strong>{text}</Text>
dataIndex: 'status', </div>
key: 'status',
render: (status) => (
<span style={getTitleStyle(status)}>{status}</span>
), ),
}, },
...months.map((month) => ({
title: month,
dataIndex: month,
key: month,
align: 'center',
width: 80,
render: (value) => <Text>{value}</Text>,
})),
]; ];
return ( return (
<div style={{ padding: 24, minHeight: 360 }}> <React.Fragment>
<Table columns={columns} dataSource={data} /> <div style={{ minHeight: 360 }}>
</div> {/* 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: 'Harian', label: 'Harian' },
{ value: 'Mingguan', label: 'Mingguan' },
{ value: 'Bulanan', label: 'Bulanan' },
]}
/>
</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' }}>
Trending Error per Plant Sub Section
</Text>
</div>
<Table
columns={columns}
dataSource={tableData}
pagination={false}
scroll={{ x: 1000 }}
size="middle"
/>
</Card>
)}
</div>
</React.Fragment>
); );
}); });

View File

@@ -1,10 +1,9 @@
import React, { memo, useEffect, useState } from 'react'; import React, { memo, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { Typography, Select, DatePicker, Button, Row, Col, Card, Statistic } from 'antd'; import { Typography, Select, DatePicker, Button, Row, Col, Card } from 'antd';
import { ResponsiveLine } from '@nivo/line'; import { ResponsiveLine } from '@nivo/line';
import { ResponsivePie } from '@nivo/pie'; import { FileTextOutlined } from '@ant-design/icons';
import { FileTextOutlined, ToolOutlined, CheckCircleOutlined, ClockCircleOutlined } from '@ant-design/icons';
import { decryptData } from '../../../components/Global/Formatter'; import { decryptData } from '../../../components/Global/Formatter';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import './trending.css'; import './trending.css';
@@ -47,8 +46,20 @@ const IndexTrending = memo(function IndexTrending() {
} }
setBreadcrumbItems([ setBreadcrumbItems([
{ title: <Text strong style={{ fontSize: '14px' }}> History</Text> }, {
{ title: <Text strong style={{ fontSize: '14px' }}>Trending</Text> }, title: (
<Text strong style={{ fontSize: '14px' }}>
History
</Text>
),
},
{
title: (
<Text strong style={{ fontSize: '14px' }}>
Trending
</Text>
),
},
]); ]);
} else { } else {
navigate('/signin'); navigate('/signin');
@@ -138,16 +149,19 @@ const IndexTrending = memo(function IndexTrending() {
return ( return (
<React.Fragment> <React.Fragment>
<div className="trending-container"> {/* Filter Section */}
{/* Filter Section */} <Card className="filter-card">
<Card className="filter-card">
<div className="filter-header"> <div className="filter-header">
<Text strong style={{ fontSize: '14px' }}> Filter Data</Text> <Text strong style={{ fontSize: '14px' }}>
Filter Data
</Text>
</div> </div>
<Row gutter={16} style={{ marginTop: '16px' }}> <Row gutter={16} style={{ marginTop: '16px' }}>
<Col xs={24} sm={12} md={6}> <Col xs={24} sm={12} md={6}>
<div className="filter-item"> <div className="filter-item">
<Text style={{ fontSize: '12px', color: '#666' }}>Plant Sub Section</Text> <Text style={{ fontSize: '12px', color: '#666' }}>
Plant Sub Section
</Text>
<Select <Select
value={plantSubSection} value={plantSubSection}
onChange={setPlantSubSection} onChange={setPlantSubSection}
@@ -155,7 +169,7 @@ const IndexTrending = memo(function IndexTrending() {
options={[ options={[
{ value: 'Semua Plant', label: 'Semua Plant' }, { value: 'Semua Plant', label: 'Semua Plant' },
{ value: 'Plant 1', label: 'Plant 1' }, { value: 'Plant 1', label: 'Plant 1' },
{ value: 'Plant 2', label: 'Plant 2' } { value: 'Plant 2', label: 'Plant 2' },
]} ]}
/> />
</div> </div>
@@ -192,7 +206,7 @@ const IndexTrending = memo(function IndexTrending() {
options={[ options={[
{ value: 'Harian', label: 'Harian' }, { value: 'Harian', label: 'Harian' },
{ value: 'Mingguan', label: 'Mingguan' }, { value: 'Mingguan', label: 'Mingguan' },
{ value: 'Bulanan', label: 'Bulanan' } { value: 'Bulanan', label: 'Bulanan' },
]} ]}
/> />
</div> </div>
@@ -221,7 +235,7 @@ const IndexTrending = memo(function IndexTrending() {
</Row> </Row>
</Card> </Card>
{/* Statistics Cards */} {/* Charts Section */}
{!canViewData ? ( {!canViewData ? (
<Card style={{ marginTop: '24px', textAlign: 'center', padding: '40px' }}> <Card style={{ marginTop: '24px', textAlign: 'center', padding: '40px' }}>
<Text style={{ fontSize: '16px', color: '#999' }}> <Text style={{ fontSize: '16px', color: '#999' }}>
@@ -233,205 +247,61 @@ const IndexTrending = memo(function IndexTrending() {
) : ( ) : (
<> <>
<Row gutter={16} style={{ marginTop: '24px' }}> <Row gutter={16} style={{ marginTop: '24px' }}>
<Col xs={24} sm={12} md={6}> {/* Line Chart */}
<Card className="stat-card stat-card-red"> <Col xs={24}>
<div className="stat-icon"> <Card className="chart-card">
<FileTextOutlined style={{ fontSize: '24px' }} /> <div className="chart-header">
<Text strong style={{ fontSize: '14px' }}>
Trending Error per Plant Sub Section
</Text>
</div> </div>
<Statistic <div style={{ height: '500px', marginTop: '16px' }}>
title={<Text style={{ fontSize: '12px', color: '#666' }}>Total Error</Text>} <ResponsiveLine
value={245} data={lineChartData}
valueStyle={{ fontSize: '24px', fontWeight: 'bold', color: '#333' }} margin={{ top: 20, right: 20, bottom: 50, left: 50 }}
/> xScale={{ type: 'point' }}
</Card> yScale={{ type: 'linear', min: 0, max: 35 }}
</Col> curve="natural"
<Col xs={24} sm={12} md={6}> axisBottom={{
<Card className="stat-card stat-card-orange"> tickSize: 5,
<div className="stat-icon"> tickPadding: 5,
<ToolOutlined style={{ fontSize: '24px' }} /> tickRotation: 0,
}}
axisLeft={{
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
}}
colors={{ datum: 'color' }}
pointSize={6}
pointColor={{ theme: 'background' }}
pointBorderWidth={2}
pointBorderColor={{ from: 'serieColor' }}
pointLabelYOffset={-12}
useMesh={true}
legends={[
{
anchor: 'bottom',
direction: 'row',
justify: false,
translateX: 0,
translateY: 50,
itemsSpacing: 10,
itemDirection: 'left-to-right',
itemWidth: 140,
itemHeight: 20,
itemOpacity: 0.75,
symbolSize: 12,
symbolShape: 'circle',
},
]}
/>
</div> </div>
<Statistic
title={<Text style={{ fontSize: '12px', color: '#666' }}>Sedang Maintenance</Text>}
value={12}
valueStyle={{ fontSize: '24px', fontWeight: 'bold', color: '#333' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card className="stat-card stat-card-green">
<div className="stat-icon">
<CheckCircleOutlined style={{ fontSize: '24px' }} />
</div>
<Statistic
title={<Text style={{ fontSize: '12px', color: '#666' }}>Selesai Diperbaiki</Text>}
value={233}
valueStyle={{ fontSize: '24px', fontWeight: 'bold', color: '#333' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card className="stat-card stat-card-blue">
<div className="stat-icon">
<ClockCircleOutlined style={{ fontSize: '24px' }} />
</div>
<Statistic
title={<Text style={{ fontSize: '12px', color: '#666' }}>Rata-rata Waktu Perbaikan</Text>}
value="2.5 jam"
valueStyle={{ fontSize: '24px', fontWeight: 'bold', color: '#333' }}
/>
</Card> </Card>
</Col> </Col>
</Row> </Row>
{/* Charts Section */}
<Row gutter={16} style={{ marginTop: '24px' }}>
{/* Line Chart */}
<Col xs={24} lg={12}>
<Card className="chart-card">
<div className="chart-header">
<Text strong style={{ fontSize: '14px' }}> Trending Error per Plant Sub Section</Text>
</div>
<div style={{ height: '300px', marginTop: '16px' }}>
<ResponsiveLine
data={lineChartData}
margin={{ top: 20, right: 20, bottom: 50, left: 50 }}
xScale={{ type: 'point' }}
yScale={{ type: 'linear', min: 0, max: 35 }}
curve="natural"
axisBottom={{
tickSize: 5,
tickPadding: 5,
tickRotation: 0
}}
axisLeft={{
tickSize: 5,
tickPadding: 5,
tickRotation: 0
}}
colors={{ datum: 'color' }}
pointSize={6}
pointColor={{ theme: 'background' }}
pointBorderWidth={2}
pointBorderColor={{ from: 'serieColor' }}
pointLabelYOffset={-12}
useMesh={true}
legends={[
{
anchor: 'bottom',
direction: 'row',
justify: false,
translateX: 0,
translateY: 50,
itemsSpacing: 10,
itemDirection: 'left-to-right',
itemWidth: 140,
itemHeight: 20,
itemOpacity: 0.75,
symbolSize: 12,
symbolShape: 'circle'
}
]}
/>
</div>
</Card>
</Col>
{/* Donut Chart */}
<Col xs={24} lg={12}>
<Card className="chart-card">
<div className="chart-header">
<Text strong style={{ fontSize: '14px' }}> Distribusi Error per Plant</Text>
</div>
<div style={{ height: '300px', marginTop: '16px' }}>
<ResponsivePie
data={donutChartData}
margin={{ top: 20, right: 80, bottom: 80, left: 80 }}
innerRadius={0.6}
padAngle={1}
cornerRadius={3}
activeOuterRadiusOffset={8}
colors={{ datum: 'data.color' }}
borderWidth={1}
borderColor={{ from: 'color', modifiers: [['darker', 0.2]] }}
arcLinkLabelsSkipAngle={10}
arcLinkLabelsTextColor="#333333"
arcLinkLabelsThickness={2}
arcLinkLabelsColor={{ from: 'color' }}
arcLabelsSkipAngle={10}
arcLabelsTextColor={{ from: 'color', modifiers: [['darker', 2]] }}
legends={[
{
anchor: 'bottom',
direction: 'row',
justify: false,
translateX: 0,
translateY: 56,
itemsSpacing: 8,
itemWidth: 80,
itemHeight: 18,
itemTextColor: '#999',
itemDirection: 'left-to-right',
itemOpacity: 1,
symbolSize: 12,
symbolShape: 'circle'
}
]}
/>
</div>
</Card>
</Col>
</Row>
{/* Bar Chart and Bottom Section */}
<Row gutter={16} style={{ marginTop: '24px' }}>
{/* Bar Chart */}
<Col xs={24} lg={12}>
<Card className="chart-card">
<div className="chart-header">
<Text strong style={{ fontSize: '14px' }}> Jenis Error Terbanyak</Text>
</div>
<div style={{ marginTop: '24px', padding: '0 20px' }}>
{barChartData.map((item, index) => (
<div key={index} style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>
<Text style={{ fontSize: '12px' }}>{item.label}</Text>
<Text strong style={{ fontSize: '12px' }}>{item.value}</Text>
</div>
<div style={{
width: '100%',
height: '24px',
backgroundColor: '#f0f0f0',
borderRadius: '4px',
overflow: 'hidden'
}}>
<div style={{
width: `${(item.value / 70) * 100}%`,
height: '100%',
backgroundColor: '#FF6B4A',
transition: 'width 0.3s ease'
}} />
</div>
</div>
))}
</div>
</Card>
</Col>
{/* Perbandingan Error Section */}
<Col xs={24} lg={12}>
<Card className="chart-card">
<div className="chart-header">
<Text strong style={{ fontSize: '14px' }}> Perbandingan Error Bulan Ini vs Bulan Lalu</Text>
</div>
<div style={{ marginTop: '24px', padding: '20px', textAlign: 'center' }}>
<Text style={{ color: '#999' }}>Data akan ditampilkan di sini</Text>
</div>
</Card>
</Col>
</Row>
</> </>
)} )}
</div>
</React.Fragment> </React.Fragment>
); );
}); });