Merge pull request 'lavoce' (#19) from lavoce into main
Reviewed-on: #19
This commit is contained in:
@@ -319,8 +319,28 @@ const EditBrandDevice = () => {
|
|||||||
setIsErrorCodeFormReadOnly(true);
|
setIsErrorCodeFormReadOnly(true);
|
||||||
setEditingErrorCodeKey(record.key);
|
setEditingErrorCodeKey(record.key);
|
||||||
|
|
||||||
|
// Load solutions to solution form
|
||||||
if (record.solution && record.solution.length > 0) {
|
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({});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { memo, useState, useEffect } from 'react';
|
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 {
|
import {
|
||||||
CloseCircleFilled,
|
CloseCircleFilled,
|
||||||
WarningFilled,
|
WarningFilled,
|
||||||
@@ -11,16 +11,22 @@ import {
|
|||||||
SendOutlined,
|
SendOutlined,
|
||||||
MailOutlined,
|
MailOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
FileTextOutlined,
|
|
||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
|
MobileOutlined,
|
||||||
|
CloseOutlined,
|
||||||
|
BookOutlined,
|
||||||
|
ToolOutlined,
|
||||||
|
FilePdfOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const { Text, Paragraph, Link } = Typography;
|
const { Text, Paragraph, Link } = Typography;
|
||||||
|
|
||||||
// Dummy data untuk notifikasi
|
// Dummy data untuk notifikasi device
|
||||||
const initialNotifications = [
|
const initialDeviceNotifications = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
type: 'critical',
|
type: 'critical',
|
||||||
@@ -31,7 +37,13 @@ const initialNotifications = [
|
|||||||
location: 'Lantai 2, Area Produksi A, Zona 3',
|
location: 'Lantai 2, Area Produksi A, Zona 3',
|
||||||
details: 'Terjadi kenaikan suhu melebihi ambang batas pada Compressor Unit A. Pengecekan potensi kerusakan dibutuhkan.',
|
details: 'Terjadi kenaikan suhu melebihi ambang batas pada Compressor Unit A. Pengecekan potensi kerusakan dibutuhkan.',
|
||||||
link: 'https://tinyurl.com/compA85',
|
link: 'https://tinyurl.com/compA85',
|
||||||
|
subsection: 'Air Dryer A',
|
||||||
|
value: '85° C',
|
||||||
|
threshold: '≤75° C',
|
||||||
isRead: false,
|
isRead: false,
|
||||||
|
plc: 'PLC-001',
|
||||||
|
status: 'Butuh Info lebih',
|
||||||
|
tag: 'A1-TEMP',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -43,7 +55,13 @@ const initialNotifications = [
|
|||||||
location: 'Lantai 1, Area Produksi C, Zona 1',
|
location: 'Lantai 1, Area Produksi C, Zona 1',
|
||||||
details: 'Tekanan mendekati ambang batas atas. Perlu monitoring lebih lanjut oleh engineer.',
|
details: 'Tekanan mendekati ambang batas atas. Perlu monitoring lebih lanjut oleh engineer.',
|
||||||
link: 'https://tinyurl.com/compC72',
|
link: 'https://tinyurl.com/compC72',
|
||||||
|
subsection: 'Main Compressor',
|
||||||
|
value: '7.2 bar',
|
||||||
|
threshold: '<7.5 bar',
|
||||||
isRead: false,
|
isRead: false,
|
||||||
|
plc: 'PLC-002',
|
||||||
|
status: 'Monitoring',
|
||||||
|
tag: 'C1-PRESSURE',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -55,14 +73,62 @@ const initialNotifications = [
|
|||||||
location: 'Lantai 2, Area Produksi B, Zona 2',
|
location: 'Lantai 2, Area Produksi B, Zona 2',
|
||||||
details: 'Getaran pada Unit B telah kembali normal setelah perbaikan.',
|
details: 'Getaran pada Unit B telah kembali normal setelah perbaikan.',
|
||||||
link: 'https://tinyurl.com/compBresolved',
|
link: 'https://tinyurl.com/compBresolved',
|
||||||
|
subsection: 'Motor Bearing',
|
||||||
|
value: 'Normal',
|
||||||
|
threshold: 'N/A',
|
||||||
isRead: true,
|
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 ListNotification = memo(function ListNotification(props) {
|
||||||
const [notifications, setNotifications] = useState(initialNotifications);
|
const [notifications, setNotifications] = useState(initialDeviceNotifications);
|
||||||
const [activeTab, setActiveTab] = useState('all');
|
const [activeTab, setActiveTab] = useState('all');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
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();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -74,33 +140,30 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
|
|
||||||
const getIconAndColor = (type) => {
|
const getIconAndColor = (type) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'critical':
|
case 'critical': return { IconComponent: CloseCircleFilled, color: '#ff4d4f', bgColor: '#fff1f0' };
|
||||||
return {
|
case 'warning': return { IconComponent: WarningFilled, color: '#faad14', bgColor: '#fffbe6' };
|
||||||
IconComponent: CloseCircleFilled,
|
case 'resolved': return { IconComponent: CheckCircleFilled, color: '#52c41a', bgColor: '#f6ffed' };
|
||||||
color: '#ff4d4f',
|
default: return { IconComponent: InfoCircleFilled, color: '#1890ff', bgColor: '#e6f7ff' };
|
||||||
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: <ExclamationCircleOutlined />,
|
||||||
|
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) => {
|
const handleMarkAsRead = (id) => {
|
||||||
setNotifications((prev) =>
|
setNotifications((prev) =>
|
||||||
prev.map((n) => (n.id === id ? { ...n, isRead: true } : n))
|
prev.map((n) => (n.id === id ? { ...n, isRead: true } : n))
|
||||||
@@ -108,21 +171,14 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filteredNotifications = notifications
|
const filteredNotifications = notifications
|
||||||
.filter((n) => {
|
.filter(n => (activeTab === 'all' || (activeTab === 'unread' && !n.isRead) || (activeTab === 'read' && n.isRead)))
|
||||||
if (activeTab === 'all') return true;
|
.filter(n => {
|
||||||
if (activeTab === 'unread') return !n.isRead;
|
|
||||||
if (activeTab === 'read') return n.isRead;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.filter((n) => {
|
|
||||||
if (!searchTerm) return true;
|
if (!searchTerm) return true;
|
||||||
const searchableText = `${n.title} ${n.issue} ${n.description} ${n.location} ${n.details}`.toLowerCase();
|
const searchableText = `${n.title} ${n.issue} ${n.description} ${n.location} ${n.details}`.toLowerCase();
|
||||||
return searchableText.includes(searchTerm.toLowerCase());
|
return searchableText.includes(searchTerm.toLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
const getUnreadCount = () => {
|
const getUnreadCount = () => notifications.filter((n) => !n.isRead).length;
|
||||||
return notifications.filter((n) => !n.isRead).length;
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabButtonStyle = (isActive) => ({
|
const tabButtonStyle = (isActive) => ({
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
@@ -137,187 +193,493 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const renderDeviceNotifications = () => (
|
||||||
|
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
||||||
|
{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 (
|
||||||
|
<Card
|
||||||
|
key={notification.id}
|
||||||
|
style={{
|
||||||
|
backgroundColor: notification.isRead ? '#ffffff' : '#f6f9ff',
|
||||||
|
borderColor: notification.isRead ? '#f0f0f0' : '#d6e4ff',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={() => handleMarkAsRead(notification.id)}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', gap: '16px', alignItems: 'flex-start' }}>
|
||||||
|
<div style={{ width: '40px', height: '40px', borderRadius: '50%', backgroundColor: bgColor, color: color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '22px', flexShrink: 0 }}>
|
||||||
|
<IconComponent style={{ fontSize: '22px' }} />
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Row align="top">
|
||||||
|
<Col flex="220px">
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
|
<div>
|
||||||
|
<Text strong>{notification.title}</Text>
|
||||||
|
<div style={{ marginTop: '4px' }}><Text style={{ color }}>{notification.issue}</Text></div>
|
||||||
|
</div>
|
||||||
|
{!notification.isRead && <Badge color="red" status="processing" style={{ marginLeft: '8px', marginTop: '4px' }} />}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col flex="auto">
|
||||||
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', marginBottom: '12px' }}>
|
||||||
|
<MailOutlined style={{ marginTop: '4px', color: '#1890ff' }} />
|
||||||
|
<Paragraph style={{ color: '#595959', margin: 0, flex: 1 }}>
|
||||||
|
<Text strong>{notification.description}</Text><br />
|
||||||
|
{notification.details}
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
<Space direction="vertical" size={4} style={{ fontSize: '13px', color: '#8c8c8c' }}>
|
||||||
|
<Space><ClockCircleOutlined /><Text type="secondary">{notification.timestamp}</Text></Space>
|
||||||
|
<Space><EnvironmentOutlined /><Text type="secondary">{notification.location}</Text></Space>
|
||||||
|
<Space>
|
||||||
|
<LinkOutlined /><Link href={notification.link} target="_blank">{notification.link}</Link>
|
||||||
|
<Button
|
||||||
|
type="link" icon={<SendOutlined />} style={{ paddingLeft: '8px' }}
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleResend(notification); }}
|
||||||
|
>
|
||||||
|
Resend
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col flex="120px" style={{ textAlign: 'center' }} align="bottom">
|
||||||
|
<Space style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||||
|
<Button type="text" icon={<UserOutlined style={{ color: '#1890ff' }} />} title="User History" style={{ border: '1px solid #1890ff', borderRadius: '4px' }} onClick={(e) => { e.stopPropagation(); setModalContent('user'); }} />
|
||||||
|
<Button type="text" icon={<EyeOutlined style={{ color: '#1890ff' }} />} title="Details" style={{ border: '1px solid #1890ff', borderRadius: '4px' }} onClick={(e) => { e.stopPropagation(); setModalContent('details'); setSelectedNotification(notification); }} />
|
||||||
|
<Button type="text" icon={<HistoryOutlined style={{ color: '#1890ff' }} />} title="Log History" style={{ border: '1px solid #1890ff', borderRadius: '4px' }} onClick={(e) => { e.stopPropagation(); setModalContent('log'); }} />
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderUserHistory = () => (
|
||||||
|
<>
|
||||||
|
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
||||||
|
{userHistoryData.map(user => (
|
||||||
|
<Card key={user.id} style={{ borderColor: '#91d5ff' }}>
|
||||||
|
<Row align="middle" justify="space-between">
|
||||||
|
<Col>
|
||||||
|
<Space align="center">
|
||||||
|
<Text strong>{user.name}</Text>
|
||||||
|
<Text>|</Text>
|
||||||
|
<Text><MobileOutlined /> {user.phone}</Text>
|
||||||
|
<Text>|</Text>
|
||||||
|
<Badge status="success" text={user.status} />
|
||||||
|
</Space>
|
||||||
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
<Space align="center">
|
||||||
|
<CheckCircleFilled style={{ color: '#52c41a' }} />
|
||||||
|
<Text type="secondary">Success Delivered at {user.timestamp}</Text>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Button type="primary" ghost icon={<SendOutlined />}>Resend</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderLogHistory = () => (
|
||||||
|
<>
|
||||||
|
<div style={{ padding: '0 16px', position: 'relative' }}>
|
||||||
|
{/* Garis vertikal yang menyambung */}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', top: '7px', left: '23px', bottom: '7px',
|
||||||
|
width: '2px', backgroundColor: '#91d5ff', zIndex: 0
|
||||||
|
}}></div>
|
||||||
|
|
||||||
|
{logHistoryData.map((log, index) => (
|
||||||
|
<Row key={log.id} wrap={false} style={{ marginBottom: '16px', position: 'relative', zIndex: 1 }}>
|
||||||
|
{/* Kolom Kiri: Branch/Timeline */}
|
||||||
|
<Col style={{ position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', marginRight: '16px' }}>
|
||||||
|
<div style={{
|
||||||
|
width: '14px', height: '14px', backgroundColor: '#fff', border: '3px solid #1890ff',
|
||||||
|
borderRadius: '50%', zIndex: 1, flexShrink: 0
|
||||||
|
}}></div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Kolom Kanan: Card */}
|
||||||
|
<Col flex="auto">
|
||||||
|
<Card size="small" style={{ borderColor: '#91d5ff' }}>
|
||||||
|
<Row gutter={[16, 8]} align="middle">
|
||||||
|
<Col xs={24} md={12}>
|
||||||
|
<Space direction="vertical" size={4}>
|
||||||
|
<Space>
|
||||||
|
<ClockCircleOutlined />
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>Added at {log.timestamp}</Text>
|
||||||
|
</Space>
|
||||||
|
<div>
|
||||||
|
<Text strong>Added by: {log.addedBy.name}</Text>
|
||||||
|
<span style={{ marginLeft: '8px', border: '1px solid #52c41a', color: '#52c41a', padding: '2px 6px', borderRadius: '4px', fontSize: '12px' }}>
|
||||||
|
<MobileOutlined /> {log.addedBy.phone}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={12}>
|
||||||
|
<Paragraph style={{ color: '#595959', margin: 0, fontSize: '13px' }}>
|
||||||
|
{log.description}
|
||||||
|
</Paragraph>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDetailsNotification = () => {
|
||||||
|
if (!selectedNotification) return null;
|
||||||
|
|
||||||
|
const { IconComponent, color } = getIconAndColor(selectedNotification.type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{/* Kolom Kiri: Data Kompresor */}
|
||||||
|
<Col span={12}>
|
||||||
|
<Card title="" size="small" style={{ height: '100%', borderColor: '#d4380d' }} bodyStyle={{ padding: '12px' }}>
|
||||||
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
|
<Row gutter={16} align="middle">
|
||||||
|
<Col>
|
||||||
|
<div style={{
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: '#d4380d',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: '18px'
|
||||||
|
}}><CloseOutlined /></div>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Text>{selectedNotification.title}</Text>
|
||||||
|
<div style={{ marginTop: '2px' }}><Text strong style={{ fontSize: '16px' }}>{selectedNotification.issue}</Text></div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div>
|
||||||
|
<Text strong>Plant Subsection</Text>
|
||||||
|
<div>{selectedNotification.subsection}</div>
|
||||||
|
<Text strong style={{ display: 'block', marginTop: '8px' }}>Time</Text>
|
||||||
|
<div>{selectedNotification.timestamp.split(' ')[1]} WIB</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
border: '1px solid #d4380d',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '8px',
|
||||||
|
background: 'linear-gradient(to right, #ffe7e6, #ffffff)'
|
||||||
|
}}>
|
||||||
|
<Row justify="space-around" align="middle">
|
||||||
|
<Col>
|
||||||
|
<Text style={{ fontSize: '12px', color: color }}>Value</Text>
|
||||||
|
<div style={{ fontWeight: 'bold', fontSize: '16px', color: color }}>{selectedNotification.value.includes('°') ? selectedNotification.value : `${selectedNotification.value}° C`}</div>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>Treshold</Text>
|
||||||
|
<div style={{ fontWeight: 500 }}>{selectedNotification.threshold.includes('°') ? selectedNotification.threshold : `${selectedNotification.threshold}° C`}</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Kolom Kanan: Informasi Teknis */}
|
||||||
|
<Col span={12}>
|
||||||
|
<Card title="Informasi Teknis" size="small" style={{ height: '100%' }}>
|
||||||
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
|
<div>
|
||||||
|
<Text strong>PLC</Text>
|
||||||
|
<div>{selectedNotification.plc}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text strong>Status</Text>
|
||||||
|
<div style={{ color: '#faad14', fontWeight: 500 }}>{selectedNotification.status}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text strong>Tag</Text>
|
||||||
|
<div style={{ fontFamily: 'monospace', backgroundColor: '#f0f0f0', padding: '2px 6px', borderRadius: '4px', display: 'inline-block' }}>{selectedNotification.tag}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }} bodyStyle={{ padding: '12px' }}>
|
||||||
|
<Space>
|
||||||
|
<BookOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||||
|
<Text strong style={{ fontSize: '16px', color: '#262626' }}>Handling Guideline</Text>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }} bodyStyle={{ padding: '12px' }}>
|
||||||
|
<Space>
|
||||||
|
<ToolOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||||
|
<Text strong style={{ fontSize: '16px', color: '#262626' }}>Spare Part</Text>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }} bodyStyle={{ padding: '12px' }} onClick={() => setModalContent('log')}>
|
||||||
|
<Space>
|
||||||
|
<HistoryOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||||
|
<Text strong style={{ fontSize: '16px', color: '#262626' }}>Log Activity</Text>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={[16, 16]} style={{ marginTop: '16px' }}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card size="small" style={{ height: '100%' }}>
|
||||||
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
|
<Card
|
||||||
|
size="small" bodyStyle={{ padding: '8px 12px' }} hoverable
|
||||||
|
extra={<Text type="secondary" style={{ fontSize: '10px' }}>PDF</Text>}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<div>
|
||||||
|
<Text style={{ fontSize: '12px', color: '#262626' }}>
|
||||||
|
<FilePdfOutlined style={{ marginRight: '8px' }} /> Error 303.pdf
|
||||||
|
</Text>
|
||||||
|
<Link href="#" target="_blank" style={{ fontSize: '12px', display: 'block' }}>
|
||||||
|
lihat disini
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
size="small" bodyStyle={{ padding: '8px 12px' }} hoverable
|
||||||
|
extra={<Text type="secondary" style={{ fontSize: '10px' }}>PDF</Text>}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<div>
|
||||||
|
<Text style={{ fontSize: '12px', color: '#262626' }}>
|
||||||
|
<FilePdfOutlined style={{ marginRight: '8px' }} /> Error 303.pdf
|
||||||
|
</Text>
|
||||||
|
<Link href="#" target="_blank" style={{ fontSize: '12px', display: 'block' }}>
|
||||||
|
lihat disini
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
size="small" bodyStyle={{ padding: '8px 12px' }} hoverable
|
||||||
|
extra={<Text type="secondary" style={{ fontSize: '10px' }}>PDF</Text>}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<div>
|
||||||
|
<Text style={{ fontSize: '12px', color: '#262626' }}>
|
||||||
|
<FilePdfOutlined style={{ marginRight: '8px' }} /> Error 303.pdf
|
||||||
|
</Text>
|
||||||
|
<Link href="#" target="_blank" style={{ fontSize: '12px', display: 'block' }}>
|
||||||
|
lihat disini
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
size="small" bodyStyle={{ padding: '8px 12px' }}
|
||||||
|
extra={<Text type="secondary" style={{ fontSize: '10px' }}>Text</Text>}
|
||||||
|
>
|
||||||
|
<Paragraph style={{ fontSize: '12px', margin: 0 }}>
|
||||||
|
<Text strong>Error 303:</Text> Sensor suhu tidak merespon. Lakukan reset manual pada panel kontrol.
|
||||||
|
</Paragraph>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card size="small" style={{ height: '100%' }}>
|
||||||
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
|
<Card size="small" bodyStyle={{ padding: '12px' }} hoverable>
|
||||||
|
<Row gutter={16} align="top">
|
||||||
|
<Col span={7} style={{ textAlign: 'center' }}>
|
||||||
|
<div style={{ width: '100%', height: '60px', backgroundColor: '#f0f0f0', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '4px', marginBottom: '8px' }}>
|
||||||
|
<ToolOutlined style={{ fontSize: '24px', color: '#bfbfbf' }} />
|
||||||
|
</div>
|
||||||
|
<Text style={{ fontSize: '12px', color: '#52c41a', fontWeight: 500 }}>Available</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={17}>
|
||||||
|
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||||
|
<Text strong>Air Filter</Text>
|
||||||
|
<Paragraph style={{ fontSize: '12px', margin: 0, color: '#595959' }}>
|
||||||
|
Filters incoming air to remove dust and impurities before compression.
|
||||||
|
</Paragraph>
|
||||||
|
<div style={{ border: '1px solid #d9d9d9', borderRadius: '4px', padding: '4px 8px', fontSize: '11px', color: '#8c8c8c', marginTop: '8px' }}>
|
||||||
|
Part No: AF-2024-X2 | Condition: New | Compatible with Model XR-60
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
<Card size="small" bodyStyle={{ padding: '12px' }} hoverable>
|
||||||
|
<Row gutter={16} align="top">
|
||||||
|
<Col span={7} style={{ textAlign: 'center' }}>
|
||||||
|
<div style={{ width: '100%', height: '60px', backgroundColor: '#f0f0f0', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '4px', marginBottom: '8px' }}>
|
||||||
|
<ToolOutlined style={{ fontSize: '24px', color: '#bfbfbf' }} />
|
||||||
|
</div>
|
||||||
|
<Text style={{ fontSize: '12px', color: '#ff4d4f', fontWeight: 500 }}>Not Available</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={17}>
|
||||||
|
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||||
|
<Text strong>Compressor Oil</Text>
|
||||||
|
<Paragraph style={{ fontSize: '12px', margin: 0, color: '#595959' }}>
|
||||||
|
Special synthetic oil to lubricate and cool down the compressor unit.
|
||||||
|
</Paragraph>
|
||||||
|
<div style={{ border: '1px solid #d9d9d9', borderRadius: '4px', padding: '4px 8px', fontSize: '11px', color: '#8c8c8c', marginTop: '8px' }}>
|
||||||
|
Part No: OL-SYN-550 | Condition: New | Compatible with Model XR-60
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card size="small" style={{ height: '100%' }}>
|
||||||
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
|
<Card size="small" bodyStyle={{ padding: '8px 12px', backgroundColor: isAddingLog ? '#fafafa' : '#fff' }}>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||||
|
{isAddingLog && (
|
||||||
|
<>
|
||||||
|
<Text strong style={{ fontSize: '12px' }}>Add New Log / Update Progress</Text>
|
||||||
|
<Input.TextArea rows={2} placeholder="Tuliskan update penanganan di sini..." />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Button type={isAddingLog ? "primary" : "dashed"} size="small" block icon={!isAddingLog && <PlusOutlined />} onClick={() => setIsAddingLog(!isAddingLog)}>
|
||||||
|
{isAddingLog ? "Submit Log" : "Add Log"}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
{logHistoryData.slice(0, 2).map(log => ( // Menampilkan 2 log terbaru sebagai pratinjau
|
||||||
|
<Card key={log.id} size="small" bodyStyle={{ padding: '8px 12px' }}>
|
||||||
|
<Paragraph style={{ fontSize: '12px', margin: 0 }} ellipsis={{ rows: 2 }}>
|
||||||
|
<Text strong>{log.addedBy.name}:</Text> {log.description}
|
||||||
|
</Paragraph>
|
||||||
|
<Text type="secondary" style={{ fontSize: '11px' }}>{log.timestamp}</Text>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<div style={{ textAlign: 'center', paddingTop: '8px' }}>
|
||||||
|
<Button type="link" style={{ padding: 0 }} onClick={() => setModalContent('log')}>
|
||||||
|
View All Log History
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Card>
|
<Card>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
<h2
|
<h2 style={{ fontSize: '20px', fontWeight: 600, margin: '0 0 4px 0', color: '#262626' }}>Notification</h2>
|
||||||
style={{
|
|
||||||
fontSize: '20px',
|
|
||||||
fontWeight: 600,
|
|
||||||
margin: '0 0 4px 0',
|
|
||||||
color: '#262626',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Notification
|
|
||||||
</h2>
|
|
||||||
<p style={{ margin: '0 0 16px 0', color: '#8c8c8c', fontSize: '14px' }}>
|
<p style={{ margin: '0 0 16px 0', color: '#8c8c8c', fontSize: '14px' }}>
|
||||||
Riwayat notifikasi yang dikirim ke engineer
|
Riwayat notifikasi yang dikirim ke engineer
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Row>
|
<Row justify="space-between" align="middle" style={{ marginBottom: '24px' }}>
|
||||||
<Col span={6}>
|
<Col>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder="Search notifications..."
|
placeholder="Search notifications..."
|
||||||
onSearch={(value) => setSearchTerm(value)}
|
onSearch={setSearchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
style={{ marginBottom: '24px', width: 300 }}
|
style={{ width: 300 }}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<div style={{ borderBottom: '1px solid #f0f0f0', marginBottom: '24px' }}>
|
||||||
{/* Tabs */}
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
<div style={{ borderBottom: '1px solid #f0f0f0', marginBottom: '24px' }}>
|
<button onClick={() => setActiveTab('all')} style={tabButtonStyle(activeTab === 'all')}>All</button>
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<button onClick={() => setActiveTab('unread')} style={{ ...tabButtonStyle(activeTab === 'unread'), display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<button
|
Not read yet
|
||||||
onClick={() => setActiveTab('all')}
|
{getUnreadCount() > 0 && <Badge count={getUnreadCount()} style={{ backgroundColor: '#ff4d4f' }} />}
|
||||||
style={tabButtonStyle(activeTab === 'all')}
|
</button>
|
||||||
>
|
<button onClick={() => setActiveTab('read')} style={tabButtonStyle(activeTab === 'read')}>Already read</button>
|
||||||
All
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('unread')}
|
|
||||||
style={{
|
|
||||||
...tabButtonStyle(activeTab === 'unread'),
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '8px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Not read yet
|
|
||||||
{getUnreadCount() > 0 && (
|
|
||||||
<Badge
|
|
||||||
count={getUnreadCount()}
|
|
||||||
style={{
|
|
||||||
backgroundColor: '#ff4d4f',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('read')}
|
|
||||||
style={tabButtonStyle(activeTab === 'read')}
|
|
||||||
>
|
|
||||||
Already read
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Notification List */}
|
|
||||||
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
|
||||||
{filteredNotifications.length === 0 ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '40px 0',
|
|
||||||
color: '#8c8c8c',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Tidak ada notifikasi
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
filteredNotifications.map((notification) => {
|
|
||||||
const { IconComponent, color, bgColor } = getIconAndColor(
|
|
||||||
notification.type
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
{renderDeviceNotifications()}
|
||||||
<Card
|
|
||||||
key={notification.id}
|
|
||||||
style={{
|
|
||||||
backgroundColor: notification.isRead ? '#ffffff' : '#f6f9ff',
|
|
||||||
borderColor: notification.isRead ? '#f0f0f0' : '#d6e4ff',
|
|
||||||
}}
|
|
||||||
onClick={() => handleMarkAsRead(notification.id)}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'flex-start' }}>
|
|
||||||
{/* Icon */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '40px',
|
|
||||||
height: '40px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
backgroundColor: bgColor,
|
|
||||||
color: color,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
fontSize: '22px',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconComponent style={{ fontSize: '22px' }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<Row align="top">
|
|
||||||
{/* Left: Title and Issue */}
|
|
||||||
<Col flex="220px">
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
||||||
<div>
|
|
||||||
<Text strong>{notification.title}</Text>
|
|
||||||
<div style={{ marginTop: '4px' }}>
|
|
||||||
<Text style={{ color }}>{notification.issue}</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!notification.isRead && (
|
|
||||||
<Badge color="red" status="processing" style={{ marginLeft: '8px', marginTop: '4px' }} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* Middle: Description and Details */}
|
|
||||||
<Col flex="auto">
|
|
||||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', marginBottom: '12px' }}>
|
|
||||||
<MailOutlined style={{ marginTop: '4px', color: '#1890ff' }} />
|
|
||||||
<Paragraph style={{ color: '#595959', margin: 0, flex: 1 }}>
|
|
||||||
<Text strong>{notification.description}</Text>
|
|
||||||
<br />
|
|
||||||
{notification.details}
|
|
||||||
</Paragraph>
|
|
||||||
</div>
|
|
||||||
<Space direction="vertical" size={4} style={{ fontSize: '13px', color: '#8c8c8c' }}>
|
|
||||||
<Space>
|
|
||||||
<ClockCircleOutlined />
|
|
||||||
<Text type="secondary">{notification.timestamp}</Text>
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<EnvironmentOutlined />
|
|
||||||
<Text type="secondary">{notification.location}</Text>
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<LinkOutlined />
|
|
||||||
<Link href={notification.link} target="_blank">{notification.link}</Link>
|
|
||||||
<Button type="link" icon={<SendOutlined />} style={{ paddingLeft: '8px' }}>
|
|
||||||
Resend
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* Right: Action Icons */}
|
|
||||||
<Col flex="120px" style={{ textAlign: 'center' }} align="bottom">
|
|
||||||
<Space style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
|
||||||
<Button type="text" icon={<UserOutlined style={{ color: '#1890ff' }} />} title="User History" style={{ border: '1px solid #1890ff', borderRadius: '4px' }} />
|
|
||||||
<Button type="text" icon={<EyeOutlined style={{ color: '#1890ff' }} />} title="Details" style={{ border: '1px solid #1890ff', borderRadius: '4px' }} />
|
|
||||||
<Button type="text" icon={<HistoryOutlined style={{ color: '#1890ff' }} />} title="Log History" style={{ border: '1px solid #1890ff', borderRadius: '4px' }} />
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
{modalContent === 'details' ? (
|
||||||
|
<div style={{ backgroundColor: '#f6ffed', border: '1px solid #b7eb8f', borderRadius: '4px', padding: '4px 12px', margin: '0 -16px' }}>
|
||||||
|
<Typography.Title level={4} style={{ margin: 0, color: '#262626' }}>
|
||||||
|
Error Notification Detail
|
||||||
|
</Typography.Title>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||||
|
{modalContent === 'user' && 'User History Notification'}
|
||||||
|
{modalContent === 'log' && 'Log History Notification'}
|
||||||
|
</Typography.Title>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
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={
|
||||||
|
<div style={{ textAlign: 'right' }}>
|
||||||
|
<Button type="primary" ghost onClick={() => {
|
||||||
|
setModalContent(null);
|
||||||
|
setSelectedNotification(null);
|
||||||
|
// Reset state isAddingLog saat modal ditutup
|
||||||
|
if (modalContent === 'details') {
|
||||||
|
setIsAddingLog(false);
|
||||||
|
}
|
||||||
|
}}>Close</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{modalContent === 'user' && renderUserHistory()}
|
||||||
|
{modalContent === 'log' && renderLogHistory()}
|
||||||
|
{modalContent === 'details' && renderDetailsNotification()}
|
||||||
|
</Modal>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ListNotification;
|
export default ListNotification;
|
||||||
102
src/pages/notification/component/LogHistoryModal.jsx
Normal file
102
src/pages/notification/component/LogHistoryModal.jsx
Normal file
@@ -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) => (
|
||||||
|
<span>
|
||||||
|
<ClockCircleOutlined style={{ marginRight: 8 }} />
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 <Tag color={color}>{text.toUpperCase()}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Details',
|
||||||
|
dataIndex: 'details',
|
||||||
|
key: 'details',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const LogHistoryModal = ({ visible, onCancel, notificationData }) => {
|
||||||
|
const logHistoryData = getDummyLogHistory(notificationData);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<Text strong>
|
||||||
|
Log History: <Text type="secondary">{notificationData?.title}</Text>
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
open={visible}
|
||||||
|
onCancel={onCancel}
|
||||||
|
footer={null}
|
||||||
|
width={800}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={logHistoryData}
|
||||||
|
pagination={{ pageSize: 5 }}
|
||||||
|
style={{ marginTop: 24 }}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogHistoryModal;
|
||||||
112
src/pages/notification/component/UserHistoryModal.jsx
Normal file
112
src/pages/notification/component/UserHistoryModal.jsx
Normal file
@@ -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 <Tag icon={<CheckCircleOutlined />} color="success">Delivered</Tag>;
|
||||||
|
case 'sent':
|
||||||
|
return <Tag icon={<SyncOutlined spin />} color="processing">Sent</Tag>;
|
||||||
|
case 'failed':
|
||||||
|
return <Tag color="error">Failed</Tag>;
|
||||||
|
default:
|
||||||
|
return <Tag color="default">{status}</Tag>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<Text strong style={{ fontSize: '18px' }}>
|
||||||
|
User History Notification
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
open={visible}
|
||||||
|
onCancel={onCancel}
|
||||||
|
footer={[
|
||||||
|
<Button key="back" onClick={onCancel}>
|
||||||
|
Close
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
width={600}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<div style={{ maxHeight: '60vh', overflowY: 'auto', padding: '8px' }}>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
{userData.map((user) => (
|
||||||
|
<Card key={user.id} size="small" style={{ width: '100%' }}>
|
||||||
|
<Row align="middle" justify="space-between">
|
||||||
|
<Col>
|
||||||
|
<Space align="center">
|
||||||
|
<Avatar size="large" icon={<UserOutlined />} />
|
||||||
|
<div>
|
||||||
|
<Text strong>{user.name}</Text>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
|
<PhoneOutlined style={{ color: '#8c8c8c' }} />
|
||||||
|
<Text type="secondary">{user.phone}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Space align="center" size="large">
|
||||||
|
{getStatusTag(user.status)}
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SendOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log(`Resend to ${user.name}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Resend
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserHistoryModal;
|
||||||
58
src/pages/notification/detail/UserHistory.jsx
Normal file
58
src/pages/notification/detail/UserHistory.jsx
Normal file
@@ -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 (
|
||||||
|
<Card>
|
||||||
|
<Row justify="space-between" align="middle" style={{ marginBottom: '20px' }}>
|
||||||
|
<Col>
|
||||||
|
<Space align="center">
|
||||||
|
<Button type="text" icon={<ArrowLeftOutlined />} onClick={onBack} />
|
||||||
|
<Typography.Title level={4} style={{ margin: 0 }}>User History Notification</Typography.Title>
|
||||||
|
</Space>
|
||||||
|
<Text type="secondary" style={{ marginLeft: '40px' }}>
|
||||||
|
{notification.title} - {notification.issue}
|
||||||
|
</Text>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
||||||
|
{userHistoryData.map(user => (
|
||||||
|
<Card key={user.id} style={{ backgroundColor: '#e6f7ff', borderColor: '#91d5ff' }}>
|
||||||
|
<Row align="middle" justify="space-between">
|
||||||
|
<Col>
|
||||||
|
<Space align="center">
|
||||||
|
<Text strong>{user.name}</Text>
|
||||||
|
<Text>|</Text>
|
||||||
|
<Text><MobileOutlined /> {user.phone}</Text>
|
||||||
|
<Text>|</Text>
|
||||||
|
<Badge status="success" text={user.status} />
|
||||||
|
</Space>
|
||||||
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
<Space align="center">
|
||||||
|
<CheckCircleFilled style={{ color: '#52c41a' }} />
|
||||||
|
<Text type="secondary">Success Delivered at {user.timestamp}</Text>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Button type="primary" ghost icon={<SendOutlined />}>Resend</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserHistory;
|
||||||
Reference in New Issue
Block a user