From 7ae0fecb61610cf421bd6c9222edfdd8e75edd07 Mon Sep 17 00:00:00 2001 From: smanylov Date: Thu, 26 Mar 2026 17:30:30 +0700 Subject: [PATCH] add complain registration, add view complaint inforation, add status change for marches when user click on mat --- src/app/[locale]/pages/file/[id]/page.tsx | 4 +- src/app/actions/violationActions.ts | 138 +++++++++++++++- .../hooks/react-query/useFileViolations.ts | 39 +++++ src/app/styles/pages-styles.scss | 149 ++++++++++-------- .../violation-table/case-complaint.tsx | 137 ++++++++++++++++ .../violation-table/case-violation.tsx | 14 ++ .../file-page-violation-info.tsx | 98 ++++++++++++ .../file-page-violations-list.tsx | 105 +----------- 8 files changed, 518 insertions(+), 166 deletions(-) create mode 100644 src/app/hooks/react-query/useFileViolations.ts create mode 100644 src/app/ui/file-page/violation-table/case-complaint.tsx create mode 100644 src/app/ui/file-page/violation-table/case-violation.tsx create mode 100644 src/app/ui/file-page/violation-table/file-page-violation-info.tsx rename src/app/ui/file-page/{ => violation-table}/file-page-violations-list.tsx (60%) diff --git a/src/app/[locale]/pages/file/[id]/page.tsx b/src/app/[locale]/pages/file/[id]/page.tsx index 35256ed..91d2672 100644 --- a/src/app/[locale]/pages/file/[id]/page.tsx +++ b/src/app/[locale]/pages/file/[id]/page.tsx @@ -6,7 +6,7 @@ import FilePageViolationInfo from '@/app/ui/file-page/file-page-violation-info'; import ViolationPpageFileStatistic from '@/app/ui/file-page/file-page-file-statistic'; import FilePageFileInfo from '@/app/ui/file-page/file-page-file-info'; import FilePageActions from '@/app/ui/file-page/file-page-actions'; -import FilePageViolationsList from '@/app/ui/file-page/file-page-violations-list'; +import FilePageViolationsList from '@/app/ui/file-page/violation-table/file-page-violations-list'; import FilePageNote from '@/app/ui/file-page/file-page-note'; import { getFileViolations } from '@/app/actions/violationActions'; import { viewFileInfo } from '@/app/actions/fileEntity'; @@ -62,7 +62,6 @@ export default async function Page({ const currentPage = Number(page) || 1; try { - const fileViolations = await getFileViolations(id, currentPage); const fileInfo: FileDetails = await viewFileInfo(id); return ( @@ -78,7 +77,6 @@ export default async function Page({
diff --git a/src/app/actions/violationActions.ts b/src/app/actions/violationActions.ts index f3265e6..1e4c9e5 100644 --- a/src/app/actions/violationActions.ts +++ b/src/app/actions/violationActions.ts @@ -22,10 +22,10 @@ export async function getViolationSearchStatus() { if (text) { try { const parsed = JSON.parse(text); - console.log('Parsed:', parsed); + return parsed; } catch (e) { - console.log('Not JSON, raw text:', text); + return { status: text === 'Task not found' ? 'task-not-found' : text }; @@ -257,4 +257,138 @@ export async function fetchViolationStatistic() { } catch (error) { return null } +} + +interface complainBody { + previousText: string, + errorMessage: string | null, + success: boolean +} + +export async function createComplaint( + state: complainBody | undefined, + formData: FormData +): Promise { + const email = await getSessionData('email'); + const violationId = formData.get('violationId') as string || ''; + const text = formData.get('note') as string || ''; + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 30013, + message_body: { + action: 'create', + violation_id: violationId, + text: text, + email: email + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + let parsed = await response.json(); + + if (parsed?.message_code === 0) { + return { + errorMessage: violationId, + previousText: text, + success: true + }; + } else { + throw parsed; + } + } else { + throw (`${response.status}`); + } + } catch (error) { + return { + errorMessage: violationId, + previousText: text, + success: false + } + } +} + +export async function fetchComplainInfo(id: number) { + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 30013, + message_body: { + action: 'get_by_violation', + violation_id: id + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + let parsed = await response.json(); + + if (parsed?.message_code === 0) { + return { + body: parsed?.message_body + } + } else { + throw parsed; + } + } else { + throw (`${response.status}`); + } + } catch (error) { + return { + errorMessage: 'error' + } + } +} + +export async function updateMatchStatus(id: number, status: 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: 30009, + message_body: { + action: 'updateStatus', + violation_id: id, + status: status, + token: token + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + let parsed = await response.json(); + + if (parsed?.message_body.status === 'SHOWED') { + return true + } else { + throw parsed; + } + } else { + throw (`${response.status}`); + } + } catch (error) { + return false + } } \ No newline at end of file diff --git a/src/app/hooks/react-query/useFileViolations.ts b/src/app/hooks/react-query/useFileViolations.ts new file mode 100644 index 0000000..bf0b963 --- /dev/null +++ b/src/app/hooks/react-query/useFileViolations.ts @@ -0,0 +1,39 @@ +import { useQuery } from '@tanstack/react-query'; +import { getFileViolations } from '@/app/actions/violationActions'; + +export interface ViolationFileDetail { + id: number; + url: string; + page_url: string; + page_title: string; + host: string; + status: string; + created_date: string; + file_id: string; +} + +interface ViolationFile { + violations: ViolationFileDetail[], + total_elements: number, + total_pages: number, + current_page: number, + page_size: number, + has_next: boolean, + has_previous: boolean +} + +export const useFileViolations = (id: string, page: number) => { + return useQuery({ + queryKey: ['fileViolations', id, page], + queryFn: () => { + return getFileViolations(id, page) + }, + select: (data: ViolationFile | null) => { + if (!data) { + return null + } + return data; + }, + retry: false + }); +}; \ No newline at end of file diff --git a/src/app/styles/pages-styles.scss b/src/app/styles/pages-styles.scss index 965b196..b574651 100644 --- a/src/app/styles/pages-styles.scss +++ b/src/app/styles/pages-styles.scss @@ -4361,56 +4361,6 @@ } } - .action-buttons { - display: flex; - gap: 10px; - flex-direction: column; - /* flex-wrap: wrap; */ - - .btn-action { - flex: 1; - /* min-width: 150px; */ - width: 100%; - padding: 12px 20px; - border-radius: 12px; - font-weight: 600; - border: none; - cursor: pointer; - transition: all 0.3s ease; - font-size: 14px; - } - - .btn-warning { - background: linear-gradient(135deg, #f59e0b, #d97706); - color: white; - - &:hover { - transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(245, 158, 11, 0.4); - } - } - - .btn-success { - background: linear-gradient(135deg, #10b981, #059669); - color: white; - - &:hover { - transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(16, 185, 129, 0.4); - } - } - - .btn-secondary { - background: linear-gradient(135deg, #64748b, #475569); - color: white; - - &:hover { - transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(100, 116, 139, 0.4); - } - } - } - .btn-small { padding: 6px 12px; border-radius: 8px; @@ -4421,6 +4371,10 @@ transition: all 0.3s ease; text-decoration: none; display: inline-block; + + &:disabled { + opacity: 0.6; + } } .btn-primary-small { @@ -4547,22 +4501,21 @@ } .violation-info { - /* background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); - border: 1px solid #e2e8f0; - border-radius: 12px; - */ background: #fff; border: 1px solid #f3f4f6; border-radius: 20px; box-shadow: 0 4px 20px #0000001a; - padding: 15px; display: flex; flex-direction: column; + height: 100%; + min-height: 500px; &-content { display: flex; flex-direction: column; + flex: 1; + min-height: 0; } &-grid { @@ -4583,22 +4536,19 @@ } &-image { - margin-bottom: 20px; - /* padding-bottom: 15px; */ - /* border-bottom: 2px solid #e6e8eb; */ + width: 300px; + height: 200px; img { - max-width: 300px; width: 100%; - + height: 100%; + object-fit: contain; } } &-source { margin-bottom: 20px; - /* padding-bottom: 15px; */ - /* border-bottom: 2px solid #e6e8eb; */ span { color: #64748b; @@ -4614,6 +4564,81 @@ border-left: 1px solid #e2e8f0; padding-left: 15px; } + + &-header { + display: grid; + grid-template-columns: 300px auto; + gap: 10px; + flex-shrink: 0; + + border-bottom: 1px solid rgb(226, 232, 240); + } + + &-case-grid { + display: grid; + grid-template-rows: 1fr 1fr; + gap: 5px; + flex: 1; + min-height: 0; + overflow: auto; + } + + &-case { + padding: 10px; + + &:first-child { + border-bottom: 1px solid rgb(226, 232, 240); + padding-bottom: 10px; + } + + .btn-action { + flex: 1; + width: 100%; + padding: 12px 20px; + border-radius: 12px; + font-weight: 600; + border: none; + cursor: pointer; + transition: all 0.3s ease; + font-size: 14px; + max-width: 300px; + } + + .btn-case { + background: linear-gradient(135deg, #6366f1, #8b5cf6); + color: white; + margin-bottom: 20px; + + &:hover { + transform: translateY(-2px); + } + } + +/* .btn-warning { + background: linear-gradient(135deg, #f59e0b, #d97706); + color: white; + } + + .btn-success { + background: linear-gradient(135deg, #10b981, #059669); + color: white; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(16, 185, 129, 0.4); + } + } + + .btn-secondary { + background: linear-gradient(135deg, #64748b, #475569); + color: white; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(100, 116, 139, 0.4); + } + } */ + } } .pagination { diff --git a/src/app/ui/file-page/violation-table/case-complaint.tsx b/src/app/ui/file-page/violation-table/case-complaint.tsx new file mode 100644 index 0000000..a13f3ed --- /dev/null +++ b/src/app/ui/file-page/violation-table/case-complaint.tsx @@ -0,0 +1,137 @@ +'use client' + +import { ViolationFileDetail } from '@/app/hooks/react-query/useFileViolations'; +import { useActionState, useEffect, useState } from 'react'; +import { useTranslations } from 'next-intl'; +import { createComplaint, fetchComplainInfo } from '@/app/actions/violationActions'; +import { toast } from 'sonner'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; + +interface complaintInfo { + id: number, + status: string, + complaint_text: string, + violation_id: number, + email: string, + created_at: string, + updated_at: string, + not_moderated: boolean +} + +export default function CaseComplaint({ selectedViolation }: { selectedViolation: ViolationFileDetail }) { + const [showCreateCase, setShowCreateCase] = useState(false); + const t = useTranslations('Global'); + const [state, formAction, isPending] = useActionState(createComplaint, undefined); + const queryClient = useQueryClient(); + + const { data: complainInfo, isLoading: isLoadingComplain } = useQuery({ + queryKey: ['complainInfo', selectedViolation.id], + queryFn: () => fetchComplainInfo(selectedViolation.id), + select: (data) => { + if (data) { + return data + } + }, + enabled: selectedViolation.status !== 'NEW' && + selectedViolation.status !== 'CREATED' && + selectedViolation.status !== 'SHOWED', + }); + + useEffect(() => { + if (state?.success) { + toast.success('Жалоба зарегистрирована'); + queryClient.invalidateQueries({ queryKey: ['fileViolations'] }); + queryClient.invalidateQueries({ queryKey: ['complainInfo', selectedViolation.id] }); + } else if (state && !state.success) { + toast.warning('Жалоба не зарегистрирована'); + } + }, [state, queryClient, selectedViolation.id]); + + function toggleForm() { + setShowCreateCase(!showCreateCase); + } + + if (selectedViolation.status === 'NEW' || selectedViolation.status === 'CREATED' || selectedViolation.status === 'SHOWED') { + return ( +
+ {!showCreateCase ? ( + + ) : ( +
+ + +
+
+ + + +
+
+
+ )} +
+ ); + } else { + if (isLoadingComplain) { + return ( +
+
Загрузка...
+
+ ); + } + + return ( +
+ {complainInfo?.body.content ? ( + +
+ {complainInfo.body.content.map((item: complaintInfo) => { + return ( +
+

+ Информация о жалобе +

+
+

ID: {item.id}

+

Статус: {item.status}

+

Текст жалобы: {item.complaint_text}

+

ID нарушения: {item.violation_id}

+

Email: {item.email}

+

Дата создания: {item.created_at}

+

Дата обновления: {item.updated_at}

+
+
+ ); + })} +
+ ) : ( +
Нет информации о жалобе
+ )} +
+ ); + } +} \ No newline at end of file diff --git a/src/app/ui/file-page/violation-table/case-violation.tsx b/src/app/ui/file-page/violation-table/case-violation.tsx new file mode 100644 index 0000000..8a4fe9a --- /dev/null +++ b/src/app/ui/file-page/violation-table/case-violation.tsx @@ -0,0 +1,14 @@ +import { ViolationFileDetail } from '@/app/hooks/react-query/useFileViolations'; + +export default function CaseViolation({ selectedViolation }: { selectedViolation: ViolationFileDetail }) { + + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/src/app/ui/file-page/violation-table/file-page-violation-info.tsx b/src/app/ui/file-page/violation-table/file-page-violation-info.tsx new file mode 100644 index 0000000..31691e3 --- /dev/null +++ b/src/app/ui/file-page/violation-table/file-page-violation-info.tsx @@ -0,0 +1,98 @@ +'use client' + +import Link from 'next/link'; +import { formatDate } from '@/app/lib/formatDate'; +import { useTranslations } from 'next-intl'; +import { ViolationFileDetail } from '@/app/hooks/react-query/useFileViolations'; +import CaseComplaint from '@/app/ui/file-page/violation-table/case-complaint'; +import CaseViolation from '@/app/ui/file-page/violation-table/case-violation'; +import { updateMatchStatus } from '@/app/actions/violationActions'; +import { useQueryClient } from '@tanstack/react-query'; +import { useEffect, useRef } from 'react'; + +export default function FilePageViolationInfo({ selectedViolation }: { selectedViolation: ViolationFileDetail | null }) { + const t = useTranslations('Global'); + const queryClient = useQueryClient(); + + useEffect(() => { + const updateStatus = async () => { + if (selectedViolation && (selectedViolation.status === 'CREATED' || selectedViolation.status === 'NEW')) { + const response = await updateMatchStatus(selectedViolation.id, 'SHOWED'); + if (response) { + queryClient.invalidateQueries({ queryKey: ['fileViolations'] }); + } + } + }; + + updateStatus(); + }, [selectedViolation, queryClient]); + + if (!selectedViolation) { + return ( +
+ not found +
+ ) + } + + return ( +
+
+

+ {selectedViolation?.page_title} +

+ +
+
+ {selectedViolation?.page_title} +
+ +
+
+ + {t('source')}: + + + {selectedViolation?.page_url} + +
+
+ + {t('date')} {selectedViolation?.created_date ? formatDate(selectedViolation?.created_date) : '#'} + +
+
+ {selectedViolation?.status} +
+
+
+ +
+ + +
+
+
+ ) +} \ No newline at end of file diff --git a/src/app/ui/file-page/file-page-violations-list.tsx b/src/app/ui/file-page/violation-table/file-page-violations-list.tsx similarity index 60% rename from src/app/ui/file-page/file-page-violations-list.tsx rename to src/app/ui/file-page/violation-table/file-page-violations-list.tsx index 71b92c7..6397749 100644 --- a/src/app/ui/file-page/file-page-violations-list.tsx +++ b/src/app/ui/file-page/violation-table/file-page-violations-list.tsx @@ -4,37 +4,15 @@ import { useTranslations } from 'next-intl'; import Link from 'next/link'; import { useRouter, usePathname } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { formatDate } from '@/app/lib/formatDate'; -import ViolationPageNote from '@/app/ui/file-page/file-page-note'; - -interface ViolationFileDetail { - id: number; - url: string; - page_url: string; - page_title: string; - host: string; - status: string; - created_date: string; - file_id: string; -} - -interface ViolationFile { - violations: ViolationFileDetail[], - total_elements: number, - total_pages: number, - current_page: number, - page_size: number, - has_next: boolean, - has_previous: boolean -} +import { useState } from 'react'; +import FilePageViolationInfo from '@/app/ui/file-page/violation-table/file-page-violation-info'; +import { useFileViolations } from '@/app/hooks/react-query/useFileViolations'; +import { ViolationFileDetail } from '@/app/hooks/react-query/useFileViolations'; export default function FilePageViolationsList({ - fileViolations, currentPage, fileId }: { - fileViolations: ViolationFile, currentPage: number, fileId: string }) { @@ -42,6 +20,7 @@ export default function FilePageViolationsList({ const router = useRouter(); const pathname = usePathname(); const [selectedViolation, setSelectedViolation] = useState(null); + const { data: fileViolations } = useFileViolations(fileId, currentPage); if (!fileViolations?.violations) { return null; @@ -147,80 +126,8 @@ export default function FilePageViolationsList({ })}
-
-
-
-

- {selectedViolation?.page_title} -

-
- {selectedViolation?.page_title} -
-
+ - - {t('source')}: - - - {selectedViolation?.page_url} - -
-
- - {t('date')} {selectedViolation?.created_date ? formatDate(selectedViolation?.created_date) : '#'} - -
-
- {selectedViolation?.status} -
- -
- Тут думаю нужно будет отображать инфу о работе с нарушением и её статус -
- -
- -
-
- - - - - -
-
-
- {/* */} -
{fileViolations.total_pages > 1 && (