add appeal modal window, add file status
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.118.0",
|
||||
"version": "0.119.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 2999",
|
||||
|
||||
@@ -162,4 +162,44 @@ export async function downloadFile(fileId: string, fileName: string) {
|
||||
contentType: response.headers.get('content-type') || 'application/octet-stream',
|
||||
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 Image from 'next/image';
|
||||
import { filePermisionChange } from '@/app/actions/trackingActions';
|
||||
import {FileAppealModalWindow} from '@/app/ui/modal-windows/file-appeal-modal-window';
|
||||
|
||||
export type FileItem = {
|
||||
id: string;
|
||||
@@ -41,7 +42,7 @@ export type FileItem = {
|
||||
violations?: number | undefined;
|
||||
size?: number | undefined;
|
||||
uploadDate?: number;
|
||||
status?: string;
|
||||
status: string;
|
||||
monitoring: string;
|
||||
protectStatus: string;
|
||||
_original?: ApiFile;
|
||||
@@ -295,6 +296,32 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
|
||||
});
|
||||
}, [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(() => {
|
||||
if (!apiResponse?.items?.length) return false;
|
||||
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)}
|
||||
</span>
|
||||
<br />
|
||||
<span className="table-item-extension">{cutFileExtension(row.original.fileName)}</span> <span className="table-item-protected">
|
||||
{row.original?.protectStatus ? t(row.original?.protectStatus) : t('error')}
|
||||
</span>
|
||||
<span className="table-item-extension">{cutFileExtension(row.original.fileName)}</span> <StatusBadge status={row.original.status} protectStatus={row.original.protectStatus} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -614,36 +639,53 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="actions-group">
|
||||
<Link
|
||||
href={`/pages/file/${row.original.id}`}
|
||||
className="bg-violet-500 hover:bg-violet-600"
|
||||
title={t('view')}
|
||||
>
|
||||
<IconEye />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="actions-group">
|
||||
{row.original.protectStatus === 'PROTECTED' && (
|
||||
{/* "BLOCKED" "REMOVED" */}
|
||||
{(row.original.status !== 'BLOCKED' && row.original.status !== 'REMOVED') ? (
|
||||
<>
|
||||
<div className="actions-group">
|
||||
<Link
|
||||
href={`/pages/file/${row.original.id}`}
|
||||
className="bg-violet-500 hover:bg-violet-600"
|
||||
title={t('view')}
|
||||
>
|
||||
<IconEye />
|
||||
</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
|
||||
onClick={() => handleDownload(row.original)}
|
||||
disabled={isFileLoading}
|
||||
className="table-action-download"
|
||||
title={t('download')}
|
||||
onClick={() => {
|
||||
handleAppeal(row.original)
|
||||
}}
|
||||
className="table-action-appeal"
|
||||
title={t('file-an-appeal')}
|
||||
>
|
||||
<IconFileDownload />
|
||||
<IconInfo />
|
||||
</button>
|
||||
)}
|
||||
<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);
|
||||
};
|
||||
|
||||
const handleAppeal = async (file: FileItem) => {
|
||||
|
||||
setOpenWindowChildren(() => {
|
||||
return (
|
||||
<FileAppealModalWindow fileId={file.id} setWindowClose={setOpenWindow} setWindowChildren={setOpenWindowChildren} />
|
||||
)
|
||||
})
|
||||
setOpenWindow(true);
|
||||
};
|
||||
|
||||
const handleDownload = async (file: FileItem) => {
|
||||
setIsFileLoading(true)
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
|
||||
.btn,
|
||||
.btn-s,
|
||||
.btn-m {
|
||||
.btn-l {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
@@ -197,7 +197,8 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
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 {
|
||||
background: transparent;
|
||||
border: 2px solid v.$p-color;
|
||||
|
||||
@@ -930,13 +930,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.table-item-protected {
|
||||
color: #10b981;
|
||||
.table-item-status-badge {
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
margin-left: 5px;
|
||||
padding-left: 5px;
|
||||
|
||||
&.protected {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
&.moderation {
|
||||
color: v.$color-warning;
|
||||
}
|
||||
|
||||
&.blocked {
|
||||
color: v.$red;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '•';
|
||||
color: v.$text-s;
|
||||
@@ -1016,11 +1027,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.table-action-delete {
|
||||
background-color: #e80a14;
|
||||
.table-action-appeal {
|
||||
background-color: #9f0712;
|
||||
|
||||
&:hover {
|
||||
background-color: #9f0712;
|
||||
background-color: v.$red;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3799,7 +3810,7 @@
|
||||
}
|
||||
|
||||
.info-header {
|
||||
background: linear-gradient(0deg, #6366f1 50%, #8b5cf6 130%);
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid v.$border-color-1;
|
||||
@@ -3847,6 +3858,84 @@
|
||||
padding: 12px 20px;
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -480,7 +480,19 @@
|
||||
"total-complaints": "Total complaints",
|
||||
"total-claims": "Total claims",
|
||||
"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": {
|
||||
"and": "and",
|
||||
|
||||
@@ -480,7 +480,19 @@
|
||||
"total-complaints": "Всего нарушений",
|
||||
"total-claims": "Всего претензий",
|
||||
"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": {
|
||||
"and": "и",
|
||||
|
||||
Reference in New Issue
Block a user