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}
+
+
+
+
+

+
+
+
+
+
+ {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}
-
-
-

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