ab3eebe274
- Dashboard.jsx: счётчики (активных / всего / завершено за 24ч из /operations/summary), фильтры по статусу, сетка карточек операций (title, статус-бейдж, подразделение, время обновления), клик → рабочее пространство (анализ по карточке операции). - Роут /dashboard, «/» → дашборд (было /desktop). - CaseForm: после создания карточки автоматически создаётся поисковая операция (title «Поиск: имя, N лет») — операция не блокирует анализ. - VCL-density, палитра ЦОУ (#ff4d00 акценты, статусы цветом).
225 lines
7.1 KiB
React
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;
|