From c6957b46c6b0e4f16bba4e5ba85296172463ee4b Mon Sep 17 00:00:00 2001 From: Rafiafrzl Date: Thu, 9 Oct 2025 16:45:52 +0700 Subject: [PATCH] add notification management with list and detail views, including modal handling and state management --- src/pages/notification/IndexNotification.jsx | 56 ++- .../component/DetailNotification.jsx | 169 ++++++++ .../component/ListNotification.jsx | 399 ++++++++++++++++++ 3 files changed, 617 insertions(+), 7 deletions(-) create mode 100644 src/pages/notification/component/DetailNotification.jsx create mode 100644 src/pages/notification/component/ListNotification.jsx diff --git a/src/pages/notification/IndexNotification.jsx b/src/pages/notification/IndexNotification.jsx index 3f45aa4..a4828dd 100644 --- a/src/pages/notification/IndexNotification.jsx +++ b/src/pages/notification/IndexNotification.jsx @@ -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: • Notifikasi } + { + title: ( + + • Notifikasi + + ), + }, ]); } 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 ( -
-

Notifikasi Page

-
+ + + + ); }); diff --git a/src/pages/notification/component/DetailNotification.jsx b/src/pages/notification/component/DetailNotification.jsx new file mode 100644 index 0000000..2f19a7e --- /dev/null +++ b/src/pages/notification/component/DetailNotification.jsx @@ -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 ( + + {selectedData && ( +
+ {/* Header with Icon and Status */} +
+
+ {IconComponent && } +
+
+ + {selectedData.type.toUpperCase()} + +
+ {selectedData.title} +
+
+
+ + + + {/* Information Grid */} + + +
+
+ PLC +
+
+ {selectedData.plc} +
+
+ + +
+
Tag
+
+ {selectedData.tag} +
+
+ +
+ + + +
+
+ Engineer +
+
+ {selectedData.engineer} +
+
+ + +
+
+ Waktu +
+
+ {selectedData.time} +
+
+ +
+ + + + {/* Status */} +
+
Status
+ + {selectedData.isRead ? 'Sudah Dibaca' : 'Belum Dibaca'} + +
+ + {/* Additional Info */} +
+
+ Catatan: Notifikasi ini telah dikirim ke engineer yang bersangkutan + untuk ditindaklanjuti sesuai dengan prosedur yang berlaku. +
+
+
+ )} +
+ ); +}); + +export default DetailNotification; diff --git a/src/pages/notification/component/ListNotification.jsx b/src/pages/notification/component/ListNotification.jsx new file mode 100644 index 0000000..6ed7f4a --- /dev/null +++ b/src/pages/notification/component/ListNotification.jsx @@ -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 ( + + + + +

+ Notifikasi +

+

+ Riwayat notifikasi yang dikirim ke engineer +

+ + {/* Tabs */} +
+
+ + + +
+
+ + {/* Notification List */} +
+ {filteredNotifications.length === 0 ? ( +
+ Tidak ada notifikasi +
+ ) : ( + filteredNotifications.map((notification) => { + const { IconComponent, color, bgColor } = getIconAndColor( + notification.type + ); + + return ( +
{ + 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 && ( +
+ )} + +
+ {/* Icon */} +
+ +
+ + {/* Content */} +
+
+ {notification.title} +
+ +
+ + {notification.plc} + + + • + + + Tag: {notification.tag} + +
+ +
+ + Engineer:{' '} + + {notification.engineer} + + + + {notification.time} + +
+
+ + {/* Button */} + +
+
+ ); + }) + )} +
+ + + + + ); +}); + +export default ListNotification;