1390 lines
71 KiB
JavaScript
1390 lines
71 KiB
JavaScript
import React, { memo, useState, useEffect } from 'react';
|
|
import {
|
|
Button,
|
|
Row,
|
|
Col,
|
|
Card,
|
|
Badge,
|
|
Input,
|
|
Typography,
|
|
Space,
|
|
Divider,
|
|
Modal,
|
|
Tag,
|
|
message,
|
|
Spin,
|
|
Pagination,
|
|
} from 'antd';
|
|
import {
|
|
CloseCircleFilled,
|
|
WarningFilled,
|
|
CheckCircleFilled,
|
|
InfoCircleFilled,
|
|
ClockCircleOutlined,
|
|
EnvironmentOutlined,
|
|
LinkOutlined,
|
|
SendOutlined,
|
|
MailOutlined,
|
|
UserOutlined,
|
|
HistoryOutlined,
|
|
EyeOutlined,
|
|
MobileOutlined,
|
|
CloseOutlined,
|
|
BookOutlined,
|
|
ToolOutlined,
|
|
FilePdfOutlined,
|
|
PlusOutlined,
|
|
ExclamationCircleOutlined,
|
|
SearchOutlined,
|
|
} from '@ant-design/icons';
|
|
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
|
import { getAllNotification, getNotificationLogByNotificationId } from '../../../api/notification';
|
|
|
|
const { Text, Paragraph, Link: AntdLink } = Typography;
|
|
|
|
// Transform API response to component format
|
|
const transformNotificationData = (apiData) => {
|
|
return apiData.map((item, index) => ({
|
|
id: `notification-${item.notification_error_id}-${index}`, // Unique key prefix with array index
|
|
type: item.is_read ? 'resolved' : item.is_delivered ? 'warning' : 'critical',
|
|
title: item.error_code_name || 'Unknown Error',
|
|
issue: item.error_code || item.error_code_name || 'Unknown Error',
|
|
description: `${item.error_code} - ${item.error_code_name || ''}`,
|
|
timestamp: item.created_at
|
|
? new Date(item.created_at).toLocaleString('id-ID', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}) + ' WIB'
|
|
: 'N/A',
|
|
location: item.plant_sub_section_name || item.device_location || 'Location not specified',
|
|
details: item.message_error_issue || 'No details available',
|
|
link: `/verification-sparepart/${item.notification_error_id}`, // Dummy URL untuk verifikasi spare part
|
|
subsection: item.plant_sub_section_name || 'N/A',
|
|
isRead: item.is_read,
|
|
status: item.is_read ? 'Resolved' : item.is_delivered ? 'Delivered' : 'Pending',
|
|
tag: item.error_code,
|
|
errorCode: item.error_code,
|
|
solutionName: item.error_code?.solution?.[0]?.solution_name || 'N/A',
|
|
typeSolution: item.error_code?.solution?.[0]?.type_solution || 'N/A',
|
|
pathSolution:
|
|
item.error_code?.solution?.[0]?.path_document ||
|
|
item.error_code?.solution?.[0]?.path_solution ||
|
|
'N/A',
|
|
error_code: item.error_code,
|
|
}));
|
|
};
|
|
|
|
// 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',
|
|
},
|
|
];
|
|
|
|
const ListNotification = memo(function ListNotification(props) {
|
|
const [notifications, setNotifications] = useState([]);
|
|
const [activeTab, setActiveTab] = useState('all');
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [searchValue, setSearchValue] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [modalContent, setModalContent] = useState(null); // 'user', 'log', 'details', or null
|
|
const [isAddingLog, setIsAddingLog] = useState(false);
|
|
const [selectedNotification, setSelectedNotification] = useState(null);
|
|
const [logHistoryData, setLogHistoryData] = useState([]);
|
|
const [logLoading, setLogLoading] = useState(false);
|
|
const [pagination, setPagination] = useState({
|
|
current_page: 1,
|
|
current_limit: 10,
|
|
total_limit: 0,
|
|
total_page: 1,
|
|
});
|
|
const navigate = useNavigate();
|
|
|
|
// Fetch notifications from API
|
|
const fetchNotifications = async (page = 1, limit = 10, isRead = null) => {
|
|
setLoading(true);
|
|
try {
|
|
const queryParams = new URLSearchParams({
|
|
page: page.toString(),
|
|
limit: limit.toString(),
|
|
});
|
|
|
|
if (isRead !== null) {
|
|
queryParams.append('is_read', isRead.toString());
|
|
}
|
|
|
|
const response = await getAllNotification(queryParams);
|
|
if (response && response.data) {
|
|
const transformedData = transformNotificationData(response.data);
|
|
setNotifications(transformedData);
|
|
|
|
// Update pagination with API response or calculate from data
|
|
if (response.paging) {
|
|
setPagination({
|
|
current_page: response.paging.current_page || page,
|
|
current_limit: response.paging.current_limit || limit,
|
|
total_limit: response.paging.total_limit || transformedData.length,
|
|
total_page:
|
|
response.paging.total_page || Math.ceil(transformedData.length / limit),
|
|
});
|
|
} else {
|
|
// Fallback: calculate pagination from data
|
|
const totalItems = transformedData.length;
|
|
setPagination((prev) => ({
|
|
...prev,
|
|
current_page: page,
|
|
current_limit: limit,
|
|
total_limit: totalItems,
|
|
total_page: Math.ceil(totalItems / limit),
|
|
}));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching notifications:', error);
|
|
setNotifications([]);
|
|
} finally {
|
|
setTimeout(() => {
|
|
setLoading(false);
|
|
}, 500);
|
|
}
|
|
};
|
|
|
|
const handlePaginationChange = (page, pageSize) => {
|
|
setPagination((prev) => ({
|
|
...prev,
|
|
current_page: page,
|
|
current_limit: pageSize,
|
|
}));
|
|
|
|
// Fetch notifications with new pagination
|
|
const isReadFilter = activeTab === 'read' ? 1 : activeTab === 'unread' ? 0 : null;
|
|
fetchNotifications(page, pageSize, isReadFilter);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) {
|
|
navigate('/signin');
|
|
return;
|
|
}
|
|
|
|
// Fetch notifications on component mount and when tab changes
|
|
const isReadFilter = activeTab === 'read' ? 1 : activeTab === 'unread' ? 0 : null;
|
|
fetchNotifications(pagination.current_page, pagination.current_limit, isReadFilter);
|
|
}, [activeTab]);
|
|
|
|
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 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);
|
|
|
|
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, type: 'resolved', status: 'Resolved' } : n
|
|
)
|
|
);
|
|
};
|
|
|
|
const handleSearch = () => {
|
|
setSearchTerm(searchValue);
|
|
};
|
|
|
|
const handleSearchClear = () => {
|
|
setSearchValue('');
|
|
setSearchTerm('');
|
|
};
|
|
|
|
const getUnreadCount = () => notifications.filter((n) => !n.isRead).length;
|
|
|
|
// Filter notifications based on search term
|
|
const getFilteredNotifications = () => {
|
|
if (!searchTerm) return notifications;
|
|
// Search by title and error code name
|
|
return notifications.filter((n) => {
|
|
const searchableText = `${n.title} ${n.issue}`.toLowerCase();
|
|
return searchableText.includes(searchTerm.toLowerCase());
|
|
});
|
|
};
|
|
|
|
// Fetch log history from API
|
|
const fetchLogHistory = async (notificationId) => {
|
|
try {
|
|
setLogLoading(true);
|
|
const response = await getNotificationLogByNotificationId(notificationId);
|
|
if (response && response.data) {
|
|
// Transform API data to component format
|
|
const transformedLogs = response.data.map((log) => ({
|
|
id: log.notification_error_log_id,
|
|
timestamp: log.created_at
|
|
? new Date(log.created_at).toLocaleString('id-ID', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}) + ' WIB'
|
|
: 'N/A',
|
|
addedBy: {
|
|
name: log.contact_name || 'Unknown',
|
|
phone: log.contact_phone || 'N/A',
|
|
},
|
|
description: log.notification_error_log_description || '',
|
|
}));
|
|
setLogHistoryData(transformedLogs);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching log history:', err);
|
|
setLogHistoryData([]); // Set empty array on error
|
|
} finally {
|
|
setLogLoading(false);
|
|
}
|
|
};
|
|
|
|
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',
|
|
});
|
|
|
|
const renderDeviceNotifications = () => {
|
|
const filteredNotifications = getFilteredNotifications();
|
|
return (
|
|
<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',
|
|
}}
|
|
>
|
|
<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,
|
|
}}
|
|
>
|
|
{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 />
|
|
<AntdLink
|
|
href={notification.link}
|
|
target="_blank"
|
|
>
|
|
{notification.link}
|
|
</AntdLink>
|
|
<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');
|
|
}}
|
|
/>
|
|
<RouterLink
|
|
to={`/notification-detail/${
|
|
notification.id.split('-')[1]
|
|
}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<Button
|
|
type="text"
|
|
icon={
|
|
<EyeOutlined
|
|
style={{ color: '#1890ff' }}
|
|
/>
|
|
}
|
|
title="Details"
|
|
style={{
|
|
border: '1px solid #1890ff',
|
|
borderRadius: '4px',
|
|
}}
|
|
/>
|
|
</RouterLink>
|
|
<Button
|
|
type="text"
|
|
icon={
|
|
<HistoryOutlined
|
|
style={{ color: '#1890ff' }}
|
|
/>
|
|
}
|
|
title="Log History"
|
|
style={{
|
|
border: '1px solid #1890ff',
|
|
borderRadius: '4px',
|
|
}}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
|
|
// Set the selected notification for the log history
|
|
const notificationId =
|
|
notification.id.split('-')[1];
|
|
setSelectedNotification(notification);
|
|
|
|
// Fetch log history for the selected notification
|
|
fetchLogHistory(notificationId);
|
|
|
|
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 = () => (
|
|
<>
|
|
{logLoading ? (
|
|
<div style={{ textAlign: 'center', padding: '24px' }}>
|
|
<Spin size="large" />
|
|
</div>
|
|
) : logHistoryData.length === 0 ? (
|
|
<div style={{ textAlign: 'center', padding: '24px', color: '#8c8c8c' }}>
|
|
Tidak ada log history
|
|
</div>
|
|
) : (
|
|
<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="middle" style={{ width: '100%' }}>
|
|
<Row gutter={[16, 8]}>
|
|
{/* Kolom Kiri: Data Kompresor */}
|
|
<Col span={12}>
|
|
<Card
|
|
title=""
|
|
size="small"
|
|
style={{ height: '100%', borderColor: '#d4380d' }}
|
|
bodyStyle={{ padding: '12px' }}
|
|
>
|
|
<Space direction="vertical" size="middle" 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' }}>
|
|
Date & Time
|
|
</Text>
|
|
<div>{selectedNotification.timestamp}</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,
|
|
}}
|
|
>
|
|
N/A
|
|
</div>
|
|
</Col>
|
|
<Col>
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
Treshold
|
|
</Text>
|
|
<div style={{ fontWeight: 500 }}>N/A</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, 8]}>
|
|
<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={() => {
|
|
// Set the selected notification for the log history if not already set
|
|
if (selectedNotification) {
|
|
const notificationId =
|
|
selectedNotification.id.split('-')[1];
|
|
// Fetch log history for the selected notification
|
|
fetchLogHistory(notificationId);
|
|
}
|
|
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, 8]} style={{ marginTop: '0' }}>
|
|
<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>
|
|
{logLoading ? (
|
|
<div style={{ textAlign: 'center', padding: '12px' }}>
|
|
<Spin size="small" />
|
|
</div>
|
|
) : logHistoryData.length === 0 ? (
|
|
<div
|
|
style={{
|
|
textAlign: 'center',
|
|
padding: '12px',
|
|
color: '#8c8c8c',
|
|
}}
|
|
>
|
|
Tidak ada log history
|
|
</div>
|
|
) : (
|
|
logHistoryData.map((log) => (
|
|
<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>
|
|
))
|
|
)}
|
|
</Space>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
</Space>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<Card>
|
|
<Row>
|
|
<Col xs={24}>
|
|
<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' }}>
|
|
Riwayat notifikasi yang dikirim ke engineer
|
|
</p>
|
|
|
|
<Row justify="space-between" align="middle" gutter={[8, 8]}>
|
|
<Col xs={24} sm={24} md={12} lg={12}>
|
|
<Input.Search
|
|
placeholder="Search by notification name or error code name..."
|
|
value={searchValue}
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
setSearchValue(value);
|
|
if (value === '') {
|
|
handleSearchClear();
|
|
}
|
|
}}
|
|
onSearch={handleSearch}
|
|
allowClear={{
|
|
clearIcon: <span onClick={handleSearchClear}>✕</span>,
|
|
}}
|
|
enterButton={
|
|
<Button
|
|
type="primary"
|
|
icon={<SearchOutlined />}
|
|
style={{
|
|
backgroundColor: '#23A55A',
|
|
borderColor: '#23A55A',
|
|
}}
|
|
>
|
|
Search
|
|
</Button>
|
|
}
|
|
size="large"
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
<div style={{ borderBottom: '1px solid #f0f0f0', marginBottom: '24px' }}>
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|
<button
|
|
onClick={() => setActiveTab('all')}
|
|
style={tabButtonStyle(activeTab === 'all')}
|
|
>
|
|
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>
|
|
|
|
<Spin spinning={loading}>{renderDeviceNotifications()}</Spin>
|
|
|
|
{/* PAGINATION */}
|
|
<Row justify="space-between" align="middle" style={{ marginTop: '16px' }}>
|
|
<Col>
|
|
<div>
|
|
Menampilkan {pagination.current_limit} data halaman{' '}
|
|
{pagination.current_page} dari total {pagination.total_limit}{' '}
|
|
data
|
|
</div>
|
|
</Col>
|
|
<Col>
|
|
<Pagination
|
|
showSizeChanger
|
|
onChange={handlePaginationChange}
|
|
onShowSizeChange={handlePaginationChange}
|
|
current={pagination.current_page}
|
|
pageSize={pagination.current_limit}
|
|
total={pagination.total_limit}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
</Col>
|
|
</Row>
|
|
</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>
|
|
);
|
|
});
|
|
|
|
export default ListNotification;
|