lavoce #23

Merged
bragaz_rexita merged 7 commits from lavoce into main 2025-11-28 05:10:27 +00:00
2 changed files with 242 additions and 202 deletions
Showing only changes of commit 14f8a5d472 - Show all commits

View File

@@ -1,9 +1,21 @@
import { SendRequest } from '../components/Global/ApiRequest'; import { SendRequest } from '../components/Global/ApiRequest';
export const getAllNotification = async () => { const getAllNotification = async (queryParams) => {
const response = await SendRequest({ const response = await SendRequest({
method: 'get', method: 'get',
prefix: 'notification', prefix: `notification?${queryParams.toString()}`,
}); });
return response.data; return response.data;
}; };
const getNotificationById = async (id) => {
const response = await SendRequest({
method: 'get',
prefix: `notification/${id}`,
});
return response.data;
};
export { getAllNotification, getNotificationById };

View File

@@ -146,21 +146,43 @@ const ListNotification = memo(function ListNotification(props) {
const navigate = useNavigate(); const navigate = useNavigate();
// Fetch notifications from API // Fetch notifications from API
const fetchNotifications = async () => { const fetchNotifications = async (page = 1, limit = 10, isRead = null) => {
setLoading(true); setLoading(true);
try { try {
const response = await getAllNotification(); 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) { if (response && response.data) {
const transformedData = transformNotificationData(response.data); const transformedData = transformNotificationData(response.data);
setNotifications(transformedData); setNotifications(transformedData);
// Update pagination with mock data (since API doesn't provide pagination info) // Update pagination with API response or calculate from data
const totalItems = transformedData.length; if (response.paging) {
setPagination((prev) => ({ setPagination({
...prev, current_page: response.paging.current_page || page,
total_limit: totalItems, current_limit: response.paging.current_limit || limit,
total_page: Math.ceil(totalItems / prev.current_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) { } catch (error) {
console.error('Error fetching notifications:', error); console.error('Error fetching notifications:', error);
@@ -178,13 +200,10 @@ const ListNotification = memo(function ListNotification(props) {
current_page: page, current_page: page,
current_limit: pageSize, current_limit: pageSize,
})); }));
};
// Get paginated notifications // Fetch notifications with new pagination
const getPaginatedNotifications = () => { const isReadFilter = activeTab === 'read' ? true : activeTab === 'unread' ? false : null;
const startIndex = (pagination.current_page - 1) * pagination.current_limit; fetchNotifications(page, pageSize, isReadFilter);
const endIndex = startIndex + pagination.current_limit;
return filteredNotifications.slice(startIndex, endIndex);
}; };
useEffect(() => { useEffect(() => {
@@ -194,9 +213,10 @@ const ListNotification = memo(function ListNotification(props) {
return; return;
} }
// Fetch notifications on component mount // Fetch notifications on component mount and when tab changes
fetchNotifications(); const isReadFilter = activeTab === 'read' ? true : activeTab === 'unread' ? false : null;
}, []); fetchNotifications(pagination.current_page, pagination.current_limit, isReadFilter);
}, [activeTab]);
const getIconAndColor = (type) => { const getIconAndColor = (type) => {
switch (type) { switch (type) {
@@ -248,22 +268,17 @@ const ListNotification = memo(function ListNotification(props) {
setSearchTerm(''); setSearchTerm('');
}; };
const filteredNotifications = notifications const getUnreadCount = () => notifications.filter((n) => !n.isRead).length;
.filter((n) => {
const matchesTab = // Filter notifications based on search term
activeTab === 'all' || const getFilteredNotifications = () => {
(activeTab === 'unread' && !n.isRead) || if (!searchTerm) return notifications;
(activeTab === 'read' && n.isRead); // Search by title and error code name
return matchesTab; return notifications.filter((n) => {
})
.filter((n) => {
if (!searchTerm) return true;
// Search by title and error code name
const searchableText = `${n.title} ${n.issue}`.toLowerCase(); const searchableText = `${n.title} ${n.issue}`.toLowerCase();
return searchableText.includes(searchTerm.toLowerCase()); return searchableText.includes(searchTerm.toLowerCase());
}); });
};
const getUnreadCount = () => notifications.filter((n) => !n.isRead).length;
const tabButtonStyle = (isActive) => ({ const tabButtonStyle = (isActive) => ({
padding: '12px 16px', padding: '12px 16px',
@@ -279,8 +294,7 @@ const ListNotification = memo(function ListNotification(props) {
}); });
const renderDeviceNotifications = () => { const renderDeviceNotifications = () => {
const paginatedNotifications = getPaginatedNotifications(); const filteredNotifications = getFilteredNotifications();
return ( return (
<Space direction="vertical" size="middle" style={{ display: 'flex' }}> <Space direction="vertical" size="middle" style={{ display: 'flex' }}>
{filteredNotifications.length === 0 ? ( {filteredNotifications.length === 0 ? (
@@ -288,200 +302,215 @@ const ListNotification = memo(function ListNotification(props) {
Tidak ada notifikasi Tidak ada notifikasi
</div> </div>
) : ( ) : (
paginatedNotifications.map((notification) => { filteredNotifications.map((notification) => {
const { IconComponent, color, bgColor } = getIconAndColor(notification.type); const { IconComponent, color, bgColor } = getIconAndColor(
return ( notification.type
<Card );
key={notification.id} return (
style={{ <Card
backgroundColor: notification.isRead ? '#ffffff' : '#f6f9ff', key={notification.id}
borderColor: notification.isRead ? '#f0f0f0' : '#d6e4ff', style={{
cursor: 'pointer', backgroundColor: notification.isRead ? '#ffffff' : '#f6f9ff',
}} borderColor: notification.isRead ? '#f0f0f0' : '#d6e4ff',
onClick={() => handleMarkAsRead(notification.id)} cursor: 'pointer',
> }}
<div style={{ display: 'flex', gap: '16px', alignItems: 'flex-start' }}> onClick={() => handleMarkAsRead(notification.id)}
>
<div <div
style={{ style={{
width: '40px',
height: '40px',
borderRadius: '50%',
backgroundColor: bgColor,
color: color,
display: 'flex', display: 'flex',
alignItems: 'center', gap: '16px',
justifyContent: 'center', alignItems: 'flex-start',
fontSize: '22px',
flexShrink: 0,
}} }}
> >
<IconComponent style={{ fontSize: '22px' }} /> <div
</div> style={{
<div style={{ flex: 1 }}> width: '40px',
<Row align="top"> height: '40px',
<Col flex="220px"> borderRadius: '50%',
<div backgroundColor: bgColor,
style={{ color: color,
display: 'flex', display: 'flex',
justifyContent: 'space-between', alignItems: 'center',
alignItems: 'flex-start', justifyContent: 'center',
}} fontSize: '22px',
> flexShrink: 0,
<div> }}
<Text strong>{notification.title}</Text> >
<div style={{ marginTop: '4px' }}> <IconComponent style={{ fontSize: '22px' }} />
<Text style={{ color }}> </div>
{notification.issue} <div style={{ flex: 1 }}>
</Text> <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> </div>
{!notification.isRead && (
<Badge
color="red"
status="processing"
style={{
marginLeft: '8px',
marginTop: '4px',
}}
/>
)}
</div> </div>
{!notification.isRead && ( </Col>
<Badge <Col flex="auto">
color="red" <div
status="processing" style={{
display: 'flex',
gap: '8px',
alignItems: 'flex-start',
marginBottom: '12px',
}}
>
<MailOutlined
style={{ style={{
marginLeft: '8px',
marginTop: '4px', marginTop: '4px',
color: '#1890ff',
}} }}
/> />
)} <Paragraph
</div> style={{
</Col> color: '#595959',
<Col flex="auto"> margin: 0,
<div flex: 1,
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 {notification.details}
</Button> </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> </Space>
</Space> </Col>
</Col> <Col
<Col flex="120px"
flex="120px" style={{ textAlign: 'center' }}
style={{ textAlign: 'center' }} align="bottom"
align="bottom"
>
<Space
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
> >
<Button <Space
type="text"
icon={
<UserOutlined
style={{ color: '#1890ff' }}
/>
}
title="User History"
style={{ style={{
border: '1px solid #1890ff', display: 'flex',
borderRadius: '4px', justifyContent: 'center',
alignItems: 'center',
height: '100%',
}} }}
onClick={(e) => {
e.stopPropagation();
setModalContent('user');
}}
/>
<RouterLink
to={`/detail-notification/${
notification.id.split('-')[1]
}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
> >
<Button <Button
type="text" type="text"
icon={ icon={
<EyeOutlined <UserOutlined
style={{ color: '#1890ff' }} style={{ color: '#1890ff' }}
/> />
} }
title="Details" title="User History"
style={{ style={{
border: '1px solid #1890ff', border: '1px solid #1890ff',
borderRadius: '4px', borderRadius: '4px',
}} }}
onClick={(e) => {
e.stopPropagation();
setModalContent('user');
}}
/> />
</RouterLink> <RouterLink
<Button to={`/detail-notification/${
type="text" notification.id.split('-')[1]
icon={ }`}
<HistoryOutlined target="_blank"
style={{ color: '#1890ff' }} 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>
title="Log History" <Button
style={{ type="text"
border: '1px solid #1890ff', icon={
borderRadius: '4px', <HistoryOutlined
}} style={{ color: '#1890ff' }}
onClick={(e) => { />
e.stopPropagation(); }
setModalContent('log'); title="Log History"
}} style={{
/> border: '1px solid #1890ff',
</Space> borderRadius: '4px',
</Col> }}
</Row> onClick={(e) => {
e.stopPropagation();
setModalContent('log');
}}
/>
</Space>
</Col>
</Row>
</div>
</div> </div>
</div> </Card>
</Card> );
); })
}) )}
)}
</Space> </Space>
); );
}; };
@@ -1212,16 +1241,15 @@ const ListNotification = memo(function ListNotification(props) {
</div> </div>
</div> </div>
<Spin spinning={loading}> <Spin spinning={loading}>{renderDeviceNotifications()}</Spin>
{renderDeviceNotifications()}
</Spin>
{/* PAGINATION */} {/* PAGINATION */}
<Row justify="space-between" align="middle" style={{ marginTop: '16px' }}> <Row justify="space-between" align="middle" style={{ marginTop: '16px' }}>
<Col> <Col>
<div> <div>
Menampilkan {pagination.current_limit} data halaman{' '} Menampilkan {pagination.current_limit} data halaman{' '}
{pagination.current_page} dari total {pagination.total_limit} data {pagination.current_page} dari total {pagination.total_limit}{' '}
data
</div> </div>
</Col> </Col>
<Col> <Col>