add TrackingHistoryView
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.108.0",
|
||||
"version": "0.109.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 2999",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -165,3 +165,41 @@ export async function filePermisionChange(fileId: string, permissions: boolean)
|
||||
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}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user