"""HTML fragments of the dashboard. Builders return strings (testable); `st.html` renders them.
Every value coming from the API is escaped: patient free text never reaches these views, but the API is still
treated as untrusted input.
"""
from __future__ import annotations
from html import escape
import streamlit as st
from app.api.schemas import PatientListItem
from dashboard import theme
from dashboard.components import helpers as h
def chip(text: str) -> str:
return f'{escape(text)}'
def badge(text: str, tone: str, tip: str = "", icon: str = "") -> str:
tip_attr = f' data-tip="{escape(tip)}" title="{escape(tip)}" tabindex="0"' if tip else ""
cls = " mt-tip" if tip else ""
lead = f'' if icon == "dot" else icon
return f'{lead}{escape(text)}'
def bar(fraction: float | None, tone: str) -> str:
pct = 0 if fraction is None else max(0, min(100, round(fraction * 100)))
return f'
'
def page_header(title: str, subtitle: str = "", kicker: str = "") -> str:
k = f'{escape(kicker)}
' if kicker else ""
s = f'{subtitle}
' if subtitle else ""
return f''
def kpi_tiles(k: h.Kpis) -> str:
tiles = [
("Всего пациентов", str(k.total), "Под вашим наблюдением", theme.INK_2),
("Срочно", str(k.red), "Кризисные уведомления", theme.TONES["red"].solid),
("Требуют внимания", str(k.yellow + k.grey), f"Из них только инфо: {k.grey}", theme.TONES["yellow"].solid),
("Без уведомлений", str(k.green), "Продолжают наблюдение", theme.TONES["green"].solid),
]
body = "".join(
f'{escape(lbl)}
'
f'
{val}
{escape(hint)}
'
for lbl, val, hint, accent in tiles
)
return f'{body}
'
LIST_COLUMNS = ["Пациент", "Статус", "Шкалы", "Приём 7 дн.", "Риск прерывания"]
def list_header() -> str:
return '' + "".join(f"
{c}
" for c in LIST_COLUMNS) + "
"
def _score(value: int | None, trend: str) -> str:
if value is None:
return '—'
t = h.trend_view(trend)
return (
f'{value} '
f''
f"{t.text}"
)
def patient_row(p: PatientListItem, *, detailed: bool = False) -> str:
"""One row of the patient list; the grid matches `list_header()`."""
status = h.status_view(p.status_color)
chips = "".join(chip(h.MODULE_LABELS.get(m, m)) for m in p.modules)
adh = h.adherence_view(p.adherence_7d)
risk = h.risk_view(p.discontinuation_risk)
details = ""
if detailed:
details = (
''
f'
Терапия{escape(p.drug or "Нет назначения")} · {chips}
'
f'
Последнее событие{escape(p.last_event or "Нет событий")}
'
'
Факторы риска прерывания'
f"{escape(', '.join(p.discontinuation_risk.factors) or 'Значимых факторов нет')}
"
)
return (
''
f'
{escape(p.display_name)}
'
f'
{h.age_label(p.age)} · день терапии {p.therapy_day}
'
f'
{badge(status.text, status.tone, status.hint, icon="dot")}'
f'
Открытых уведомлений: {p.open_alerts}
'
f'
PHQ-9{_score(p.phq9_last, p.trend)}
'
f'
PCL-5{_score(p.pcl5_last, p.trend)}
'
'
Приём за 7 дней'
f'{adh.text}{bar(p.adherence_7d, adh.tone)}
'
'
Риск прерывания'
f"{badge(risk.text, risk.tone, risk.hint)}
"
f"{details}
"
)
def workspace_header(doctor: str, page: str) -> str:
return (
'Кабинет врача / '
f' {escape(page)}
+'
f"
{escape(doctor)}Врач-психиатр · демо
"
)
def empty_state(title: str, hint: str) -> str:
return f'{escape(title)}{escape(hint)}
'
def banner(html_body: str, tone: str = "red", pulse: bool = False) -> str:
lead = '' if pulse else ""
return f''
def footer() -> None:
st.html(
'"
)