'use client'; import { useState, useMemo, useEffect, ReactNode, useCallback } from 'react'; import { useReactTable, getCoreRowModel, getSortedRowModel, getPaginationRowModel, getFilteredRowModel, ColumnDef, SortingState, ColumnFiltersState, PaginationState } from '@tanstack/react-table'; import { IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconInfo, IconEye, IconCheck, IconImageFile, IconLink } from '@/app/ui/icons/icons'; import { useTranslations, useLocale } from 'next-intl'; import DropDownList from '@/app/components/DropDownList'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { getUserFilesData, viewFileInfo } from '@/app/actions/fileEntity'; import ModalWindow from '@/app/components/ModalWindow'; import { toast } from 'sonner'; import { pluralize } from '@/app/lib/pluralize'; import { convertBytes } from '@/app/lib/convertBytes'; import { FileInfoModalWindow } from '@/app/ui/modal-windows/file-info-modal-window'; import { FileTypeIcon } from '@/app/components/FileTypeIcon'; import { fetchReferralUserStats } from '@/app/actions/referralsActions'; import { MonitoringDropDown } from '@/app/components/tanstak-table/MonitoringDropDown'; import { formatDate, formatDateTime } from '@/app/lib/formatDate'; import { useViewport } from '@/app/hooks/useViewport'; import { downloadFile } from '@/app/actions/fileEntity'; import { useDebouncedCallback } from 'use-debounce'; import Link from 'next/link'; import { SelectedFilesAction } from '@/app/components/tanstak-table/SelectedFilesActions'; import Image from 'next/image'; import { filePermisionChange } from '@/app/actions/trackingActions'; export type FileItem = { id: string; fileName: string; fileType: string; violations?: number | undefined; size?: number | undefined; uploadDate?: number; status?: string; monitoring: string; protectStatus: string; _original?: ApiFile; supportId: number; thumbnailFileUrl: string; permissions: { DOWNLOAD: boolean; } }; type ApiFile = { id: string; originalFileName: string; mimeType: string; violations: number; fileSize: number; updatedAt: string; status: string; monitoring: string; protectStatus: string; supportId: number; fileName: string; thumbnailFileUrl: string; permissions: { DOWNLOAD: boolean; } }; type ApiResponse = { files?: ApiFile[]; total_count: number; page: number; page_size: number; }; const cutFileExtension = (fileName: string) => { const lastDotIndex = fileName.lastIndexOf('.'); const extension = fileName.substring(lastDotIndex + 1); return extension; } export default function TanstakFilesTable({ fileType, showFileLink }: { fileType: string, showFileLink?: boolean }) { const [sorting, setSorting] = useState([]); const [dateFilter, setDateFilter] = useState('month'); const [typeFilter, setTypeFilter] = useState(fileType); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10, }); /* const [query, setQuery] = useState(''); */ const [searchInputValue, setSearchInputValue] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const [selectedFiles, setSelectedFiles] = useState>(new Set()); const debouncedSetSearchQuery = useDebouncedCallback( (value: string) => { setSearchQuery(value); setPagination(prev => ({ ...prev, pageIndex: 0 })); }, 500 ); const handleSearchInputChange = useCallback((e: React.ChangeEvent) => { const value = e.target.value; setSearchInputValue(value); debouncedSetSearchQuery(value); }, [debouncedSetSearchQuery]); const handleClearSearch = useCallback(() => { setSearchInputValue(''); setSearchQuery(''); debouncedSetSearchQuery.cancel(); setPagination(prev => ({ ...prev, pageIndex: 0 })); }, [debouncedSetSearchQuery]); const getSortParams = useMemo(() => { if (sorting.length === 0) { return { sortBy: '', sortOrder: undefined }; } const sort = sorting[0]; const sortByMap: Record = { 'id': 'supportId', 'fileName': 'fileName', 'size': 'fileSize', 'uploadDate': 'updatedAt', 'monitoring': 'monitoring' }; return { sortBy: sortByMap[sort.id] || sort.id, sortOrder: sort.desc ? 'desc' : 'asc' }; }, [sorting]); // Преобразование фильтра типа const getTypeParam = useMemo(() => { return typeFilter !== 'all' ? typeFilter : ''; }, [typeFilter]); // Преобразование фильтра даты const getDateParam = useMemo(() => { return dateFilter !== 'all' ? dateFilter : ''; }, [dateFilter]); const { data: apiResponse, isLoading, isFetching, isError, error, refetch } = useQuery({ queryKey: [ 'userFilesData', pagination.pageIndex, pagination.pageSize, getSortParams.sortBy, getSortParams.sortOrder, getTypeParam, getDateParam, searchQuery ], queryFn: () => getUserFilesData({ page: pagination.pageIndex + 1, pageSize: pagination.pageSize, sortBy: getSortParams.sortBy, sortOrder: getSortParams?.sortOrder, type: getTypeParam, date: getDateParam, query: searchQuery }), select: (data: any): { items: FileItem[]; total_count: number; page: number; pageSize: number; } => { if (!data?.files) { return { items: [], total_count: 0, page: 1, pageSize: pagination.pageSize }; } const items = data.files.map((item: ApiFile) => ({ id: item.id, fileName: item.originalFileName, fileType: item.mimeType.toLowerCase(), violations: item.violations, size: item.fileSize, uploadDate: new Date(item.updatedAt).getTime(), status: item.status, monitoring: item.monitoring, protectStatus: item.protectStatus, supportId: item.supportId, _original: item, thumbnailFileUrl: item.thumbnailFileUrl, permissions: { DOWNLOAD: item.permissions.DOWNLOAD, } })); return { items, total_count: data.total_count || data.files.length, page: data.page || pagination.pageIndex + 1, pageSize: data.page_size || pagination.pageSize }; }, refetchInterval: 30000, placeholderData: (previousData) => previousData // для оптимистичных обновлений }); const queryClient = useQueryClient(); // Данные для таблицы const tableData = apiResponse?.items || []; const totalItems = apiResponse?.total_count || 0; const totalPages = Math.ceil(totalItems / pagination.pageSize); const [openWindow, setOpenWindow] = useState(false); const [openWindowChildren, setOpenWindowChildren] = useState(null); const [isFileLoading, setIsFileLoading] = useState(false); const t = useTranslations("Global"); const locale = useLocale(); 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 '...' /* + extension */; } return nameWithoutExtension.substring(0, maxNameLength) + '...' /* + extension */; } function selectHandler(fileId: string) { setSelectedFiles((prev) => { const newSet = new Set(prev) if (newSet.has(fileId)) { newSet.delete(fileId) } else { newSet.add(fileId) } return newSet; }); } const selectAllHandler = useCallback(() => { if (!apiResponse?.items?.length) return; setSelectedFiles((prev) => { const allIds = new Set(apiResponse.items.map(item => item.id)); const isAllSelected = Array.from(allIds).every(id => prev.has(id)); if (isAllSelected) { const newSet = new Set(prev); allIds.forEach(id => newSet.delete(id)); return newSet; } else { return new Set([...prev, ...allIds]); } }); }, [apiResponse]); const isAllSelected = useMemo(() => { if (!apiResponse?.items?.length) return false; const allIds = new Set(apiResponse.items.map(item => item.id)); return Array.from(allIds).every(id => selectedFiles.has(id)); }, [apiResponse, selectedFiles]); // Определение колонок const columns = useMemo[]>( () => [ { accessorKey: 'selectd', header: ({ column }) => (
{ selectAllHandler(); }} >
), cell: ({ row }) => { return (
{ selectHandler(row.original.id); }} >
) }, }, { accessorKey: 'id', header: ({ column }) => (
ID
), cell: ({ row }) => { return (
{row.original.supportId ? row.original.supportId : '-'}
) }, }, { accessorKey: 'fileName', header: ({ column }) => (
{t('file')}
), cell: ({ row }) => (
{row.original.fileType === 'image' && row.original?.thumbnailFileUrl.length !== 0 ? ( {row.original?.fileName} { const target = e.target as HTMLImageElement; target.src = '/images/no-image.png'; }} /> ) : ( )}
{cutFileName(row.original.fileName)}
{cutFileExtension(row.original.fileName)} {row.original?.protectStatus ? t(row.original?.protectStatus) : t('error')}
), enableColumnFilter: false, }, { accessorKey: 'size', header: ({ column }) => (
{t('size')}
), cell: ({ row }) => (
{row.original.size !== undefined ? convertBytes(row.original.size) : '-'}
), }, { accessorKey: 'uploadDate', header: ({ column }) => (
{t('upload-date')}
), cell: ({ row }) => { return (
{row.original.uploadDate ? ( <> {formatDate(row.original.uploadDate)}
{formatDateTime(row.original.uploadDate)}
) : (
-
)}
) }, enableColumnFilter: false, }, { accessorKey: 'monitoring', header: ({ column }) => (
{t('monitoring-status')}
), cell: ({ row }) => { return (
{row.original.fileType === 'image' ? ( ) : (
{t('monitoring-is-not-supported')}
{t('for-this-file-type')}
)}
) }, }, { accessorKey: 'downloadPermision', header: ({ column }) => (
Можно скачать по ссылке
), cell: ({ row }) => { return (
) }, }, { id: 'actions', header: () => { return (
{t('actions')}
) }, cell: ({ row }) => (
{(showFileLink && row.original.fileType === 'document') ? (
) : ( <>
{row.original.protectStatus === 'PROTECTED' && ( )}
)}
), enableSorting: false, enableColumnFilter: false, }, ], [viewport.device, selectedFiles, selectAllHandler] ); const handleView = async (file: FileItem) => { const fileInfo = await viewFileInfo(file.id); setOpenWindowChildren(() => { return ( ) }) setOpenWindow(true); }; const handleDownload = async (file: FileItem) => { setIsFileLoading(true) try { const result = await downloadFile( file.id, file._original?.fileName || file.fileName || `file-${file.id}` ); const byteCharacters = atob(result.data); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i) } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], { type: result.contentType }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = result.fileName; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); toast.success(`${t('file-is-downloading')} - ${file.fileName}`); } catch (error) { toast.error(t('failed-to-download-file')); } finally { setIsFileLoading(false); } } const changeDownloadPermition = async (fileId: string, permission: boolean) => { const response = await filePermisionChange(fileId, !permission); if (response) { queryClient.invalidateQueries({ queryKey: ['userFilesData'] }); toast.success('Разрешение на скачивание изменено.'); } else { toast.warning('Не удалось изменить разрешение на скачивание.'); } } // Фильтрация по типу файла и дате const filteredData = useMemo(() => { let result = tableData; if (!result) { return []; } // Фильтр по типу файла if (typeFilter !== 'all') { result = result.filter(item => item.fileType === typeFilter); } // Фильтр по дате if (dateFilter !== 'all') { const now = Date.now(); const oneDay = 24 * 60 * 60 * 1000; const sevenDays = 7 * oneDay; const thirtyDays = 30 * oneDay; switch (dateFilter) { case 'today': const todayStart = new Date().setHours(0, 0, 0, 0); result = result.filter(item => { if (item.uploadDate) { return item.uploadDate >= todayStart } else { return 0 } }); break; case 'week': const weekAgo = now - sevenDays; result = result.filter(item => { if (item.uploadDate) { return item.uploadDate >= weekAgo; } }); break; case 'month': const monthAgo = now - thirtyDays; result = result.filter(item => { if (item.uploadDate) { return item.uploadDate >= monthAgo; } }); break; case 'older': const monthAgo2 = now - thirtyDays; result = result.filter(item => { if (item.uploadDate) { return item.uploadDate < monthAgo2; } }); break; } } return result; }, [/* query, */ typeFilter, dateFilter]); useEffect(() => { const currentPageRows = table.getRowModel().rows; const pageCount = table.getPageCount(); if (currentPageRows.length === 0 && pagination.pageIndex > 0 && pageCount > 0) { table.setPageIndex(pagination.pageIndex - 1); } }, [filteredData, pagination.pageIndex]); // Создание таблицы const table = useReactTable({ data: tableData, columns, state: { sorting, pagination, columnVisibility: { monitoring: showFileLink ? false : true, downloadPermision: showFileLink ? true : false, } }, 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(), }); const handleDateFilterChange = (newDateFilter: string) => { setDateFilter(newDateFilter); setPagination(prev => ({ ...prev, pageIndex: 0 })); }; const handleTypeFilterChange = (newTypeFilter: string) => { setTypeFilter(newTypeFilter); setPagination(prev => ({ ...prev, pageIndex: 0 })); }; const pluralizeFiles = (number: number) => { const translate = [t('file'), t('files-few'), t('files')]; return pluralize(number, translate[0], translate[1], translate[2], locale); }; useEffect(() => { if (tableData.length === 0 && pagination.pageIndex > 0 && totalPages > 0) { setPagination(prev => ({ ...prev, pageIndex: Math.max(0, totalPages - 1) })); } }, [tableData, pagination.pageIndex, totalPages]); return (

