ui: переработка страницы результатов — логические блоки, та же палитра

Проблема: 10 инфо-карточек в один столбец, карта с зонами — в самом
низу, немедленные действия терялись в середине, формула занимала
пол-экрана раньше вывода.

Новая структура (сверху вниз = порядок чтения штабом):
1. Шапка-панель решения: срочность | радиус (+множитель профилей
   отдельным чипом) | критические действия списком — решение видно
   без прокрутки; summary свёрнут в одну строку.
2. Главный ряд: КАРТА (2/3) + список ЗОН (1/3) рядом — зоны и карта
   теперь одно визуальное целое; geo-чипы (💧/🚂) и предупреждения
   геослоя компактными плашками в шапке карты.
3. Ряд поведение+действия: психотип (тактика 2×2 сеткой), теги
   профилей, unmodeled-пометки; немедленные действия нумерованным
   списком, КРИТИЧНО-пункты акцентной рамкой.
4. Ряд обоснование радиуса: формула моно-шрифтом + коэффициенты
   чипами (НормС/СП/СКД/СУ/СУТ/ВВС/ВП с подсказками) + психотип-
   поправки полос чипами.
5. Низ: справка (обстоятельства/здоровье/среда) сеткой 3 колонки.

Цветовая схема не тронута: var(--accent) #ff4d00, зоны #ff4d00/
#ff9f1a/#ffd166, тёмная тема ЦОУ. Направления зон — кириллицей
(С/СВ/В/...). Все поля бэкенда рендерятся (проверено живым analyze).
This commit is contained in:
2026-09-09 13:53:17 +03:00
parent 4531ac50b9
commit 32af8975f9
2 changed files with 714 additions and 395 deletions
+486 -202
View File
@@ -4,19 +4,24 @@
color: var(--text-primary);
}
/* Header */
/* ===== Header: кейс + панель решения ===== */
.result-header {
background: linear-gradient(135deg, rgba(255, 77, 0, 0.1) 0%, rgba(255, 77, 0, 0.05) 100%);
border-bottom: 2px solid rgba(255, 77, 0, 0.3);
padding: 32px 24px;
margin-bottom: 32px;
padding: 24px 24px 20px;
margin-bottom: 24px;
}
.header-top {
max-width: 1400px;
margin: 0 auto;
}
.result-header h1 {
font-size: 36px;
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
margin: 0 0 16px 0;
margin: 0 0 10px 0;
display: flex;
align-items: center;
gap: 12px;
@@ -30,8 +35,8 @@
.case-info {
display: flex;
flex-wrap: wrap;
gap: 16px;
font-size: 16px;
gap: 12px;
font-size: 14px;
color: var(--text-secondary);
align-items: center;
}
@@ -41,167 +46,244 @@
font-weight: 700;
}
/* Content */
/* Панель решения: срочность | радиус | критические действия */
.decision-strip {
max-width: 1400px;
margin: 18px 0 0 0;
display: grid;
grid-template-columns: 200px 220px 1fr;
gap: 12px;
}
.decision-cell {
background: var(--bg-secondary);
border: 1px solid rgba(255, 77, 0, 0.25);
border-radius: 10px;
padding: 12px 16px;
min-width: 0;
}
.decision-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.6px;
color: var(--text-secondary);
margin-bottom: 6px;
}
.decision-value {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: baseline;
gap: 8px;
line-height: 1.2;
}
.radius-mult {
font-size: 14px;
font-weight: 600;
color: var(--accent);
background: rgba(255, 77, 0, 0.12);
padding: 2px 8px;
border-radius: 6px;
}
/* Срочность — та же цветовая схема (акцентные тона ЦОУ) */
.urgency-cell.urgency-критический {
border-color: #ff4d00;
background: rgba(255, 77, 0, 0.12);
}
.urgency-cell.urgency-критический .decision-value { color: #ff4d00; }
.urgency-cell.urgency-высокий {
border-color: #ff9f1a;
background: rgba(255, 159, 26, 0.12);
}
.urgency-cell.urgency-высокий .decision-value { color: #ff9f1a; }
.urgency-cell.urgency-средний {
border-color: #ffd166;
background: rgba(255, 209, 102, 0.1);
}
.urgency-cell.urgency-средний .decision-value { color: #ffd166; }
.urgency-cell.urgency-стандартный .decision-value { color: var(--text-primary); }
.warnings-cell { display: flex; flex-direction: column; }
.decision-warnings {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
font-size: 13px;
line-height: 1.45;
color: var(--text-primary);
}
.decision-warnings li::before {
content: '🚨 ';
color: #ff4d00;
}
.decision-warnings.muted {
color: var(--text-secondary);
font-size: 13px;
}
.header-summary {
max-width: 1400px;
margin: 14px 0 0 0;
font-size: 14px;
line-height: 1.6;
color: var(--text-secondary);
}
/* ===== Content ===== */
.result-content {
max-width: 1400px;
margin: 0 auto;
padding: 0 24px 48px;
display: flex;
flex-direction: column;
gap: 24px;
gap: 20px;
}
/* Urgency Block */
.urgency-block {
padding: 24px;
border-radius: 12px;
border: 2px solid;
/* ===== Ряд 1: карта + зоны ===== */
.main-row {
display: grid;
gap: 16px;
align-items: start;
}
.main-row.with-map {
grid-template-columns: minmax(0, 1.7fr) minmax(320px, 1fr);
}
.main-row.no-map {
grid-template-columns: 1fr;
}
.section-head {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.urgency-block.urgency-высокий {
background: rgba(255, 77, 0, 0.15);
border-color: #ff4d00;
.section-head h2,
.info-card h3 {
font-size: 17px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.urgency-block.urgency-средний {
background: rgba(255, 159, 26, 0.15);
border-color: #ff9f1a;
}
.urgency-block.urgency-стандартный {
background: rgba(255, 209, 102, 0.15);
border-color: #ffd166;
}
.urgency-level {
font-size: 24px;
font-weight: 700;
letter-spacing: 0.5px;
}
.urgency-block.urgency-высокий .urgency-level {
color: #ff4d00;
}
.urgency-block.urgency-средний .urgency-level {
color: #ff9f1a;
}
.urgency-block.urgency-стандартный .urgency-level {
color: #ffd166;
}
.urgency-reason {
font-size: 16px;
line-height: 1.6;
color: var(--text-secondary);
}
/* Info Cards */
.map-section,
.zones-panel,
.info-card {
background: var(--bg-secondary);
border: 1px solid rgba(255, 77, 0, 0.2);
border-radius: 12px;
padding: 24px;
padding: 18px;
min-width: 0;
}
.info-card h3 {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 20px 0;
/* Гео-чипы в шапке карты */
.geo-chips {
display: flex;
align-items: center;
gap: 8px;
}
.info-card p {
margin: 12px 0;
line-height: 1.8;
.geo-chip {
font-size: 12px;
font-weight: 600;
padding: 4px 10px;
border-radius: 6px;
border: 1px solid var(--border);
color: var(--text-secondary);
background: var(--input-bg);
}
.geo-chip.water { border-color: rgba(47, 129, 247, 0.5); color: #79b8ff; }
.geo-chip.rail { border-color: rgba(138, 47, 47, 0.7); color: #ff9d9d; }
/* Предупреждения геослоя — компактные плашки */
.geo-warning {
font-size: 13px;
line-height: 1.5;
color: #ff4d00;
background: rgba(255, 77, 0, 0.08);
border: 1px solid rgba(255, 77, 0, 0.4);
border-radius: 8px;
padding: 8px 12px;
margin-bottom: 10px;
}
.geo-warning.rail {
color: #ff7b72;
border-color: rgba(138, 47, 47, 0.7);
background: rgba(138, 47, 47, 0.12);
}
.map-empty {
height: 320px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
background: var(--input-bg);
border-radius: 8px;
border: 1px dashed var(--border);
font-size: 14px;
}
.map-info {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 13px;
color: var(--text-secondary);
}
.info-card p strong {
color: var(--text-primary);
font-weight: 600;
}
.map-info .sep { color: rgba(255, 77, 0, 0.5); }
.info-card hr {
margin: 16px 0;
border: none;
border-top: 1px solid rgba(255, 77, 0, 0.2);
}
/* Distance Info */
.distance-info {
display: grid;
gap: 24px;
}
.distance-value {
font-size: 48px;
font-weight: 700;
color: var(--accent);
text-align: center;
padding: 24px;
background: rgba(255, 77, 0, 0.1);
border-radius: 12px;
border: 2px solid rgba(255, 77, 0, 0.3);
}
.distance-details {
display: grid;
gap: 8px;
}
.distance-details p {
padding: 12px 16px;
background: rgba(255, 77, 0, 0.05);
border-radius: 8px;
border-left: 3px solid var(--accent);
margin: 0;
font-size: 15px;
}
/* Zones Section */
.zones-section {
background: var(--bg-secondary);
border: 1px solid rgba(255, 77, 0, 0.2);
border-radius: 12px;
padding: 24px;
}
.zones-section h2 {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 24px 0;
/* ===== Зоны ===== */
.zones-count {
font-size: 13px;
color: var(--text-secondary);
}
.zones-list {
display: grid;
gap: 16px;
display: flex;
flex-direction: column;
gap: 10px;
max-height: 560px;
overflow-y: auto;
padding-right: 4px;
}
.zone-card {
background: rgba(255, 77, 0, 0.05);
border-radius: 10px;
padding: 20px;
border-left: 4px solid;
transition: all 0.2s ease;
background: rgba(255, 77, 0, 0.04);
border-radius: 8px;
padding: 12px 14px;
border-left: 3px solid var(--border);
transition: background 0.15s ease;
}
.zone-card:hover {
background: rgba(255, 77, 0, 0.08);
transform: translateX(4px);
}
.zone-card.priority-1 {
border-left-color: #ff4d00;
background: rgba(255, 77, 0, 0.1);
background: rgba(255, 77, 0, 0.09);
}
.zone-card.priority-2 {
@@ -216,95 +298,308 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
gap: 16px;
gap: 10px;
}
.zone-title {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
gap: 10px;
min-width: 0;
}
.zone-priority {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
width: 26px;
height: 26px;
background: var(--accent);
color: var(--bg-primary);
border-radius: 50%;
font-weight: 700;
font-size: 16px;
font-size: 13px;
flex-shrink: 0;
}
.zone-name {
font-size: 18px;
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.zone-score {
font-size: 28px;
font-weight: 700;
color: var(--accent);
/* Направление / дистанция / score — справа компактно */
.zone-meta {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.zone-details {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 12px;
font-size: 14px;
.zone-dir {
font-size: 12px;
font-weight: 700;
color: var(--text-secondary);
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: 4px;
padding: 2px 6px;
min-width: 26px;
text-align: center;
}
.zone-details .separator {
color: rgba(255, 77, 0, 0.5);
.zone-dist {
font-size: 13px;
color: var(--text-secondary);
font-variant-numeric: tabular-nums;
}
.zone-score {
font-size: 17px;
font-weight: 700;
color: var(--accent);
min-width: 48px;
text-align: right;
font-variant-numeric: tabular-nums;
}
.zone-reasoning {
padding: 12px 16px;
background: rgba(255, 77, 0, 0.08);
border-radius: 6px;
margin-top: 8px;
font-size: 12.5px;
color: var(--text-secondary);
line-height: 1.5;
}
.zones-empty {
padding: 24px;
text-align: center;
color: var(--text-secondary);
font-size: 14px;
}
/* ===== Ряд 2: поведение + действия ===== */
.behavior-row {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
gap: 20px;
align-items: stretch;
}
.profile-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.profile-tag {
font-size: 12px;
font-weight: 600;
color: var(--accent);
background: rgba(255, 77, 0, 0.1);
border: 1px solid rgba(255, 77, 0, 0.35);
border-radius: 12px;
padding: 3px 10px;
}
.profile-tag.unmodeled {
color: var(--text-secondary);
border-color: var(--border);
background: var(--input-bg);
}
.prediction {
margin: 12px 0 0 0;
font-size: 14.5px;
line-height: 1.6;
color: var(--text-primary);
}
/* Тактика психотипа — сетка 2×2 */
.tactics-grid {
margin-top: 12px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.tactic {
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
font-size: 13px;
line-height: 1.5;
color: var(--text-secondary);
}
.tactic-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(255, 77, 0, 0.8);
margin-bottom: 4px;
font-weight: 600;
}
.unmodeled-note {
margin-top: 10px;
padding: 8px 12px;
background: var(--input-bg);
border-left: 3px solid var(--border);
border-radius: 6px;
font-size: 12.5px;
color: var(--text-secondary);
line-height: 1.6;
}
/* Map Section */
.map-section {
background: var(--bg-secondary);
border: 1px solid rgba(255, 77, 0, 0.2);
border-radius: 12px;
padding: 24px;
.unmodeled-note p { margin: 2px 0; }
/* Немедленные действия — нумерованный список с акцентом */
.actions-list {
margin: 12px 0 0 0;
padding: 0;
list-style: none;
counter-reset: action;
display: flex;
flex-direction: column;
gap: 8px;
}
.map-section h2 {
font-size: 24px;
.actions-list li {
counter-increment: action;
display: flex;
gap: 10px;
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
}
.actions-list li::before {
content: counter(action);
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--accent);
color: var(--bg-primary);
font-weight: 700;
font-size: 12px;
}
.actions-list li.critical {
border-color: #ff4d00;
background: rgba(255, 77, 0, 0.1);
font-weight: 600;
}
.actions-list li.critical::before {
background: #ff4d00;
}
/* ===== Ряд 3: обоснование радиуса ===== */
.radius-formula {
font-size: 12px;
color: var(--text-secondary);
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
}
.coeff-grid {
margin-top: 14px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
}
.coeff-chip {
background: var(--input-bg);
border: 1px solid var(--border);
border-left: 3px solid var(--accent);
border-radius: 8px;
padding: 10px 12px;
}
.coeff-name {
font-size: 13px;
color: var(--text-secondary);
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 6px;
}
.coeff-value {
font-size: 16px;
font-weight: 700;
color: var(--text-primary);
font-variant-numeric: tabular-nums;
}
.coeff-hint {
margin-top: 3px;
font-size: 11.5px;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Психотип-поправки */
.psy-mods {
margin-top: 14px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.psy-mods-title {
font-size: 13px;
color: var(--text-secondary);
margin-right: 4px;
}
.psy-mod-chip {
font-size: 12.5px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 20px 0;
background: rgba(255, 77, 0, 0.07);
border: 1px solid rgba(255, 77, 0, 0.25);
border-radius: 6px;
padding: 4px 10px;
font-variant-numeric: tabular-nums;
}
.map-info {
margin-top: 16px;
padding: 16px;
background: rgba(255, 77, 0, 0.05);
border-radius: 8px;
font-size: 14px;
/* ===== Ряд 4: справочные данные ===== */
.reference-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 20px;
align-items: start;
}
.info-card p {
margin: 8px 0;
font-size: 13.5px;
line-height: 1.6;
color: var(--text-secondary);
}
.map-info p {
margin: 4px 0;
.info-card p strong {
color: var(--text-primary);
font-weight: 600;
}
/* Loading & Error States */
/* ===== Loading & Error ===== */
.loading-state,
.error-state {
display: flex;
@@ -354,45 +649,34 @@
}
.error-state button:hover {
background: #ff6a1a;
background: var(--accent-hover);
transform: translateY(-2px);
}
/* Responsive */
@media (max-width: 768px) {
.result-header {
padding: 24px 16px;
/* ===== Responsive ===== */
@media (max-width: 1100px) {
.main-row.with-map {
grid-template-columns: 1fr;
}
.result-header h1 {
font-size: 28px;
.behavior-row {
grid-template-columns: 1fr;
}
.result-content {
padding: 0 16px 32px;
}
.distance-value {
font-size: 36px;
padding: 20px;
}
.zone-header {
flex-direction: column;
align-items: flex-start;
}
.zone-score {
font-size: 24px;
}
.case-info {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.separator {
display: none;
.reference-grid {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 768px) {
.result-header { padding: 20px 16px 16px; }
.result-header h1 { font-size: 22px; }
.decision-strip {
grid-template-columns: 1fr 1fr;
}
.warnings-cell { grid-column: 1 / -1; }
.result-content { padding: 0 16px 32px; }
.reference-grid { grid-template-columns: 1fr; }
.coeff-grid { grid-template-columns: 1fr 1fr; }
.case-info { flex-direction: column; align-items: flex-start; gap: 6px; }
.separator { display: none; }
.tactics-grid { grid-template-columns: 1fr; }
}
+196 -161
View File
@@ -5,10 +5,12 @@ import { apiFetch } from "../../api/client";
import "./AnalysisResult.css";
const GENDER_LABEL = { m: 'М', f: 'Ж' };
const DIRECTION_RU = { N: 'С', NE: 'СВ', E: 'В', SE: 'ЮВ', S: 'Ю', SW: 'ЮЗ', W: 'З', NW: 'СЗ' };
// Маппинг уровня срочности бэкенда → CSS-класс (фронтовые классы в Cyrillic)
const urgencyClass = (urgency) => {
const u = (urgency || '').toLowerCase();
if (u.includes('крит')) return 'критический';
if (u.includes('высок')) return 'высокий';
if (u.includes('сред')) return 'средний';
return 'стандартный';
@@ -119,12 +121,32 @@ const AnalysisResult = () => {
const urgencyLevel = urgencyClass(analysisData?.urgency);
const profiles = analysisData?.active_profiles || [];
const warnings = analysisData?.critical_warnings || [];
const unmodeled = analysisData?.unmodeled_profiles || [];
const tnpLat = caseData.tnp_lat || caseData.gps_lat;
const tnpLon = caseData.tnp_lon || caseData.gps_lon;
const waterCount = (waterData?.features || []).filter(f => f.layer !== 'railway').length;
const railCount = (waterData?.features || []).filter(f => f.layer === 'railway').length;
// Коэффициенты формулы Экстремум — компактные чипы (название: значение)
const coeffChips = [
{ label: 'Время', value: `${caseData.elapsed_hours || 1} ч`, hint: 'от потери' },
{ label: 'НормС', value: coeffs.base_speed, hint: `баз. скорость, ${caseData.age_years} лет` },
{ label: 'СП', value: coeffs.terrain, hint: caseData.terrain?.join(', ') || 'местность' },
{ label: 'СКД', value: coeffs.diagnosis, hint: caseData.diagnosis_type?.join(', ') || 'без диагноза' },
{ label: 'СУ', value: coeffs.urgency, hint: caseData.has_transport && caseData.has_transport !== 'none' ? caseData.has_transport : 'без транспорта' },
{ label: 'СУТ', value: coeffs.fatigue?.toFixed(2), hint: 'усталость' },
{ label: 'ВВС', value: coeffs.time_of_day, hint: analysisData?.time_of_day || 'время суток' },
{ label: 'ВП', value: coeffs.weather, hint: caseData.weather || caseData.precipitation || 'без осадков' },
];
// Средний ряд: карта + зоны рядом
const hasMap = tnpLat && tnpLon;
return (
<div className="analysis-result">
{/* ===== Шапка: кейс + срочность + радиус ===== */}
<div className="result-header">
<div className="header-top">
<h1><span className="logo">ВЕКТОР</span> Результаты анализа</h1>
<div className="case-info">
<span>{caseData.child_name || 'Ребёнок'}, {caseData.age_years} лет{genderLabel ? ` (${genderLabel})` : ''}</span>
@@ -135,7 +157,7 @@ const AnalysisResult = () => {
{analysisData && (
<>
<span className="separator">•</span>
<span style={{ color: analysisData.fallback_used ? '#8b949e' : '#34c759' }}>
<span style={{ color: analysisData.fallback_used ? 'var(--text-secondary)' : 'var(--success)' }}>
{analysisData.fallback_used ? 'Локальный расчёт' : 'ИИ-анализ'}
</span>
</>
@@ -143,209 +165,222 @@ const AnalysisResult = () => {
</div>
</div>
<div className="result-content">
{/* Блок 1: Уровень срочности */}
{analysisData?.urgency && (
<div className={`urgency-block urgency-${urgencyLevel}`}>
<div className="urgency-level">{analysisData.urgency}</div>
<div className="urgency-reason">{analysisData.summary || ''}</div>
{/* Панель решения: срочность | радиус | действия */}
<div className="decision-strip">
<div className={`decision-cell urgency-cell urgency-${urgencyLevel}`}>
<div className="decision-label">Срочность</div>
<div className="decision-value">{analysisData?.urgency || '—'}</div>
</div>
)}
{/* Блок 2: Расчёт по алгоритму Экстремум */}
<div className="info-card">
<h3>📏 Расчёт по алгоритму Экстремум</h3>
<div className="distance-info">
<div className="distance-value">{analysisData?.max_distance_km} км</div>
<div className="distance-details">
<p><strong>Формула:</strong> Distance = Time × НормС × СП × СКД × СУ × СУТ × ВВС × ВП</p>
<hr style={{ margin: '8px 0', border: 'none', borderTop: '1px solid #30363d' }} />
<p><strong>Время (Time):</strong> {caseData.elapsed_hours || 1} ч</p>
<p><strong>Базовая скорость (НормС):</strong> {coeffs.base_speed} км/ч (возраст {caseData.age_years} лет)</p>
<p><strong>Местность (СП):</strong> {coeffs.terrain} ({caseData.terrain?.join(', ') || 'не указана'})</p>
<p><strong>Диагноз (СКД):</strong> {coeffs.diagnosis} {caseData.has_diagnosis ? `(${caseData.diagnosis_type?.join(', ')})` : '(нет)'}</p>
<p><strong>Транспорт (СУ):</strong> {coeffs.urgency} {caseData.has_transport && caseData.has_transport !== 'none' ? `(${caseData.has_transport})` : '(нет)'}</p>
<p><strong>Усталость (СУТ):</strong> {coeffs.fatigue?.toFixed(2)} (снижение 5%/час)</p>
<p><strong>Время суток (ВВС):</strong> {coeffs.time_of_day} ({analysisData?.time_of_day})</p>
<p><strong>Погода (ВП):</strong> {coeffs.weather} ({caseData.weather || caseData.precipitation || 'нет'})</p>
<hr style={{ margin: '8px 0', border: 'none', borderTop: '1px solid #30363d' }} />
<p><strong>Радиус поиска:</strong> {analysisData?.search_radius_km} км × {analysisData?.distance_multiplier} (профили)</p>
</div>
</div>
</div>
{/* Блок 3: Активные профили и критические предупреждения */}
{(profiles.length > 0 || warnings.length > 0) && (
<div className="info-card">
<h3>⚠️ Активные профили</h3>
{profiles.map((p, i) => (
<div key={i} style={{ marginBottom: '10px', padding: '10px', background: '#161b22', borderRadius: '6px' }}>
<p style={{ margin: '0 0 4px 0' }}><strong>{p.name}</strong></p>
{p.note && <p style={{ fontSize: '13px', color: '#8b949e', margin: '2px 0' }}>{p.note}</p>}
{p.critical_warning && (
<p style={{ fontSize: '13px', color: '#ff4d00', margin: '4px 0 0 0' }}>🚨 {p.critical_warning}</p>
<div className="decision-cell">
<div className="decision-label">Радиус поиска</div>
<div className="decision-value radius">
{analysisData?.max_distance_km} км
{analysisData?.distance_multiplier !== 1 && (
<span className="radius-mult">×{analysisData?.distance_multiplier}</span>
)}
</div>
))}
</div>
)}
{/* Блок 4: Психотип и модель поведения */}
{analysisData?.psychotype && psyMods && (
<div className="info-card">
<h3>🧠 Психотип и модель поведения</h3>
<p><strong>Психотип:</strong> {analysisData.psychotype}</p>
<p><strong>Модель движения:</strong> {psyMods.description}</p>
<div style={{ marginTop: '12px', padding: '12px', background: '#161b22', borderRadius: '6px' }}>
<p style={{ fontSize: '14px', color: '#8b949e', margin: 0 }}><strong>Модификаторы зон:</strong></p>
<p style={{ fontSize: '13px', color: '#8b949e', margin: '4px 0 0 0' }}>
• 0-500м: ×{psyMods.zone_0_500}<br />
• 500-1500м: ×{psyMods.zone_500_1500}<br />
• 1500-2500м: ×{psyMods.zone_1500_2500}<br />
• 2500м+: ×{psyMods.zone_2500plus}
</p>
</div>
{psyRec && (
<div style={{ marginTop: '10px' }}>
<p style={{ fontSize: '14px', color: '#8b949e', margin: '0 0 4px 0' }}><strong>Тактика поиска:</strong></p>
<p style={{ fontSize: '13px', margin: '2px 0' }}><strong>Приоритетные зоны:</strong> {psyRec.priority_zones}</p>
<p style={{ fontSize: '13px', margin: '2px 0' }}><strong>Патрун:</strong> {psyRec.search_pattern}</p>
<p style={{ fontSize: '13px', margin: '2px 0' }}><strong>Ключевые объекты:</strong> {psyRec.key_locations}</p>
<p style={{ fontSize: '13px', margin: '2px 0' }}><strong>Связь:</strong> {psyRec.communication}</p>
</div>
)}
</div>
)}
{/* Блок 5: Обстоятельства потери */}
<div className="info-card">
<h3>📋 Обстоятельства потери</h3>
<p><strong>Причина:</strong> {caseData.reason || caseData.loss_reason || 'не указана'}</p>
<p><strong>Последнее известное место:</strong> {caseData.last_known_place || 'не указано'}</p>
{caseData.direction && <p><strong>Направление движения:</strong> {caseData.direction}</p>}
{caseData.behavior_description && (
<p><strong>Описание:</strong> {caseData.behavior_description}</p>
)}
{analysisData?.behavioral_prediction && (
<p><strong>Поведенческий прогноз:</strong> {analysisData.behavioral_prediction}</p>
)}
</div>
{/* Блок 6: Зоны поиска */}
<div className="zones-section">
<h2>Приоритетные зоны поиска</h2>
<div className="zones-list">
{zones.slice(0, 8).map((zone, idx) => (
<div key={idx} className={`zone-card priority-${Math.min(idx + 1, 3)}`}>
<div className="zone-header">
<div className="zone-title">
<span className="zone-priority">#{idx + 1}</span>
<span className="zone-name">{zone.name}</span>
</div>
<div className="zone-score">{zone.score}%</div>
</div>
<div className="zone-details">
<span><strong>Направление:</strong> {zone.direction}</span>
<span className="separator">•</span>
<span><strong>Дистанция:</strong> {zone.distance}</span>
</div>
<div className="zone-reasoning">{zone.reasoning}</div>
</div>
))}
</div>
</div>
{/* Блок 7: Немедленные действия */}
{analysisData?.immediate_actions?.length > 0 && (
<div className="info-card">
<h3>🚑 Немедленные действия</h3>
<ul style={{ margin: 0, paddingLeft: '20px' }}>
{analysisData.immediate_actions.map((a, i) => (
<li key={i} style={{ marginBottom: '4px' }}>{a}</li>
))}
<div className="decision-cell warnings-cell">
<div className="decision-label">Критические действия</div>
{warnings.length > 0 ? (
<ul className="decision-warnings">
{warnings.map((w, i) => <li key={i}>{w.warning}</li>)}
</ul>
</div>
) : (
<div className="decision-warnings muted">Нет критических предупреждений</div>
)}
</div>
</div>
{analysisData?.summary && <p className="header-summary">{analysisData.summary}</p>}
</div>
{/* Блок 8: Карта */}
{tnpLat && tnpLon && (
<div className="result-content">
{/* ===== Ряд 1: Карта + зоны ===== */}
<div className={`main-row ${hasMapClass(hasMap)}`}>
<div className="map-section">
<div className="section-head">
<h2>Карта зон поиска</h2>
{(waterData?.count > 0) && (
<div className="geo-chips">
<span className="geo-chip water">💧 Водоёмы: {waterCount}</span>
{railCount > 0 && <span className="geo-chip rail">🚂 Ж/д: {railCount}</span>}
</div>
)}
</div>
{waterData?.warning && (
<div style={{ marginBottom: '10px', padding: '10px', background: '#2d1b0e', border: '1px solid #ff4d00', borderRadius: '6px' }}>
<p style={{ margin: 0, fontSize: '13px', color: '#ff4d00' }}>
<div className="geo-warning">
🚨 {waterData.warning}
</p>
</div>
)}
{waterData?.railway_warning && (waterData.features || []).some((f) => f.layer === 'railway') && (
<div style={{ marginBottom: '10px', padding: '10px', background: '#2d1b0e', border: '1px solid #8a2f2f', borderRadius: '6px' }}>
<p style={{ margin: 0, fontSize: '13px', color: '#ff7b72' }}>
{waterData?.railway_warning && railCount > 0 && (
<div className="geo-warning rail">
🚂 {waterData.railway_warning}
</p>
</div>
)}
{waterData?.count > 0 && (
<p style={{ margin: '0 0 8px 0', fontSize: '13px', color: '#8b949e' }}>
💧 Водоёмы в зоне поиска: {waterData.features.filter(f => f.layer !== 'railway').length}
{' '}· 🚂 Ж/д линий: {waterData.features.filter(f => f.layer === 'railway').length}
{waterData.warning && ' — приоритетная проверка'} (данные OSM)
</p>
)}
{hasMap ? (
<SearchMap zones={zones}
maxDistance={parseFloat(analysisData?.max_distance_km)}
tnpLat={parseFloat(tnpLat)}
tnpLon={parseFloat(tnpLon)}
waterFeatures={waterData?.features || []}
/>
<div className="map-info" style={{ marginTop: "12px", fontSize: "14px", color: "#8b949e" }}>
<p>Координаты ТНП: {tnpLat}, {tnpLon}</p>
{caseData.tnp_address && <p>Адрес: {caseData.tnp_address}</p>}
</div>
</div>
) : (
<div className="map-empty">ТНП не задана — карта недоступна</div>
)}
<div className="map-info">
<span>ТНП: {tnpLat ? Number(tnpLat).toFixed(5) : '—'}, {tnpLon ? Number(tnpLon).toFixed(5) : '—'}</span>
{caseData.tnp_address && <span className="sep">·</span>}
{caseData.tnp_address && <span>{caseData.tnp_address}</span>}
</div>
</div>
{/* Блок 9: Здоровье и особенности */}
{(caseData.has_diagnosis || caseData.cant_swim || caseData.fitness_level || caseData.has_transport) && (
<div className="info-card">
<h3>🏥 Здоровье и особенности</h3>
{caseData.has_diagnosis && (
<p><strong>Диагнозы:</strong> {caseData.diagnosis_type?.join(', ') || 'есть'}</p>
<div className="zones-panel">
<div className="section-head">
<h2>Зоны поиска</h2>
<span className="zones-count">{zones.length} зон</span>
</div>
<div className="zones-list">
{zones.slice(0, 8).map((zone, idx) => (
<div key={idx} className={`zone-card priority-${Math.min(idx + 1, 3)}`}>
<div className="zone-header">
<div className="zone-title">
<span className="zone-priority">{idx + 1}</span>
<span className="zone-name">{zone.name}</span>
</div>
<div className="zone-meta">
<span className="zone-dir">{DIRECTION_RU[zone.direction] || zone.direction}</span>
<span className="zone-dist">{zone.distance}</span>
<div className="zone-score">{zone.score}%</div>
</div>
</div>
{zone.reasoning && <div className="zone-reasoning">{zone.reasoning}</div>}
</div>
))}
{zones.length === 0 && <div className="zones-empty">Зоны не рассчитаны</div>}
</div>
</div>
</div>
{/* ===== Ряд 2: Поведение + действия ===== */}
<div className="behavior-row">
<div className="info-card behavior-card">
<div className="section-head">
<h3>Поведение и профиль</h3>
{(profiles.length > 0 || unmodeled.length > 0) && (
<div className="profile-tags">
{profiles.map((p, i) => <span key={i} className="profile-tag">{p.name}</span>)}
{unmodeled.map((u, i) => <span key={`u${i}`} className="profile-tag unmodeled" title={u.note}>{u.profile}*</span>)}
</div>
)}
{caseData.fitness_level && (
<p><strong>Физическая подготовка:</strong> {caseData.fitness_level}</p>
</div>
{analysisData?.behavioral_prediction && (
<p className="prediction">{analysisData.behavioral_prediction}</p>
)}
{caseData.cant_swim && <p>⚠️ <strong>Не умеет плавать</strong> — приоритет ближайшим водоёмам</p>}
{caseData.has_transport && caseData.has_transport !== 'none' && (
<p><strong>Транспорт:</strong> {caseData.has_transport}</p>
{psyRec && (
<div className="tactics-grid">
<div className="tactic"><div className="tactic-label">Приоритетные зоны</div><div>{psyRec.priority_zones}</div></div>
<div className="tactic"><div className="tactic-label">Паттерн поиска</div><div>{psyRec.search_pattern}</div></div>
<div className="tactic"><div className="tactic-label">Ключевые объекты</div><div>{psyRec.key_locations}</div></div>
<div className="tactic"><div className="tactic-label">Связь</div><div>{psyRec.communication}</div></div>
</div>
)}
{(analysisData?.unmodeled_profiles || []).length > 0 && (
<div style={{ marginTop: '10px', padding: '10px', background: '#161b22', borderRadius: '6px' }}>
<p style={{ fontSize: '13px', color: '#8b949e', margin: '0 0 4px 0' }}>
<strong>Вне поведенческой модели</strong> (на скоринг зон не влияют):
</p>
{analysisData.unmodeled_profiles.map((u, i) => (
<p key={i} style={{ fontSize: '13px', color: '#8b949e', margin: '2px 0' }}>
• {u.profile}: {u.note}
</p>
{unmodeled.length > 0 && (
<div className="unmodeled-note">
{unmodeled.map((u, i) => (
<p key={i}>• {u.profile}: {u.note}</p>
))}
</div>
)}
</div>
<div className="info-card actions-card">
<h3>Немедленные действия</h3>
<ol className="actions-list">
{(analysisData?.immediate_actions || []).map((a, i) => (
<li key={i} className={a.startsWith('КРИТИЧНО') ? 'critical' : ''}>{a}</li>
))}
</ol>
</div>
</div>
{/* ===== Ряд 3: Обоснование радиуса (формула Экстремум) ===== */}
{analysisData && (
<div className="info-card radius-card">
<div className="section-head">
<h3>Обоснование радиуса</h3>
<span className="radius-formula">Distance = Time × НормС × СП × СКД × СУ × СУТ × ВВС × ВП</span>
</div>
<div className="coeff-grid">
{coeffChips.map((c, i) => (
<div key={i} className="coeff-chip">
<div className="coeff-name">{c.label} <span className="coeff-value">{c.value}</span></div>
<div className="coeff-hint">{c.hint}</div>
</div>
))}
</div>
{psyMods && (
<div className="psy-mods">
<span className="psy-mods-title">Психотип {psychoRu(analysisData?.psychotype)} — поправки полос:</span>
{['0–500 м', '500–1500 м', '1500–2500 м', '2500 м+'].map((band, i) => (
<span key={i} className="psy-mod-chip">{band}: ×{[psyMods.zone_0_500, psyMods.zone_500_1500, psyMods.zone_1500_2500, psyMods.zone_2500plus][i]}</span>
))}
</div>
)}
</div>
)}
{/* Блок 10: Условия среды */}
{/* ===== Ряд 4: Справочные данные (3 колонки) ===== */}
<div className="reference-grid">
<div className="info-card">
<h3>🌤 Условия среды</h3>
<h3>Обстоятельства потери</h3>
<p><strong>Причина:</strong> {caseData.reason || caseData.loss_reason || 'не указана'}</p>
<p><strong>Последнее известное место:</strong> {caseData.last_known_place || 'не указано'}</p>
{caseData.direction && <p><strong>Направление движения:</strong> {caseData.direction}</p>}
{caseData.behavior_description && <p><strong>Описание:</strong> {caseData.behavior_description}</p>}
</div>
<div className="info-card">
<h3>Здоровье и особенности</h3>
{caseData.has_diagnosis ? (
<p><strong>Диагнозы:</strong> {caseData.diagnosis_type?.join(', ')}</p>
) : (
<p><strong>Диагнозы:</strong> нет</p>
)}
{caseData.fitness_level && <p><strong>Физподготовка:</strong> {caseData.fitness_level}</p>}
{caseData.cant_swim && <p>⚠️ <strong>Не умеет плавать</strong> — приоритет ближайшим водоёмам</p>}
{caseData.has_transport && caseData.has_transport !== 'none' && (
<p><strong>Транспорт:</strong> {caseData.has_transport}</p>
)}
</div>
<div className="info-card">
<h3>Условия среды</h3>
<p><strong>Сезон:</strong> {caseData.season || 'не указан'}</p>
{caseData.temperature_c != null && <p><strong>Температура:</strong> {caseData.temperature_c}°C</p>}
<p><strong>Осадки:</strong> {caseData.weather || caseData.precipitation || 'нет'}</p>
{caseData.visibility && <p><strong>Видимость:</strong> {caseData.visibility}</p>}
</div>
</div>
</div>
</div>
);
};
// РУ-названия психотипов (psychotype_service отдаёт slug'и)
const PSY_RU = {
dominant: 'Доминантный',
harmonic: 'Гармоничный',
anxious: 'Тревожный',
introvert_passive: 'Интроверт пассивный',
introvert_active: 'Интроверт активный',
};
const psychoRu = (p) => PSY_RU[p] || p || '';
// Класс вёрстки главного ряда: карта+зоны / только зоны
function hasMapClass(hasMap) {
return hasMap ? 'with-map' : 'no-map';
}
export default AnalysisResult;