2026-03-30 19:11:48 +07:00
|
|
|
'use client'
|
|
|
|
|
|
|
|
|
|
import { useTranslations } from 'next-intl';
|
2026-04-05 15:06:08 +07:00
|
|
|
import { IconCheck, IconArrowLeft, IconArrowRight, IconDoubleArrowLeft, IconDoubleArrowRight } from '@/app/ui/icons/icons';
|
2026-04-04 16:14:56 +07:00
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
import { useAllNotifications } from '@/app/hooks/react-query/useAllNotificationsList';
|
2026-04-13 15:10:24 +07:00
|
|
|
import { notificationsMarkAsRead, notificationsDelete, NotificationBody } from '@/app/actions/notificationActions';
|
2026-04-05 15:06:08 +07:00
|
|
|
import { useQueryClient } from '@tanstack/react-query';
|
2026-04-06 13:02:38 +07:00
|
|
|
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
2026-03-30 19:11:48 +07:00
|
|
|
|
2026-04-13 15:10:24 +07:00
|
|
|
import {
|
|
|
|
|
useReactTable,
|
|
|
|
|
getCoreRowModel,
|
|
|
|
|
getPaginationRowModel,
|
|
|
|
|
flexRender,
|
|
|
|
|
createColumnHelper,
|
|
|
|
|
RowSelectionState,
|
|
|
|
|
} from '@tanstack/react-table';
|
2026-05-14 14:30:20 +07:00
|
|
|
import { NotificationTextHandler } from '@/app/components/NotificationTextHandler';
|
2026-04-13 15:10:24 +07:00
|
|
|
|
2026-03-30 19:11:48 +07:00
|
|
|
export function NotificationsList() {
|
|
|
|
|
const t = useTranslations('Global');
|
2026-04-08 11:35:30 +07:00
|
|
|
const norificationTranslate = useTranslations('Notifications');
|
2026-04-05 15:06:08 +07:00
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
2026-04-13 15:10:24 +07:00
|
|
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
2026-04-05 15:06:08 +07:00
|
|
|
const [loadingIds, setLoadingIds] = useState<Set<number>>(new Set());
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
2026-04-13 15:10:24 +07:00
|
|
|
const [pagination, setPagination] = useState({
|
|
|
|
|
pageIndex: 0,
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
});
|
2026-04-05 15:06:08 +07:00
|
|
|
|
2026-04-13 15:10:24 +07:00
|
|
|
const { data: allNotifications, isLoading } = useAllNotifications(
|
|
|
|
|
pagination.pageIndex,
|
|
|
|
|
pagination.pageSize
|
|
|
|
|
);
|
2026-04-05 15:06:08 +07:00
|
|
|
|
|
|
|
|
const totalPages = allNotifications?.totalPages || 0;
|
2026-04-13 15:10:24 +07:00
|
|
|
const totalRecords = allNotifications?.totalElements || 0;
|
2026-04-05 15:06:08 +07:00
|
|
|
|
2026-04-13 15:10:24 +07:00
|
|
|
const columnHelper = createColumnHelper<NotificationBody>();
|
|
|
|
|
|
|
|
|
|
const columns = [
|
|
|
|
|
columnHelper.display({
|
|
|
|
|
id: 'select',
|
|
|
|
|
header: ({ table }) => {
|
|
|
|
|
const currentPageRows = table.getCoreRowModel().rows;
|
|
|
|
|
const unreadRowsOnPage = currentPageRows.filter(row => row.original.status !== 'READIED');
|
2026-04-24 14:30:03 +07:00
|
|
|
const allNotificationSelectedOnPage = currentPageRows.every(row => rowSelection[row.original.id]);
|
|
|
|
|
|
|
|
|
|
const unreadIdsSet = new Set(unreadRowsOnPage.map(row => row.original.id));
|
|
|
|
|
const selectedUnreadIds = Object.keys(rowSelection)
|
|
|
|
|
.map(Number)
|
|
|
|
|
.filter(id => unreadIdsSet.has(id));
|
2026-04-13 15:10:24 +07:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="select-all-header">
|
2026-04-24 14:30:03 +07:00
|
|
|
{selectedUnreadIds.length > 0 && (
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-primary"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const selectedIds = Object.keys(rowSelection).map(Number);
|
|
|
|
|
notificationActionHandler(selectedUnreadIds, 'mark-as-read');
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{t('mark-as-read')} ({selectedUnreadIds.length})
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
{Object.keys(rowSelection).length > 0 && (
|
|
|
|
|
<>
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-primary"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const selectedIds = Object.keys(rowSelection).map(Number);
|
|
|
|
|
notificationActionHandler(selectedIds, 'remove');
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{t('remove')} ({Object.keys(rowSelection).length})
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-05-13 10:57:20 +07:00
|
|
|
{allNotifications?.notifications.length !== 0 && (
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-primary"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (allNotificationSelectedOnPage) {
|
2026-04-13 15:10:24 +07:00
|
|
|
|
2026-05-13 10:57:20 +07:00
|
|
|
const newSelection = { ...rowSelection };
|
|
|
|
|
currentPageRows.forEach(row => {
|
|
|
|
|
delete newSelection[row.original.id];
|
|
|
|
|
});
|
|
|
|
|
setRowSelection(newSelection);
|
|
|
|
|
} else {
|
2026-04-13 15:10:24 +07:00
|
|
|
|
2026-05-13 10:57:20 +07:00
|
|
|
const newSelection = { ...rowSelection };
|
|
|
|
|
currentPageRows.forEach(row => {
|
|
|
|
|
newSelection[row.original.id] = true;
|
|
|
|
|
});
|
|
|
|
|
setRowSelection(newSelection);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{allNotificationSelectedOnPage ? t('deselect') : t('select-all')}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2026-04-13 15:10:24 +07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
cell: ({ row }) => {
|
|
|
|
|
const isReadied = row.original.status === 'READIED';
|
|
|
|
|
const isSelected = !!rowSelection[row.original.id];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="notification-check">
|
2026-04-24 14:30:03 +07:00
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setRowSelection((prev) => {
|
|
|
|
|
const newSelection = { ...prev };
|
|
|
|
|
|
|
|
|
|
if (newSelection[row.original.id]) {
|
|
|
|
|
delete newSelection[row.original.id];
|
|
|
|
|
} else {
|
|
|
|
|
newSelection[row.original.id] = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return newSelection;
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{isSelected && <IconCheck />}
|
|
|
|
|
</button>
|
2026-04-13 15:10:24 +07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
columnHelper.accessor('message', {
|
|
|
|
|
id: 'message',
|
2026-05-14 14:30:20 +07:00
|
|
|
header: () => null,
|
2026-04-13 15:10:24 +07:00
|
|
|
cell: ({ row }) => {
|
|
|
|
|
const item = row.original;
|
|
|
|
|
return (
|
|
|
|
|
<div className="notification-body">
|
|
|
|
|
<div className="notification-header">
|
2026-05-14 14:30:20 +07:00
|
|
|
<div
|
|
|
|
|
className="notification-title"
|
|
|
|
|
>
|
|
|
|
|
{norificationTranslate.has(item.message)
|
|
|
|
|
? norificationTranslate(item.message)
|
|
|
|
|
: item.message} ({item.id})
|
|
|
|
|
</div>
|
2026-04-13 15:10:24 +07:00
|
|
|
</div>
|
|
|
|
|
<div className="notification-text">
|
2026-05-14 14:30:20 +07:00
|
|
|
<NotificationTextHandler notificationType={item.type} notificationMessage={item.message} notificationContext={item.context} />
|
2026-04-13 15:10:24 +07:00
|
|
|
</div>
|
|
|
|
|
<div className="notification-footer">
|
|
|
|
|
<div className="notification-date">
|
|
|
|
|
{item.createdAt
|
|
|
|
|
? `${formatDate(item.createdAt)}: ${formatDateTime(item.createdAt)}`
|
|
|
|
|
: '---'}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-remove"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
notificationActionHandler([item.id], 'remove');
|
|
|
|
|
}}
|
|
|
|
|
disabled={loadingIds.has(item.id)}
|
|
|
|
|
>
|
|
|
|
|
{t('remove')}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const table = useReactTable({
|
|
|
|
|
data: allNotifications?.notifications || [],
|
|
|
|
|
columns,
|
|
|
|
|
state: {
|
|
|
|
|
rowSelection,
|
|
|
|
|
pagination,
|
|
|
|
|
},
|
|
|
|
|
enableRowSelection: true,
|
|
|
|
|
onRowSelectionChange: setRowSelection,
|
|
|
|
|
onPaginationChange: setPagination,
|
|
|
|
|
getCoreRowModel: getCoreRowModel(),
|
|
|
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
|
|
|
manualPagination: true,
|
|
|
|
|
pageCount: totalPages,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function notificationActionHandler(ids: number[], action: 'remove' | 'mark-as-read') {
|
|
|
|
|
setLoadingIds(prev => {
|
|
|
|
|
const newSet = new Set(prev);
|
|
|
|
|
ids.forEach(id => newSet.add(id));
|
|
|
|
|
return newSet;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let response;
|
|
|
|
|
|
|
|
|
|
if (action === 'mark-as-read') {
|
|
|
|
|
response = await notificationsMarkAsRead(ids);
|
|
|
|
|
} else if (action === 'remove') {
|
|
|
|
|
response = await notificationsDelete(ids);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (response?.success) {
|
2026-04-24 14:30:03 +07:00
|
|
|
setRowSelection({});
|
2026-04-13 15:10:24 +07:00
|
|
|
await queryClient.invalidateQueries({ queryKey: ['activeNotifications'] });
|
|
|
|
|
await queryClient.invalidateQueries({ queryKey: ['allNotifications'] });
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage = action === 'mark-as-read' ? 'Failed to mark as read' : 'Failed to remove';
|
|
|
|
|
setError(errorMessage);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoadingIds(prev => {
|
|
|
|
|
const newSet = new Set(prev);
|
|
|
|
|
ids.forEach(id => newSet.delete(id));
|
|
|
|
|
return newSet;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-05 15:06:08 +07:00
|
|
|
|
|
|
|
|
const getPageNumbers = (): (number | string)[] => {
|
|
|
|
|
const delta: number = 2;
|
2026-04-13 15:10:24 +07:00
|
|
|
const currentPageNum = pagination.pageIndex;
|
2026-04-05 15:06:08 +07:00
|
|
|
const range: number[] = [];
|
|
|
|
|
const rangeWithDots: (number | string)[] = [];
|
|
|
|
|
let l: number | undefined;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < totalPages; i++) {
|
|
|
|
|
if (i === 0 || i === totalPages - 1 || (i >= currentPageNum - delta && i <= currentPageNum + delta)) {
|
|
|
|
|
range.push(i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
range.forEach((i: number) => {
|
|
|
|
|
if (l !== undefined) {
|
|
|
|
|
if (i - l === 2) {
|
|
|
|
|
rangeWithDots.push(l + 1);
|
|
|
|
|
} else if (i - l !== 1) {
|
|
|
|
|
rangeWithDots.push('...');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
rangeWithDots.push(i);
|
|
|
|
|
l = i;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return rangeWithDots;
|
|
|
|
|
};
|
2026-03-30 19:11:48 +07:00
|
|
|
|
|
|
|
|
return (
|
2026-04-13 15:10:24 +07:00
|
|
|
<div className="notifications-list-wrapper">
|
|
|
|
|
<div className="notifications-list-header">
|
|
|
|
|
<h4>{t('all-notifications')}</h4>
|
|
|
|
|
<div className="notifications-table-header">
|
|
|
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
|
|
|
<div key={headerGroup.id} className="table-row n-header">
|
|
|
|
|
{headerGroup.headers.map((header) => (
|
|
|
|
|
<div key={header.id} className="table-cell">
|
|
|
|
|
{flexRender(
|
|
|
|
|
header.column.columnDef.header,
|
|
|
|
|
header.getContext()
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
{/* <div
|
2026-03-31 13:47:27 +07:00
|
|
|
className="notifications-list-action"
|
2026-03-30 19:11:48 +07:00
|
|
|
>
|
2026-03-31 13:47:27 +07:00
|
|
|
{(selectedFiles.size !== 0) && (
|
2026-03-31 14:00:51 +07:00
|
|
|
<>
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-primary"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
clearHandler();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{t('deselect')}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-primary"
|
|
|
|
|
onClick={() => {
|
2026-04-05 15:06:08 +07:00
|
|
|
const array = Array.from(selectedFiles);
|
2026-04-07 12:45:09 +07:00
|
|
|
notificationActionHandler(array, 'mark-as-read');
|
2026-03-31 14:00:51 +07:00
|
|
|
}}
|
|
|
|
|
>
|
2026-04-07 12:45:09 +07:00
|
|
|
{t('mark-as-read')}
|
2026-03-31 14:00:51 +07:00
|
|
|
</button>
|
|
|
|
|
</>
|
2026-03-31 13:47:27 +07:00
|
|
|
)}
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-primary"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
selectAllHandler();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{t('select-all')}
|
|
|
|
|
</button>
|
2026-04-13 15:10:24 +07:00
|
|
|
</div> */}
|
2026-03-31 13:47:27 +07:00
|
|
|
</div>
|
2026-04-13 15:10:24 +07:00
|
|
|
|
|
|
|
|
<div className="notifications-list-body">
|
|
|
|
|
{allNotifications?.notifications.length !== 0 ? (
|
|
|
|
|
<div className="notifications-table">
|
|
|
|
|
<div className="notifications-table-body">
|
|
|
|
|
{table.getRowModel().rows.map((row) => (
|
2026-03-30 19:11:48 +07:00
|
|
|
<div
|
2026-04-13 15:10:24 +07:00
|
|
|
key={row.id}
|
|
|
|
|
className={`table-row ${loadingIds.has(row.original.id) ? 'loading' : ''} ${row.original.status}`}
|
2026-03-31 13:47:27 +07:00
|
|
|
>
|
2026-04-13 15:10:24 +07:00
|
|
|
{row.getVisibleCells().map((cell) => (
|
|
|
|
|
<div key={cell.id} className="table-cell">
|
|
|
|
|
{flexRender(
|
|
|
|
|
cell.column.columnDef.cell,
|
|
|
|
|
cell.getContext()
|
2026-04-10 16:31:22 +07:00
|
|
|
)}
|
2026-03-31 13:47:27 +07:00
|
|
|
</div>
|
2026-04-13 15:10:24 +07:00
|
|
|
))}
|
2026-03-30 19:11:48 +07:00
|
|
|
</div>
|
2026-04-13 15:10:24 +07:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="no-notifications">{t('no-notifications')}</div>
|
2026-03-31 13:47:27 +07:00
|
|
|
)}
|
2026-03-30 19:11:48 +07:00
|
|
|
</div>
|
2026-04-05 15:06:08 +07:00
|
|
|
|
2026-04-13 15:10:24 +07:00
|
|
|
<div className="notifications-list-footer">
|
2026-04-05 15:06:08 +07:00
|
|
|
{totalPages > 0 && (
|
|
|
|
|
<div className="pagination-controls">
|
|
|
|
|
<button
|
|
|
|
|
className="arrow"
|
2026-04-24 14:30:03 +07:00
|
|
|
onClick={() => {
|
|
|
|
|
setRowSelection({});
|
|
|
|
|
table.firstPage();
|
|
|
|
|
}}
|
2026-04-13 15:10:24 +07:00
|
|
|
disabled={!table.getCanPreviousPage()}
|
2026-04-05 15:06:08 +07:00
|
|
|
>
|
|
|
|
|
<IconDoubleArrowLeft />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
2026-04-24 14:30:03 +07:00
|
|
|
onClick={() => {
|
|
|
|
|
setRowSelection({});
|
|
|
|
|
table.previousPage()
|
|
|
|
|
}}
|
2026-04-05 15:06:08 +07:00
|
|
|
className="arrow"
|
2026-04-13 15:10:24 +07:00
|
|
|
disabled={!table.getCanPreviousPage()}
|
2026-04-05 15:06:08 +07:00
|
|
|
>
|
|
|
|
|
<IconArrowLeft />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div className="pagination-controls-pages">
|
|
|
|
|
{getPageNumbers().map((pageNum, index) => (
|
|
|
|
|
<button
|
|
|
|
|
key={index}
|
2026-04-24 14:30:03 +07:00
|
|
|
onClick={() => {
|
|
|
|
|
setRowSelection({});
|
|
|
|
|
typeof pageNum === 'number' && table.setPageIndex(pageNum);
|
|
|
|
|
}}
|
2026-04-13 15:10:24 +07:00
|
|
|
className={pagination.pageIndex === pageNum ? 'current' : 'other'}
|
2026-04-05 15:06:08 +07:00
|
|
|
disabled={pageNum === '...'}
|
|
|
|
|
>
|
|
|
|
|
{pageNum === '...' ? '...' : (pageNum as number) + 1}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button
|
2026-04-24 14:30:03 +07:00
|
|
|
onClick={() => {
|
|
|
|
|
setRowSelection({});
|
|
|
|
|
table.nextPage();
|
|
|
|
|
}}
|
2026-04-05 15:06:08 +07:00
|
|
|
className="arrow"
|
2026-04-13 15:10:24 +07:00
|
|
|
disabled={!table.getCanNextPage()}
|
2026-04-05 15:06:08 +07:00
|
|
|
>
|
|
|
|
|
<IconArrowRight />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="arrow"
|
2026-04-24 14:30:03 +07:00
|
|
|
onClick={() => {
|
|
|
|
|
setRowSelection({});
|
|
|
|
|
table.lastPage();
|
|
|
|
|
}}
|
2026-04-13 15:10:24 +07:00
|
|
|
disabled={!table.getCanNextPage()}
|
2026-04-05 15:06:08 +07:00
|
|
|
>
|
|
|
|
|
<IconDoubleArrowRight />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-04-13 15:10:24 +07:00
|
|
|
|
|
|
|
|
{error && <div className="error-message">{error}</div>}
|
2026-03-30 19:11:48 +07:00
|
|
|
</div>
|
2026-04-13 15:10:24 +07:00
|
|
|
);
|
2026-03-30 19:11:48 +07:00
|
|
|
}
|