каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений, таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет их через JSON Schema в generated.ts, тест ловит забытую регенерацию. Стенд: docker compose postgres + backend, health отвечает, база досягаема из контейнера бэкенда. Makefile не врёт — нереализованные цели падают и называют карточку, которая их закроет. frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
This commit is contained in:
parent
4425c7d4ca
commit
7b7dd38e9f
36 changed files with 4997 additions and 0 deletions
151
backend/app/domain/kio.py
Normal file
151
backend/app/domain/kio.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
"""Карточка информационного обмена (КИО).
|
||||
|
||||
Это не дизайн формы, это норматив: docs/spec/NORMATIVES.md#карточка-информационного-обмена-кио
|
||||
Обязательность полей задаётся сценарием (`required_fields`), а не моделью:
|
||||
в ложном вызове число пострадавших не требуется, в ЧС требуется.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.domain.classifiers import DDSCode, IncidentType
|
||||
|
||||
|
||||
class ResponseStatus(StrEnum):
|
||||
REGISTERED = "registered"
|
||||
TRANSFERRED = "transferred"
|
||||
IN_PROGRESS = "in_progress"
|
||||
CLOSED = "closed"
|
||||
|
||||
|
||||
class FireDetails(BaseModel):
|
||||
"""Уточняющие поля 01 — МЧС."""
|
||||
|
||||
fire_nature: str | None = None
|
||||
object_kind: str | None = None
|
||||
floors: int | None = None
|
||||
gasified: bool | None = None
|
||||
people_inside: bool | None = None
|
||||
|
||||
|
||||
class PoliceDetails(BaseModel):
|
||||
"""Уточняющие поля 02 — полиция."""
|
||||
|
||||
offence_kind: str | None = None
|
||||
suspects: str | None = None
|
||||
vehicle: str | None = None
|
||||
|
||||
|
||||
class MedicalDetails(BaseModel):
|
||||
"""Уточняющие поля 03 — медицина."""
|
||||
|
||||
reason: str | None = None
|
||||
conscious: bool | None = None
|
||||
breathing: bool | None = None
|
||||
can_move: bool | None = None
|
||||
age: int | None = None
|
||||
|
||||
|
||||
class UtilityDetails(BaseModel):
|
||||
"""Уточняющие поля 04 / ЖКХ."""
|
||||
|
||||
failure_kind: str | None = None
|
||||
scale: str | None = None
|
||||
threat_to_residents: bool | None = None
|
||||
|
||||
|
||||
class KIO(BaseModel):
|
||||
"""Полная карточка. Наблюдателям уходит целиком (`kio.state`),
|
||||
курсанту — дельтой (`kio.patch`)."""
|
||||
|
||||
# Служебное — заполняется системой
|
||||
card_id: UUID = Field(default_factory=uuid4)
|
||||
registered_at: datetime | None = None
|
||||
response_status: ResponseStatus = ResponseStatus.REGISTERED
|
||||
|
||||
# Звонящий
|
||||
caller_number: str | None = None
|
||||
caller_name: str | None = None
|
||||
caller_contact: str | None = None
|
||||
language: str = "ru"
|
||||
|
||||
# Место
|
||||
okato: str | None = None
|
||||
address: str | None = None
|
||||
street: str | None = None
|
||||
building: str | None = None
|
||||
entrance: str | None = None
|
||||
floor: str | None = None
|
||||
coords: tuple[float, float] | None = None
|
||||
|
||||
# Происшествие
|
||||
incident_type: IncidentType | None = None
|
||||
description: str | None = None
|
||||
victims_count: int | None = None
|
||||
is_emergency: bool = False
|
||||
life_threat: bool = False
|
||||
evacuation_needed: bool = False
|
||||
|
||||
# ДДС
|
||||
dds: DDSCode | None = None
|
||||
dispatch_order_at: datetime | None = None
|
||||
arrival_at: datetime | None = None
|
||||
|
||||
# Уточняющие по службе
|
||||
fire: FireDetails | None = None
|
||||
police: PoliceDetails | None = None
|
||||
medical: MedicalDetails | None = None
|
||||
utility: UtilityDetails | None = None
|
||||
|
||||
|
||||
#: Поля, которые курсант не редактирует: их проставляет система.
|
||||
READ_ONLY_FIELDS: frozenset[str] = frozenset(
|
||||
{"card_id", "registered_at", "caller_number", "response_status"}
|
||||
)
|
||||
|
||||
|
||||
def get_field(card: KIO, path: str) -> Any:
|
||||
"""Значение поля по пути вида `floor` или `fire.floors`."""
|
||||
value: Any = card
|
||||
for part in path.split("."):
|
||||
if value is None:
|
||||
return None
|
||||
value = getattr(value, part, None)
|
||||
return value
|
||||
|
||||
|
||||
def missing_fields(card: KIO, required: list[str]) -> list[str]:
|
||||
"""Незаполненные обязательные поля — основание отметки E5.
|
||||
|
||||
Обязательность приходит из сценария, пустой список означает,
|
||||
что сценарий требований к карточке не предъявляет.
|
||||
"""
|
||||
empty: list[str] = []
|
||||
for path in required:
|
||||
value = get_field(card, path)
|
||||
if value is None or (isinstance(value, str) and not value.strip()):
|
||||
empty.append(path)
|
||||
return empty
|
||||
|
||||
|
||||
def apply_patch(card: KIO, fields: dict[str, Any]) -> KIO:
|
||||
"""Применить дельту `kio.patch`. Служебные поля игнорируются.
|
||||
|
||||
Вложенные поля приходят плоским путём: {"fire.floors": 5}.
|
||||
"""
|
||||
data = card.model_dump()
|
||||
for path, value in fields.items():
|
||||
if path in READ_ONLY_FIELDS:
|
||||
continue
|
||||
head, _, tail = path.partition(".")
|
||||
if not tail:
|
||||
data[head] = value
|
||||
continue
|
||||
nested = data.get(head) or {}
|
||||
nested[tail] = value
|
||||
data[head] = nested
|
||||
return KIO.model_validate(data)
|
||||
Loading…
Reference in a new issue