add notification management with list and detail views, including modal handling and state management

This commit is contained in:
2025-10-09 16:45:52 +07:00
parent a7af974108
commit c6957b46c6
3 changed files with 617 additions and 7 deletions

View File

@@ -1,29 +1,71 @@
import React, { memo, useEffect } from 'react';
import React, { memo, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useBreadcrumb } from '../../layout/LayoutBreadcrumb';
import { Typography } from 'antd';
import { Form, Typography } from 'antd';
import ListNotification from './component/ListNotification';
import DetailNotification from './component/DetailNotification';
const { Text } = Typography;
const IndexNotification = memo(function IndexNotification() {
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' }}> Notifikasi</Text> }
{
title: (
<Text strong style={{ fontSize: '14px' }}>
Notifikasi
</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 (
<div>
<h1>Notifikasi Page</h1>
</div>
<React.Fragment>
<ListNotification
actionMode={actionMode}
setActionMode={setActionMode}
selectedData={selectedData}
setSelectedData={setSelectedData}
/>
<DetailNotification
visible={isModalVisible}
onCancel={handleCancel}
form={form}
selectedData={selectedData}
/>
</React.Fragment>
);
});

View File

@@ -0,0 +1,169 @@
import React, { memo } from 'react';
import { Modal, Row, Col, Tag, Divider } from 'antd';
import { CloseCircleFilled, WarningFilled, CheckCircleFilled, InfoCircleFilled } from '@ant-design/icons';
const DetailNotification = memo(function DetailNotification({ visible, onCancel, form, selectedData }) {
const getIconAndColor = (type) => {
switch (type) {
case 'critical':
return {
IconComponent: CloseCircleFilled,
color: '#ff4d4f',
bgColor: '#fff1f0',
tagColor: 'error',
};
case 'warning':
return {
IconComponent: WarningFilled,
color: '#faad14',
bgColor: '#fffbe6',
tagColor: 'warning',
};
case 'resolved':
return {
IconComponent: CheckCircleFilled,
color: '#52c41a',
bgColor: '#f6ffed',
tagColor: 'success',
};
default:
return {
IconComponent: InfoCircleFilled,
color: '#1890ff',
bgColor: '#e6f7ff',
tagColor: 'processing',
};
}
};
const { IconComponent, color, bgColor, tagColor } = selectedData ? getIconAndColor(selectedData.type) : {};
return (
<Modal
title="Detail Notifikasi"
open={visible}
onCancel={onCancel}
onOk={onCancel}
okText="Tutup"
cancelButtonProps={{ style: { display: 'none' } }}
width={700}
>
{selectedData && (
<div>
{/* Header with Icon and Status */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
marginBottom: '24px',
padding: '16px',
backgroundColor: '#fafafa',
borderRadius: '8px',
}}
>
<div
style={{
width: '64px',
height: '64px',
borderRadius: '50%',
backgroundColor: bgColor,
color: color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '32px',
flexShrink: 0,
}}
>
{IconComponent && <IconComponent style={{ fontSize: '32px' }} />}
</div>
<div style={{ flex: 1 }}>
<Tag color={tagColor} style={{ marginBottom: '8px', fontSize: '12px' }}>
{selectedData.type.toUpperCase()}
</Tag>
<div style={{ fontSize: '16px', fontWeight: 600, color: '#262626' }}>
{selectedData.title}
</div>
</div>
</div>
<Divider style={{ margin: '16px 0' }} />
{/* Information Grid */}
<Row gutter={[16, 16]}>
<Col span={12}>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
PLC
</div>
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
{selectedData.plc}
</div>
</div>
</Col>
<Col span={12}>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>Tag</div>
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
{selectedData.tag}
</div>
</div>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={12}>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
Engineer
</div>
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
{selectedData.engineer}
</div>
</div>
</Col>
<Col span={12}>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
Waktu
</div>
<div style={{ fontSize: '14px', color: '#262626', fontWeight: 500 }}>
{selectedData.time}
</div>
</div>
</Col>
</Row>
<Divider style={{ margin: '16px 0' }} />
{/* Status */}
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '8px' }}>Status</div>
<Tag color={selectedData.isRead ? 'default' : 'blue'}>
{selectedData.isRead ? 'Sudah Dibaca' : 'Belum Dibaca'}
</Tag>
</div>
{/* 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> Notifikasi ini telah dikirim ke engineer yang bersangkutan
untuk ditindaklanjuti sesuai dengan prosedur yang berlaku.
</div>
</div>
</div>
)}
</Modal>
);
});
export default DetailNotification;

View File

@@ -0,0 +1,399 @@
import React, { memo, useState, useEffect } from 'react';
import { Button, Row, Col, Card, Badge } from 'antd';
import {
CloseCircleFilled,
WarningFilled,
CheckCircleFilled,
InfoCircleFilled,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
// Dummy data untuk notifikasi
const initialNotifications = [
{
id: 1,
type: 'critical',
title: 'Compressor Unit A - Overheat detected (85°C)',
plc: 'PLC-001',
tag: 'A1-TEMP',
engineer: 'Siti Nurhaliza',
time: '2 menit lalu',
status: 'unread',
isRead: false,
},
{
id: 2,
type: 'warning',
title: 'Compressor Unit C - Pressure slightly high (7.2 bar)',
plc: 'PLC-003',
tag: 'C3-PRESS',
engineer: 'Joko Widodo',
time: '15 menit lalu',
status: 'unread',
isRead: false,
},
{
id: 3,
type: 'resolved',
title: 'Compressor Unit B - Vibration issue resolved',
plc: 'PLC-002',
tag: 'B2-VIB',
engineer: 'Rudi Santoso',
time: '1 jam lalu',
status: 'read',
isRead: true,
},
{
id: 4,
type: 'critical',
title: 'Compressor Unit E - Low oil pressure (1.5 bar)',
plc: 'PLC-005',
tag: 'E1-OIL',
engineer: 'Ahmad Yani',
time: '2 jam lalu',
status: 'unread',
isRead: false,
},
{
id: 5,
type: 'warning',
title: 'Compressor Unit D - Temperature rising (78°C)',
plc: 'PLC-004',
tag: 'D2-TEMP',
engineer: 'Budi Santoso',
time: '3 jam lalu',
status: 'read',
isRead: true,
},
{
id: 6,
type: 'resolved',
title: 'Compressor Unit F - Maintenance completed',
plc: 'PLC-006',
tag: 'F1-MAIN',
engineer: 'Dewi Lestari',
time: '5 jam lalu',
status: 'read',
isRead: true,
},
];
const ListNotification = memo(function ListNotification(props) {
const [notifications, setNotifications] = useState(initialNotifications);
const [activeTab, setActiveTab] = useState('all');
const navigate = useNavigate();
useEffect(() => {
const token = localStorage.getItem('token');
if (!token) {
navigate('/signin');
}
}, [navigate]);
const getIconAndColor = (type) => {
switch (type) {
case 'critical':
return {
IconComponent: CloseCircleFilled,
color: '#ff4d4f',
bgColor: '#fff1f0',
};
case 'warning':
return {
IconComponent: WarningFilled,
color: '#faad14',
bgColor: '#fffbe6',
};
case 'resolved':
return {
IconComponent: CheckCircleFilled,
color: '#52c41a',
bgColor: '#f6ffed',
};
default:
return {
IconComponent: InfoCircleFilled,
color: '#1890ff',
bgColor: '#e6f7ff',
};
}
};
const filterNotifications = (status) => {
if (status === 'all') return notifications;
if (status === 'unread') return notifications.filter((n) => !n.isRead);
if (status === 'read') return notifications.filter((n) => n.isRead);
return notifications;
};
const getUnreadCount = () => {
return notifications.filter((n) => !n.isRead).length;
};
const handleViewDetail = (notification) => {
props.setSelectedData(notification);
props.setActionMode('preview');
// Mark as read
setNotifications((prev) =>
prev.map((n) => (n.id === notification.id ? { ...n, isRead: true, status: 'read' } : n))
);
};
const filteredNotifications = filterNotifications(activeTab);
const tabButtonStyle = (isActive) => ({
padding: '12px 16px',
border: 'none',
background: 'none',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 500,
color: isActive ? '#FF6B35' : '#595959',
borderBottom: isActive ? '2px solid #FF6B35' : '2px solid transparent',
marginBottom: '-1px',
transition: 'all 0.3s',
});
return (
<React.Fragment>
<Card>
<Row>
<Col xs={24}>
<h2
style={{
fontSize: '20px',
fontWeight: 600,
margin: '0 0 4px 0',
color: '#262626',
}}
>
Notifikasi
</h2>
<p style={{ margin: '0 0 24px 0', color: '#8c8c8c', fontSize: '14px' }}>
Riwayat notifikasi yang dikirim ke engineer
</p>
{/* Tabs */}
<div style={{ borderBottom: '1px solid #f0f0f0', marginBottom: '24px' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={() => setActiveTab('all')}
style={tabButtonStyle(activeTab === 'all')}
>
Semua
</button>
<button
onClick={() => setActiveTab('unread')}
style={{
...tabButtonStyle(activeTab === 'unread'),
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
Belum Dibaca
{getUnreadCount() > 0 && (
<Badge
count={getUnreadCount()}
style={{
backgroundColor: '#ff4d4f',
}}
/>
)}
</button>
<button
onClick={() => setActiveTab('read')}
style={tabButtonStyle(activeTab === 'read')}
>
Sudah Dibaca
</button>
</div>
</div>
{/* Notification List */}
<div>
{filteredNotifications.length === 0 ? (
<div
style={{
textAlign: 'center',
padding: '40px 0',
color: '#8c8c8c',
}}
>
Tidak ada notifikasi
</div>
) : (
filteredNotifications.map((notification) => {
const { IconComponent, color, bgColor } = getIconAndColor(
notification.type
);
return (
<div
key={notification.id}
style={{
marginBottom: '12px',
backgroundColor: '#ffffff',
border: '1px solid #f0f0f0',
borderRadius: '8px',
padding: '16px',
position: 'relative',
transition: 'all 0.3s',
cursor: 'pointer',
}}
onMouseEnter={(e) => {
e.currentTarget.style.boxShadow =
'0 2px 8px rgba(0,0,0,0.06)';
e.currentTarget.style.backgroundColor = '#f6f9ff';
e.currentTarget.style.borderColor = '#d6e4ff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.backgroundColor = '#ffffff';
e.currentTarget.style.borderColor = '#f0f0f0';
}}
>
{/* Dot for unread */}
{!notification.isRead && (
<div
style={{
position: 'absolute',
top: '16px',
right: '16px',
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: '#ff4d4f',
}}
/>
)}
<div
style={{
display: 'flex',
gap: '16px',
alignItems: 'flex-start',
}}
>
{/* Icon */}
<div
style={{
width: '48px',
height: '48px',
borderRadius: '50%',
backgroundColor: bgColor,
color: color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24px',
flexShrink: 0,
}}
>
<IconComponent style={{ fontSize: '24px' }} />
</div>
{/* Content */}
<div style={{ flex: 1 }}>
<div
style={{
fontSize: '15px',
fontWeight: 600,
marginBottom: '4px',
color: '#262626',
}}
>
{notification.title}
</div>
<div
style={{
display: 'flex',
gap: '8px',
marginBottom: '4px',
flexWrap: 'wrap',
}}
>
<span
style={{
fontSize: '13px',
color: '#8c8c8c',
}}
>
{notification.plc}
</span>
<span
style={{
fontSize: '13px',
color: '#d9d9d9',
}}
>
</span>
<span
style={{
fontSize: '13px',
color: '#8c8c8c',
}}
>
Tag: {notification.tag}
</span>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
flexWrap: 'wrap',
}}
>
<span
style={{
fontSize: '13px',
color: '#8c8c8c',
}}
>
Engineer:{' '}
<span style={{ color: '#595959' }}>
{notification.engineer}
</span>
</span>
<span
style={{
fontSize: '13px',
color: '#8c8c8c',
}}
>
{notification.time}
</span>
</div>
</div>
{/* Button */}
<Button
type="link"
onClick={() => handleViewDetail(notification)}
style={{
color: '#FF6B35',
fontSize: '14px',
fontWeight: 500,
}}
>
Lihat Detail
</Button>
</div>
</div>
);
})
)}
</div>
</Col>
</Row>
</Card>
</React.Fragment>
);
});
export default ListNotification;