Files
no-copy-frontend/src/app/ui/notifications/notifications-list.tsx
T

403 lines
11 KiB
TypeScript
Raw Normal View History

'use client'
import { useTranslations } from 'next-intl';
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';
import { useQueryClient } from '@tanstack/react-query';
2026-04-06 13:02:38 +07:00
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
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
export function NotificationsList() {
const t = useTranslations('Global');
2026-04-08 11:35:30 +07:00
const norificationTranslate = useTranslations('Notifications');
const queryClient = useQueryClient();
2026-04-13 15:10:24 +07:00
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
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-13 15:10:24 +07:00
const { data: allNotifications, isLoading } = useAllNotifications(
pagination.pageIndex,
pagination.pageSize
);
const totalPages = allNotifications?.totalPages || 0;
2026-04-13 15:10:24 +07:00
const totalRecords = allNotifications?.totalElements || 0;
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>
</>
)}
{allNotifications?.notifications.length !== 0 && (
<button
className="btn btn-primary"
onClick={() => {
if (allNotificationSelectedOnPage) {
2026-04-13 15:10:24 +07:00
const newSelection = { ...rowSelection };
currentPageRows.forEach(row => {
delete newSelection[row.original.id];
});
setRowSelection(newSelection);
} else {
2026-04-13 15:10:24 +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;
});
}
}
const getPageNumbers = (): (number | string)[] => {
const delta: number = 2;
2026-04-13 15:10:24 +07:00
const currentPageNum = pagination.pageIndex;
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;
};
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-31 13:47:27 +07:00
{(selectedFiles.size !== 0) && (
<>
<button
className="btn btn-primary"
onClick={() => {
clearHandler();
}}
>
{t('deselect')}
</button>
<button
className="btn btn-primary"
onClick={() => {
const array = Array.from(selectedFiles);
notificationActionHandler(array, 'mark-as-read');
}}
>
{t('mark-as-read')}
</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) => (
<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
))}
</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
)}
</div>
2026-04-13 15:10:24 +07:00
<div className="notifications-list-footer">
{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()}
>
<IconDoubleArrowLeft />
</button>
<button
2026-04-24 14:30:03 +07:00
onClick={() => {
setRowSelection({});
table.previousPage()
}}
className="arrow"
2026-04-13 15:10:24 +07:00
disabled={!table.getCanPreviousPage()}
>
<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'}
disabled={pageNum === '...'}
>
{pageNum === '...' ? '...' : (pageNum as number) + 1}
</button>
))}
</div>
<button
2026-04-24 14:30:03 +07:00
onClick={() => {
setRowSelection({});
table.nextPage();
}}
className="arrow"
2026-04-13 15:10:24 +07:00
disabled={!table.getCanNextPage()}
>
<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()}
>
<IconDoubleArrowRight />
</button>
</div>
)}
</div>
2026-04-13 15:10:24 +07:00
{error && <div className="error-message">{error}</div>}
</div>
2026-04-13 15:10:24 +07:00
);
}