462 lines
12 KiB
TypeScript
462 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { useHistoryView, HistoryViewItem } from '@/app/hooks/react-query/useHistoryView';
|
|
import { useEffect, useState, useMemo } from 'react';
|
|
import { IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter } from '@/app/ui/icons/icons';
|
|
import DropDownList from '@/app/components/DropDownList';
|
|
import { useReactTable, getCoreRowModel, getSortedRowModel, getPaginationRowModel, SortingState, ColumnDef } from '@tanstack/react-table';
|
|
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
|
import { useTranslations } from 'next-intl';
|
|
import { useViewport } from '@/app/hooks/useViewport';
|
|
import { useLocale } from 'next-intl';
|
|
|
|
export function TrackingHistoryView() {
|
|
const [pagination, setPagination] = useState({
|
|
pageIndex: 0,
|
|
pageSize: 10,
|
|
});
|
|
const [sorting, setSorting] = useState<SortingState>([]);
|
|
const t = useTranslations('Global');
|
|
const locale = useLocale();
|
|
|
|
const getSortParams = useMemo(() => {
|
|
if (sorting.length === 0) {
|
|
return { sortBy: '', sortOrder: undefined };
|
|
}
|
|
|
|
const sort = sorting[0];
|
|
const sortByMap: Record<string, string> = {
|
|
'document': 'docName',
|
|
'view_date': 'createdAt',
|
|
'viewer': 'viewerName',
|
|
'location': locale === 'ru' ? 'geoNameRu' : 'geoNameEn'
|
|
};
|
|
|
|
return {
|
|
sortBy: sortByMap[sort.id] || sort.id,
|
|
sortOrder: sort.desc ? 'desc' : 'asc' as 'desc' | 'asc'
|
|
};
|
|
}, [sorting]);
|
|
|
|
const {
|
|
data: apiResponse,
|
|
isLoading,
|
|
isFetching,
|
|
isError,
|
|
error,
|
|
refetch
|
|
} = useHistoryView(pagination.pageIndex, pagination.pageSize, locale, getSortParams.sortOrder, getSortParams.sortBy);
|
|
|
|
const totalElements = apiResponse?.totalElements || 0
|
|
const tableData = apiResponse?.history || [];
|
|
const totalPages = apiResponse?.totalPages || 0;
|
|
|
|
const viewport = useViewport();
|
|
|
|
const getMaxLengthByWidth = (): number => {
|
|
if (viewport.device === 'MOBILE') return 22;
|
|
if (viewport.device === 'TABLET') return 30;
|
|
if (viewport.device === 'DESKTOP') return 50;
|
|
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 '...';
|
|
}
|
|
|
|
return nameWithoutExtension.substring(0, maxNameLength) + '...';
|
|
}
|
|
|
|
function EditLocationName({ location }: { location: string | undefined | null }) {
|
|
if (!location) return (
|
|
<div></div>
|
|
);
|
|
|
|
const [city, country] = location.split('/');
|
|
|
|
const translatedCity = city === 'unknow' ? t('not-defined-city') : city;
|
|
const translatedCountry = country === 'unknow' ? t('not-defined-country') : country;
|
|
|
|
return (
|
|
<div>
|
|
{t('city')}: <strong title={translatedCity}>{translatedCity}</strong>
|
|
<br />
|
|
{t('country')}: <strong title={translatedCountry}>{translatedCountry}</strong>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const columns = useMemo<ColumnDef<HistoryViewItem>[]>(
|
|
() => [
|
|
{
|
|
accessorKey: 'document',
|
|
header: ({ column }) => (
|
|
<div className="column">
|
|
<span>
|
|
{t('document')}
|
|
</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>
|
|
}
|
|
</button>
|
|
</div>
|
|
),
|
|
cell: ({ row }) => (
|
|
<div className="text-center table-item">
|
|
{row.original.document}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'view_date',
|
|
header: ({ column }) => (
|
|
<div className="column">
|
|
<span>
|
|
{t('date')} / {t('time')}
|
|
</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>
|
|
}
|
|
</button>
|
|
</div>
|
|
),
|
|
cell: ({ row }) => {
|
|
return (
|
|
<div className="text-center table-item">
|
|
{row.original.view_date ? (
|
|
<>
|
|
{formatDate(row.original.view_date)}
|
|
<br />
|
|
{formatDateTime(row.original.view_date)}
|
|
<br />
|
|
</>
|
|
) : (
|
|
<div>-</div>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
enableColumnFilter: false,
|
|
},
|
|
{
|
|
accessorKey: 'viewer',
|
|
header: ({ column }) => (
|
|
<div className="column">
|
|
<span>
|
|
{t('who-opened')}
|
|
</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>
|
|
}
|
|
</button>
|
|
</div>
|
|
),
|
|
cell: ({ row }) => {
|
|
return (
|
|
<div className="text-center font-semibold table-item">
|
|
{row.original.viewer}
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'location',
|
|
header: ({ column }) => (
|
|
<div className="column column-location">
|
|
<span>
|
|
{t('location')}
|
|
</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>
|
|
}
|
|
</button>
|
|
</div>
|
|
),
|
|
cell: ({ row }) => {
|
|
return (
|
|
<div className="text-center font-semibold table-item column-location">
|
|
{row.original.country === 'unknow/unknow' ? (
|
|
t('not-defined')
|
|
) : (
|
|
<EditLocationName location={row.original.country} />
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
}
|
|
],
|
|
[]
|
|
);
|
|
|
|
// Создание таблицы
|
|
const table = useReactTable({
|
|
data: tableData,
|
|
columns,
|
|
state: {
|
|
sorting,
|
|
pagination
|
|
},
|
|
pageCount: totalPages,
|
|
manualPagination: true,
|
|
manualSorting: true,
|
|
manualFiltering: true,
|
|
onPaginationChange: setPagination,
|
|
onSortingChange: (updater) => {
|
|
const newSorting = typeof updater === 'function' ? updater(sorting) : updater;
|
|
setSorting(newSorting);
|
|
setPagination(prev => ({ ...prev, pageIndex: 0 }));
|
|
},
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (tableData.length === 0 && pagination.pageIndex > 0 && totalPages > 0) {
|
|
setPagination(prev => ({ ...prev, pageIndex: Math.max(0, totalPages - 1) }));
|
|
}
|
|
}, [tableData, pagination.pageIndex, totalPages]);
|
|
|
|
return (
|
|
<div
|
|
className="block-wrapper"
|
|
>
|
|
<div className="tanstak-table-wrapper">
|
|
|
|
{/* Фильтры */}
|
|
<div className="tanstak-table-filtres">
|
|
<h3
|
|
className="tanstak-table-title"
|
|
>
|
|
{t('history-of-opened')}
|
|
</h3>
|
|
<div className="table-filtres-wrapper text-filter-search end">
|
|
<div className="table-filtres-item">
|
|
<div className="table-filtres-label">{t('items-per-page')}:</div>
|
|
<DropDownList
|
|
value={table.getState().pagination.pageSize}
|
|
callBack={(newSize: string) => {
|
|
table.setPageSize(Number(newSize));
|
|
setPagination(prev => ({ ...prev, pageIndex: 0 }));
|
|
}}
|
|
>
|
|
{['5', '10', '20', '50', '100'].map(pageSize => (
|
|
<li key={pageSize} value={pageSize}>
|
|
{t('show')} {pageSize}
|
|
</li>
|
|
))}
|
|
</DropDownList>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Таблица */}
|
|
<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}
|
|
>
|
|
{header.isPlaceholder
|
|
? null
|
|
: typeof header.column.columnDef.header === 'function'
|
|
? header.column.columnDef.header(header.getContext())
|
|
: header.column.columnDef.header as string}
|
|
</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>
|
|
)}
|
|
|
|
{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}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))
|
|
) : (
|
|
<tr>
|
|
{!isLoading && (
|
|
<td colSpan={columns.length} className="text-center">
|
|
{t('no-data-for-selected-filters')}
|
|
</td>
|
|
)}
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Пагинация */}
|
|
<div className="tanstak-table-pagination">
|
|
<div className="pagination-info">
|
|
<span className="pagination-info-pages">
|
|
{t('page')}{' '}
|
|
<strong>
|
|
{pagination.pageIndex + 1} {t('out-of')} {totalPages || 1}
|
|
</strong> | {t('total-document-openings')}: {totalElements}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="pagination-controls">
|
|
<button
|
|
className="arrow"
|
|
onClick={() => table.firstPage()}
|
|
disabled={!table.getCanPreviousPage() || isLoading}
|
|
>
|
|
<IconDoubleArrowLeft />
|
|
</button>
|
|
<button
|
|
className="arrow"
|
|
onClick={() => table.previousPage()}
|
|
disabled={!table.getCanPreviousPage() || isLoading}
|
|
>
|
|
<IconArrowLeft />
|
|
</button>
|
|
|
|
<div className="pagination-controls-pages">
|
|
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
|
let pageIndex;
|
|
if (totalPages <= 5) {
|
|
pageIndex = i;
|
|
} else if (pagination.pageIndex <= 2) {
|
|
pageIndex = i;
|
|
} else if (pagination.pageIndex >= totalPages - 3) {
|
|
pageIndex = totalPages - 5 + i;
|
|
} else {
|
|
pageIndex = pagination.pageIndex - 2 + i;
|
|
}
|
|
|
|
if (pageIndex >= 0 && pageIndex < totalPages) {
|
|
return (
|
|
<button
|
|
key={pageIndex}
|
|
className={pagination.pageIndex === pageIndex ? 'current' : 'other'}
|
|
onClick={() => table.setPageIndex(pageIndex)}
|
|
disabled={isLoading}
|
|
>
|
|
{pageIndex + 1}
|
|
</button>
|
|
);
|
|
}
|
|
return null;
|
|
})}
|
|
</div>
|
|
|
|
<button
|
|
className="arrow"
|
|
onClick={() => table.nextPage()}
|
|
disabled={!table.getCanNextPage() || isLoading}
|
|
>
|
|
<IconArrowRight />
|
|
</button>
|
|
<button
|
|
className="arrow"
|
|
onClick={() => table.lastPage()}
|
|
disabled={!table.getCanNextPage() || isLoading}
|
|
>
|
|
<IconDoubleArrowRight />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div >
|
|
</div>
|
|
)
|
|
} |