add TrackingHistoryView

This commit is contained in:
smanylov
2026-05-06 19:03:29 +07:00
parent af6b997ceb
commit 8a72dbaf68
6 changed files with 496 additions and 39 deletions
+2
View File
@@ -2,6 +2,7 @@
import { TrackingStatistic } from '@/app/ui/tracking-page/tracking-statistic';
import FilesTable from '@/app/ui/dashboard/files-table';
import { TrackingHistoryView } from '@/app/ui/tracking-page/tracking-history-view';
export default async function Page() {
const FILE_TYPE = "document";
@@ -12,6 +13,7 @@ export default async function Page() {
>
<TrackingStatistic />
<FilesTable fileType={FILE_TYPE} showFileLink={true} />
<TrackingHistoryView />
</div>
)
}
+38
View File
@@ -158,6 +158,44 @@ export async function filePermisionChange(fileId: string, permissions: boolean)
return false;
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function fetchHistoryView(page: number, size: number) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30028,
message_body: {
token: token,
action: 'history_view',
page: page,
size: size
}
}),
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
return null;
}
} else {
throw new Error(`${response.status}`);
}
-38
View File
@@ -1,38 +0,0 @@
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { code } = await request.json();
try {
const response = await fetch('https://oauth.yandex.ru/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: 'b356a7ceac0f4c50a3b434220f86bce0',
client_secret: '0800f7774f7d4e19ad4b434f10afd4ab',
})
});
const data = await response.json();
console.log(data);
if (data.access_token) {
const userInfo = await fetch('https://login.yandex.ru/info?format=json', {
headers: { 'Authorization': `OAuth ${data.access_token}` }
}).then(r => r.json());
console.log(userInfo);
// Тут мне нужно будет отправить сделать запрос в бек с userInfo и там уже редиректить на дешборд
return NextResponse.json({ success: true });
}
} catch (error) {
console.error('Token exchange failed:', error);
}
return NextResponse.json({ success: false }, { status: 400 });
}
@@ -0,0 +1,37 @@
import { useQuery } from '@tanstack/react-query';
import { fetchHistoryView } from '@/app/actions/trackingActions';
export interface HistoryViewItem {
city: string;
country: string;
viewer: string;
document: string;
view_date: string;
}
export interface UseHistoryView {
documentsCount: number;
history: HistoryViewItem[];
pageNumber: number;
pageSize: number;
totalElements: number;
totalPages: number;
uniqueCityCount: number;
uniqueCountryCount: number;
}
export const useHistoryView = (page: number, size: number) => {
return useQuery({
queryKey: ['violationStatistic'],
queryFn: () => {
return fetchHistoryView(page, size)
},
select: (data: UseHistoryView | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,418 @@
'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, IconFileDownload, IconInfo, IconEye, IconCheck, IconLink } 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';
export function TrackingHistoryView() {
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
});
const [sorting, setSorting] = useState<SortingState>([]);
const t = useTranslations('Global');
const {
data: apiResponse,
isLoading,
isFetching,
isError,
error,
refetch
} = useHistoryView(pagination.pageIndex, pagination.pageSize);
useEffect(() => {
console.log(apiResponse);
}, [apiResponse]);
const tableData = apiResponse?.history || [];
const totalItems = apiResponse?.history.length || 0;
const totalPages = Math.ceil(totalItems / pagination.pageSize);
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) + '...';
}
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>
Дата / время
</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>
viewer
</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">
<span>
Местоположение
</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.city} {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(),
});
return (
<div
className="block-wrapper"
>
<div className="tanstak-table-wrapper">
<h3
className="tanstak-table-title"
>
История открытий
</h3>
{/* Фильтры */}
<div className="tanstak-table-filtres">
<div className="table-filtres-wrapper text-filter-search">
<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>
</span>
<span className="pagination-info-files">
| {t('shown')} {tableData.length} {t('out-of')} {totalItems}
</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>
)
}