continue: create case

This commit is contained in:
smanylov
2026-04-07 19:05:44 +07:00
parent c57685065a
commit 50f45a133b
11 changed files with 519 additions and 28 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "no-copy-frontend", "name": "no-copy-frontend",
"version": "0.80.0", "version": "0.81.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 2999", "dev": "next dev -p 2999",
+12
View File
@@ -92,6 +92,18 @@ export const createComplaintSchema = z
.trim(), .trim(),
}) })
export const createCaseSchema = z
.object({
textArea: z
.string()
.min(6, { error: 'text-area-error-fill-complaint-text' })
.trim(),
caseTitle: z
.string()
.min(6, { error: 'text-area-error-fill-complaint-text' })
.trim(),
})
export const localDevelopmentUrl = process.env.DEV_URL ? process.env.DEV_URL : 'http://localhost'; export const localDevelopmentUrl = process.env.DEV_URL ? process.env.DEV_URL : 'http://localhost';
export const API_BASE_URL = process.env.NODE_ENV === 'development' export const API_BASE_URL = process.env.NODE_ENV === 'development'
+173 -1
View File
@@ -1,7 +1,7 @@
'use server' 'use server'
import { getSessionData } from '@/app/actions/session'; import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL, createComplaintSchema } from '@/app/actions/definitions'; import { API_BASE_URL, createComplaintSchema, createCaseSchema } from '@/app/actions/definitions';
export async function getViolationSearchStatus() { export async function getViolationSearchStatus() {
const token = await getSessionData('token'); const token = await getSessionData('token');
@@ -441,3 +441,175 @@ export async function updateMatchStatus(id: number, status: MatchStatus) {
return false return false
} }
} }
export async function fetchCaseInfo(id: number) {
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: 30017,
message_body: {
action: 'get_byId',
token: token,
id: id
}
}), */
body: JSON.stringify({
version: 1,
msg_id: 30017,
message_body: {
action: 'get_all',
token: token,
pageSize: 10,
pageNumber: 0,
sortBy:"createdAt",
sortDir: "desc",
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 if (parsed?.message_code !== 0 && parsed?.message_desc === 'Law case not found') {
return {
body: null
}
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return {
errorMessage: 'error'
}
}
}
interface caseBody {
previousText: string,
previousCaseTitle: string,
violationID: string | null,
success: boolean,
errorMessage?: Record<string, string> | null
}
export async function createCase(
state: caseBody | undefined,
formData: FormData
): Promise<caseBody> {
const email = await getSessionData('email');
const token = await getSessionData('token');
const violationId = formData.get('violationId') as string || '';
const textArea = formData.get('note') as string || '';
const caseTitle = formData.get('caseTitle') as string || '';
const amount = formData.get('amount') as string || '';
const validatedFields = createCaseSchema.safeParse({
textArea,
caseTitle
});
if (!validatedFields.success) {
const errors: Record<string, string> = {}
JSON.parse(validatedFields.error.message).forEach((obj: {
message: string,
path: string
}) => {
if (errors[obj.path[0]]) {
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
} else {
errors[obj.path[0]] = obj.message;
}
});
return {
violationID: violationId,
previousText: textArea,
previousCaseTitle: caseTitle,
success: false,
errorMessage: errors
}
}
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30017,
message_body: {
action: 'create',
violation_id: violationId,
description: textArea,
email: email,
token: token,
name: caseTitle,
priority: 'HIGH',
amount: Number(amount)
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed?.message_code === 0) {
return {
violationID: violationId,
previousCaseTitle: caseTitle,
previousText: textArea,
success: true
};
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
const errors: Record<string, string> = {}
if (error && typeof error === 'object' && 'message_code' in error) {
const err = error as { message_code: number };
if (err.message_code === 2) {
errors['server'] = 'the-complaint-has-not-been-registered'
return {
violationID: violationId,
previousText: textArea,
previousCaseTitle: caseTitle,
success: false,
errorMessage: errors
}
}
}
errors['server'] = 'error-save';
return {
violationID: violationId,
previousText: textArea,
previousCaseTitle: caseTitle,
success: false,
errorMessage: errors
}
}
}
+36
View File
@@ -4764,6 +4764,42 @@
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
} }
} }
.note-form-flex {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.note-case-input {
border: 1px solid v.$border-color-1;
border-radius: 8px;
box-sizing: border-box;
background: #f8fafc;
padding: 5px;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
&:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
}
.note-case-label {
font-weight: 600;
}
.note-form-group {
input {
display: block;
}
}
} }
.popular-questions { .popular-questions {
@@ -59,7 +59,7 @@ export default function DashboardUserViolations() {
className="section-add-file btn view-all-link" className="section-add-file btn view-all-link"
> >
<Link href='/pages/violations'> <Link href='/pages/violations'>
{t('add')} {t('all')}
</Link> </Link>
</div> </div>
</div> </div>
@@ -6,6 +6,7 @@ import { useTranslations } from 'next-intl';
import { createComplaint, fetchComplainInfo } from '@/app/actions/violationActions'; import { createComplaint, fetchComplainInfo } from '@/app/actions/violationActions';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { MatchStatus } from '@/app/actions/violationActions';
interface complaintInfo { interface complaintInfo {
id: number, id: number,
@@ -18,7 +19,10 @@ interface complaintInfo {
not_moderated: boolean not_moderated: boolean
} }
export default function CaseComplaint({ selectedViolation }: { selectedViolation: ViolationFileDetail }) { const ENABLED_STATUSES = ['COMPLAINT_IN_WORK', 'COMPLAINT_AND_LEGAL_IN_WORK'];
type UpdateStatusHandler = (id: number, status: MatchStatus) => Promise<boolean>;
export default function CaseComplaint({ selectedViolation, updateStatusHandler }: { selectedViolation: ViolationFileDetail, updateStatusHandler: UpdateStatusHandler }) {
const t = useTranslations('Global'); const t = useTranslations('Global');
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -38,26 +42,26 @@ export default function CaseComplaint({ selectedViolation }: { selectedViolation
return data return data
} }
}, },
enabled: localViolation.status !== 'NEW' && enabled: ENABLED_STATUSES.includes(localViolation.status),
localViolation.status !== 'CREATED' &&
localViolation.status !== 'SHOWED',
}); });
useEffect(() => { useEffect(() => {
if (state?.success) { if (state?.success) {
toast.success(t('the-complaint-has-been-registered')); toast.success(t('the-complaint-has-been-registered'));
const newStatus = selectedViolation.status === 'LEGAL_IN_WORK'
? 'COMPLAINT_AND_LEGAL_IN_WORK'
: 'COMPLAINT_IN_WORK';
setLocalViolation(prev => ({ setLocalViolation(prev => ({
...prev, ...prev,
status: 'COMPLAINT_IN_WORK' status: newStatus
})); }));
queryClient.invalidateQueries({ queryKey: ['fileViolations'] }); updateStatusHandler(selectedViolation.id, newStatus);
queryClient.invalidateQueries({ queryKey: ['complainInfo', localViolation.id] });
setShowCreateCase(false); setShowCreateCase(false);
} else if (state && !state.success) { } else if (state && !state.success) {
console.log('state.errorMessage');
console.log(state.errorMessage); console.log(state.errorMessage);
if (state.errorMessage?.server) { if (state.errorMessage?.server) {
toast.warning(t(state.errorMessage.server)); toast.warning(t(state.errorMessage.server));
@@ -65,13 +69,13 @@ export default function CaseComplaint({ selectedViolation }: { selectedViolation
toast.warning(t('the-complaint-has-not-been-registered')); toast.warning(t('the-complaint-has-not-been-registered'));
} }
} }
}, [state, queryClient, localViolation.id]); }, [state, updateStatusHandler, selectedViolation.id]);
function toggleForm() { function toggleForm() {
setShowCreateCase(!showCreateCase); setShowCreateCase(!showCreateCase);
} }
if (localViolation.status === 'NEW' || localViolation.status === 'CREATED' || localViolation.status === 'SHOWED') { if (!ENABLED_STATUSES.includes(localViolation.status)) {
return ( return (
<div className="violation-info-case"> <div className="violation-info-case">
{!showCreateCase ? ( {!showCreateCase ? (
@@ -1,14 +1,255 @@
'use client'
import { ViolationFileDetail } from '@/app/hooks/react-query/useFileViolations'; import { ViolationFileDetail } from '@/app/hooks/react-query/useFileViolations';
import { useEffect, useState, useActionState } from 'react';
import { fetchCaseInfo, MatchStatus, createCase } from '@/app/actions/violationActions';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
export default function CaseViolation({ selectedViolation }: { selectedViolation: ViolationFileDetail }) { interface CaseInfo {
id: number;
return ( name: string;
<div description: string;
className="violation-info-case" amount: number;
> priority: string;
<button type="button" className="btn-action btn-case"> type: string;
Создание дела lawyer: string | null;
</button> createdAt: string;
</div> updatedAt: string;
) pageNumber: number;
pageSize: number;
totalElements: number;
totalPages: number;
violationId: number;
content: string | null;
}
const ENABLED_STATUSES = ['LEGAL_IN_WORK', 'COMPLAINT_AND_LEGAL_IN_WORK'];
type UpdateStatusHandler = (id: number, status: MatchStatus) => Promise<boolean>;
export default function CaseViolation({ selectedViolation, updateStatusHandler }: { selectedViolation: ViolationFileDetail, updateStatusHandler: UpdateStatusHandler }) {
const t = useTranslations('Global');
const queryClient = useQueryClient();
const [showCreateCase, setShowCreateCase] = useState(false);
const [localViolation, setLocalViolation] = useState<ViolationFileDetail>(selectedViolation);
const [state, formAction, isPending] = useActionState(createCase, undefined);
const { data: caseInfo, isLoading: isLoadingCase } = useQuery({
queryKey: ['caseInfo', localViolation.id],
queryFn: () => fetchCaseInfo(localViolation.id),
select: (data) => {
if (data) {
return data
}
},
enabled: ENABLED_STATUSES.includes(localViolation.status)
});
useEffect(() => {
if (state?.success) {
toast.success(t('the-complaint-has-been-registered'));
const newStatus = selectedViolation.status === 'COMPLAINT_IN_WORK'
? 'COMPLAINT_AND_LEGAL_IN_WORK'
: 'LEGAL_IN_WORK';
setLocalViolation(prev => ({
...prev,
status: newStatus
}));
updateStatusHandler(selectedViolation.id, newStatus);
setShowCreateCase(false);
} else if (state && !state.success) {
console.log(state.errorMessage);
if (state.errorMessage?.server) {
toast.warning(t(state.errorMessage.server));
} else {
toast.warning(t('the-complaint-has-not-been-registered'));
}
}
}, [state, updateStatusHandler, selectedViolation.id]);
function toggleForm() {
setShowCreateCase(!showCreateCase);
}
if (!ENABLED_STATUSES.includes(localViolation.status)) {
return (
<div className="violation-info-case">
{!showCreateCase ? (
<button
type="button"
className="btn-action btn-case"
onClick={toggleForm}
>
Создание дела
</button>
) : (
<div>
<button
type="button"
className="btn-action btn-case"
onClick={toggleForm}
>
{t('cancel')}
</button>
<div className="note-form">
<form action={formAction}>
<input type="hidden" name="violationId" value={localViolation.id} />
<div
className="note-form-flex"
>
<section
className="note-form-group"
>
<label
className="note-case-label"
htmlFor="caseTitle"
>
Название дела
</label>
<input
type="text"
name="caseTitle"
className="note-case-input"
/>
{state?.errorMessage?.caseTitle && (
<p
className="text-sm text-red-500"
>
{
state?.errorMessage?.caseTitle.split('&').map((e, index) => {
return (
<span key={index}>
{t(e)}
<br />
</span>
)
})
}
</p>
)}
</section>
<section
className="note-form-group"
>
<label
className="note-case-label"
htmlFor="amount"
>
Сумма ущерба
</label>
<input
type="number"
name="amount"
className="note-case-input"
/>
{state?.errorMessage?.amount && (
<p
className="text-sm text-red-500"
>
{
state?.errorMessage?.amount.split('&').map((e, index) => {
return (
<span key={index}>
{t(e)}
<br />
</span>
)
})
}
</p>
)}
</section>
</div>
<section>
<textarea
name="note"
className="note-textarea"
placeholder={`Текст дела...`}>
</textarea>
{state?.errorMessage?.textArea && (
<p
className="text-sm text-red-500"
>
{
state?.errorMessage?.textArea.split('&').map((e, index) => {
return (
<span key={index}>
{t(e)}
<br />
</span>
)
})
}
</p>
)}
</section>
<div
className="flex gap-3"
>
<button
type="submit"
className="btn-small btn-primary-small"
disabled={isPending}
>
{t('submit-violation')}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
} else {
if (isLoadingCase) {
return (
<div className="violation-info-case">
<div>{t('loading')}...</div>
</div>
);
}
return (
<div className="violation-info-case">
{caseInfo?.body?.content ? (
<div>
{caseInfo.body.content.map((item: CaseInfo) => {
return (
<div key={item.id}>
<h3 className="mb-3">
{t('complaint-Information')}
</h3>
<div className="complaint-details">
<p><strong>ID:</strong> {item.id}</p>
<p><strong>{t('name')}:</strong> {item.name}</p>
<p><strong>{t('description')}:</strong> {item.description}</p>
<p><strong>{t('amount')}:</strong> {item.amount}</p>
<p><strong>{t('priority')}:</strong> {item.priority}</p>
<p><strong>{t('type')}:</strong> {item.type}</p>
<p><strong>{t('lawyer')}:</strong> {item.lawyer || t('not-assigned')}</p>
<p><strong>{t('violation-id')}:</strong> {item.violationId}</p>
<p><strong>{t('date-of-creation')}:</strong> {item.createdAt ? `${formatDate(item.createdAt)}: ${formatDateTime(item.createdAt)}` : '---'}</p>
<p><strong>{t('date-of-update')}:</strong> {item.createdAt ? `${formatDate(item.createdAt)}: ${formatDateTime(item.createdAt)}` : '---'}</p>
{item.content && <p><strong>{t('content')}:</strong> {item.content}</p>}
</div>
</div>
);
})}
</div>
) : (
<div>
{t('no-information-about-the-complaint')}
</div>
)}
</div>
);
}
} }
@@ -37,6 +37,7 @@ export default function FilePageViolationInfo({ selectedViolation }: { selectedV
toast.success('Статус обновлен'); toast.success('Статус обновлен');
} }
queryClient.invalidateQueries({ queryKey: ['fileViolations'] }); queryClient.invalidateQueries({ queryKey: ['fileViolations'] });
queryClient.invalidateQueries({ queryKey: ['complainInfo', id] });
return true; return true;
} else { } else {
setLocalViolation(selectedViolation); setLocalViolation(selectedViolation);
@@ -153,8 +154,16 @@ export default function FilePageViolationInfo({ selectedViolation }: { selectedV
<div <div
className="violation-info-case-grid" className="violation-info-case-grid"
> >
<CaseComplaint key={"complaint_" + selectedViolation.id} selectedViolation={selectedViolation} /> <CaseComplaint
<CaseViolation key={"violation_" + selectedViolation.id} selectedViolation={selectedViolation} /> key={"complaint_" + selectedViolation.id}
selectedViolation={selectedViolation}
updateStatusHandler={updateStatusHandler}
/>
<CaseViolation
key={"violation_" + selectedViolation.id}
selectedViolation={selectedViolation}
updateStatusHandler={updateStatusHandler}
/>
</div> </div>
</div> </div>
</div> </div>
@@ -3,12 +3,17 @@
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { convertBytes } from '@/app/lib/convertBytes'; import { convertBytes } from '@/app/lib/convertBytes';
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo'; import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
import { useEffect } from 'react';
export default function ProtectionStatistic({ fileType }: { fileType: string }) { export default function ProtectionStatistic({ fileType }: { fileType: string }) {
const t = useTranslations('Global'); const t = useTranslations('Global');
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo(); const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
useEffect(() => {
console.log(filesInfo);
}, [filesInfo])
const getFileTypeData = (fileType: string) => { const getFileTypeData = (fileType: string) => {
switch (fileType) { switch (fileType) {
case 'image': case 'image':
@@ -35,7 +40,9 @@ export default function ProtectionStatistic({ fileType }: { fileType: string })
<div className="protection-statistic-stat-label">{t(`${fileType}-protected`)}</div> <div className="protection-statistic-stat-label">{t(`${fileType}-protected`)}</div>
</div> </div>
<div className="protection-statistic-stat-card"> <div className="protection-statistic-stat-card">
<div className="protection-statistic-stat-number">0</div> <div className="protection-statistic-stat-number">
{currentData?.check ?? 0}
</div>
<div className="protection-statistic-stat-label">{t(`${fileType}-checked`)}</div> <div className="protection-statistic-stat-label">{t(`${fileType}-checked`)}</div>
</div> </div>
<div className="protection-statistic-stat-card"> <div className="protection-statistic-stat-card">
+6 -1
View File
@@ -402,7 +402,12 @@
"the-debit-occurs-1-day-before-the-end-of-the-subscription": "The debit occurs 1 day before the end of the subscription.", "the-debit-occurs-1-day-before-the-end-of-the-subscription": "The debit occurs 1 day before the end of the subscription.",
"you-can-unlink-your-card-and-cancel-auto-renewal-at-any-time": "You can unlink your card and cancel auto-renewal at any time.", "you-can-unlink-your-card-and-cancel-auto-renewal-at-any-time": "You can unlink your card and cancel auto-renewal at any time.",
"delete-a-bank-card": "Delete a bank card", "delete-a-bank-card": "Delete a bank card",
"no-new-notifications": "There are no new notifications" "no-new-notifications": "There are no new notifications",
"description": "Description",
"priority": "Priority",
"type": "Type",
"lawyer": "Lawyer",
"not-assigned": "Not assigned"
}, },
"Login-register-form": { "Login-register-form": {
"and": "and", "and": "and",
+6 -1
View File
@@ -402,7 +402,12 @@
"the-debit-occurs-1-day-before-the-end-of-the-subscription": "Списание происходит за 1 день до окончания подписки.", "the-debit-occurs-1-day-before-the-end-of-the-subscription": "Списание происходит за 1 день до окончания подписки.",
"you-can-unlink-your-card-and-cancel-auto-renewal-at-any-time": "Вы можете отвязать карту и отменить автопродление в любой момент.", "you-can-unlink-your-card-and-cancel-auto-renewal-at-any-time": "Вы можете отвязать карту и отменить автопродление в любой момент.",
"delete-a-bank-card": "Удалить банковскую карту", "delete-a-bank-card": "Удалить банковскую карту",
"no-new-notifications": "Новых уведомлений нет" "no-new-notifications": "Новых уведомлений нет",
"description": "Описание",
"priority": "Приоритет",
"type": "Тип",
"lawyer": "Адвокат",
"not-assigned": "Не назначено"
}, },
"Login-register-form": { "Login-register-form": {
"and": "и", "and": "и",