Import Vector lab project

This commit is contained in:
root
2026-06-06 18:31:55 +00:00
commit fcc22063ed
105 changed files with 31639 additions and 0 deletions
@@ -0,0 +1,344 @@
.case-form {
max-width: 800px;
margin: 0 auto;
padding: 24px;
background: #0a0c0f;
min-height: 100vh;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #21262d;
}
.form-header h1 {
margin: 0;
font-size: 28px;
color: #e6edf3;
font-weight: 700;
}
.step-indicator {
font-size: 14px;
color: #8b949e;
font-weight: 500;
}
.form-content {
margin-bottom: 24px;
}
.form-step {
background: #161b22;
border: 1px solid #21262d;
border-radius: 12px;
padding: 24px;
}
.form-step h2 {
margin: 0 0 20px 0;
font-size: 20px;
color: #e6edf3;
font-weight: 600;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: #e6edf3;
font-weight: 500;
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group textarea,
.form-group select {
width: 100%;
padding: 10px 12px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
color: #e6edf3;
font-size: 14px;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #ff4d00;
}
.form-group textarea {
resize: vertical;
font-family: inherit;
}
.button-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.button-group button {
padding: 10px 16px;
background: #21262d;
color: #e6edf3;
border: 1px solid #30363d;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.button-group button:hover {
background: #30363d;
border-color: #ff4d00;
}
.button-group button.selected {
background: #ff4d00;
color: #ffffff;
border-color: #ff4d00;
font-weight: 600;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.checkbox-label:hover {
background: #161b22;
border-color: #ff4d00;
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #ff4d00;
}
.form-navigation {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.form-navigation button {
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.form-navigation button:first-child {
background: #21262d;
color: #e6edf3;
border: 1px solid #30363d;
}
.form-navigation button:first-child:hover {
background: #30363d;
}
.form-navigation button:last-child {
background: #ff4d00;
color: #ffffff;
}
.form-navigation button:last-child:hover {
background: #ff6a2e;
}
.form-navigation button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error-banner {
background: #da3633;
color: white;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
}
.error-banner div {
margin-bottom: 4px;
}
.error-banner div:last-child {
margin-bottom: 0;
}
/* Психотип - улучшенная читаемость */
.psychotype-section {
background: #161b22;
border-left: 4px solid #ff4d00;
padding: 24px;
}
.section-header {
margin-bottom: 24px;
}
.section-subtitle {
margin: 8px 0 0 0;
font-size: 14px;
color: #8b949e;
font-style: italic;
}
.psychotype-question {
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid #21262d;
}
.psychotype-question:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.question-label {
display: block;
margin-bottom: 16px;
font-size: 16px;
color: #e6edf3;
font-weight: 600;
}
.button-group-vertical {
display: flex;
flex-direction: column;
gap: 12px;
}
.psychotype-btn {
padding: 16px 20px;
background: #0d1117;
color: #e6edf3;
border: 2px solid #30363d;
border-radius: 8px;
font-size: 15px;
text-align: left;
cursor: pointer;
transition: all 0.2s;
width: 100%;
}
.psychotype-btn:hover {
background: #161b22;
border-color: #ff4d00;
transform: translateX(4px);
}
.psychotype-btn.selected {
background: #ff4d00;
color: #ffffff;
border-color: #ff4d00;
font-weight: 600;
}
@media (max-width: 768px) {
.case-form {
padding: 16px;
}
.form-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.form-step {
padding: 16px;
}
.form-navigation {
flex-direction: column;
}
.form-navigation button {
width: 100%;
}
.button-group {
flex-direction: column;
}
.button-group button {
width: 100%;
}
}
/* GPS Button */
.gps-button {
width: 100%;
padding: 12px 16px;
background: #238636;
color: #ffffff;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.gps-button:hover {
background: #2ea043;
}
.gps-button:disabled {
background: #21262d;
color: #8b949e;
cursor: not-allowed;
}
/* Datetime input */
input[type="datetime-local"] {
width: 100%;
padding: 10px 12px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
color: #e6edf3;
font-size: 14px;
transition: border-color 0.2s;
}
input[type="datetime-local"]:focus {
outline: none;
border-color: #ff4d00;
}
@@ -0,0 +1,212 @@
import React, { useState } from "react";
import Step1Child from "./Step1Child";
import Step2Health from "./Step2Health";
import Step2bPsychotype from "./Step2bPsychotype";
import Step3Circumstances from "./Step3Circumstances";
import Step4Environment from "./Step4Environment";
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 [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("Укажите дату и время потери");
}
if (!formData.last_known_place) {
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 fetch("/api/v1/cases", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Ошибка при создании карточки");
}
const result = await response.json();
console.log("Case created:", result);
// Перенаправляем на страницу анализа с ID карточки
window.location.href = `/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;
@@ -0,0 +1,92 @@
import React from "react";
const Step1Child = ({ data, updateData }) => {
return (
<div className="form-step">
<h2>Шаг 1: Ребёнок</h2>
<div className="form-group">
<label>Имя *</label>
<input
type="text"
value={data.child_name || ""}
onChange={(e) => updateData({ child_name: e.target.value })}
placeholder="Имя ребёнка"
/>
</div>
<div className="form-group">
<label>Возраст (лет) *</label>
<input
type="number"
value={data.age_years || ""}
onChange={(e) => updateData({ age_years: parseInt(e.target.value) })}
min="0"
max="120"
/>
</div>
<div className="form-group">
<label>Пол *</label>
<div className="button-group">
<button
type="button"
className={data.gender === "М" ? "selected" : ""}
onClick={() => updateData({ gender: "М" })}
>
Мужской
</button>
<button
type="button"
className={data.gender === "Ж" ? "selected" : ""}
onClick={() => updateData({ gender: "Ж" })}
>
Женский
</button>
</div>
</div>
<div className="form-group">
<label>Одежда: верх (цвет, тип) *</label>
<input
type="text"
value={data.clothes_upper || ""}
onChange={(e) => updateData({ clothes_upper: e.target.value })}
placeholder="Красная куртка"
/>
</div>
<div className="form-group">
<label>Одежда: низ (цвет, тип)</label>
<input
type="text"
value={data.clothes_lower || ""}
onChange={(e) => updateData({ clothes_lower: e.target.value })}
placeholder="Синие джинсы"
/>
</div>
<div className="form-group">
<label>Обувь (тип, цвет)</label>
<input
type="text"
value={data.clothes_shoes || ""}
onChange={(e) => updateData({ clothes_shoes: e.target.value })}
placeholder="Чёрные кроссовки"
/>
</div>
<div className="form-group">
<label>Особые приметы</label>
<textarea
value={data.special_marks || ""}
onChange={(e) => updateData({ special_marks: e.target.value })}
placeholder="Родинка на щеке, носит очки..."
rows={3}
/>
</div>
</div>
);
};
export default Step1Child;
@@ -0,0 +1,188 @@
import React from "react";
const Step2Health = ({ data, updateData }) => {
const toggleDiagnosis = (diagnosis) => {
const current = data.diagnosis_type || [];
const updated = current.includes(diagnosis)
? current.filter((d) => d !== diagnosis)
: [...current, diagnosis];
updateData({
diagnosis_type: updated,
has_diagnosis: updated.length > 0
});
};
return (
<div className="form-step">
<h2>Шаг 2: Здоровье и особенности</h2>
<div className="form-group">
<label>Заболевания / диагнозы *</label>
<div className="checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
checked={data.diagnosis_type?.includes("здоров")}
onChange={() => toggleDiagnosis("здоров")}
/>
Здоров
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.diagnosis_type?.includes("РАС")}
onChange={() => toggleDiagnosis("РАС")}
/>
РАС / Аутизм
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.diagnosis_type?.includes("эпилепсия")}
onChange={() => toggleDiagnosis("эпилепсия")}
/>
Эпилепсия
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.diagnosis_type?.includes("СДВГ")}
onChange={() => toggleDiagnosis("СДВГ")}
/>
СДВГ
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.diagnosis_type?.includes("ЗПР")}
onChange={() => toggleDiagnosis("ЗПР")}
/>
Задержка ПР / ЗПР
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.diagnosis_type?.includes("ДЦП")}
onChange={() => toggleDiagnosis("ДЦП")}
/>
ДЦП
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.diagnosis_type?.includes("слабое_зрение")}
onChange={() => toggleDiagnosis("слабое_зрение")}
/>
Слабое зрение
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.diagnosis_type?.includes("слабый_слух")}
onChange={() => toggleDiagnosis("слабый_слух")}
/>
Слабый слух
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.cant_swim}
onChange={(e) => updateData({ cant_swim: e.target.checked })}
/>
Не умеет плавать
</label>
</div>
</div>
<div className="form-group">
<label>Уровень физподготовки</label>
<div className="button-group">
<button
type="button"
className={data.fitness_level === "low" ? "selected" : ""}
onClick={() => updateData({ fitness_level: "low" })}
>
Низкий
</button>
<button
type="button"
className={data.fitness_level === "medium" ? "selected" : ""}
onClick={() => updateData({ fitness_level: "medium" })}
>
Средний
</button>
<button
type="button"
className={data.fitness_level === "high" ? "selected" : ""}
onClick={() => updateData({ fitness_level: "high" })}
>
Высокий
</button>
</div>
</div>
<div className="form-group">
<label>Наличие транспорта *</label>
<div className="button-group">
<button
type="button"
className={data.has_transport === "none" ? "selected" : ""}
onClick={() => updateData({ has_transport: "none" })}
>
Нет
</button>
<button
type="button"
className={data.has_transport === "bike" ? "selected" : ""}
onClick={() => updateData({ has_transport: "bike" })}
>
Велосипед
</button>
<button
type="button"
className={data.has_transport === "scooter" ? "selected" : ""}
onClick={() => updateData({ has_transport: "scooter" })}
>
Самокат
</button>
<button
type="button"
className={data.has_transport === "other" ? "selected" : ""}
onClick={() => updateData({ has_transport: "other" })}
>
Другое
</button>
</div>
</div>
<div className="form-group">
<label>Телефон *</label>
<div className="button-group">
<button
type="button"
className={data.phone_status === "answers" ? "selected" : ""}
onClick={() => updateData({ phone_status: "answers" })}
>
Есть, отвечает
</button>
<button
type="button"
className={data.phone_status === "silent" ? "selected" : ""}
onClick={() => updateData({ phone_status: "silent" })}
>
Есть, не отвечает
</button>
<button
type="button"
className={data.phone_status === "none" ? "selected" : ""}
onClick={() => updateData({ phone_status: "none" })}
>
Нет
</button>
</div>
</div>
</div>
);
};
export default Step2Health;
@@ -0,0 +1,152 @@
import React from "react";
const Step2bPsychotype = ({ data, updateData }) => {
const updateAnswer = (question, value) => {
updateData({
psychotype_answers: {
...data.psychotype_answers,
[question]: value
}
});
};
const isSelected = (question, value) => {
return data.psychotype_answers?.[question] === value;
};
return (
<div className="form-step psychotype-section">
<div className="section-header">
<h2>Шаг 2б: Поведение ребёнка</h2>
<p className="section-subtitle">
Спросить у родителей. Помогает уточнить зону поиска. Можно пропустить.
</p>
</div>
<div className="psychotype-question">
<label className="question-label">
Как ведёт себя в незнакомой обстановке?
</label>
<div className="button-group-vertical">
<button
type="button"
className={isSelected("unfamiliar_behavior", "explore") ? "psychotype-btn selected" : "psychotype-btn"}
onClick={() => updateAnswer("unfamiliar_behavior", "explore")}
>
Активно исследует
</button>
<button
type="button"
className={isSelected("unfamiliar_behavior", "wait") ? "psychotype-btn selected" : "psychotype-btn"}
onClick={() => updateAnswer("unfamiliar_behavior", "wait")}
>
Ждёт и наблюдает
</button>
<button
type="button"
className={isSelected("unfamiliar_behavior", "freeze") ? "psychotype-btn selected" : "psychotype-btn"}
onClick={() => updateAnswer("unfamiliar_behavior", "freeze")}
>
Замирает
</button>
<button
type="button"
className={isSelected("unfamiliar_behavior", "panic") ? "psychotype-btn selected" : "psychotype-btn"}
onClick={() => updateAnswer("unfamiliar_behavior", "panic")}
>
Паникует
</button>
</div>
</div>
<div className="psychotype-question">
<label className="question-label">
Реакция на стресс и неудачи?
</label>
<div className="button-group-vertical">
<button
type="button"
className={isSelected("stress_reaction", "angry") ? "psychotype-btn selected" : "psychotype-btn"}
onClick={() => updateAnswer("stress_reaction", "angry")}
>
Злится, кричит
</button>
<button
type="button"
className={isSelected("stress_reaction", "cry") ? "psychotype-btn selected" : "psychotype-btn"}
onClick={() => updateAnswer("stress_reaction", "cry")}
>
Плачет, замыкается
</button>
<button
type="button"
className={isSelected("stress_reaction", "calm") ? "psychotype-btn selected" : "psychotype-btn"}
onClick={() => updateAnswer("stress_reaction", "calm")}
>
Спокойно ищет выход
</button>
</div>
</div>
<div className="psychotype-question">
<label className="question-label">
Лидер или ведомый?
</label>
<div className="button-group-vertical">
<button
type="button"
className={isSelected("leadership", "always_leader") ? "psychotype-btn selected" : "psychotype-btn"}
onClick={() => updateAnswer("leadership", "always_leader")}
>
Всегда лидер
</button>
<button
type="button"
className={isSelected("leadership", "sometimes") ? "psychotype-btn selected" : "psychotype-btn"}
onClick={() => updateAnswer("leadership", "sometimes")}
>
Иногда
</button>
<button
type="button"
className={isSelected("leadership", "always_follower") ? "psychotype-btn selected" : "psychotype-btn"}
onClick={() => updateAnswer("leadership", "always_follower")}
>
Всегда ведомый
</button>
</div>
</div>
<div className="psychotype-question">
<label className="question-label">
Любит рисковать?
</label>
<div className="button-group-vertical">
<button
type="button"
className={isSelected("risk_taking", "very") ? "psychotype-btn selected" : "psychotype-btn"}
onClick={() => updateAnswer("risk_taking", "very")}
>
Очень
</button>
<button
type="button"
className={isSelected("risk_taking", "sometimes") ? "psychotype-btn selected" : "psychotype-btn"}
onClick={() => updateAnswer("risk_taking", "sometimes")}
>
Иногда
</button>
<button
type="button"
className={isSelected("risk_taking", "no_cautious") ? "psychotype-btn selected" : "psychotype-btn"}
onClick={() => updateAnswer("risk_taking", "no_cautious")}
>
Нет, осторожный
</button>
</div>
</div>
</div>
);
};
export default Step2bPsychotype;
@@ -0,0 +1,162 @@
import React from "react";
const Step3Circumstances = ({ data, updateData }) => {
return (
<div className="form-step">
<h2>Шаг 3: Обстоятельства потери</h2>
<div className="form-group">
<label>Причина потери *</label>
<div className="button-group">
<button
type="button"
className={data.reason === "ушёл_сам" ? "selected" : ""}
onClick={() => updateData({ reason: "ушёл_сам" })}
>
Ушёл сам
</button>
<button
type="button"
className={data.reason === "заблудился" ? "selected" : ""}
onClick={() => updateData({ reason: "заблудился" })}
>
Заблудился
</button>
<button
type="button"
className={data.reason === "отстал_от_группы" ? "selected" : ""}
onClick={() => updateData({ reason: "отстал_от_группы" })}
>
Отстал от группы
</button>
<button
type="button"
className={data.reason === "конфликт" ? "selected" : ""}
onClick={() => updateData({ reason: "конфликт" })}
>
После конфликта
</button>
<button
type="button"
className={data.reason === "неизвестно" ? "selected" : ""}
onClick={() => updateData({ reason: "неизвестно" })}
>
Неизвестно
</button>
</div>
</div>
<div className="form-group">
<label>Дата и время потери *</label>
<input
type="datetime-local"
value={data.lost_time || ""}
onChange={(e) => updateData({ lost_time: e.target.value })}
/>
</div>
<div className="form-group">
<label>Прошло времени с момента потери (часов)</label>
<input
type="number"
value={data.elapsed_hours || ""}
onChange={(e) => updateData({ elapsed_hours: parseFloat(e.target.value) })}
min="0"
step="0.5"
placeholder="Например: 2.5"
/>
</div>
<div className="form-group">
<label>Последнее известное место *</label>
<input
type="text"
value={data.last_known_place || ""}
onChange={(e) => updateData({ last_known_place: e.target.value })}
placeholder="Деревня Заречье, у магазина"
/>
</div>
<div className="form-group">
<label>Предполагаемое направление движения</label>
<div className="button-group">
<button
type="button"
className={data.direction === "N" ? "selected" : ""}
onClick={() => updateData({ direction: "N" })}
>
Север
</button>
<button
type="button"
className={data.direction === "NE" ? "selected" : ""}
onClick={() => updateData({ direction: "NE" })}
>
Северо-Восток
</button>
<button
type="button"
className={data.direction === "E" ? "selected" : ""}
onClick={() => updateData({ direction: "E" })}
>
Восток
</button>
<button
type="button"
className={data.direction === "SE" ? "selected" : ""}
onClick={() => updateData({ direction: "SE" })}
>
Юго-Восток
</button>
<button
type="button"
className={data.direction === "S" ? "selected" : ""}
onClick={() => updateData({ direction: "S" })}
>
Юг
</button>
<button
type="button"
className={data.direction === "SW" ? "selected" : ""}
onClick={() => updateData({ direction: "SW" })}
>
Юго-Запад
</button>
<button
type="button"
className={data.direction === "W" ? "selected" : ""}
onClick={() => updateData({ direction: "W" })}
>
Запад
</button>
<button
type="button"
className={data.direction === "NW" ? "selected" : ""}
onClick={() => updateData({ direction: "NW" })}
>
Северо-Запад
</button>
<button
type="button"
className={data.direction === "unknown" ? "selected" : ""}
onClick={() => updateData({ direction: "unknown" })}
>
Неизвестно
</button>
</div>
</div>
<div className="form-group">
<label>Описание обстоятельств</label>
<textarea
value={data.circumstances_description || ""}
onChange={(e) => updateData({ circumstances_description: e.target.value })}
placeholder="Подробное описание: что произошло, кто видел последним, что говорил ребёнок..."
rows={5}
/>
</div>
</div>
);
};
export default Step3Circumstances;
@@ -0,0 +1,274 @@
import React, { useEffect } from "react";
const Step4Environment = ({ data, updateData }) => {
const toggleTerrain = (terrain) => {
const current = data.terrain || [];
const updated = current.includes(terrain)
? current.filter((t) => t !== terrain)
: [...current, terrain];
updateData({ terrain: updated });
};
// Автоматически определяем сезон из даты потери
useEffect(() => {
if (data.lost_time && !data.season) {
const date = new Date(data.lost_time);
const month = date.getMonth() + 1; // 1-12
let season = 'лето';
if (month >= 3 && month <= 5) season = 'весна';
else if (month >= 6 && month <= 8) season = 'лето';
else if (month >= 9 && month <= 11) season = 'осень';
else season = 'зима'; // 12, 1, 2
updateData({ season });
}
}, [data.lost_time]);
const handleGetLocation = () => {
if (!navigator.geolocation) {
alert("Геолокация не поддерживается вашим браузером");
return;
}
updateData({ gps_loading: true });
navigator.geolocation.getCurrentPosition(
(position) => {
updateData({
tnp_lat: position.coords.latitude.toFixed(6),
tnp_lon: position.coords.longitude.toFixed(6),
gps_loading: false
});
},
(error) => {
alert("Ошибка получения координат: " + error.message);
updateData({ gps_loading: false });
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}
);
};
return (
<div className="form-step">
<h2>Шаг 5: Среда и местность</h2>
{/* Сезон определяется автоматически, показываем только для информации */}
{data.season && (
<div className="form-group">
<label>Сезон (определен автоматически)</label>
<div style={{ padding: "12px", background: "#161b22", borderRadius: "6px", color: "#8b949e" }}>
{data.season.charAt(0).toUpperCase() + data.season.slice(1)}
</div>
</div>
)}
<div className="form-group">
<label>Температура (°C)</label>
<input
type="number"
value={data.temperature || ""}
onChange={(e) => updateData({ temperature: parseFloat(e.target.value) })}
min="-40"
max="40"
step="1"
placeholder="Например: 15"
/>
</div>
<div className="form-group">
<label>Осадки</label>
<div className="button-group">
<button
type="button"
className={data.precipitation === "нет" ? "selected" : ""}
onClick={() => updateData({ precipitation: "нет" })}
>
Нет
</button>
<button
type="button"
className={data.precipitation === "дождь" ? "selected" : ""}
onClick={() => updateData({ precipitation: "дождь" })}
>
Дождь
</button>
<button
type="button"
className={data.precipitation === "снег" ? "selected" : ""}
onClick={() => updateData({ precipitation: "снег" })}
>
Снег
</button>
<button
type="button"
className={data.precipitation === "туман" ? "selected" : ""}
onClick={() => updateData({ precipitation: "туман" })}
>
Туман
</button>
</div>
</div>
<div className="form-group">
<label>Видимость</label>
<div className="button-group">
<button
type="button"
className={data.visibility === "хорошая" ? "selected" : ""}
onClick={() => updateData({ visibility: "хорошая" })}
>
Хорошая
</button>
<button
type="button"
className={data.visibility === "средняя" ? "selected" : ""}
onClick={() => updateData({ visibility: "средняя" })}
>
Средняя
</button>
<button
type="button"
className={data.visibility === "плохая" ? "selected" : ""}
onClick={() => updateData({ visibility: "плохая" })}
>
Плохая
</button>
</div>
</div>
<div className="form-group">
<label>Типы местности (можно выбрать несколько) *</label>
<div className="checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
checked={data.terrain?.includes("лес")}
onChange={() => toggleTerrain("лес")}
/>
Лес
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.terrain?.includes("поле")}
onChange={() => toggleTerrain("поле")}
/>
Поле
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.terrain?.includes("болото")}
onChange={() => toggleTerrain("болото")}
/>
Болото
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.terrain?.includes("водоём")}
onChange={() => toggleTerrain("водоём")}
/>
Водоём (река, озеро)
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.terrain?.includes("населённый_пункт")}
onChange={() => toggleTerrain("населённый_пункт")}
/>
Населённый пункт
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.terrain?.includes("дороги")}
onChange={() => toggleTerrain("дороги")}
/>
Дороги / тропы
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.terrain?.includes("заброшенные_здания")}
onChange={() => toggleTerrain("заброшенные_здания")}
/>
Заброшенные здания
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.terrain?.includes("сельхоз")}
onChange={() => toggleTerrain("сельхоз")}
/>
Сельхоз угодья
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.terrain?.includes("овраги")}
onChange={() => toggleTerrain("овраги")}
/>
Овраги / ямы
</label>
</div>
</div>
<div className="form-group">
<label>Координаты точки потери (GPS) *</label>
<div style={{ marginBottom: "12px" }}>
<button
type="button"
className="gps-button"
onClick={handleGetLocation}
disabled={data.gps_loading}
>
{data.gps_loading ? "Получение координат..." : "📍 Получить текущие координаты"}
</button>
</div>
<div style={{ display: "flex", gap: "12px" }}>
<div style={{ flex: 1 }}>
<label style={{ fontSize: "12px", color: "#8b949e" }}>Широта</label>
<input
type="number"
value={data.tnp_lat || ""}
onChange={(e) => updateData({ tnp_lat: e.target.value })}
placeholder="53.9000"
step="0.000001"
/>
</div>
<div style={{ flex: 1 }}>
<label style={{ fontSize: "12px", color: "#8b949e" }}>Долгота</label>
<input
type="number"
value={data.tnp_lon || ""}
onChange={(e) => updateData({ tnp_lon: e.target.value })}
placeholder="27.5667"
step="0.000001"
/>
</div>
</div>
</div>
<div className="form-group">
<label>Или введите адрес</label>
<input
type="text"
value={data.tnp_address || ""}
onChange={(e) => updateData({ tnp_address: e.target.value })}
placeholder="Минск, ул. Ленина 10"
/>
<p style={{ fontSize: "12px", color: "#8b949e", marginTop: "4px" }}>
Адрес будет преобразован в координаты автоматически
</p>
</div>
</div>
);
};
export default Step4Environment;
@@ -0,0 +1,159 @@
import React from "react";
const Step5Resources = ({ data, updateData }) => {
const toggleEquipment = (equipment) => {
const current = data.equipment || [];
const updated = current.includes(equipment)
? current.filter((e) => e !== equipment)
: [...current, equipment];
updateData({ equipment: updated });
};
return (
<div className="form-step">
<h2>Шаг 5: Ресурсы для поиска</h2>
<div className="form-group">
<label>Количество поисковых групп</label>
<input
type="number"
value={data.search_groups || ""}
onChange={(e) => updateData({ search_groups: parseInt(e.target.value) })}
min="0"
max="100"
placeholder="Например: 3"
/>
</div>
<div className="form-group">
<label>Количество человек в поиске</label>
<input
type="number"
value={data.search_people || ""}
onChange={(e) => updateData({ search_people: parseInt(e.target.value) })}
min="0"
max="1000"
placeholder="Например: 25"
/>
</div>
<div className="form-group">
<label>Поисковая собака</label>
<div className="button-group">
<button
type="button"
className={data.has_dog === true ? "selected" : ""}
onClick={() => updateData({ has_dog: true })}
>
Да
</button>
<button
type="button"
className={data.has_dog === false ? "selected" : ""}
onClick={() => updateData({ has_dog: false })}
>
Нет
</button>
</div>
</div>
<div className="form-group">
<label>Дополнительное оборудование (можно выбрать несколько)</label>
<div className="checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
checked={data.equipment?.includes("дрон")}
onChange={() => toggleEquipment("дрон")}
/>
Дрон / БПЛА
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.equipment?.includes("тепловизор")}
onChange={() => toggleEquipment("тепловизор")}
/>
Тепловизор
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.equipment?.includes("квадроцикл")}
onChange={() => toggleEquipment("квадроцикл")}
/>
Квадроцикл / мотовездеход
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.equipment?.includes("лодка")}
onChange={() => toggleEquipment("лодка")}
/>
Лодка
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.equipment?.includes("вертолёт")}
onChange={() => toggleEquipment("вертолёт")}
/>
Вертолёт
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.equipment?.includes("рация")}
onChange={() => toggleEquipment("рация")}
/>
Рации
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.equipment?.includes("gps")}
onChange={() => toggleEquipment("gps")}
/>
GPS навигаторы
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.equipment?.includes("медик")}
onChange={() => toggleEquipment("медик")}
/>
Медицинская бригада
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.equipment?.includes("прожектор")}
onChange={() => toggleEquipment("прожектор")}
/>
Прожекторы / фонари
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={data.equipment?.includes("мегафон")}
onChange={() => toggleEquipment("мегафон")}
/>
Мегафон / громкоговоритель
</label>
</div>
</div>
<div className="form-group">
<label>Дополнительная информация о ресурсах</label>
<textarea
value={data.resources_notes || ""}
onChange={(e) => updateData({ resources_notes: e.target.value })}
placeholder="Особенности организации поиска, доступные ресурсы, ограничения..."
rows={4}
/>
</div>
</div>
);
};
export default Step5Resources;
@@ -0,0 +1 @@
export { default } from "./CaseForm";