diff --git a/src/pages/master/brandDevice/EditBrandDevice.jsx b/src/pages/master/brandDevice/EditBrandDevice.jsx index b0e392e..8366cef 100644 --- a/src/pages/master/brandDevice/EditBrandDevice.jsx +++ b/src/pages/master/brandDevice/EditBrandDevice.jsx @@ -319,8 +319,28 @@ const EditBrandDevice = () => { setIsErrorCodeFormReadOnly(true); setEditingErrorCodeKey(record.key); + // Load solutions to solution form if (record.solution && record.solution.length > 0) { - setSolutionsForExistingRecord(record.solution, errorCodeForm); + setSolutionsForExistingRecord(record.solution, solutionForm); + } else { + resetSolutionFields(); + } + + // Load spareparts to sparepart form + if (record.sparepart && record.sparepart.length > 0) { + setSparepartForExistingRecord(record.sparepart, sparepartForm); + + // Load sparepart images + const newSparepartImages = {}; + record.sparepart.forEach(sparepart => { + if (sparepart.sparepart_image) { + newSparepartImages[sparepart.id || sparepart.key] = sparepart.sparepart_image; + } + }); + setSparepartImages(newSparepartImages); + } else { + resetSparepartFields(); + setSparepartImages({}); } }; diff --git a/src/pages/notification/component/ListNotification.jsx b/src/pages/notification/component/ListNotification.jsx index 8812841..433bdc6 100644 --- a/src/pages/notification/component/ListNotification.jsx +++ b/src/pages/notification/component/ListNotification.jsx @@ -1,5 +1,5 @@ import React, { memo, useState, useEffect } from 'react'; -import { Button, Row, Col, Card, Badge, Input, Typography, Space } from 'antd'; +import { Button, Row, Col, Card, Badge, Input, Typography, Space, Divider, Modal, Tag, message } from 'antd'; import { CloseCircleFilled, WarningFilled, @@ -11,16 +11,22 @@ import { SendOutlined, MailOutlined, UserOutlined, - FileTextOutlined, HistoryOutlined, EyeOutlined, + MobileOutlined, + CloseOutlined, + BookOutlined, + ToolOutlined, + FilePdfOutlined, + PlusOutlined, + ExclamationCircleOutlined, } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; const { Text, Paragraph, Link } = Typography; -// Dummy data untuk notifikasi -const initialNotifications = [ +// Dummy data untuk notifikasi device +const initialDeviceNotifications = [ { id: 1, type: 'critical', @@ -31,7 +37,13 @@ const initialNotifications = [ location: 'Lantai 2, Area Produksi A, Zona 3', details: 'Terjadi kenaikan suhu melebihi ambang batas pada Compressor Unit A. Pengecekan potensi kerusakan dibutuhkan.', link: 'https://tinyurl.com/compA85', + subsection: 'Air Dryer A', + value: '85° C', + threshold: '≤75° C', isRead: false, + plc: 'PLC-001', + status: 'Butuh Info lebih', + tag: 'A1-TEMP', }, { id: 2, @@ -43,7 +55,13 @@ const initialNotifications = [ location: 'Lantai 1, Area Produksi C, Zona 1', details: 'Tekanan mendekati ambang batas atas. Perlu monitoring lebih lanjut oleh engineer.', link: 'https://tinyurl.com/compC72', + subsection: 'Main Compressor', + value: '7.2 bar', + threshold: '<7.5 bar', isRead: false, + plc: 'PLC-002', + status: 'Monitoring', + tag: 'C1-PRESSURE', }, { id: 3, @@ -55,14 +73,62 @@ const initialNotifications = [ location: 'Lantai 2, Area Produksi B, Zona 2', details: 'Getaran pada Unit B telah kembali normal setelah perbaikan.', link: 'https://tinyurl.com/compBresolved', + subsection: 'Motor Bearing', + value: 'Normal', + threshold: 'N/A', isRead: true, + plc: 'PLC-003', + status: 'Resolved', + tag: 'B1-VIBRATION', }, ]; +// Dummy data untuk user history +const userHistoryData = [ + { id: 1, name: 'John Doe', phone: '081234567890', status: 'Delivered', timestamp: '04-11-2025 11:40 WIB' }, + { id: 2, name: 'Jane Smith', phone: '087654321098', status: 'Delivered', timestamp: '04-11-2025 11:41 WIB' }, + { id: 3, name: 'Peter Jones', phone: '082345678901', status: 'Delivered', timestamp: '04-11-2025 11:42 WIB' }, +]; + +// Dummy data untuk log history +const logHistoryData = [ + { + id: 1, + timestamp: '04-11-2025 11:55 WIB', + addedBy: { + name: 'Budi Santoso', + phone: '081122334455' + }, + description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.' + }, + { + id: 2, + timestamp: '04-11-2025 11:45 WIB', + addedBy: { + name: 'John Doe', + phone: '081234567890' + }, + description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.', + }, + { + id: 3, + timestamp: '04-11-2025 11:40 WIB', + addedBy: { + name: 'Jane Smith', + phone: '087654321098' + }, + description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.', + } +]; + + const ListNotification = memo(function ListNotification(props) { - const [notifications, setNotifications] = useState(initialNotifications); + const [notifications, setNotifications] = useState(initialDeviceNotifications); const [activeTab, setActiveTab] = useState('all'); const [searchTerm, setSearchTerm] = useState(''); + const [modalContent, setModalContent] = useState(null); // 'user', 'log', 'details', or null + const [isAddingLog, setIsAddingLog] = useState(false); + const [selectedNotification, setSelectedNotification] = useState(null); const navigate = useNavigate(); useEffect(() => { @@ -74,33 +140,30 @@ const ListNotification = memo(function ListNotification(props) { 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', - }; + 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 handleResend = (notification) => { + Modal.confirm({ + title: 'Confirm Resend', + icon: , + content: `Are you sure you want to resend the notification for "${notification.title}"?`, + okText: 'Resend', + cancelText: 'Cancel', + onOk() { + console.log('Resending notification:', notification.id); + // Di sini Anda bisa menambahkan logika pemanggilan API + message.success(`Notification for "${notification.title}" has been resent successfully.`); + }, + onCancel() { + console.log('Resend cancelled'); + }, + }); + }; const handleMarkAsRead = (id) => { setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, isRead: true } : n)) @@ -108,21 +171,14 @@ const ListNotification = memo(function ListNotification(props) { }; const filteredNotifications = notifications - .filter((n) => { - if (activeTab === 'all') return true; - if (activeTab === 'unread') return !n.isRead; - if (activeTab === 'read') return n.isRead; - return true; - }) - .filter((n) => { + .filter(n => (activeTab === 'all' || (activeTab === 'unread' && !n.isRead) || (activeTab === 'read' && n.isRead))) + .filter(n => { if (!searchTerm) return true; const searchableText = `${n.title} ${n.issue} ${n.description} ${n.location} ${n.details}`.toLowerCase(); return searchableText.includes(searchTerm.toLowerCase()); }); - const getUnreadCount = () => { - return notifications.filter((n) => !n.isRead).length; - }; + const getUnreadCount = () => notifications.filter((n) => !n.isRead).length; const tabButtonStyle = (isActive) => ({ padding: '12px 16px', @@ -137,187 +193,493 @@ const ListNotification = memo(function ListNotification(props) { transition: 'all 0.3s', }); + const renderDeviceNotifications = () => ( + + {filteredNotifications.length === 0 ? ( +
Tidak ada notifikasi
+ ) : ( + filteredNotifications.map((notification) => { + const { IconComponent, color, bgColor } = getIconAndColor(notification.type); + return ( + handleMarkAsRead(notification.id)} + > +
+
+ +
+
+ + +
+
+ {notification.title} +
{notification.issue}
+
+ {!notification.isRead && } +
+ + +
+ + + {notification.description}
+ {notification.details} +
+
+ + {notification.timestamp} + {notification.location} + + {notification.link} + + + + + + +
+
+
+ ); + }) + )} +
+ ); + + const renderUserHistory = () => ( + <> + + {userHistoryData.map(user => ( + + + + + {user.name} + | + {user.phone} + | + + + + + + Success Delivered at {user.timestamp} + + + + + + + + ))} + + + ); + + const renderLogHistory = () => ( + <> +
+ {/* Garis vertikal yang menyambung */} +
+ + {logHistoryData.map((log, index) => ( + + {/* Kolom Kiri: Branch/Timeline */} + +
+ + + {/* Kolom Kanan: Card */} + + + + + + + + Added at {log.timestamp} + +
+ Added by: {log.addedBy.name} + + {log.addedBy.phone} + +
+
+ + + + {log.description} + + +
+
+ +
+ ))} +
+ + ); + + const renderDetailsNotification = () => { + if (!selectedNotification) return null; + + const { IconComponent, color } = getIconAndColor(selectedNotification.type); + + return ( + + + {/* Kolom Kiri: Data Kompresor */} + + + + + +
+ + + {selectedNotification.title} +
{selectedNotification.issue}
+ +
+
+ Plant Subsection +
{selectedNotification.subsection}
+ Time +
{selectedNotification.timestamp.split(' ')[1]} WIB
+
+ +
+ + + Value +
{selectedNotification.value.includes('°') ? selectedNotification.value : `${selectedNotification.value}° C`}
+ + + Treshold +
{selectedNotification.threshold.includes('°') ? selectedNotification.threshold : `${selectedNotification.threshold}° C`}
+ +
+
+
+
+ + + {/* Kolom Kanan: Informasi Teknis */} + + + +
+ PLC +
{selectedNotification.plc}
+
+
+ Status +
{selectedNotification.status}
+
+
+ Tag +
{selectedNotification.tag}
+
+
+
+ +
+
+ + + + + + Handling Guideline + + + + + + + + Spare Part + + + + + setModalContent('log')}> + + + Log Activity + + + + + + + + + PDF} + > +
+
+ + Error 303.pdf + + + lihat disini + +
+
+
+ PDF} + > +
+
+ + Error 303.pdf + + + lihat disini + +
+
+
+ PDF} + > +
+
+ + Error 303.pdf + + + lihat disini + +
+
+
+ Text} + > + + Error 303: Sensor suhu tidak merespon. Lakukan reset manual pada panel kontrol. + + +
+
+ + + + + + + +
+ +
+ Available + + + + Air Filter + + Filters incoming air to remove dust and impurities before compression. + +
+ Part No: AF-2024-X2 | Condition: New | Compatible with Model XR-60 +
+
+ +
+
+ + + +
+ +
+ Not Available + + + + Compressor Oil + + Special synthetic oil to lubricate and cool down the compressor unit. + +
+ Part No: OL-SYN-550 | Condition: New | Compatible with Model XR-60 +
+
+ +
+
+
+
+ + + + + + + {isAddingLog && ( + <> + Add New Log / Update Progress + + + )} + + + + {logHistoryData.slice(0, 2).map(log => ( // Menampilkan 2 log terbaru sebagai pratinjau + + + {log.addedBy.name}: {log.description} + + {log.timestamp} + + ))} +
+ +
+
+
+ +
+
+
+ ); + }; + return ( -

- Notification -

+

Notification

Riwayat notifikasi yang dikirim ke engineer

- - + + setSearchTerm(value)} + onSearch={setSearchTerm} onChange={(e) => setSearchTerm(e.target.value)} - style={{ marginBottom: '24px', width: 300 }} + style={{ width: 300 }} /> - - {/* Tabs */} -
-
- - - -
-
- - {/* Notification List */} - - {filteredNotifications.length === 0 ? ( -
- Tidak ada notifikasi +
+
+ + +
- ) : ( - filteredNotifications.map((notification) => { - const { IconComponent, color, bgColor } = getIconAndColor( - notification.type - ); +
- return ( - handleMarkAsRead(notification.id)} - > -
- {/* Icon */} -
- -
- - {/* Content */} -
- - {/* Left: Title and Issue */} - -
-
- {notification.title} -
- {notification.issue} -
-
- {!notification.isRead && ( - - )} -
- - - {/* Middle: Description and Details */} - -
- - - {notification.description} -
- {notification.details} -
-
- - - - {notification.timestamp} - - - - {notification.location} - - - - {notification.link} - - - - - - {/* Right: Action Icons */} - - -
-
-
- ); - }) - )} - + {renderDeviceNotifications()} + + + {modalContent === 'details' ? ( +
+ + Error Notification Detail + +
+ ) : ( + + {modalContent === 'user' && 'User History Notification'} + {modalContent === 'log' && 'Log History Notification'} + + )} +
+ } + open={modalContent !== null} + onCancel={() => { + setModalContent(null); + // Reset state isAddingLog saat modal ditutup + if (modalContent === 'details') { + setIsAddingLog(false); + } + setSelectedNotification(null); + }} + closable={false} // Menghilangkan tombol 'x' di pojok kanan atas + width={modalContent === 'details' ? 900 : 700} + footer={ +
+ +
+ } + > + {modalContent === 'user' && renderUserHistory()} + {modalContent === 'log' && renderLogHistory()} + {modalContent === 'details' && renderDetailsNotification()} +
); }); -export default ListNotification; +export default ListNotification; \ No newline at end of file diff --git a/src/pages/notification/component/LogHistoryModal.jsx b/src/pages/notification/component/LogHistoryModal.jsx new file mode 100644 index 0000000..debebc8 --- /dev/null +++ b/src/pages/notification/component/LogHistoryModal.jsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { Modal, Table, Tag, Typography } from 'antd'; +import { ClockCircleOutlined, UserOutlined } from '@ant-design/icons'; +import dayjs from 'dayjs'; + +const { Text } = Typography; + +// Dummy data untuk log history +const getDummyLogHistory = (notification) => { + if (!notification) return []; + return [ + { + key: '1', + timestamp: dayjs().subtract(2, 'hour').format('DD-MM-YYYY HH:mm:ss'), + activity: 'Notification Created', + details: `System generated a ${notification.type} notification for: ${notification.issue}`, + }, + { + key: '2', + timestamp: dayjs().subtract(1, 'hour').format('DD-MM-YYYY HH:mm:ss'), + activity: 'Notification Sent', + details: 'Sent to 2 engineers', + }, + { + key: '3', + timestamp: dayjs().subtract(30, 'minute').format('DD-MM-YYYY HH:mm:ss'), + activity: 'Notification Read', + details: 'Read by Engineer A', + }, + { + key: '4', + timestamp: dayjs().subtract(5, 'minute').format('DD-MM-YYYY HH:mm:ss'), + activity: 'Resend Triggered', + details: 'Notification resent by Admin', + }, + ]; +}; + +const columns = [ + { + title: 'Timestamp', + dataIndex: 'timestamp', + key: 'timestamp', + render: (text) => ( + + + {text} + + ), + }, + { + title: 'Activity', + dataIndex: 'activity', + key: 'activity', + render: (text) => { + let color = 'blue'; + if (text.includes('Created')) { + color = 'geekblue'; + } else if (text.includes('Sent')) { + color = 'purple'; + } else if (text.includes('Read')) { + color = 'green'; + } else if (text.includes('Triggered')) { + color = 'orange'; + } + return {text.toUpperCase()}; + }, + }, + { + title: 'Details', + dataIndex: 'details', + key: 'details', + }, +]; + +const LogHistoryModal = ({ visible, onCancel, notificationData }) => { + const logHistoryData = getDummyLogHistory(notificationData); + + return ( + + Log History: {notificationData?.title} + + } + open={visible} + onCancel={onCancel} + footer={null} + width={800} + destroyOnClose + > + + + ); +}; + +export default LogHistoryModal; diff --git a/src/pages/notification/component/UserHistoryModal.jsx b/src/pages/notification/component/UserHistoryModal.jsx new file mode 100644 index 0000000..9cf832e --- /dev/null +++ b/src/pages/notification/component/UserHistoryModal.jsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { Modal, Typography, Card, Row, Col, Avatar, Tag, Button, Space } from 'antd'; +import { UserOutlined, PhoneOutlined, CheckCircleOutlined, SyncOutlined, SendOutlined } from '@ant-design/icons'; + +const { Text } = Typography; + +// Dummy data baru untuk user history +const getDummyUsers = (notification) => { + if (!notification) return []; + return [ + { + id: '1', + name: 'Budi Santoso', + phone: '081234567890', + status: 'delivered', + }, + { + id: '2', + name: 'Citra Lestari', + phone: '082345678901', + status: 'sent', + }, + { + id: '3', + name: 'Agus Wijaya', + phone: '083456789012', + status: 'failed', + }, + { + id: '4', + name: 'Dewi Anggraini', + phone: '084567890123', + status: 'delivered', + }, + ]; +}; + +const UserHistoryModal = ({ visible, onCancel, notificationData }) => { + const userData = getDummyUsers(notificationData); + + const getStatusTag = (status) => { + switch (status) { + case 'delivered': + return } color="success">Delivered; + case 'sent': + return } color="processing">Sent; + case 'failed': + return Failed; + default: + return {status}; + } + }; + + return ( + + User History Notification + + } + open={visible} + onCancel={onCancel} + footer={[ + , + ]} + width={600} + destroyOnClose + > +
+ + {userData.map((user) => ( + + +
+ + } /> +
+ {user.name} +
+ + {user.phone} +
+
+
+ + + + {getStatusTag(user.status)} + + + + + + ))} + + + + ); +}; + +export default UserHistoryModal; diff --git a/src/pages/notification/detail/UserHistory.jsx b/src/pages/notification/detail/UserHistory.jsx new file mode 100644 index 0000000..9ec87bb --- /dev/null +++ b/src/pages/notification/detail/UserHistory.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Button, Row, Col, Card, Badge, Typography, Space, Divider } from 'antd'; +import { SendOutlined, MobileOutlined, CheckCircleFilled, ArrowLeftOutlined } from '@ant-design/icons'; + +const { Text } = Typography; + +// Dummy data for user history +const userHistoryData = [ + { id: 1, name: 'John Doe', phone: '081234567890', status: 'Delivered', timestamp: '04-11-2025 11:40 WIB' }, + { id: 2, name: 'Jane Smith', phone: '087654321098', status: 'Delivered', timestamp: '04-11-2025 11:41 WIB' }, + { id: 3, name: 'Peter Jones', phone: '082345678901', status: 'Delivered', timestamp: '04-11-2025 11:42 WIB' }, +]; + +const UserHistory = ({ notification, onBack }) => { + return ( + + + + + + + {user.name} + | + {user.phone} + | + + + + + + Success Delivered at {user.timestamp} + + + + + + + + ))} + + + ); +}; + +export default UserHistory;