Enhance notification list with search functionality and improved layout
This commit is contained in:
@@ -1,79 +1,60 @@
|
|||||||
import React, { memo, useState, useEffect } from 'react';
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
import { Button, Row, Col, Card, Badge } from 'antd';
|
import { Button, Row, Col, Card, Badge, Input, Typography, Space } from 'antd';
|
||||||
import {
|
import {
|
||||||
CloseCircleFilled,
|
CloseCircleFilled,
|
||||||
WarningFilled,
|
WarningFilled,
|
||||||
CheckCircleFilled,
|
CheckCircleFilled,
|
||||||
InfoCircleFilled,
|
InfoCircleFilled,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
EnvironmentOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
SendOutlined,
|
||||||
|
MailOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
HistoryOutlined,
|
||||||
|
EyeOutlined,
|
||||||
} 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;
|
||||||
|
|
||||||
// Dummy data untuk notifikasi
|
// Dummy data untuk notifikasi
|
||||||
const initialNotifications = [
|
const initialNotifications = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
type: 'critical',
|
type: 'critical',
|
||||||
title: 'Compressor Unit A - Overheat detected (85°C)',
|
title: 'Compressor Unit A',
|
||||||
plc: 'PLC-001',
|
issue: 'Overheat detected (85°C)',
|
||||||
tag: 'A1-TEMP',
|
description: '⚠️ Compressor Unit A - Overheat Detected (85°C) 🚨',
|
||||||
engineer: 'Siti Nurhaliza',
|
timestamp: '04-11-2025 11.39 WIB',
|
||||||
time: '2 menit lalu',
|
location: 'Lantai 2, Area Produksi A, Zona 3',
|
||||||
status: 'unread',
|
details: 'Terjadi kenaikan suhu melebihi ambang batas pada Compressor Unit A. Pengecekan potensi kerusakan dibutuhkan.',
|
||||||
|
link: 'https://tinyurl.com/compA85',
|
||||||
isRead: false,
|
isRead: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
title: 'Compressor Unit C - Pressure slightly high (7.2 bar)',
|
title: 'Compressor Unit C',
|
||||||
plc: 'PLC-003',
|
issue: 'Pressure slightly high (7.2 bar)',
|
||||||
tag: 'C3-PRESS',
|
description: '🔧 Compressor Unit C - Pressure High (7.2 bar)',
|
||||||
engineer: 'Joko Widodo',
|
timestamp: '04-11-2025 11.30 WIB',
|
||||||
time: '15 menit lalu',
|
location: 'Lantai 1, Area Produksi C, Zona 1',
|
||||||
status: 'unread',
|
details: 'Tekanan mendekati ambang batas atas. Perlu monitoring lebih lanjut oleh engineer.',
|
||||||
|
link: 'https://tinyurl.com/compC72',
|
||||||
isRead: false,
|
isRead: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
type: 'resolved',
|
type: 'resolved',
|
||||||
title: 'Compressor Unit B - Vibration issue resolved',
|
title: 'Compressor Unit B',
|
||||||
plc: 'PLC-002',
|
issue: 'Vibration issue resolved',
|
||||||
tag: 'B2-VIB',
|
description: '✅ Compressor Unit B - Vibration Resolved',
|
||||||
engineer: 'Rudi Santoso',
|
timestamp: '04-11-2025 10.05 WIB',
|
||||||
time: '1 jam lalu',
|
location: 'Lantai 2, Area Produksi B, Zona 2',
|
||||||
status: 'read',
|
details: 'Getaran pada Unit B telah kembali normal setelah perbaikan.',
|
||||||
isRead: true,
|
link: 'https://tinyurl.com/compBresolved',
|
||||||
},
|
|
||||||
{
|
|
||||||
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,
|
isRead: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -81,6 +62,7 @@ const initialNotifications = [
|
|||||||
const ListNotification = memo(function ListNotification(props) {
|
const ListNotification = memo(function ListNotification(props) {
|
||||||
const [notifications, setNotifications] = useState(initialNotifications);
|
const [notifications, setNotifications] = useState(initialNotifications);
|
||||||
const [activeTab, setActiveTab] = useState('all');
|
const [activeTab, setActiveTab] = useState('all');
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -119,29 +101,29 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterNotifications = (status) => {
|
const handleMarkAsRead = (id) => {
|
||||||
if (status === 'all') return notifications;
|
setNotifications((prev) =>
|
||||||
if (status === 'unread') return notifications.filter((n) => !n.isRead);
|
prev.map((n) => (n.id === id ? { ...n, isRead: true } : n))
|
||||||
if (status === 'read') return notifications.filter((n) => n.isRead);
|
);
|
||||||
return notifications;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
if (!searchTerm) return true;
|
||||||
|
const searchableText = `${n.title} ${n.issue} ${n.description} ${n.location} ${n.details}`.toLowerCase();
|
||||||
|
return searchableText.includes(searchTerm.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
const getUnreadCount = () => {
|
const getUnreadCount = () => {
|
||||||
return notifications.filter((n) => !n.isRead).length;
|
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) => ({
|
const tabButtonStyle = (isActive) => ({
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@@ -170,10 +152,21 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
>
|
>
|
||||||
Notification
|
Notification
|
||||||
</h2>
|
</h2>
|
||||||
<p style={{ margin: '0 0 24px 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>
|
||||||
|
<Col span={6}>
|
||||||
|
<Input.Search
|
||||||
|
placeholder="Search notifications..."
|
||||||
|
onSearch={(value) => setSearchTerm(value)}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
style={{ marginBottom: '24px', width: 300 }}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div style={{ borderBottom: '1px solid #f0f0f0', marginBottom: '24px' }}>
|
<div style={{ borderBottom: '1px solid #f0f0f0', marginBottom: '24px' }}>
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
@@ -212,7 +205,7 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Notification List */}
|
{/* Notification List */}
|
||||||
<div>
|
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
||||||
{filteredNotifications.length === 0 ? (
|
{filteredNotifications.length === 0 ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -230,165 +223,96 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Card
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
style={{
|
style={{
|
||||||
marginBottom: '12px',
|
backgroundColor: notification.isRead ? '#ffffff' : '#f6f9ff',
|
||||||
backgroundColor: '#ffffff',
|
borderColor: notification.isRead ? '#f0f0f0' : '#d6e4ff',
|
||||||
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';
|
|
||||||
}}
|
}}
|
||||||
|
onClick={() => handleMarkAsRead(notification.id)}
|
||||||
>
|
>
|
||||||
{/* Dot for unread */}
|
<div style={{ display: 'flex', gap: '16px', alignItems: 'flex-start' }}>
|
||||||
{!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 */}
|
{/* Icon */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '48px',
|
width: '40px',
|
||||||
height: '48px',
|
height: '40px',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
backgroundColor: bgColor,
|
backgroundColor: bgColor,
|
||||||
color: color,
|
color: color,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
fontSize: '24px',
|
fontSize: '22px',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconComponent style={{ fontSize: '24px' }} />
|
<IconComponent style={{ fontSize: '22px' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div
|
<Row align="top">
|
||||||
style={{
|
{/* Left: Title and Issue */}
|
||||||
fontSize: '15px',
|
<Col flex="220px">
|
||||||
fontWeight: 600,
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
marginBottom: '4px',
|
<div>
|
||||||
color: '#262626',
|
<Text strong>{notification.title}</Text>
|
||||||
}}
|
<div style={{ marginTop: '4px' }}>
|
||||||
>
|
<Text style={{ color }}>{notification.issue}</Text>
|
||||||
{notification.title}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!notification.isRead && (
|
||||||
|
<Badge color="red" status="processing" style={{ marginLeft: '8px', marginTop: '4px' }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<div
|
{/* Middle: Description and Details */}
|
||||||
style={{
|
<Col flex="auto">
|
||||||
display: 'flex',
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', marginBottom: '12px' }}>
|
||||||
gap: '8px',
|
<MailOutlined style={{ marginTop: '4px', color: '#1890ff' }} />
|
||||||
marginBottom: '4px',
|
<Paragraph style={{ color: '#595959', margin: 0, flex: 1 }}>
|
||||||
flexWrap: 'wrap',
|
<Text strong>{notification.description}</Text>
|
||||||
}}
|
<br />
|
||||||
>
|
{notification.details}
|
||||||
<span
|
</Paragraph>
|
||||||
style={{
|
</div>
|
||||||
fontSize: '13px',
|
<Space direction="vertical" size={4} style={{ fontSize: '13px', color: '#8c8c8c' }}>
|
||||||
color: '#8c8c8c',
|
<Space>
|
||||||
}}
|
<ClockCircleOutlined />
|
||||||
>
|
<Text type="secondary">{notification.timestamp}</Text>
|
||||||
{notification.plc}
|
</Space>
|
||||||
</span>
|
<Space>
|
||||||
<span
|
<EnvironmentOutlined />
|
||||||
style={{
|
<Text type="secondary">{notification.location}</Text>
|
||||||
fontSize: '13px',
|
</Space>
|
||||||
color: '#d9d9d9',
|
<Space>
|
||||||
}}
|
<LinkOutlined />
|
||||||
>
|
<Link href={notification.link} target="_blank">{notification.link}</Link>
|
||||||
•
|
<Button type="link" icon={<SendOutlined />} style={{ paddingLeft: '8px' }}>
|
||||||
</span>
|
Resend
|
||||||
<span
|
</Button>
|
||||||
style={{
|
</Space>
|
||||||
fontSize: '13px',
|
</Space>
|
||||||
color: '#8c8c8c',
|
</Col>
|
||||||
}}
|
|
||||||
>
|
|
||||||
Tag: {notification.tag}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
{/* Right: Action Icons */}
|
||||||
style={{
|
<Col flex="120px" style={{ textAlign: 'center' }} align="bottom">
|
||||||
display: 'flex',
|
<Space style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||||
alignItems: 'center',
|
<Button type="text" icon={<UserOutlined style={{ color: '#1890ff' }} />} title="User History" style={{ border: '1px solid #1890ff', borderRadius: '4px' }} />
|
||||||
gap: '8px',
|
<Button type="text" icon={<EyeOutlined style={{ color: '#1890ff' }} />} title="Details" style={{ border: '1px solid #1890ff', borderRadius: '4px' }} />
|
||||||
flexWrap: 'wrap',
|
<Button type="text" icon={<HistoryOutlined style={{ color: '#1890ff' }} />} title="Log History" style={{ border: '1px solid #1890ff', borderRadius: '4px' }} />
|
||||||
}}
|
</Space>
|
||||||
>
|
</Col>
|
||||||
<span
|
</Row>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
{/* Button */}
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
onClick={() => handleViewDetail(notification)}
|
|
||||||
style={{
|
|
||||||
color: '#FF6B35',
|
|
||||||
fontSize: '14px',
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
View Detail
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -397,5 +321,3 @@ const ListNotification = memo(function ListNotification(props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default ListNotification;
|
export default ListNotification;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user