From 5d521358f7be05e30ff1291898852c52eb209d21 Mon Sep 17 00:00:00 2001 From: smanylov Date: Thu, 23 Apr 2026 18:08:11 +0700 Subject: [PATCH] add two tanstack tables for claims and complaints --- src/app/[locale]/pages/violations/page.tsx | 9 +- src/app/hooks/react-query/useLastCases.ts | 13 +- .../hooks/react-query/useLastComplaints.ts | 13 +- .../tables/violations-my-claims-table.tsx | 343 +++++++++++++++++ .../tables/violations-my-complaints-table.tsx | 344 ++++++++++++++++++ src/i18n/messages/en.json | 1 + src/i18n/messages/ru.json | 1 + 7 files changed, 710 insertions(+), 14 deletions(-) create mode 100644 src/app/ui/violations/tables/violations-my-claims-table.tsx create mode 100644 src/app/ui/violations/tables/violations-my-complaints-table.tsx diff --git a/src/app/[locale]/pages/violations/page.tsx b/src/app/[locale]/pages/violations/page.tsx index 740562e..2e8808c 100644 --- a/src/app/[locale]/pages/violations/page.tsx +++ b/src/app/[locale]/pages/violations/page.tsx @@ -7,6 +7,8 @@ import AnalyticsCardGeography from '@/app/ui/reports/analytics-card-geography'; import AnalyticsCardDistribution from '@/app/ui/reports/analytics-card-distribution'; import ViolationsMyClaims from '@/app/ui/violations/violations-my-claims'; import ViolationsLastCombinedCases from '@/app/ui/violations/violations-last-combined-cases'; +import { ViolationsMyComplaintsTable } from '@/app/ui/violations/tables/violations-my-complaints-table'; +import { ViolationsMyClaimsTable } from '@/app/ui/violations/tables/violations-my-claims-table'; export default function Page() { return ( @@ -19,13 +21,16 @@ export default function Page() { -{/* + {/* */}
- +
+ + +
) } \ No newline at end of file diff --git a/src/app/hooks/react-query/useLastCases.ts b/src/app/hooks/react-query/useLastCases.ts index 2237161..a2f5263 100644 --- a/src/app/hooks/react-query/useLastCases.ts +++ b/src/app/hooks/react-query/useLastCases.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { fetchLastCase, ComplaintsCaseQueryParams } from '@/app/actions/violationActions'; -interface Case { +export interface Case { amount: number; content: null | string; created_at: string; @@ -21,15 +21,15 @@ interface Case { } export interface useLastCase { content: Case[], - page: 1, - size: 5, - totalElements: 10, - totalPages: 2, + page: number, + size: number, + totalElements: number, + totalPages: number, } export const useLastCases = (params: ComplaintsCaseQueryParams) => { return useQuery({ - queryKey: ['lastCases'], + queryKey: ['lastCases', params.page, params.size, params.sort_by, params.sort_direction], queryFn: () => { return fetchLastCase(params) }, @@ -39,6 +39,7 @@ export const useLastCases = (params: ComplaintsCaseQueryParams) => { } return data; }, + placeholderData: (previousData) => previousData, retry: false }); }; \ No newline at end of file diff --git a/src/app/hooks/react-query/useLastComplaints.ts b/src/app/hooks/react-query/useLastComplaints.ts index 580862a..cab05fc 100644 --- a/src/app/hooks/react-query/useLastComplaints.ts +++ b/src/app/hooks/react-query/useLastComplaints.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { fetchLastComplaint, ComplaintsCaseQueryParams } from '@/app/actions/violationActions'; -interface Complaint { +export interface Complaint { complaint_text: string; created_at: string; email: string; @@ -14,15 +14,15 @@ interface Complaint { } export interface useLastComplaint { content: Complaint[], - page: 1, - size: 5, - totalElements: 10, - totalPages: 2, + page: number, + size: number, + totalElements: number, + totalPages: number, } export const useLastComplaints = (params: ComplaintsCaseQueryParams) => { return useQuery({ - queryKey: ['lastComplaints'], + queryKey: ['lastComplaints', params.page, params.size, params.sort_by, params.sort_direction], queryFn: () => { return fetchLastComplaint(params) }, @@ -32,6 +32,7 @@ export const useLastComplaints = (params: ComplaintsCaseQueryParams) => { } return data; }, + placeholderData: (previousData) => previousData, retry: false }); }; \ No newline at end of file diff --git a/src/app/ui/violations/tables/violations-my-claims-table.tsx b/src/app/ui/violations/tables/violations-my-claims-table.tsx new file mode 100644 index 0000000..33c752b --- /dev/null +++ b/src/app/ui/violations/tables/violations-my-claims-table.tsx @@ -0,0 +1,343 @@ +'use client' + +import { useState, useMemo, useCallback } from 'react'; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getPaginationRowModel, + getFilteredRowModel, + ColumnDef, + SortingState, +} from '@tanstack/react-table'; +import { IconArrowUp, IconArrowDown, IconFilter, IconArrowRight, IconArrowLeft, IconDoubleArrowLeft, IconDoubleArrowRight, IconEye } from '@/app/ui/icons/icons'; +import { useTranslations } from 'next-intl'; +import { useLastCases, Case } from '@/app/hooks/react-query/useLastCases'; +import { formatDate, formatDateTime } from '@/app/lib/formatDate'; +import Link from 'next/link'; + +export function ViolationsMyClaimsTable() { + const [sorting, setSorting] = useState([]); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 5, + }); + const t = useTranslations("Global"); + + const getSortParams: { + sort_by: string; + sort_direction: 'asc' | 'desc' + } = useMemo(() => { + if (sorting.length === 0) { + return { sort_by: 'createdAt', sort_direction: 'desc' as const }; + } + + const sort = sorting[0]; + const sortByMap: Record = { + 'caseName': 'name', + 'createdAt': 'created_at', + 'updatedAt': 'updated_at', + 'status': 'type' + }; + + return { + sort_by: sortByMap[sort.id] || sort.id, + sort_direction: sort.desc ? 'desc' : 'asc' as const + }; + }, [sorting]); + + const { + data: apiResponse, + isLoading, + isFetching, + } = useLastCases({ + page: pagination.pageIndex + 1, + size: pagination.pageSize, + sort_by: getSortParams.sort_by, + sort_direction: getSortParams.sort_direction ? getSortParams.sort_direction : 'desc', + }); + + const tableData = apiResponse?.content || []; + const totalItems = apiResponse?.totalElements || 0; + const totalPages = apiResponse?.totalPages || 0; + + const columns = useMemo[]>( + () => [ + { + accessorKey: 'caseName', + header: ({ column }) => ( +
+ {t('case-name')} + +
+ ), + cell: ({ row }) => ( +
+ + {row.original.name.length > 50 + ? `${row.original.name.substring(0, 50)}...` + : row.original.name} + +
+ ), + }, + { + accessorKey: 'createdAt', + header: ({ column }) => ( +
+ {t('date-of-creation')} + +
+ ), + cell: ({ row }) => ( +
+ {formatDate(new Date(row.original.created_at).getTime())} +
+ {formatDateTime(new Date(row.original.created_at).getTime())} +
+ ), + }, + { + accessorKey: 'updatedAt', + header: ({ column }) => ( +
+ {t('date-of-update')} + +
+ ), + cell: ({ row }) => ( +
+ {formatDate(new Date(row.original.updated_at).getTime())} +
+ {formatDateTime(new Date(row.original.updated_at).getTime())} +
+ ), + }, + { + accessorKey: 'status', + header: ({ column }) => ( +
+ {t('status')} + +
+ ), + cell: ({ row }) => { + const status = row.original.type; + const statusColor = status === 'ACTIVE' ? 'text-green-600' : 'text-gray-500'; + return ( +
+ {t(status.toLowerCase())} +
+ ); + }, + }, + { + id: 'actions', + header: () =>
{t('actions')}
, + cell: ({ row }) => ( +
+
+ + + +
+
+ ), + enableSorting: false, + }, + ], + [] + ); + + const table = useReactTable({ + data: tableData, + columns, + state: { + sorting, + pagination, + }, + pageCount: totalPages, + manualPagination: true, + manualSorting: 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 ( +
+

{t('claims')}

+ +
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.length > 0 ? ( + table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + )) + ) : ( + + + + )} + +
+ {header.isPlaceholder + ? null + : typeof header.column.columnDef.header === 'function' + ? header.column.columnDef.header(header.getContext()) + : header.column.columnDef.header as string} +
+ {typeof cell.column.columnDef.cell === 'function' + ? cell.column.columnDef.cell(cell.getContext()) + : cell.getValue() as string} +
+ {t('no-data')} +
+
+ + {/* Pagination */} +
+
+ + {t('page')}{' '} + + {pagination.pageIndex + 1} {t('out-of')} {totalPages || 1} + + + + | {t('shown')} {tableData.length} {t('out-of')} {totalItems} + +
+ +
+ + + +
+ {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 ( + + ); + } + return null; + })} +
+ + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/ui/violations/tables/violations-my-complaints-table.tsx b/src/app/ui/violations/tables/violations-my-complaints-table.tsx new file mode 100644 index 0000000..f4ba520 --- /dev/null +++ b/src/app/ui/violations/tables/violations-my-complaints-table.tsx @@ -0,0 +1,344 @@ +'use client' + +import { useLastComplaints, Complaint } from '@/app/hooks/react-query/useLastComplaints'; +import { useState, useMemo, useCallback, useEffect } from 'react'; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getPaginationRowModel, + getFilteredRowModel, + ColumnDef, + SortingState, +} from '@tanstack/react-table'; +import { IconArrowUp, IconArrowDown, IconFilter, IconArrowRight, IconArrowLeft, IconDoubleArrowLeft, IconDoubleArrowRight, IconEye } from '@/app/ui/icons/icons'; +import { useTranslations } from 'next-intl'; +import { formatDate, formatDateTime } from '@/app/lib/formatDate'; +import Link from 'next/link'; + +export function ViolationsMyComplaintsTable() { + + const [sorting, setSorting] = useState([]); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 5, + }); + const t = useTranslations("Global"); + + const getSortParams: { + sort_by: string; + sort_direction: 'asc' | 'desc' + } = useMemo(() => { + if (sorting.length === 0) { + return { sort_by: 'createdAt', sort_direction: 'desc' as const }; + } + + const sort = sorting[0]; + const sortByMap: Record = { + 'caseName': 'name', + 'createdAt': 'created_at', + 'updatedAt': 'updated_at', + 'status': 'type' + }; + + return { + sort_by: sortByMap[sort.id] || sort.id, + sort_direction: sort.desc ? 'desc' : 'asc' as const + }; + }, [sorting]); + + const { + data: apiResponse, + isLoading, + isFetching, + } = useLastComplaints({ + page: pagination.pageIndex + 1, + size: pagination.pageSize, + sort_by: getSortParams.sort_by, + sort_direction: getSortParams.sort_direction ? getSortParams.sort_direction : 'desc', + }); + + const tableData = apiResponse?.content || []; + const totalItems = apiResponse?.totalElements || 0; + const totalPages = apiResponse?.totalPages || 0; + + const columns = useMemo[]>( + () => [ + { + accessorKey: 'caseName', + header: ({ column }) => ( +
+ {t('complaint-name')} + +
+ ), + cell: ({ row }) => ( +
+ + {`COM-${row.original.id}`} + +
+ ), + }, + { + accessorKey: 'createdAt', + header: ({ column }) => ( +
+ {t('date-of-creation')} + +
+ ), + cell: ({ row }) => ( +
+ {formatDate(new Date(row.original.created_at).getTime())} +
+ {formatDateTime(new Date(row.original.created_at).getTime())} +
+ ), + }, + { + accessorKey: 'updatedAt', + header: ({ column }) => ( +
+ {t('date-of-update')} + +
+ ), + cell: ({ row }) => ( +
+ {formatDate(new Date(row.original.updated_at).getTime())} +
+ {formatDateTime(new Date(row.original.updated_at).getTime())} +
+ ), + }, + { + accessorKey: 'status', + header: ({ column }) => ( +
+ {t('status')} + +
+ ), + cell: ({ row }) => { + const status = row.original.status; + const statusColor = status === 'ACTIVE' ? 'text-green-600' : 'text-gray-500'; + return ( +
+ {/* {t(status.toLowerCase())} */} + {status} +
+ ); + }, + }, + { + id: 'actions', + header: () =>
{t('actions')}
, + cell: ({ row }) => ( +
+
+ + + +
+
+ ), + enableSorting: false, + }, + ], + [] + ); + + const table = useReactTable({ + data: tableData, + columns, + state: { + sorting, + pagination, + }, + pageCount: totalPages, + manualPagination: true, + manualSorting: 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 ( +
+

{t('complaints')}

+ +
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.length > 0 ? ( + table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + )) + ) : ( + + + + )} + +
+ {header.isPlaceholder + ? null + : typeof header.column.columnDef.header === 'function' + ? header.column.columnDef.header(header.getContext()) + : header.column.columnDef.header as string} +
+ {typeof cell.column.columnDef.cell === 'function' + ? cell.column.columnDef.cell(cell.getContext()) + : cell.getValue() as string} +
+ {t('no-data')} +
+
+ + {/* Pagination */} +
+
+ + {t('page')}{' '} + + {pagination.pageIndex + 1} {t('out-of')} {totalPages || 1} + + + + | {t('shown')} {tableData.length} {t('out-of')} {totalItems} + +
+ +
+ + + +
+ {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 ( + + ); + } + return null; + })} +
+ + + +
+
+
+ ); + +} \ No newline at end of file diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 0e7a849..0059d77 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -431,6 +431,7 @@ "file-a-complaint": "File a complaint", "you-can": "You can", "complaint": "Complaint", + "complaint-name": "Complaint name", "recent-complaints": "Recent complaints", "recent-claims": "Recent claims", "no-complaints": "No complaints", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index b0f6775..e7b9051 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -431,6 +431,7 @@ "file-a-complaint": "Подать жалобу", "you-can": "Вы можете", "complaint": "Жалоба", + "complaint-name": "Название жалобы", "recent-complaints": "Недавние жалобы", "recent-claims": "Недавние претензии", "no-complaints": "Жалоб нет",