feat(dashboard): replace home with approved minimal design
CI / ruff · pytest (pull_request) Successful in 2m1s
CI / ruff · pytest (pull_request) Successful in 2m1s
Refs #73
This commit is contained in:
+41
-24
@@ -20,7 +20,7 @@ def chip(text: str) -> str:
|
||||
|
||||
|
||||
def badge(text: str, tone: str, tip: str = "", icon: str = "") -> str:
|
||||
tip_attr = f' data-tip="{escape(tip)}"' if tip else ""
|
||||
tip_attr = f' data-tip="{escape(tip)}" title="{escape(tip)}" tabindex="0"' if tip else ""
|
||||
cls = " mt-tip" if tip else ""
|
||||
lead = f'<span class="mt-dot" style="background:{theme.TONES[tone].solid}"></span>' if icon == "dot" else icon
|
||||
return f'<span class="mt-badge t-{tone}{cls}"{tip_attr}>{lead}{escape(text)}</span>'
|
||||
@@ -39,10 +39,10 @@ def page_header(title: str, subtitle: str = "", kicker: str = "") -> str:
|
||||
|
||||
def kpi_tiles(k: h.Kpis) -> str:
|
||||
tiles = [
|
||||
("На сопровождении", str(k.total), "пациентов в кабинете", theme.TONES["blue"].solid),
|
||||
("🔴 Красная зона", str(k.red), "острый сигнал — связаться сегодня", theme.TONES["red"].solid),
|
||||
("🟡 Жёлтая зона", str(k.yellow), "врачу ≤ 24 ч или к визиту", theme.TONES["yellow"].solid),
|
||||
("Риск прерывания терапии", str(k.high_risk), "высокий индекс (правила, не ML)", theme.TONES["red"].solid),
|
||||
("Всего пациентов", 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'<div class="mt-kpi" style="--kpi-accent:{accent}"><div class="lbl">{escape(lbl)}</div>'
|
||||
@@ -52,7 +52,7 @@ def kpi_tiles(k: h.Kpis) -> str:
|
||||
return f'<div class="mt-kpis">{body}</div>'
|
||||
|
||||
|
||||
LIST_COLUMNS = ["Пациент", "Терапия", "PHQ-9", "PCL-5", "Приём 7 дн.", "Риск прерывания", "Последнее событие", "Алерты"]
|
||||
LIST_COLUMNS = ["Пациент", "Статус", "Шкалы", "Приём 7 дн.", "Риск прерывания"]
|
||||
|
||||
|
||||
def list_header() -> str:
|
||||
@@ -65,38 +65,55 @@ def _score(value: int | None, trend: str) -> str:
|
||||
t = h.trend_view(trend)
|
||||
return (
|
||||
f'<span class="mt-num">{value}</span> '
|
||||
f'<span class="mt-tip" data-tip="{escape(t.hint)}" style="color:{theme.TONES[t.tone].ink};font-weight:700">'
|
||||
f'<span title="{escape(t.hint)}" aria-label="{escape(t.hint)}" '
|
||||
f'style="color:{theme.TONES[t.tone].ink};font-weight:600">'
|
||||
f"{t.text}</span>"
|
||||
)
|
||||
|
||||
|
||||
def patient_row(p: PatientListItem) -> str:
|
||||
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)
|
||||
tone = theme.TONES[status.tone]
|
||||
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)
|
||||
if p.open_alerts:
|
||||
count_style = f"background:{tone.tint};color:{tone.ink}"
|
||||
else:
|
||||
count_style = f"background:{theme.TONES['grey'].tint};color:{theme.INK_3}"
|
||||
details = ""
|
||||
if detailed:
|
||||
details = (
|
||||
'<div class="mt-row-details">'
|
||||
f'<div><span class="mt-field-label">Терапия</span>{escape(p.drug or "Нет назначения")} · {chips}</div>'
|
||||
f'<div><span class="mt-field-label">Последнее событие</span>{escape(p.last_event or "Нет событий")}</div>'
|
||||
'<div><span class="mt-field-label">Факторы риска прерывания</span>'
|
||||
f"{escape(', '.join(p.discontinuation_risk.factors) or 'Значимых факторов нет')}</div></div>"
|
||||
)
|
||||
return (
|
||||
'<div class="mt-row">'
|
||||
f'<div><div class="mt-name"><span class="mt-dot mt-tip" data-tip="{escape(status.text + " — " + status.hint)}"'
|
||||
f' style="background:{tone.solid}"></span>{escape(p.display_name)}</div>'
|
||||
f'<div class="mt-meta">{h.age_label(p.age)} · {chips}</div></div>'
|
||||
f'<div>{escape(p.drug or "—")}<div class="mt-meta">день терапии {p.therapy_day}</div></div>'
|
||||
f"<div>{_score(p.phq9_last, p.trend)}</div>"
|
||||
f"<div>{_score(p.pcl5_last, p.trend)}</div>"
|
||||
f'<div><span class="mt-num">{adh.text}</span>{bar(p.adherence_7d, adh.tone)}</div>'
|
||||
f"<div>{badge(risk.text, risk.tone, risk.hint)}</div>"
|
||||
f'<div class="mt-event">{escape(p.last_event or "—")}</div>'
|
||||
f'<div><span class="mt-count" style="{count_style}">{p.open_alerts}</span></div>'
|
||||
"</div>"
|
||||
f'<div class="mt-person"><div class="mt-name">{escape(p.display_name)}</div>'
|
||||
f'<div class="mt-meta">{h.age_label(p.age)} · день терапии {p.therapy_day}</div></div>'
|
||||
f'<div class="mt-status">{badge(status.text, status.tone, status.hint, icon="dot")}'
|
||||
f'<div class="mt-meta">Открытых уведомлений: {p.open_alerts}</div></div>'
|
||||
f'<div class="mt-scores"><div><span class="mt-scale-label">PHQ-9</span>{_score(p.phq9_last, p.trend)}</div>'
|
||||
f'<div><span class="mt-scale-label">PCL-5</span>{_score(p.pcl5_last, p.trend)}</div></div>'
|
||||
'<div class="mt-adherence"><span class="mt-mobile-label">Приём за 7 дней</span>'
|
||||
f'<span class="mt-num">{adh.text}</span>{bar(p.adherence_7d, adh.tone)}</div>'
|
||||
'<div class="mt-risk"><span class="mt-mobile-label">Риск прерывания</span>'
|
||||
f"{badge(risk.text, risk.tone, risk.hint)}</div>"
|
||||
f"{details}</div>"
|
||||
)
|
||||
|
||||
|
||||
def workspace_header(doctor: str, page: str) -> str:
|
||||
return (
|
||||
'<div class="mt-workspace"><div>Кабинет врача <span aria-hidden="true"> / </span>'
|
||||
f' {escape(page)}</div><div class="mt-doctor"><span class="mt-doctor-icon" aria-hidden="true">+</span>'
|
||||
f"<div><strong>{escape(doctor)}</strong><small>Врач-психиатр · демо</small></div></div></div>"
|
||||
)
|
||||
|
||||
|
||||
def empty_state(title: str, hint: str) -> str:
|
||||
return f'<div class="mt-empty" role="status"><strong>{escape(title)}</strong><p>{escape(hint)}</p></div>'
|
||||
|
||||
|
||||
def banner(html_body: str, tone: str = "red", pulse: bool = False) -> str:
|
||||
lead = '<span class="mt-pulse"></span>' if pulse else ""
|
||||
return f'<div class="mt-banner t-{tone}">{lead}<div>{html_body}</div></div>'
|
||||
|
||||
Reference in New Issue
Block a user