rework violation table and violation actions

This commit is contained in:
smanylov
2026-04-29 15:15:04 +07:00
parent c0fe6cce48
commit c274cb134d
6 changed files with 341 additions and 449 deletions
+281 -257
View File
@@ -1,6 +1,6 @@
'use client'
import { useMemo, useState } from 'react';
import { useMemo, useState, useEffect } from 'react';
import { useReactTable, getCoreRowModel, getSortedRowModel, getPaginationRowModel, getFilteredRowModel, ColumnDef, SortingState, ColumnFiltersState } from '@tanstack/react-table';
import { useTranslations } from 'next-intl';
import { IconArrowUp, IconArrowDown, IconFilter, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconEye } from '@/app/ui/icons/icons';
@@ -9,8 +9,10 @@ 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';
interface ViolationFile {
interface FileViolation {
createdAt: string;
fileId: string;
fileName: string;
@@ -22,32 +24,103 @@ interface ViolationFile {
violationCount: number;
}
interface ViolationData {
content: FileViolation[];
totalPages: number;
totalElements: number;
size: number;
number: number;
}
export default function ViolationsTable() {
const {
data: violationData,
isLoading,
isError,
error,
} = useQuery<ViolationFile[]>({
queryKey: ['violationData'],
queryFn: () => getViolationFilesArray(),
select: (data) => {
return data;
},
/* refetchInterval: 30000 */
});
const t = useTranslations('Global');
const viewport = useViewport();
// Состояния
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [fileNameFilter, setFileNameFilter] = useState<string>('');
const [dateFilter, setDateFilter] = useState<string>('all');
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 };
}
case 'older': {
const monthAgo = new Date(now);
monthAgo.setMonth(monthAgo.getMonth() - 1);
monthAgo.setHours(0, 0, 0, 0);
return { end_date: formatDateTimeForAPI(monthAgo) };
}
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) => {
console.log(data);
return data;
},
placeholderData: (previousData) => previousData,
});
useEffect(() => {
if (violationData) {
setPagination(prev => ({
...prev,
pageIndex: violationData.number || 0,
}));
}
}, [violationData]);
const viewport = useViewport();
const getMaxLengthByWidth = (): number => {
if (viewport.device === 'MOBILE') return 26;
if (viewport.device === 'TABLET') return 32;
@@ -73,115 +146,83 @@ export default function ViolationsTable() {
const maxNameLength = MAX_FILE_NAME_LENGTH - extension.length - 3;
if (maxNameLength <= 0) {
return '...' /* + extension */;
return '...';
}
return nameWithoutExtension.substring(0, maxNameLength) + '...' /* + extension */;
}
return nameWithoutExtension.substring(0, maxNameLength) + '...';
};
const columns = useMemo<ColumnDef<ViolationFile>[]>(
const columns = useMemo<ColumnDef<FileViolation>[]>(
() => [
{
accessorKey: 'supportId',
header: ({ column }) => (
<div className="column">
<span>
ID
</span>
<span>ID</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
{column.getIsSorted() === 'asc' ? (
<IconArrowUp />
) : column.getIsSorted() === 'desc' ? (
<IconArrowDown />
) : (
<IconFilter />
)}
</button>
</div>
),
cell: ({ row }) => {
return (
<div>
<div className="text-center table-item table-item-id">
{row.original.supportId}
</div>
</div>
)
}
cell: ({ row }) => (
<div className="text-center table-item table-item-id">
{row.original.supportId}
</div>
),
},
{
accessorKey: 'fileName',
header: ({ column }) => (
<div className="column start">
<span>
{t('file')}
</span>
<span>{t('file')}</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
{column.getIsSorted() === 'asc' ? (
<IconArrowUp />
) : column.getIsSorted() === 'desc' ? (
<IconArrowDown />
) : (
<IconFilter />
)}
</button>
</div>
),
cell: ({ row }) => {
return (
<div>
<div
className="flex items-center space-x-2 table-item table-item-file-name"
title={row.original.fileName}
>
{cutFileName(row.original.fileName)}
</div>
</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>
),
},
{
accessorKey: 'violationCount',
header: ({ column }) => (
<div className="column">
<span>
{t('matches')}
</span>
<span>{t('matches')}</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
{column.getIsSorted() === 'asc' ? (
<IconArrowUp />
) : column.getIsSorted() === 'desc' ? (
<IconArrowDown />
) : (
<IconFilter />
)}
</button>
</div>
),
@@ -195,26 +236,18 @@ export default function ViolationsTable() {
accessorKey: 'latestViolationDate',
header: ({ column }) => (
<div className="column">
<span>
{t('date')}
</span>
<span>{t('date')}</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
{column.getIsSorted() === 'asc' ? (
<IconArrowUp />
) : column.getIsSorted() === 'desc' ? (
<IconArrowDown />
) : (
<IconFilter />
)}
</button>
</div>
),
@@ -228,93 +261,74 @@ export default function ViolationsTable() {
},
{
accessorKey: 'actions',
header: ({ column }) => (
header: () => (
<div className="column">
<span>
{t('actions')}
</span>
<span>{t('actions')}</span>
</div>
),
cell: ({ row }) => {
return (
<div className="actions">
<div
className="actions-group"
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')}
>
{/* <button
onClick={() => {
console.log(row.original.fileId);
}}
className="bg-violet-500 hover:bg-violet-600"
title={t('view')}
>
<IconEye />
</button> */}
<Link
href={`/pages/file/${row.original.fileId}`}
className="bg-violet-500 hover:bg-violet-600"
title={t('view')}
>
<IconEye />
</Link>
</div>
<IconEye />
</Link>
</div>
)
}
</div>
),
},
],
[]
)
const filteredData = useMemo(() => {
let result = violationData;
if (!result) {
return [];
}
return result
}, [violationData])
[t, viewport]
);
const table = useReactTable({
data: filteredData,
data: violationData?.content || [],
columns,
state: {
sorting,
columnFilters,
pagination
pagination,
},
pageCount: violationData?.totalPages || -1,
autoResetPageIndex: false,
manualPagination: true, // Пагинация с бека
manualSorting: false, // сортировка локально так как эндпоинт не поддерживает сортировку.
manualFiltering: true, // Фильтрация с бека
onPaginationChange: setPagination,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFilteredRowModel: getFilteredRowModel(),
initialState: {
pagination: {
pageSize: 10,
},
},
});
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>;
}
return (
<div
className="block-wrapper"
>
<div className="block-wrapper">
<div className="tab-content">
<div
className="tanstak-table-wrapper"
>
<h3
className="tanstak-table-title"
>
<div className="tanstak-table-wrapper">
<h3 className="tanstak-table-title">
{t('matches-detected')}
</h3>
{/* Фильтры */}
{/* <div className="tanstak-table-filtres">
<div className="tanstak-table-filtres">
<div className="table-filtres-wrapper">
{/* Фильтр по дате */}
<div className="table-filtres-item">
<div className="table-filtres-label">{t('date-filter')}:</div>
<DropDownList
@@ -322,48 +336,50 @@ export default function ViolationsTable() {
translatedValue={(() => {
switch (dateFilter) {
case 'all':
return t('all-dates')
return t('all-dates');
case 'week':
return t('for-a-week')
return t('for-a-week');
case 'month':
return t('for-a-month')
return t('for-a-month');
case 'older':
return t('older-than-a-month')
return t('older-than-a-month');
default:
return t('today')
return t('today');
}
})()}
callBack={setDateFilter}
callBack={handleDateFilterChange}
>
<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>
<li value="older">
{t('older-than-a-month')}
</li>
<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>
<li value="older">{t('older-than-a-month')}</li>
</DropDownList>
</div>
{/* Фильтр по имени файла */}
<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>
</div>
<div
className="table-filtres-wrapper"
>
<div className="table-filtres-wrapper">
<div className="table-filtres-item">
<div className="table-filtres-label">{t('items-per-page')}:</div>
<DropDownList
value={table.getState().pagination.pageSize}
//@ts-ignore сделал так потому что для table.setPageSize нужна цифра
callBack={table.setPageSize}
//@ts-ignore
callBack={(value: number) => {
table.setPageSize(value);
setPagination(prev => ({ ...prev, pageSize: value, pageIndex: 0 }));
}}
>
{[5, 10, 20, 50, 100].map(pageSize => (
<li key={pageSize} value={pageSize}>
@@ -373,8 +389,7 @@ export default function ViolationsTable() {
</DropDownList>
</div>
</div>
</div> */}
</div>
{/* Таблица */}
<div className="tanstak-table-block">
@@ -383,20 +398,32 @@ export default function ViolationsTable() {
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
>
<th key={header.id}>
{header.isPlaceholder
? null
: typeof header.column.columnDef.header === 'function'
? header.column.columnDef.header(header.getContext())
: header.column.columnDef.header as string}
: (header.column.columnDef.header as string)}
</th>
))}
</tr>
))}
</thead>
<tbody className="tanstak-table-body">
<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>
)}
{table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map(row => (
<tr key={row.id}>
@@ -404,16 +431,18 @@ export default function ViolationsTable() {
<td key={cell.id}>
{typeof cell.column.columnDef.cell === 'function'
? cell.column.columnDef.cell(cell.getContext())
: cell.getValue() as string}
: (cell.getValue() as string)}
</td>
))}
</tr>
))
) : (
<tr>
<td colSpan={columns.length} className="text-center">
{t('no-data-for-selected-filters')}
</td>
{!isLoading && (
<td colSpan={columns.length} className="text-center">
{t('no-data-for-selected-filters')}
</td>
)}
</tr>
)}
</tbody>
@@ -426,52 +455,51 @@ export default function ViolationsTable() {
<span className="pagination-info-pages">
{t('page')}{' '}
<strong>
{table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1}
{table.getState().pagination.pageIndex + 1} {t('out-of')} {violationData?.totalPages || 1}
</strong>
</span>
<span className="pagination-info-files">
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length}
| {t('shown')} {violationData?.content?.length || 0} {t('out-of')} {violationData?.totalElements || 0}
</span>
</div>
<div className="pagination-controls">
{table.getCanPreviousPage() && (
<button
className="arrow"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
<IconDoubleArrowLeft />
</button>
)}
{table.getCanPreviousPage() && (
<button
className="arrow"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<IconArrowLeft />
</button>
)}
<button
className="arrow"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
<IconDoubleArrowLeft />
</button>
<button
className="arrow"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<IconArrowLeft />
</button>
<div className="pagination-controls-pages">
{Array.from({ length: Math.min(5, table.getPageCount()) }, (_, i) => {
const pageIndex = Math.max(
0,
Math.min(
table.getPageCount() - 5,
table.getState().pagination.pageIndex - 2
)
) + i;
{Array.from({ length: Math.min(5, violationData?.totalPages || 0) }, (_, i) => {
const totalPages = violationData?.totalPages || 0;
const currentPage = table.getState().pagination.pageIndex;
let pageIndex = 0;
if (pageIndex < table.getPageCount()) {
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) {
return (
<button
key={pageIndex}
className={`${table.getState().pagination.pageIndex === pageIndex
? 'current'
: 'other'
}`}
className={table.getState().pagination.pageIndex === pageIndex ? 'current' : 'other'}
onClick={() => table.setPageIndex(pageIndex)}
>
{pageIndex + 1}
@@ -482,28 +510,24 @@ export default function ViolationsTable() {
})}
</div>
{table.getCanNextPage() && (
<button
className="arrow"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<IconArrowRight />
</button>
)}
{table.getCanNextPage() && (
<button
className="arrow"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
<IconDoubleArrowRight />
</button>
)}
<button
className="arrow"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<IconArrowRight />
</button>
<button
className="arrow"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
<IconDoubleArrowRight />
</button>
</div>
</div>
</div>
</div>
</div>
)
);
}