Files
no-copy-frontend/src/app/ui/violations/violations-table.tsx
T

545 lines
15 KiB
TypeScript
Raw Normal View History

2026-03-12 17:44:02 +07:00
'use client'
import { useMemo, useState, useEffect } from 'react';
2026-03-12 17:44:02 +07:00
import { useReactTable, getCoreRowModel, getSortedRowModel, getPaginationRowModel, getFilteredRowModel, ColumnDef, SortingState, ColumnFiltersState } from '@tanstack/react-table';
import { useTranslations } from 'next-intl';
2026-03-16 17:12:52 +07:00
import { IconArrowUp, IconArrowDown, IconFilter, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconEye } from '@/app/ui/icons/icons';
2026-03-12 17:44:02 +07:00
import { useQuery } from '@tanstack/react-query';
2026-03-16 17:12:52 +07:00
import { getViolationFilesArray } from '@/app/actions/violationActions';
import { useViewport } from '@/app/hooks/useViewport';
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
import Link from 'next/link';
import DropDownList from '@/app/components/DropDownList';
import { useDebounce } from 'use-debounce';
2026-03-12 17:44:02 +07:00
interface FileViolation {
2026-03-16 17:12:52 +07:00
createdAt: string;
fileId: string;
fileName: string;
fileSize: number;
latestViolationDate: string;
mimeType: string;
status: string;
supportId: number;
violationCount: number;
countComplaint: number;
countLawCase: number;
2026-03-16 17:12:52 +07:00
}
interface ViolationData {
content: FileViolation[];
totalPages: number;
totalElements: number;
size: number;
number: number;
}
2026-01-15 14:18:08 +07:00
export default function ViolationsTable() {
2026-03-12 17:44:02 +07:00
const t = useTranslations('Global');
const viewport = useViewport();
2026-03-12 17:44:02 +07:00
const [fileNameFilter, setFileNameFilter] = useState<string>('');
const [dateFilter, setDateFilter] = useState<string>('all');
2026-03-12 17:44:02 +07:00
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
});
const [sorting, setSorting] = useState<SortingState>([]);
const [debouncedFileName] = useDebounce(fileNameFilter, 500);
const getDateRange = (filter: string): { start_date?: string; end_date?: string } => {
const now = new Date();
const end_date = formatDateTimeForAPI(now);
switch (filter) {
case 'today': {
const start_date = formatDateTimeForAPI(new Date(now.setHours(0, 0, 0, 0)));
return { start_date, end_date };
}
case 'week': {
const weekAgo = new Date(now);
weekAgo.setDate(weekAgo.getDate() - 7);
weekAgo.setHours(0, 0, 0, 0);
return { start_date: formatDateTimeForAPI(weekAgo), end_date };
}
case 'month': {
const monthAgo = new Date(now);
monthAgo.setMonth(monthAgo.getMonth() - 1);
monthAgo.setHours(0, 0, 0, 0);
return { start_date: formatDateTimeForAPI(monthAgo), end_date };
}
default:
return {};
}
};
const formatDateTimeForAPI = (date: Date): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
const dateRange = getDateRange(dateFilter);
const {
data: violationData,
isLoading,
isFetching,
isError,
error,
refetch,
} = useQuery<ViolationData>({
queryKey: ['violationData', pagination.pageIndex, pagination.pageSize, debouncedFileName, dateFilter],
queryFn: () => getViolationFilesArray({
page: pagination.pageIndex,
size: pagination.pageSize,
file_name: debouncedFileName || undefined,
start_date: dateRange.start_date,
end_date: dateRange.end_date,
}),
select: (data) => {
return data;
},
placeholderData: (previousData) => previousData,
});
useEffect(() => {
if (violationData) {
setPagination(prev => ({
...prev,
pageIndex: violationData.number || 0,
}));
}
}, [violationData]);
2026-03-12 17:44:02 +07:00
2026-03-16 17:12:52 +07:00
const getMaxLengthByWidth = (): number => {
if (viewport.device === 'MOBILE') return 26;
if (viewport.device === 'TABLET') return 32;
if (viewport.device === 'DESKTOP') return 84;
return 100;
};
const cutFileName = (fileName: string) => {
const MAX_FILE_NAME_LENGTH = getMaxLengthByWidth();
const lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex <= 0) {
return fileName.length > MAX_FILE_NAME_LENGTH ? fileName.substring(0, MAX_FILE_NAME_LENGTH - 3) + '...' : fileName;
}
const nameWithoutExtension = fileName.substring(0, lastDotIndex);
const extension = fileName.substring(lastDotIndex);
if (fileName.length <= MAX_FILE_NAME_LENGTH) {
return fileName;
}
const maxNameLength = MAX_FILE_NAME_LENGTH - extension.length - 3;
if (maxNameLength <= 0) {
return '...';
2026-03-16 17:12:52 +07:00
}
return nameWithoutExtension.substring(0, maxNameLength) + '...';
};
2026-03-16 17:12:52 +07:00
const columns = useMemo<ColumnDef<FileViolation>[]>(
2026-03-12 17:44:02 +07:00
() => [
{
2026-03-16 17:12:52 +07:00
accessorKey: 'supportId',
2026-03-12 17:44:02 +07:00
header: ({ column }) => (
<div className="column">
<span>ID</span>
2026-03-12 17:44:02 +07:00
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{column.getIsSorted() === 'asc' ? (
<IconArrowUp />
) : column.getIsSorted() === 'desc' ? (
<IconArrowDown />
) : (
<IconFilter />
)}
2026-03-12 17:44:02 +07:00
</button>
</div>
),
cell: ({ row }) => (
<div className="text-center table-item table-item-id">
{row.original.supportId}
</div>
),
2026-03-12 17:44:02 +07:00
},
{
2026-04-09 13:50:18 +07:00
accessorKey: 'fileName',
2026-03-12 17:44:02 +07:00
header: ({ column }) => (
2026-03-16 17:12:52 +07:00
<div className="column start">
<span>{t('file')}</span>
2026-03-12 17:44:02 +07:00
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{column.getIsSorted() === 'asc' ? (
<IconArrowUp />
) : column.getIsSorted() === 'desc' ? (
<IconArrowDown />
) : (
<IconFilter />
)}
2026-03-12 17:44:02 +07:00
</button>
</div>
),
cell: ({ row }) => (
<div
className="flex items-center space-x-2 table-item table-item-file-name"
title={row.original.fileName}
>
{cutFileName(row.original.fileName)}
</div>
),
2026-03-16 17:12:52 +07:00
},
{
2026-04-09 13:50:18 +07:00
accessorKey: 'violationCount',
2026-03-16 17:12:52 +07:00
header: ({ column }) => (
<div className="column">
<span>{t('matches')}</span>
2026-03-16 17:12:52 +07:00
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{column.getIsSorted() === 'asc' ? (
<IconArrowUp />
) : column.getIsSorted() === 'desc' ? (
<IconArrowDown />
) : (
<IconFilter />
)}
2026-03-16 17:12:52 +07:00
</button>
</div>
),
cell: ({ row }) => (
<div className="text-center table-item">
{row.original.violationCount}
</div>
),
},
{
accessorKey: 'countComplaint',
2026-03-16 17:12:52 +07:00
header: ({ column }) => (
<div className="column">
<span>{t('complaint')}</span>
2026-03-16 17:12:52 +07:00
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{column.getIsSorted() === 'asc' ? (
<IconArrowUp />
) : column.getIsSorted() === 'desc' ? (
<IconArrowDown />
) : (
<IconFilter />
)}
2026-03-16 17:12:52 +07:00
</button>
</div>
),
cell: ({ row }) => (
<div className="text-center table-item">
{row.original.countComplaint}
</div>
),
},
{
accessorKey: 'countLawCase',
header: ({ column }) => (
<div className="column">
<span>{t('claims')}</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{column.getIsSorted() === 'asc' ? (
<IconArrowUp />
) : column.getIsSorted() === 'desc' ? (
<IconArrowDown />
) : (
<IconFilter />
)}
</button>
</div>
),
cell: ({ row }) => (
<div className="text-center table-item">
{row.original.countLawCase}
2026-03-16 17:12:52 +07:00
</div>
),
2026-03-12 17:44:02 +07:00
},
{
accessorKey: 'actions',
header: () => (
2026-03-12 17:44:02 +07:00
<div className="column">
<span>{t('actions')}</span>
2026-03-12 17:44:02 +07:00
</div>
),
cell: ({ row }) => (
<div className="actions">
<div className="actions-group">
<Link
href={`/pages/file/${row.original.fileId}`}
className="bg-violet-500 hover:bg-violet-600"
title={t('view')}
2026-03-16 17:12:52 +07:00
>
<IconEye />
</Link>
2026-03-12 17:44:02 +07:00
</div>
</div>
),
2026-03-12 17:44:02 +07:00
},
],
[t, viewport]
);
2026-03-12 17:44:02 +07:00
const table = useReactTable({
data: violationData?.content || [],
2026-03-12 17:44:02 +07:00
columns,
state: {
sorting,
pagination,
2026-03-12 17:44:02 +07:00
},
pageCount: violationData?.totalPages || -1,
2026-03-12 17:44:02 +07:00
autoResetPageIndex: false,
manualPagination: true, // Пагинация с бека
manualSorting: false, // сортировка локально так как эндпоинт не поддерживает сортировку.
manualFiltering: true, // Фильтрация с бека
2026-03-12 17:44:02 +07:00
onPaginationChange: setPagination,
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
const handleFileNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFileNameFilter(e.target.value);
setPagination(prev => ({ ...prev, pageIndex: 0 }));
};
const handleDateFilterChange = (value: string) => {
setDateFilter(value);
setPagination(prev => ({ ...prev, pageIndex: 0 }));
};
if (isError) {
return <div className="block-wrapper">Error: {error?.message || t('error')}</div>;
}
2026-01-15 14:18:08 +07:00
return (
<div className="block-wrapper">
2026-03-12 17:44:02 +07:00
<div className="tab-content">
<div className="tanstak-table-wrapper">
<h3 className="tanstak-table-title">
2026-04-21 13:11:51 +07:00
{t('matches-detected')}
2026-03-12 17:44:02 +07:00
</h3>
2026-01-15 14:18:08 +07:00
2026-03-12 17:44:02 +07:00
{/* Фильтры */}
<div className="tanstak-table-filtres">
2026-03-12 17:44:02 +07:00
<div className="table-filtres-wrapper">
{/* Фильтр по дате */}
2026-03-12 17:44:02 +07:00
<div className="table-filtres-item">
<div className="table-filtres-label">{t('date-filter')}:</div>
<DropDownList
value={dateFilter}
translatedValue={(() => {
switch (dateFilter) {
case 'all':
return t('all-dates');
2026-03-12 17:44:02 +07:00
case 'week':
return t('for-a-week');
2026-03-12 17:44:02 +07:00
case 'month':
return t('for-a-month');
2026-03-12 17:44:02 +07:00
default:
return t('today');
2026-03-12 17:44:02 +07:00
}
})()}
callBack={handleDateFilterChange}
2026-03-12 17:44:02 +07:00
>
<li value="all">{t('all-dates')}</li>
<li value="today">{t('today')}</li>
<li value="week">{t('for-a-week')}</li>
<li value="month">{t('for-a-month')}</li>
2026-03-12 17:44:02 +07:00
</DropDownList>
</div>
2026-01-15 14:18:08 +07:00
{/* Фильтр по имени файла */}
<div className="table-filtres-item">
<div className="table-filtres-label">{t('file-name')}:</div>
<input
type="text"
value={fileNameFilter}
onChange={handleFileNameChange}
placeholder={t('search')}
className="table-filtres-text-filter"
/>
</div>
2026-03-12 17:44:02 +07:00
</div>
2026-01-15 14:18:08 +07:00
<div className="table-filtres-wrapper">
2026-03-12 17:44:02 +07:00
<div className="table-filtres-item">
<div className="table-filtres-label">{t('items-per-page')}:</div>
<DropDownList
value={table.getState().pagination.pageSize}
//@ts-ignore
callBack={(value: number) => {
table.setPageSize(value);
setPagination(prev => ({ ...prev, pageSize: value, pageIndex: 0 }));
}}
2026-03-12 17:44:02 +07:00
>
{[5, 10, 20, 50, 100].map(pageSize => (
<li key={pageSize} value={pageSize}>
{t('show')} {pageSize}
</li>
))}
</DropDownList>
</div>
</div>
</div>
2026-01-15 14:18:08 +07:00
2026-03-12 17:44:02 +07:00
{/* Таблица */}
<div className="tanstak-table-block">
<table className="tanstak-table">
<thead className="tanstak-table-head">
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
2026-03-12 17:44:02 +07:00
{header.isPlaceholder
? null
: typeof header.column.columnDef.header === 'function'
? header.column.columnDef.header(header.getContext())
: (header.column.columnDef.header as string)}
2026-03-12 17:44:02 +07:00
</th>
))}
</tr>
))}
</thead>
<tbody className={`tanstak-table-body ${isFetching ? 'loading' : ''}`}>
{isLoading && (
<tr>
<td colSpan={columns.length} className="text-center relative h-12.5">
<div
className="loading-animation"
>
<div
className="global-spinner"
>
</div>
</div>
</td>
</tr>
)}
2026-03-12 17:44:02 +07:00
{table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{typeof cell.column.columnDef.cell === 'function'
? cell.column.columnDef.cell(cell.getContext())
: (cell.getValue() as string)}
2026-03-12 17:44:02 +07:00
</td>
))}
</tr>
))
) : (
<tr>
{!isLoading && (
<td colSpan={columns.length} className="text-center">
{t('no-data-for-selected-filters')}
</td>
)}
2026-03-12 17:44:02 +07:00
</tr>
)}
</tbody>
</table>
</div>
2026-01-15 14:18:08 +07:00
2026-03-12 17:44:02 +07:00
{/* Пагинация */}
<div className="tanstak-table-pagination">
<div className="pagination-info">
<span className="pagination-info-pages">
{t('page')}{' '}
<strong>
{table.getState().pagination.pageIndex + 1} {t('out-of')} {violationData?.totalPages || 1}
2026-03-12 17:44:02 +07:00
</strong>
</span>
</div>
2026-01-15 14:18:08 +07:00
2026-03-12 17:44:02 +07:00
<div className="pagination-controls">
<button
className="arrow"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
<IconDoubleArrowLeft />
</button>
<button
className="arrow"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<IconArrowLeft />
</button>
2026-01-15 14:18:08 +07:00
2026-03-12 17:44:02 +07:00
<div className="pagination-controls-pages">
{Array.from({ length: Math.min(5, violationData?.totalPages || 0) }, (_, i) => {
const totalPages = violationData?.totalPages || 0;
const currentPage = table.getState().pagination.pageIndex;
let pageIndex = 0;
2026-01-15 14:18:08 +07:00
if (totalPages <= 5) {
pageIndex = i;
} else if (currentPage <= 2) {
pageIndex = i;
} else if (currentPage >= totalPages - 3) {
pageIndex = totalPages - 5 + i;
} else {
pageIndex = currentPage - 2 + i;
}
if (pageIndex < totalPages) {
2026-03-12 17:44:02 +07:00
return (
<button
key={pageIndex}
className={table.getState().pagination.pageIndex === pageIndex ? 'current' : 'other'}
2026-03-12 17:44:02 +07:00
onClick={() => table.setPageIndex(pageIndex)}
>
{pageIndex + 1}
</button>
);
}
return null;
})}
</div>
<button
className="arrow"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<IconArrowRight />
</button>
<button
className="arrow"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
<IconDoubleArrowRight />
</button>
2026-03-12 17:44:02 +07:00
</div>
</div>
</div>
2026-01-15 14:18:08 +07:00
</div>
</div>
);
2026-01-15 14:18:08 +07:00
}