Files
vector/frontend/src/components/CaseForm/CaseForm.jsx
T
Sadmin ab3eebe274 E3 UI: дашборд «Активные поиски» — главная страница после логина
- Dashboard.jsx: счётчики (активных / всего / завершено за 24ч из
  /operations/summary), фильтры по статусу, сетка карточек операций
  (title, статус-бейдж, подразделение, время обновления), клик →
  рабочее пространство (анализ по карточке операции).
- Роут /dashboard, «/» → дашборд (было /desktop).
- CaseForm: после создания карточки автоматически создаётся поисковая
  операция (title «Поиск: имя, N лет») — операция не блокирует анализ.
- VCL-density, палитра ЦОУ (#ff4d00 акценты, статусы цветом).
2026-09-09 21:33:01 +03:00

225 lines
7.1 KiB
React

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import Step1Child from "./Step1Child";
import Step2Health from "./Step2Health";
import Step2bPsychotype from "./Step2bPsychotype";
import Step3Circumstances from "./Step3Circumstances";
import Step4Environment from "./Step4Environment";
import { apiFetch } from "../../api/client";
import "./CaseForm.css";
const INITIAL_DATA = {
has_diagnosis: false,
diagnosis_type: [],
cant_swim: false,
terrain: [],
has_dog: false,
extra_resources: []
};
const CaseForm = ({ isMobile = false }) => {
const navigate = useNavigate();
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState(INITIAL_DATA);
const [errors, setErrors] = useState([]);
const [submitting, setSubmitting] = useState(false);
// Мобильная: 3 шага, Десктоп: 5 шагов
const totalSteps = isMobile ? 3 : 5;
const updateFormData = (updates) => {
setFormData(prev => ({ ...prev, ...updates }));
};
const validateStep = (step) => {
const newErrors = [];
if (step === 1) {
if (!formData.age_years || formData.age_years < 0 || formData.age_years > 120) {
newErrors.push("Укажите корректный возраст");
}
if (!formData.gender) {
newErrors.push("Выберите пол");
}
}
// Для десктопа: Шаг 4 = Обстоятельства
if (step === 4 && !isMobile) {
if (!formData.reason) {
newErrors.push("Укажите причину потери");
}
if (!formData.lost_time) {
newErrors.push("Укажите дату и время потери");
}
}
// Для десктопа: Шаг 5 = Среда и местность
if (step === 5 && !isMobile) {
if (!formData.terrain || formData.terrain.length === 0) {
newErrors.push("Выберите хотя бы один тип местности");
}
if (!formData.tnp_lat || !formData.tnp_lon) {
newErrors.push("Укажите GPS координаты точки потери");
}
}
// Для мобильной: Шаг 3 = Обстоятельства (с GPS)
if (step === 3 && isMobile) {
if (!formData.reason) {
newErrors.push("Укажите причину потери");
}
if (!formData.tnp_lat || !formData.tnp_lon) {
newErrors.push("Укажите GPS координаты ТНП");
}
}
setErrors(newErrors);
return newErrors.length === 0;
};
const handleNext = () => {
if (validateStep(currentStep)) {
setCurrentStep(prev => Math.min(prev + 1, totalSteps));
}
};
const handleBack = () => {
setCurrentStep(prev => Math.max(prev - 1, 1));
};
const handleSubmit = async () => {
if (!validateStep(currentStep)) return;
setSubmitting(true);
setErrors([]);
try {
const payload = {
...formData,
clothes_description: [
formData.clothes_upper,
formData.clothes_lower,
formData.clothes_shoes
].filter(Boolean).join(", ")
};
// Создаем карточку
const response = await apiFetch("/api/v1/cases", {
method: "POST",
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Ошибка при создании карточки");
}
const result = await response.json();
// E3: автоматически создаём поисковую операцию для карточки.
try {
await apiFetch('/api/v1/operations', {
method: 'POST',
body: JSON.stringify({
title: `Поиск: ${formData.child_name || 'без имени'}, ${formData.age_years || '?'} лет`,
case_id: result.id,
}),
});
} catch {
/* Операция не блокирует анализ (создастся позже вручную) */
}
// Перенаправляем на страницу анализа с ID карточки.
// SPA-переход: полная перезагрузка теряла состояние приложения.
navigate(`/analysis/${result.id}`);
} catch (err) {
setErrors([err.message || "Ошибка при отправке данных"]);
} finally {
setSubmitting(false);
}
};
const renderStep = () => {
// Мобильная версия: 3 шага
// 1 = Ребёнок, 2 = Здоровье, 3 = Обстоятельства
if (isMobile) {
switch (currentStep) {
case 1: return <Step1Child data={formData} updateData={updateFormData} />;
case 2: return <Step2Health data={formData} updateData={updateFormData} />;
case 3: return <Step3Circumstances data={formData} updateData={updateFormData} />;
default: return null;
}
}
// Десктопная версия: 5 шагов
// 1 = Ребёнок, 2 = Здоровье, 3 = Психотип, 4 = Обстоятельства, 5 = Среда
switch (currentStep) {
case 1: return <Step1Child data={formData} updateData={updateFormData} />;
case 2: return <Step2Health data={formData} updateData={updateFormData} />;
case 3: return <Step2bPsychotype data={formData} updateData={updateFormData} />;
case 4: return <Step3Circumstances data={formData} updateData={updateFormData} />;
case 5: return <Step4Environment data={formData} updateData={updateFormData} />;
default: return null;
}
};
return (
<div className="case-form">
<div className="form-header">
<h1>Карточка поиска</h1>
<div className="step-indicator">
Шаг {currentStep} из {totalSteps}
</div>
</div>
{errors.length > 0 && (
<div className="error-banner">
{errors.map((err, idx) => (
<div key={idx}>{err}</div>
))}
</div>
)}
<div className="form-content">
{renderStep()}
</div>
<div className="form-navigation">
{currentStep > 1 && (
<button
type="button"
className="btn-secondary"
onClick={handleBack}
disabled={submitting}
>
Назад
</button>
)}
{currentStep < totalSteps ? (
<button
type="button"
className="btn-primary"
onClick={handleNext}
disabled={submitting}
>
Далее
</button>
) : (
<button
type="button"
className="btn-primary submit-btn"
onClick={handleSubmit}
disabled={submitting}
>
{submitting ? "Создание карточки..." : "Создать карточку"}
</button>
)}
</div>
</div>
);
};
export default CaseForm;