Завершение поиска: исход → архив + матмодель + операция completed
- POST /operations/{id}/complete: found_alive/дистанция/направление/кто нашёл/
длительность → кейс status=closed + фактические данные + прогон детерминированной
матмодели (model_prediction, accuracy_note в analysis_log) → операция
status=completed + closed_at. Аудит: operation_complete + case_closed.
- ФИКС list_closed_cases: фильтр по to_summary().analysis_log всегда давал None
(analysis_log только в to_detail) → список завершённых был пуст; теперь
фильтр по status='closed' (совместимо с TestDB).
- UI: кнопка «✓ Завершить поиск» на странице анализа (для активных операций),
модал с исходом, после завершения — плашка «Поиск завершён», в архиве
(/admin завершённые) появляется запись с прогнозом модели и точностью.
- Тесты (4): полный флоу, повторное завершение 400, аудит, появление в
/closed-cases. 230 passed.
This commit is contained in:
@@ -39,6 +39,13 @@ const AnalysisResult = () => {
|
||||
const [operation, setOperation] = useState(null);
|
||||
const [contourId, setContourId] = useState('');
|
||||
const [contourStatus, setContourStatus] = useState(null); // {ok, message}
|
||||
const [showComplete, setShowComplete] = useState(false);
|
||||
const [completeForm, setCompleteForm] = useState({
|
||||
found_alive: 'true', found_distance_km: '', found_direction: '',
|
||||
found_location_type: '', search_duration_hours: '', who_found: '', notes: '',
|
||||
});
|
||||
const [completing, setCompleting] = useState(false);
|
||||
const [completeResult, setCompleteResult] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadCaseAndAnalyze = async () => {
|
||||
@@ -286,6 +293,18 @@ const AnalysisResult = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{operation && operation.status !== 'completed' && (
|
||||
<div className="complete-row">
|
||||
<button
|
||||
className="complete-btn"
|
||||
onClick={() => { setShowComplete(true); setCompleteResult(null); }}
|
||||
>✓ Завершить поиск</button>
|
||||
<span className="complete-hint">Внесение исхода → кейс в архив + прогон матмодели</span>
|
||||
</div>
|
||||
)}
|
||||
{operation && operation.status === 'completed' && (
|
||||
<div className="complete-done">✓ Поиск завершён {operation.closed_at ? new Date(operation.closed_at).toLocaleString('ru-RU') : ''} — запись в архиве завершённых</div>
|
||||
)}
|
||||
<div className="map-info">
|
||||
<span>ТНП: {tnpLat ? Number(tnpLat).toFixed(5) : '—'}, {tnpLon ? Number(tnpLon).toFixed(5) : '—'}</span>
|
||||
{caseData.tnp_address && <span className="sep">·</span>}
|
||||
@@ -422,6 +441,79 @@ const AnalysisResult = () => {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{showComplete && (
|
||||
<div className="modal-overlay" onClick={() => setShowComplete(false)}>
|
||||
<div className="modal-box" onClick={(e) => e.stopPropagation()}>
|
||||
<h3>Завершение поиска</h3>
|
||||
<p className="modal-sub">Исход вносится в карточку, кейс уходит в архив завершённых, операция помечается завершённой.</p>
|
||||
<div className="modal-grid">
|
||||
<label>Найден?
|
||||
<select value={completeForm.found_alive} onChange={(e) => setCompleteForm({ ...completeForm, found_alive: e.target.value })}>
|
||||
<option value="true">Живым</option>
|
||||
<option value="false">Погибшим</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Дистанция, км
|
||||
<input type="number" step="0.1" value={completeForm.found_distance_km} onChange={(e) => setCompleteForm({ ...completeForm, found_distance_km: e.target.value })} />
|
||||
</label>
|
||||
<label>Направление (N/NE/E/...)
|
||||
<input value={completeForm.found_direction} onChange={(e) => setCompleteForm({ ...completeForm, found_direction: e.target.value })} />
|
||||
</label>
|
||||
<label>Кто нашёл
|
||||
<input value={completeForm.who_found} onChange={(e) => setCompleteForm({ ...completeForm, who_found: e.target.value })} />
|
||||
</label>
|
||||
<label>Длительность поиска, ч
|
||||
<input type="number" step="0.5" value={completeForm.search_duration_hours} onChange={(e) => setCompleteForm({ ...completeForm, search_duration_hours: e.target.value })} />
|
||||
</label>
|
||||
<label>Тип места находки
|
||||
<input value={completeForm.found_location_type} onChange={(e) => setCompleteForm({ ...completeForm, found_location_type: e.target.value })} />
|
||||
</label>
|
||||
</div>
|
||||
{completeResult && (
|
||||
<div className={completeResult.ok ? 'complete-ok' : 'complete-err'}>{completeResult.message}</div>
|
||||
)}
|
||||
<div className="modal-actions">
|
||||
<button className="modal-cancel" onClick={() => setShowComplete(false)}>Отмена</button>
|
||||
<button
|
||||
className="modal-confirm"
|
||||
disabled={completing}
|
||||
onClick={async () => {
|
||||
setCompleting(true);
|
||||
setCompleteResult(null);
|
||||
try {
|
||||
const f = completeForm;
|
||||
const body = {
|
||||
found_alive: f.found_alive === 'true',
|
||||
found_distance_km: f.found_distance_km ? Number(f.found_distance_km) : null,
|
||||
found_direction: f.found_direction || null,
|
||||
found_location_type: f.found_location_type || null,
|
||||
search_duration_hours: f.search_duration_hours ? Number(f.search_duration_hours) : null,
|
||||
who_found: f.who_found || null,
|
||||
};
|
||||
const r = await apiFetch(`/api/v1/operations/${operation.id}/complete`, {
|
||||
method: 'POST', body: JSON.stringify(body),
|
||||
});
|
||||
const d = await r.json().catch(() => ({}));
|
||||
if (r.ok) {
|
||||
const acc = d.accuracy_note ? ` Точность прогноза: ${d.accuracy_note}` : '';
|
||||
setCompleteResult({ ok: true, message: `Поиск завершён, кейс в архиве.${acc}` });
|
||||
setOperation({ ...operation, status: 'completed', closed_at: d.closed_at });
|
||||
setTimeout(() => setShowComplete(false), 1800);
|
||||
} else {
|
||||
setCompleteResult({ ok: false, message: d.detail || 'Ошибка завершения' });
|
||||
}
|
||||
} catch (e) {
|
||||
setCompleteResult({ ok: false, message: e.message });
|
||||
} finally {
|
||||
setCompleting(false);
|
||||
}
|
||||
}}
|
||||
>{completing ? 'Завершение...' : 'Завершить поиск'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user