{(() => { switch (fileType) { case 'image': return t('protected-image') case 'video': return t('protected-video') case 'audio': return t('protected-audio') case 'document': return t('protected-document') default: return t('table-description') } })()}

{openWindowChildren} {/* Фильтры */}
{t('date-filter')}:
{ switch (dateFilter) { case 'all': return t('all-dates'); case 'today': return t('today'); case 'week': return t('for-a-week'); case 'month': return t('for-a-month'); default: return t('all-dates'); } })()} callBack={handleDateFilterChange} >
  • {t('all-dates')}
  • {t('today')}
  • {t('for-a-week')}
  • {t('for-a-month')}
  • {fileType === "all" && (
    {t('type-filter')}:
    { switch (typeFilter) { case 'image': return t('images'); case 'video': return t('videos'); case 'audio': return t('audios'); case 'document': return t('documents'); default: return t('all-types'); } })()} callBack={handleTypeFilterChange} >
  • {t('all-types')}
  • {t('images-few')}
  • {t('videos')}
  • {t('audios')}
  • {t('documents')}
  • )}
    {t('search')}:
    {t('items-per-page')}:
    { table.setPageSize(Number(newSize)); setPagination(prev => ({ ...prev, pageIndex: 0 })); }} > {['5', '10', '20', '50', '100'].map(pageSize => (
  • {t('show')} {pageSize}
  • ))}
    {/* {isLoading && (
    loading...
    )} */} {/* Таблица */}
    {table.getHeaderGroups().map(headerGroup => ( {headerGroup.headers.map(header => ( ))} ))} {isLoading && ( )} {table.getRowModel().rows.length > 0 ? ( table.getRowModel().rows.map(row => ( {row.getVisibleCells().map(cell => ( ))} )) ) : ( {!isLoading && ( )} )}
    {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-for-selected-filters')}
    {/* Пагинация */}
    {t('page')}{' '} {pagination.pageIndex + 1} {t('out-of')} {totalPages || 1}
    {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; })}
    ); }