diff --git a/src/app/[locale]/pages/content/page.tsx b/src/app/[locale]/pages/content/page.tsx index eb83b09..4a2a853 100644 --- a/src/app/[locale]/pages/content/page.tsx +++ b/src/app/[locale]/pages/content/page.tsx @@ -1,28 +1,12 @@ +import ContentModerationTable from '@/app/ui/content/content-moderation-table'; export default function Page() { return (

Управление контентом

-
-
- 🔍 - -
- -
-
-
-
🗂️
-

Контент не найден

-

Попробуйте изменить параметры поиска

-
+
+
) diff --git a/src/app/actions/contentActions.ts b/src/app/actions/contentActions.ts new file mode 100644 index 0000000..6afbbde --- /dev/null +++ b/src/app/actions/contentActions.ts @@ -0,0 +1,48 @@ +'use server' + +import { getSessionData } from '@/app/actions/session'; +import { API_BASE_URL } from '@/app/actions/definitions'; + +export async function fetchModerationContentList(page?: number, size?: number, sortBy?: string, sortDirection?: 'asc' | 'desc' | string, nameQuery?: string) { + const token = await getSessionData('token'); + console.log('fetchModerationContentList'); + + try { + const response = await fetch(`${API_BASE_URL}/api/admin`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 20005, + message_body: { + action: 'files_for_moderation', +/* page: page, + size: size, + sort_by: sortBy || '', + sort_direction: sortDirection || 'asc', + full_name: nameQuery */ + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const parsed = await response.json(); + console.log(parsed); + + if (parsed.message_code === 0) { + return parsed.message_body; + } else { + return null; + } + + } else { + throw new Error(`${response.status}`); + } + } catch (error) { + return null; + } +} \ No newline at end of file diff --git a/src/app/hooks/react-query/useContentForModeration.ts b/src/app/hooks/react-query/useContentForModeration.ts new file mode 100644 index 0000000..a59c84e --- /dev/null +++ b/src/app/hooks/react-query/useContentForModeration.ts @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; +import {fetchModerationContentList} from '@/app/actions/contentActions'; + +export interface ContentForModeration { + +} + +export const useContentForModeration = (page?: number, size?: number, sortBy?: string, sortDirection?: 'asc' | 'desc' | string, nameQuery?: string) => { + return useQuery({ + queryKey: ['contentForModeration'], + queryFn: () => { + return fetchModerationContentList(page, size, sortBy, sortDirection, nameQuery) + }, + select: (data: any[] | null) => { + if (!data) { + return null + } + return data; + }, + retry: false + }); +}; \ No newline at end of file diff --git a/src/app/styles/global-styles.scss b/src/app/styles/global-styles.scss index 12078d5..4c4913d 100644 --- a/src/app/styles/global-styles.scss +++ b/src/app/styles/global-styles.scss @@ -355,12 +355,12 @@ flex-wrap: wrap; gap: 12px; - &-actions { +/* &-actions { display: flex; gap: 12px; flex-wrap: wrap; - } + } */ h3 { font-size: 18px; diff --git a/src/app/ui/content/content-moderation-table.tsx b/src/app/ui/content/content-moderation-table.tsx new file mode 100644 index 0000000..0c2bc0e --- /dev/null +++ b/src/app/ui/content/content-moderation-table.tsx @@ -0,0 +1,632 @@ +'use client'; + +import { useState, useMemo, useEffect, ReactNode, useCallback } from 'react'; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getPaginationRowModel, + getFilteredRowModel, + ColumnDef, + SortingState, + ColumnFiltersState, +} from '@tanstack/react-table'; +import { IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter } from '@/app/ui/icons/icons'; +import { useTranslations, useLocale } from 'next-intl'; +import DropDownList from '@/app/components/dropDownList'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import ModalWindow from '@/app/components/modalWindow'; +import { toast } from 'sonner'; +import { useDebouncedCallback } from 'use-debounce'; +import { fetchUsesInfo } from '@/app/actions/usersActions'; +import { UserInfoModalWindow } from '@/app/ui/users/user-info-modal-window'; +import { useContentForModeration, ContentForModeration } from '@/app/hooks/react-query/useContentForModeration'; + +// Форматирование даты из timestamp +const formatDate = (timestamp: number) => { + const date = new Date(timestamp); + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear(); + + return `${day}.${month}.${year}`; +}; + +const formatDateTime = (timestamp: number) => { + const date = new Date(timestamp); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + return `${hours}:${minutes}`; +}; + +const cutFileName = (userId: string) => { + const MAX_FILE_NAME_LENGTH = 26; + const lastDotIndex = userId.lastIndexOf('.'); + + if (lastDotIndex <= 0) { + return userId.length > MAX_FILE_NAME_LENGTH ? userId.substring(0, MAX_FILE_NAME_LENGTH - 3) + '...' : userId; + } + + const nameWithoutExtension = userId.substring(0, lastDotIndex); + const extension = userId.substring(lastDotIndex); + + if (userId.length <= MAX_FILE_NAME_LENGTH) { + return userId; + } + + const maxNameLength = MAX_FILE_NAME_LENGTH - extension.length - 3; + + if (maxNameLength <= 0) { + return '...' + extension; + } + + return nameWithoutExtension.substring(0, maxNameLength) + '...' + extension; +} + +export default function ContentModerationTable({ permission }: { permission: number }) { + // Состояния + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [searchInputValue, setSearchInputValue] = useState(''); + const [openWindow, setOpenWindow] = useState(false); + const [openWindowChildren, setOpenWindowChildren] = useState(null); + 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 t = useTranslations("Global"); + const locale = useLocale(); + + const getSortParams = useMemo(() => { + if (sorting.length === 0) { + return { sortBy: '', sortOrder: undefined }; + } + + const sort = sorting[0]; + const sortByMap: Record = { + 'id': 'supportId', + 'userName': 'fullName', + 'email': 'email', + 'verificationStatus': 'verificationStatus', + 'createdAt': 'createdAt', + 'tariff': 'tariff', + 'tokens': 'tokens', + 'userCompany': 'userCompany', + }; + console.log(sortByMap[sort.id]); + + return { + sortBy: sortByMap[sort.id] || sort.id, + sortDirection: sort.desc ? 'desc' : 'asc' + }; + }, [sorting]); + + const { data: tableData, + isLoading, + isError, + isFetching, + error + } = useContentForModeration(pagination.pageIndex, pagination.pageSize, getSortParams?.sortBy, getSortParams?.sortDirection, searchQuery); + + useEffect(() => { + console.log(tableData); + }, [tableData]) + + const queryClient = useQueryClient(); + + // Определение колонок + const columns = useMemo[]>(() => { + const allColumns: ColumnDef[] = [ + { + accessorKey: 'id', + header: ({ column }) => ( +
+ + ID + + +
+ ), + cell: ({ row }) => ( +
+
+
+ {'row.original.id'} +
+
+
+ ), + enableColumnFilter: false, + }, + { + accessorKey: 'userName', + header: ({ column }) => ( +
+ + Пользователь + + +
+ ), + cell: ({ row }) => { + return ( +
+ {'row.original.userName'} +
+ ) + }, + }, + { + accessorKey: 'userEmail', + header: ({ column }) => ( +
+ + email + + +
+ ), + cell: ({ row }) => { + return ( +
+ {'row.original.userEmail'} +
+ ) + }, + }, + { + accessorKey: 'verificationStatus', + header: ({ column }) => ( +
+ + статус KYC + + +
+ ), + cell: ({ row }) => ( +
+ {'row.original.verificationStatus'} +
+ ), + }, + { + accessorKey: 'tariff', + header: ({ column }) => ( +
+ + Подписка + + +
+ ), + cell: ({ row }) => { + return ( +
+ {'row.original.tariff'} +
+ ) + }, + enableColumnFilter: false, + }, + { + accessorKey: 'tokens', + header: ({ column }) => ( +
+ + Токены + + +
+ ), + cell: ({ row }) => { + return ( +
+ {'row.original.tokens'} +
+ ) + }, + }, { + accessorKey: 'userSubscription', + header: ({ column }) => ( +
+ + Дата регистрации + + +
+ ), + cell: ({ row }) => { + return ( +
+ <> + {formatDate(0)} +
+ {formatDateTime(0)} + +
+ ) + }, + } + ] + + if (permission === 3) { + allColumns.push({ + id: 'actions', + header: () =>
{t('actions')}
, + cell: ({ row }) => ( +
+
+ +
+
+ ), + enableSorting: false, + enableColumnFilter: false, + }); + } + + return allColumns; + }, [permission]); + + // Фильтрация по типу файла и дате + const filteredData = useMemo(() => { + /* let result = tableData?.users; */ + let result = []; + return [] + if (!result) { + return []; + } + + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase().trim(); + result = result.filter((item: any) => { + // Ищем по всем строковым полям + return Object.keys(item).some(key => { + const value = item[key]; + if (typeof value === 'string') { + return value.toLowerCase().includes(query); + } + // Если нужно искать по числам или другим типам + if (typeof value === 'number' || typeof value === 'boolean') { + return value.toString().toLowerCase().includes(query); + } + return false; + }); + }); + } + + return result; + }, [tableData, searchQuery]); + + 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: filteredData, + columns, + state: { + sorting, + columnFilters, + pagination + }, + manualPagination: true, + manualSorting: true, + manualFiltering: true, + /* pageCount: tableData?.pagination?.totalPages, */ + pageCount: 0, + 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 ( +
+ + {openWindowChildren} + + {/* Фильтры */} +
+
+
+
{t('search')}:
+ +
+
+ +
+
{t('items-per-page')}:
+ + {[5, 10, 20, 50, 100].map(pageSize => ( +
  • + {t('show')} {pageSize} +
  • + ))} +
    +
    +
    + + {/* Таблица */} +
    + + + {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-for-selected-filters')} +
    +
    + + {/* Пагинация */} +{/*
    +
    + + {t('page')}{' '} + + {(tableData?.pagination?.currentPage ?? 0) + 1} {t('out-of')} {tableData?.pagination?.totalPages ?? 1} + + + + | {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {tableData?.pagination?.totalElements ?? 0} + +
    + +
    + + + +
    + {(() => { + const totalPages = tableData?.pagination?.totalPages; + if (!totalPages || totalPages === 0) return null; + + const currentPageIndex = table.getState().pagination.pageIndex; + const maxVisiblePages = 5; + const visiblePagesCount = Math.min(maxVisiblePages, totalPages); + + let startPage = currentPageIndex - Math.floor(maxVisiblePages / 2); + startPage = Math.max(0, Math.min(startPage, totalPages - visiblePagesCount)); + + return Array.from({ length: visiblePagesCount }, (_, i) => { + const pageIndex = startPage + i; + + return ( + + ); + }); + })()} +
    + + + +
    +
    */} +
    + ); +} \ No newline at end of file