add appeal modal window, add file status

This commit is contained in:
smanylov
2026-05-28 20:30:16 +07:00
parent 508e0d182c
commit f04ce348ae
8 changed files with 362 additions and 42 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "no-copy-frontend", "name": "no-copy-frontend",
"version": "0.118.0", "version": "0.119.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 2999", "dev": "next dev -p 2999",
+40
View File
@@ -162,4 +162,44 @@ 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 FileAnAppeal(fileId: string, appealReason: string, additionalInfo: string) {
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: 20005,
message_body: {
action: 'submit_appeal',
file_id: fileId,
token: token,
appeal_reason: appealReason,
additional_info: additionalInfo
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return error
}
} }
@@ -33,6 +33,7 @@ import Link from 'next/link';
import { SelectedFilesAction } from '@/app/components/tanstak-table/SelectedFilesActions'; import { SelectedFilesAction } from '@/app/components/tanstak-table/SelectedFilesActions';
import Image from 'next/image'; import Image from 'next/image';
import { filePermisionChange } from '@/app/actions/trackingActions'; import { filePermisionChange } from '@/app/actions/trackingActions';
import {FileAppealModalWindow} from '@/app/ui/modal-windows/file-appeal-modal-window';
export type FileItem = { export type FileItem = {
id: string; id: string;
@@ -41,7 +42,7 @@ export type FileItem = {
violations?: number | undefined; violations?: number | undefined;
size?: number | undefined; size?: number | undefined;
uploadDate?: number; uploadDate?: number;
status?: string; status: string;
monitoring: string; monitoring: string;
protectStatus: string; protectStatus: string;
_original?: ApiFile; _original?: ApiFile;
@@ -295,6 +296,32 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
}); });
}, [apiResponse]); }, [apiResponse]);
const StatusBadge = ({ status, protectStatus }: { status: string, protectStatus: string }) => {
switch (status) {
case 'ACTIVE':
return <span className="table-item-status-badge protected">
{protectStatus ? t(protectStatus) : t('error')}
</span>;
case 'MODERATION':
return <span className="table-item-status-badge moderation">
{t('pending-moderation')}
</span>;
case 'BLOCKED':
return <span className="table-item-status-badge blocked">
{t('blocked')}
</span>;
case 'REMOVED':
return <span className="table-item-status-badge blocked">
{t('file-has-been-deleted')}
</span>;
default:
return <span className="table-item-status-badge protected">
{protectStatus ? t(protectStatus) : t('error')}
</span>;
}
}
const isAllSelected = useMemo(() => { const isAllSelected = useMemo(() => {
if (!apiResponse?.items?.length) return false; if (!apiResponse?.items?.length) return false;
const allIds = new Set(apiResponse.items.map(item => item.id)); const allIds = new Set(apiResponse.items.map(item => item.id));
@@ -431,9 +458,7 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
{cutFileName(row.original.fileName)} {cutFileName(row.original.fileName)}
</span> </span>
<br /> <br />
<span className="table-item-extension">{cutFileExtension(row.original.fileName)}</span> <span className="table-item-protected"> <span className="table-item-extension">{cutFileExtension(row.original.fileName)}</span> <StatusBadge status={row.original.status} protectStatus={row.original.protectStatus} />
{row.original?.protectStatus ? t(row.original?.protectStatus) : t('error')}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -614,36 +639,53 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
</div> </div>
) : ( ) : (
<> <>
<div className="actions-group"> {/* "BLOCKED" "REMOVED" */}
<Link {(row.original.status !== 'BLOCKED' && row.original.status !== 'REMOVED') ? (
href={`/pages/file/${row.original.id}`} <>
className="bg-violet-500 hover:bg-violet-600" <div className="actions-group">
title={t('view')} <Link
> href={`/pages/file/${row.original.id}`}
<IconEye /> className="bg-violet-500 hover:bg-violet-600"
</Link> title={t('view')}
</div> >
<div className="actions-group"> <IconEye />
{row.original.protectStatus === 'PROTECTED' && ( </Link>
</div>
<div className="actions-group">
{row.original.protectStatus === 'PROTECTED' && (
<button
onClick={() => handleDownload(row.original)}
disabled={isFileLoading}
className="table-action-download"
title={t('download')}
>
<IconFileDownload />
</button>
)}
<button
onClick={() => {
handleView(row.original)
}}
className="table-action-view"
title={t('make-check')}
>
<IconInfo />
</button>
</div>
</>
) : (
<div className="actions-group">
<button <button
onClick={() => handleDownload(row.original)} onClick={() => {
disabled={isFileLoading} handleAppeal(row.original)
className="table-action-download" }}
title={t('download')} className="table-action-appeal"
title={t('file-an-appeal')}
> >
<IconFileDownload /> <IconInfo />
</button> </button>
)} </div>
<button )}
onClick={() => {
handleView(row.original)
}}
className="table-action-view"
title={t('make-check')}
>
<IconInfo />
</button>
</div>
</> </>
)} )}
</div> </div>
@@ -666,6 +708,16 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
setOpenWindow(true); setOpenWindow(true);
}; };
const handleAppeal = async (file: FileItem) => {
setOpenWindowChildren(() => {
return (
<FileAppealModalWindow fileId={file.id} setWindowClose={setOpenWindow} setWindowChildren={setOpenWindowChildren} />
)
})
setOpenWindow(true);
};
const handleDownload = async (file: FileItem) => { const handleDownload = async (file: FileItem) => {
setIsFileLoading(true) setIsFileLoading(true)
+13 -2
View File
@@ -163,7 +163,7 @@
.btn, .btn,
.btn-s, .btn-s,
.btn-m { .btn-l {
border: none; border: none;
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
@@ -197,7 +197,8 @@
} }
.btn-primary { .btn-primary {
background: linear-gradient(135deg, v.$p-color, v.$s-color); /* background: linear-gradient(135deg, v.$p-color, v.$s-color); */
background: #6366f1;
color: v.$white; color: v.$white;
border: 2px solid transparent; border: 2px solid transparent;
@@ -222,6 +223,16 @@
} }
} }
.btn-cancel {
background: v.$bg-hover;
color: v.$text-p;
border: 2px solid v.$border-color-1;
&:hover {
transform: translateY(-2px);
}
}
.btn-outline { .btn-outline {
background: transparent; background: transparent;
border: 2px solid v.$p-color; border: 2px solid v.$p-color;
+95 -6
View File
@@ -930,13 +930,24 @@
} }
} }
.table-item-protected { .table-item-status-badge {
color: #10b981;
font-size: 12px; font-size: 12px;
position: relative; position: relative;
margin-left: 5px; margin-left: 5px;
padding-left: 5px; padding-left: 5px;
&.protected {
color: #10b981;
}
&.moderation {
color: v.$color-warning;
}
&.blocked {
color: v.$red;
}
&::before { &::before {
content: ''; content: '';
color: v.$text-s; color: v.$text-s;
@@ -1016,11 +1027,11 @@
} }
} }
.table-action-delete { .table-action-appeal {
background-color: #e80a14; background-color: #9f0712;
&:hover { &:hover {
background-color: #9f0712; background-color: v.$red;
} }
} }
@@ -3799,7 +3810,7 @@
} }
.info-header { .info-header {
background: linear-gradient(0deg, #6366f1 50%, #8b5cf6 130%); background: #6366f1;
color: white; color: white;
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid v.$border-color-1; border-bottom: 1px solid v.$border-color-1;
@@ -3847,6 +3858,84 @@
padding: 12px 20px; padding: 12px 20px;
border-bottom: 1px solid #f1f5f9; border-bottom: 1px solid #f1f5f9;
} }
.appeal-form {
margin-top: 20px;
}
.appeal-textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-family: inherit;
font-size: 14px;
resize: vertical;
}
.appeal-textarea:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
}
.appeal-textarea:disabled {
background-color: #f3f4f6;
cursor: not-allowed;
}
.required {
color: #ef4444;
}
.appeal-actions {
display: flex;
gap: 12px;
justify-content: center;
margin: 10px 0;
}
.appeal-submit-btn {
padding: 8px 20px;
background-color: #6366f1;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s;
}
.appeal-submit-btn:hover:not(:disabled) {
background-color: #4f46e5;
}
.appeal-submit-btn:disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
.appeal-cancel-btn {
padding: 8px 20px;
background-color: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.appeal-cancel-btn:hover:not(:disabled) {
background-color: #e5e7eb;
}
.appeal-cancel-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
} }
} }
} }
@@ -0,0 +1,104 @@
'use client';
import { convertBytes } from '@/app/lib/convertBytes';
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
import { useTranslations } from 'next-intl';
import { FileDetails } from '@/app/[locale]/pages/file/[id]/page';
import { FileAnAppeal } from '@/app/actions/fileEntity';
import { useState } from 'react';
import { toast } from 'sonner';
export function FileAppealModalWindow({ fileId, setWindowClose, setWindowChildren }: {
fileId: string,
setWindowClose: any,
setWindowChildren: any
}) {
const t = useTranslations('Global');
const [appealReason, setAppealReason] = useState('');
const [additionalInfo, setAdditionalInfo] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmitAppeal = async () => {
if (!appealReason.trim()) {
toast.error(t('please-enter-appeal-reason'));
return;
}
setIsSubmitting(true);
try {
const result = await FileAnAppeal(fileId, appealReason, additionalInfo);
if (result && result.status === 'success') {
toast.success(t('appeal-submitted-successfully'));
setWindowClose(false);
} else {
toast.error(result?.message || t('appeal-submission-failed'));
}
} catch (error) {
console.error('Appeal submission error:', error);
toast.error(t('appeal-submission-failed'));
} finally {
setIsSubmitting(false);
}
};
return (
<div className="modal-window-view-file">
<div className="modal-window-view-file-content">
{/* Форма подачи апелляции */}
<div className="file-info-card appeal-form">
<div className="info-header">
<h4>{t('file-an-appeal')}</h4>
</div>
<div className="info-item">
<div className="info-label">
{t('appeal-reason')} <span className="required">*</span>
</div>
<div className="info-value">
<textarea
className="appeal-textarea"
value={appealReason}
onChange={(e) => setAppealReason(e.target.value)}
placeholder={t('enter-appeal-reason')}
rows={4}
disabled={isSubmitting}
/>
</div>
</div>
<div className="info-item">
<div className="info-label">{t('additional-info')}</div>
<div className="info-value">
<textarea
className="appeal-textarea"
value={additionalInfo}
onChange={(e) => setAdditionalInfo(e.target.value)}
placeholder={t('enter-additional-info')}
rows={3}
disabled={isSubmitting}
/>
</div>
<div className="appeal-actions">
<button
className="btn btn-cancel"
onClick={() => setWindowClose(false)}
disabled={isSubmitting}
>
{t('cancel')}
</button>
<button
className="btn btn-primary"
onClick={handleSubmitAppeal}
disabled={isSubmitting}
>
{isSubmitting ? t('submitting') : t('submit-appeal')}
</button>
</div>
</div>
</div>
</div>
</div>
);
}
+13 -1
View File
@@ -480,7 +480,19 @@
"total-complaints": "Total complaints", "total-complaints": "Total complaints",
"total-claims": "Total claims", "total-claims": "Total claims",
"city": "City", "city": "City",
"closed": "Closed" "closed": "Closed",
"blocked": "Blocked",
"pending-moderation": "Pending moderation",
"file-an-appeal": "File an appeal",
"please-enter-appeal-reason": "Please enter the reason for appeal",
"appeal-submitted-successfully": "Appeal submitted successfully",
"appeal-submission-failed": "Failed to submit appeal",
"appeal-reason": "Reason for appeal",
"enter-appeal-reason": "Describe the reason for your appeal...",
"additional-info": "Additional information",
"enter-additional-info": "Any additional information that may help...",
"submit-appeal": "Submit appeal",
"submitting": "Submitting..."
}, },
"Login-register-form": { "Login-register-form": {
"and": "and", "and": "and",
+13 -1
View File
@@ -480,7 +480,19 @@
"total-complaints": "Всего нарушений", "total-complaints": "Всего нарушений",
"total-claims": "Всего претензий", "total-claims": "Всего претензий",
"city": "Город", "city": "Город",
"closed": "Закрыта" "closed": "Закрыта",
"blocked": "Заблокирован",
"pending-moderation": "На модерации",
"file-an-appeal": "Подать апелляцию",
"please-enter-appeal-reason": "Пожалуйста, укажите причину апелляции",
"appeal-submitted-successfully": "Апелляция успешно отправлена",
"appeal-submission-failed": "Не удалось отправить апелляцию",
"appeal-reason": "Причина апелляции",
"enter-appeal-reason": "Опишите причину вашей апелляции...",
"additional-info": "Дополнительная информация",
"enter-additional-info": "Любая дополнительная информация, которая может помочь...",
"submit-appeal": "Отправить апелляцию",
"submitting": "Отправка..."
}, },
"Login-register-form": { "Login-register-form": {
"and": "и", "and": "и",