Compare commits
2
Commits
e4508cb788
...
a3ff04e545
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3ff04e545 | ||
|
|
d7521e6ad1 |
@@ -1,4 +1,5 @@
|
|||||||
import ContentModerationTable from '@/app/ui/content/content-moderation-table';
|
import ContentModerationTable from '@/app/ui/content/content-moderation-table';
|
||||||
|
import ContentAppealsTable from '@/app/ui/content/content-appeals-table';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
@@ -9,6 +10,9 @@ export default function Page() {
|
|||||||
<div className="content-body">
|
<div className="content-body">
|
||||||
<ContentModerationTable permission={3} />
|
<ContentModerationTable permission={3} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="content-body">
|
||||||
|
<ContentAppealsTable permission={3} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -105,4 +105,45 @@ export async function downloadFile(fileId: string, fileName: string) {
|
|||||||
contentType: response.headers.get('content-type') || 'application/octet-stream',
|
contentType: response.headers.get('content-type') || 'application/octet-stream',
|
||||||
fileName: fileName
|
fileName: fileName
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAppealsContentList(page?: number, size?: number, sortBy?: string, sortDirection?: 'asc' | 'desc' | string) {
|
||||||
|
const token = await getSessionData('token');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/admin/info`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
msg_id: 40001,
|
||||||
|
message_body: {
|
||||||
|
action: 'all_appeals',
|
||||||
|
page: page,
|
||||||
|
page_size: size,
|
||||||
|
/* sort_by: sortDirection || 'asc',
|
||||||
|
sort_order: sortBy || '', */
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const parsed = await response.json();
|
||||||
|
|
||||||
|
if (parsed.message_code === 0) {
|
||||||
|
return parsed.message_body.message_body;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error(`${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { fetchAppealsContentList } from '@/app/actions/contentActions';
|
||||||
|
|
||||||
|
export interface AppealsFile {
|
||||||
|
additionalInfo: string;
|
||||||
|
adminComment: string;
|
||||||
|
appealId: string;
|
||||||
|
appealReason: string;
|
||||||
|
createdAt: string;
|
||||||
|
fileId: string;
|
||||||
|
resolvedAt: string;
|
||||||
|
status: 'BLOCKED' | 'APPROVED' | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentForAppeals {
|
||||||
|
appeals: AppealsFile[];
|
||||||
|
totalPages: number;
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useContentForAppeals = (
|
||||||
|
page?: number,
|
||||||
|
size?: number,
|
||||||
|
sortBy?: string,
|
||||||
|
sortDirection?: 'asc' | 'desc' | string
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['contentForAppeals', page, size, sortBy, sortDirection],
|
||||||
|
queryFn: () => {
|
||||||
|
return fetchAppealsContentList(page, size, sortBy, sortDirection);
|
||||||
|
},
|
||||||
|
select: (data: ContentForAppeals | null) => {
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
refetchInterval: 30000,
|
||||||
|
placeholderData: (previousData) => previousData // для оптимистичных обновлений
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -37,7 +37,7 @@ export const useContentForModeration = (
|
|||||||
if (!data) {
|
if (!data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return data; // теперь возвращаем весь объект с пагинацией
|
return data;
|
||||||
},
|
},
|
||||||
refetchInterval: 30000,
|
refetchInterval: 30000,
|
||||||
placeholderData: (previousData) => previousData // для оптимистичных обновлений
|
placeholderData: (previousData) => previousData // для оптимистичных обновлений
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
|
|
||||||
&__info-section,
|
&__info-section,
|
||||||
&__status-section,
|
&__status-section,
|
||||||
&__status-change-section {
|
&__status-change-section,
|
||||||
|
&__comment-section {
|
||||||
border-bottom: 1px solid #e5e7eb;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
padding-bottom: 0.75rem;
|
padding-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -92,6 +93,38 @@
|
|||||||
background-color: #93c5fd;
|
background-color: #93c5fd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__comment-label {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__comment-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: inherit;
|
||||||
|
color: #111827;
|
||||||
|
background-color: #ffffff;
|
||||||
|
resize: vertical;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import DropDownList from '@/app/components/dropDownList';
|
||||||
|
/* import { changeAppealStatus } from '@/app/actions/contentActions'; */
|
||||||
|
import { AppealsFile } from '@/app/hooks/react-query/useContentForAppeals';
|
||||||
|
|
||||||
|
interface AppealModerationModalProps {
|
||||||
|
file: AppealsFile | null;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLabel = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'BLOCKED':
|
||||||
|
return 'Заблокировано';
|
||||||
|
case 'MODERATION':
|
||||||
|
return 'На модерации';
|
||||||
|
case 'APPROVED':
|
||||||
|
return 'Одобрено';
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusBadge = ({ status }: { status: string }) => {
|
||||||
|
const getBadgeClass = () => {
|
||||||
|
switch (status) {
|
||||||
|
case 'BLOCKED':
|
||||||
|
return 'status-badge--blocked';
|
||||||
|
case 'MODERATION':
|
||||||
|
return 'status-badge--moderation';
|
||||||
|
case 'APPROVED':
|
||||||
|
return 'status-badge--approved';
|
||||||
|
default:
|
||||||
|
return 'status-badge--default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<span className={getBadgeClass()}>
|
||||||
|
{getLabel(status)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AppealModerationModal({ file, onClose }: AppealModerationModalProps) {
|
||||||
|
const [selectedStatus, setSelectedStatus] = useState<string>(file?.status || 'MODERATION');
|
||||||
|
const [adminComment, setAdminComment] = useState<string>(file?.adminComment || '');
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
/* const changeStatusMutation = useMutation({
|
||||||
|
mutationFn: async ({ appealId, status, comment }: { appealId: string; status: string; comment: string }) => {
|
||||||
|
return await changeAppealStatus(appealId, status, comment);
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data) {
|
||||||
|
toast.success('Статус апелляции успешно изменен');
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['contentForAppeals'] });
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
toast.error('Не удалось изменить статус апелляции');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error('Произошла ошибка при изменении статуса');
|
||||||
|
},
|
||||||
|
}); */
|
||||||
|
|
||||||
|
const handleStatusChange = () => {
|
||||||
|
if (!file?.appealId) {
|
||||||
|
toast.error('ID апелляции не найден');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* changeStatusMutation.mutate({
|
||||||
|
appealId: file.appealId,
|
||||||
|
status: selectedStatus,
|
||||||
|
comment: adminComment,
|
||||||
|
}); */
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!file) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="file-moderation-modal">
|
||||||
|
<h2 className="file-moderation-modal__title">Управление апелляцией</h2>
|
||||||
|
|
||||||
|
<div className="file-moderation-modal__content">
|
||||||
|
{/* Информация о файле */}
|
||||||
|
<div className="file-moderation-modal__info-section">
|
||||||
|
<label className="file-moderation-modal__label">Название файла</label>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<p className="file-moderation-modal__value">{file?.fileName ? file?.fileName : '---'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="file-moderation-modal__info-section">
|
||||||
|
<label className="file-moderation-modal__label">User ID</label>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<p className="file-moderation-modal__value">{file?.userId ? file?.userId : '---'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="file-moderation-modal__info-section">
|
||||||
|
<label className="file-moderation-modal__label">Причина апелляции</label>
|
||||||
|
<p className="file-moderation-modal__value">{file.appealReason}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="file-moderation-modal__info-section">
|
||||||
|
<label className="file-moderation-modal__label">Дополнительная информация</label>
|
||||||
|
<p className="file-moderation-modal__value">{file.additionalInfo || '—'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="file-moderation-modal__info-section">
|
||||||
|
<label className="file-moderation-modal__label">Дата создания</label>
|
||||||
|
<p className="file-moderation-modal__value">
|
||||||
|
{new Date(file.createdAt).toLocaleString('ru-RU')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{file.resolvedAt && (
|
||||||
|
<div className="file-moderation-modal__info-section">
|
||||||
|
<label className="file-moderation-modal__label">Дата решения</label>
|
||||||
|
<p className="file-moderation-modal__value">
|
||||||
|
{new Date(file.resolvedAt).toLocaleString('ru-RU')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="file-moderation-modal__status-section">
|
||||||
|
<label className="file-moderation-modal__status-label">Текущий статус</label>
|
||||||
|
<StatusBadge status={file.status} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Комментарий администратора */}
|
||||||
|
<div className="file-moderation-modal__comment-section">
|
||||||
|
<label className="file-moderation-modal__comment-label">Комментарий администратора</label>
|
||||||
|
<textarea
|
||||||
|
className="file-moderation-modal__comment-textarea"
|
||||||
|
value={adminComment}
|
||||||
|
onChange={(e) => setAdminComment(e.target.value)}
|
||||||
|
placeholder="Введите комментарий..."
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Смена статуса */}
|
||||||
|
<div className="file-moderation-modal__status-change-section">
|
||||||
|
<label className="file-moderation-modal__status-change-label">Изменить статус</label>
|
||||||
|
<DropDownList
|
||||||
|
value={getLabel(selectedStatus)}
|
||||||
|
callBack={(value: string) => setSelectedStatus(value)}
|
||||||
|
>
|
||||||
|
<li value="MODERATION">На модерации</li>
|
||||||
|
<li value="BLOCKED">Заблокировано</li>
|
||||||
|
<li value="APPROVED">Одобрено</li>
|
||||||
|
</DropDownList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Кнопки действий */}
|
||||||
|
<div className="file-moderation-modal__actions">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="btn btn-secondary"
|
||||||
|
/* disabled={changeStatusMutation.isPending} */
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleStatusChange}
|
||||||
|
className="btn btn-primary"
|
||||||
|
/* disabled={changeStatusMutation.isPending || selectedStatus === file.status} */
|
||||||
|
>
|
||||||
|
{/* {changeStatusMutation.isPending ? 'Сохранение...' : 'Сохранить'} */}
|
||||||
|
Сохранить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,492 @@
|
|||||||
|
'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, IconFileDownload } from '@/app/ui/icons/icons';
|
||||||
|
import { useTranslations } 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 { FileTypeIcon } from '@/app/components/FileTypeIcon';
|
||||||
|
import { downloadFile } from '@/app/actions/contentActions';
|
||||||
|
import { useContentForAppeals, AppealsFile } from '@/app/hooks/react-query/useContentForAppeals';
|
||||||
|
import AppealModerationModal from '@/app/ui/content/AppealModerationModal';
|
||||||
|
|
||||||
|
const cutFileExtension = (fileName: string) => {
|
||||||
|
const lastDotIndex = fileName.lastIndexOf('.');
|
||||||
|
const extension = fileName.substring(lastDotIndex + 1);
|
||||||
|
return extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cutFileName = (fileName: string) => {
|
||||||
|
const MAX_FILE_NAME_LENGTH = 30;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusBadge = ({ status }: { status: string }) => {
|
||||||
|
const statusConfig: Record<string, { className: string; label: string }> = {
|
||||||
|
'BLOCKED': { className: 'bg-red-100 text-red-800', label: 'Заблокировано' },
|
||||||
|
'MODERATION': { className: 'bg-yellow-100 text-yellow-800', label: 'На модерации' },
|
||||||
|
'APPROVED': { className: 'bg-green-100 text-green-800', label: 'Одобрено' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = statusConfig[status] || { className: 'bg-gray-100 text-gray-800', label: status };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${config.className}`}>
|
||||||
|
{config.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ContentAppealsTable({ permission }: { permission: number }) {
|
||||||
|
// Состояния
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
const [openWindow, setOpenWindow] = useState<boolean>(false);
|
||||||
|
const [selectedFile, setSelectedFile] = useState<AppealsFile | null>(null);
|
||||||
|
const [isFileLoading, setIsFileLoading] = useState(false);
|
||||||
|
|
||||||
|
const t = useTranslations("Global");
|
||||||
|
|
||||||
|
const getSortParams = useMemo(() => {
|
||||||
|
if (sorting.length === 0) {
|
||||||
|
return { sortBy: '', sortDirection: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sort = sorting[0];
|
||||||
|
const sortByMap: Record<string, string> = {
|
||||||
|
'fileName': 'originalFileName',
|
||||||
|
'userId': 'userId',
|
||||||
|
'status': 'status',
|
||||||
|
'createdAt': 'createdAt',
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sortBy: sortByMap[sort.id] || sort.id,
|
||||||
|
sortDirection: sort.desc ? 'desc' : 'asc'
|
||||||
|
};
|
||||||
|
}, [sorting]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: appealsData,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
isFetching,
|
||||||
|
error
|
||||||
|
} = useContentForAppeals(
|
||||||
|
pagination.pageIndex,
|
||||||
|
pagination.pageSize,
|
||||||
|
getSortParams.sortBy,
|
||||||
|
getSortParams.sortDirection
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Appeals data:', appealsData);
|
||||||
|
}, [appealsData]);
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const handleViewFile = useCallback((file: AppealsFile) => {
|
||||||
|
console.log('View file:', file);
|
||||||
|
setSelectedFile(file);
|
||||||
|
setOpenWindow(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCloseModal = useCallback(() => {
|
||||||
|
setOpenWindow(false);
|
||||||
|
setSelectedFile(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDownload = async (file: AppealsFile) => {
|
||||||
|
setIsFileLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await downloadFile(
|
||||||
|
file.fileId,
|
||||||
|
`file-${file.fileId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
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')}`);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(t('failed-to-download-file'));
|
||||||
|
} finally {
|
||||||
|
setIsFileLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = useMemo<ColumnDef<AppealsFile>[]>(() => {
|
||||||
|
const allColumns: ColumnDef<AppealsFile>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'fileId',
|
||||||
|
header: ({ column }) => (
|
||||||
|
<div className="column start">
|
||||||
|
<span>
|
||||||
|
fileId
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||||
|
className="sort-button hidden"
|
||||||
|
>
|
||||||
|
{column.getIsSorted() === 'asc' ?
|
||||||
|
<span><IconArrowUp /></span>
|
||||||
|
: column.getIsSorted() === 'desc' ?
|
||||||
|
<span><IconArrowDown /></span>
|
||||||
|
: <span><IconFilter /></span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center space-x-2 table-item table-item-file-name">
|
||||||
|
<FileTypeIcon type={row.original.fileId} />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium w-full truncate" title={row.original.fileId}>
|
||||||
|
<span className="font-semibold">
|
||||||
|
{cutFileName(row.original.fileId)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
enableColumnFilter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'appealId',
|
||||||
|
header: ({ column }) => (
|
||||||
|
<div className="column">
|
||||||
|
<span>
|
||||||
|
Appeal ID
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||||
|
className="sort-button hidden"
|
||||||
|
>
|
||||||
|
{column.getIsSorted() === 'asc' ?
|
||||||
|
<span><IconArrowUp /></span>
|
||||||
|
: column.getIsSorted() === 'desc' ?
|
||||||
|
<span><IconArrowDown /></span>
|
||||||
|
: <span><IconFilter /></span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center">
|
||||||
|
{row.original.appealId}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: ({ column }) => (
|
||||||
|
<div className="column">
|
||||||
|
<span>
|
||||||
|
Статус
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||||
|
className="sort-button hidden"
|
||||||
|
>
|
||||||
|
{column.getIsSorted() === 'asc' ?
|
||||||
|
<span><IconArrowUp /></span>
|
||||||
|
: column.getIsSorted() === 'desc' ?
|
||||||
|
<span><IconArrowDown /></span>
|
||||||
|
: <span><IconFilter /></span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center">
|
||||||
|
<StatusBadge status={row.original.status} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (permission === 3) {
|
||||||
|
allColumns.push({
|
||||||
|
id: 'actions',
|
||||||
|
header: () => <div className="column">{t('actions')}</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="actions">
|
||||||
|
<div className="actions-group">
|
||||||
|
<button
|
||||||
|
onClick={() => handleViewFile(row.original)}
|
||||||
|
className="bg-blue-500 hover:bg-blue-600 text-white p-2 rounded"
|
||||||
|
title="Просмотреть файл"
|
||||||
|
>
|
||||||
|
<IconEye />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleDownload(row.original)}
|
||||||
|
disabled={isFileLoading}
|
||||||
|
className="table-action-download"
|
||||||
|
title={t('download')}
|
||||||
|
>
|
||||||
|
<IconFileDownload />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableColumnFilter: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return allColumns;
|
||||||
|
}, [permission, handleViewFile, handleDownload, isFileLoading, t]);
|
||||||
|
|
||||||
|
// Данные для таблицы
|
||||||
|
const tableData = useMemo(() => {
|
||||||
|
if (!appealsData?.appeals) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return appealsData.appeals;
|
||||||
|
}, [appealsData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentPageRows = table.getRowModel().rows;
|
||||||
|
const pageCount = table.getPageCount();
|
||||||
|
|
||||||
|
if (currentPageRows.length === 0 && pagination.pageIndex > 0 && pageCount > 0) {
|
||||||
|
table.setPageIndex(pagination.pageIndex - 1);
|
||||||
|
}
|
||||||
|
}, [tableData, pagination.pageIndex]);
|
||||||
|
|
||||||
|
// Создание таблицы
|
||||||
|
const table = useReactTable({
|
||||||
|
data: tableData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
columnFilters,
|
||||||
|
pagination
|
||||||
|
},
|
||||||
|
manualPagination: true,
|
||||||
|
manualSorting: true,
|
||||||
|
manualFiltering: true,
|
||||||
|
pageCount: appealsData?.totalPages || 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()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div className="text-center text-red-600 p-4">
|
||||||
|
Ошибка загрузки данных: {error?.message || 'Неизвестная ошибка'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tanstak-table-wrapper">
|
||||||
|
<ModalWindow state={openWindow} callBack={handleCloseModal}>
|
||||||
|
<AppealModerationModal file={selectedFile} onClose={handleCloseModal} />
|
||||||
|
</ModalWindow>
|
||||||
|
|
||||||
|
{/* Фильтры */}
|
||||||
|
<div className="tanstak-table-filtres">
|
||||||
|
<h3>
|
||||||
|
Таблица апелляций
|
||||||
|
</h3>
|
||||||
|
<div className="table-filtres-item">
|
||||||
|
<div className="table-filtres-label">{t('items-per-page')}:</div>
|
||||||
|
<DropDownList
|
||||||
|
value={table.getState().pagination.pageSize}
|
||||||
|
//@ts-ignore
|
||||||
|
callBack={table.setPageSize}
|
||||||
|
>
|
||||||
|
{[5, 10, 20, 50, 100].map(pageSize => (
|
||||||
|
<li key={pageSize} value={pageSize}>
|
||||||
|
{t('show')} {pageSize}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</DropDownList>
|
||||||
|
</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 py-8">
|
||||||
|
Загрузка...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : table.getRowModel().rows.length > 0 ? (
|
||||||
|
table.getRowModel().rows.map(row => (
|
||||||
|
<tr key={row.original.appealId}>
|
||||||
|
{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>
|
||||||
|
<td colSpan={columns.length} className="text-center py-8">
|
||||||
|
{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>
|
||||||
|
{(appealsData?.currentPage ?? 0)} {t('out-of')} {appealsData?.totalPages ?? 1}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
<span className="pagination-info-files">
|
||||||
|
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {appealsData?.totalCount ?? 0}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pagination-controls">
|
||||||
|
<button
|
||||||
|
className="arrow"
|
||||||
|
onClick={() => table.firstPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<IconDoubleArrowLeft />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="arrow"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<IconArrowLeft />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="pagination-controls-pages">
|
||||||
|
{(() => {
|
||||||
|
const totalPages = appealsData?.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 (
|
||||||
|
<button
|
||||||
|
key={pageIndex}
|
||||||
|
className={currentPageIndex === pageIndex ? 'current' : 'other'}
|
||||||
|
onClick={() => table.setPageIndex(pageIndex)}
|
||||||
|
>
|
||||||
|
{pageIndex + 1}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="arrow"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<IconArrowRight />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="arrow"
|
||||||
|
onClick={() => table.lastPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<IconDoubleArrowRight />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -117,10 +117,6 @@ export default function ContentModerationTable({ permission }: { permission: num
|
|||||||
getSortParams.sortDirection
|
getSortParams.sortDirection
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('Moderation data:', moderationData);
|
|
||||||
}, [moderationData]);
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const handleViewFile = useCallback((file: ModerationFile) => {
|
const handleViewFile = useCallback((file: ModerationFile) => {
|
||||||
@@ -400,6 +396,9 @@ export default function ContentModerationTable({ permission }: { permission: num
|
|||||||
|
|
||||||
{/* Фильтры */}
|
{/* Фильтры */}
|
||||||
<div className="tanstak-table-filtres">
|
<div className="tanstak-table-filtres">
|
||||||
|
<h3>
|
||||||
|
Таблица модерации контента
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div className="table-filtres-item">
|
<div className="table-filtres-item">
|
||||||
<div className="table-filtres-label">{t('items-per-page')}:</div>
|
<div className="table-filtres-label">{t('items-per-page')}:</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user