"""«Подключить пациента»: after the in-person visit the doctor enters the prescription → one-time code + QR.""" from __future__ import annotations import io import os from datetime import date, timedelta from html import escape import qrcode import streamlit as st from app.api.schemas import ConnectPatientIn, ConnectPatientOut from dashboard.components import helpers as h from dashboard.components import state, ui NAV_ICON = ":material/person_add:" TIME_OPTIONS = ["07:00", "08:00", "09:00", "10:00", "12:00", "14:00", "18:00", "20:00", "21:00", "22:00"] MODULE_OPTIONS = ["veteran", "depression", "family"] DEMO = { "cp_name": "Андрей С.", "cp_year": 1992, "cp_sex": "м", "cp_drug": "сертралин", "cp_dose": "50 мг", "cp_times": ["08:00"], "cp_instr": "утром после еды", "cp_modules": ["veteran", "depression"], } def qr_png(data: str) -> bytes: img = qrcode.make(data, box_size=8, border=2) buf = io.BytesIO() img.save(buf, format="PNG") return buf.getvalue() def _defaults(today: date) -> None: ss = st.session_state ss.setdefault("cp_name", "") ss.setdefault("cp_year", 1990) ss.setdefault("cp_sex", "м") ss.setdefault("cp_snils", h.synthetic_snils()) ss.setdefault("cp_drug", "сертралин") ss.setdefault("cp_times", ["08:00"]) ss.setdefault("cp_instr", "утром после еды") ss.setdefault("cp_visit", today + timedelta(days=28)) ss.setdefault("cp_modules", ["veteran"]) def _fill_demo(today: date) -> None: st.session_state.update(DEMO) st.session_state["cp_snils"] = h.synthetic_snils() st.session_state["cp_visit"] = today + timedelta(days=28) def _new_snils() -> None: st.session_state["cp_snils"] = h.synthetic_snils() def _reset() -> None: st.session_state.pop("cp_result", None) for k in [*DEMO, "cp_snils", "cp_visit", "cp_consent"]: st.session_state.pop(k, None) def _result(res: ConnectPatientOut, name: str) -> None: link = h.absolute_link(res.deep_link, os.environ.get("DASHBOARD_PUBLIC_URL", "http://localhost:8501")) with st.container(border=True): st.html( f'
Пациент подключён · {escape(name)}
' '
Покажите QR пациенту или продиктуйте код — бот спросит ' "согласие и начнёт сопровождение. Код одноразовый.
" ) c1, c2 = st.columns([1, 1.4], vertical_alignment="center") with c1: st.image(qr_png(link), width=220, caption="QR для подключения") with c2: st.html(f'
Код подключения
{escape(res.code)}
') st.link_button("Открыть ссылку", link, icon=":material/open_in_new:") st.code(link, language=None) b1, b2 = st.columns(2) with b1: if st.button("Карточка пациента", icon=":material/person:", width="stretch"): st.switch_page(state.PATIENTS_PAGE, query_params={"patient": res.token}) with b2: st.button("Подключить ещё", icon=":material/add:", on_click=_reset, width="stretch") def main() -> None: today = state.today() or date.today() _defaults(today) st.html( ui.page_header( "Подключить пациента", "После очного приёма: назначение уходит в МИС (мок), пациент получает код и начинает сопровождение. " "Только синтетические данные.", kicker="Кабинет врача", ) ) res: ConnectPatientOut | None = st.session_state.get("cp_result") if res is not None: _result(res, st.session_state.get("cp_result_name", "")) return st.button("Заполнить демо-данными (Андрей)", icon=":material/auto_fix_high:", on_click=_fill_demo, args=(today,)) left, right = st.columns(2, gap="large") with left, st.container(border=True): st.html('
Пациент
') st.text_input("Имя и инициал фамилии", key="cp_name", placeholder="Андрей С.", help="Без полного ФИО") a, b = st.columns(2) with a: st.number_input("Год рождения", min_value=1940, max_value=today.year - 18, step=1, key="cp_year") with b: st.radio("Пол", ["м", "ж"], key="cp_sex", horizontal=True) s1, s2 = st.columns([3, 1], vertical_alignment="bottom") with s1: st.text_input("СНИЛС (синтетический)", key="cp_snils") with s2: st.button("Новый", icon=":material/refresh:", on_click=_new_snils, width="stretch") st.pills( "Модули сопровождения", MODULE_OPTIONS, selection_mode="multi", key="cp_modules", format_func=lambda m: h.MODULE_LABELS[m], ) with right, st.container(border=True): st.html('
Назначение
') d1, d2 = st.columns([1.4, 1]) with d1: drug = st.selectbox("Препарат (МНН)", list(h.DRUGS), key="cp_drug") with d2: doses = h.DRUGS[drug] if st.session_state.get("cp_dose") not in doses: st.session_state["cp_dose"] = doses[1] if len(doses) > 1 else doses[0] st.selectbox("Доза", doses, key="cp_dose") st.multiselect("Время приёма", TIME_OPTIONS, key="cp_times") st.text_input("Указания", key="cp_instr") st.date_input("Следующий визит", key="cp_visit", min_value=today, format="DD.MM.YYYY") consent = st.checkbox( "Пациент подписал информированное добровольное согласие на дистанционное наблюдение (323-ФЗ, ст. 20, 36.2)", key="cp_consent", ) if not st.button("Подключить и получить код", type="primary", icon=":material/qr_code_2:"): return ss = st.session_state errors = [] if not ss["cp_name"].strip(): errors.append("укажите имя") if not h.is_valid_snils(ss["cp_snils"]): errors.append("СНИЛС не проходит контрольную сумму — нажмите «Новый»") if not ss["cp_times"]: errors.append("выберите время приёма") if not ss["cp_modules"]: errors.append("выберите хотя бы один модуль") if not consent: errors.append("нужно согласие пациента") if errors: st.error("Проверьте форму: " + "; ".join(errors)) return body = ConnectPatientIn( doctor_id=state.client().doctor_id, display_name=ss["cp_name"].strip(), birth_year=int(ss["cp_year"]), sex="m" if ss["cp_sex"] == "м" else "f", snils=ss["cp_snils"], drug=ss["cp_drug"], dose=ss["cp_dose"], times=sorted(ss["cp_times"]), instructions=ss["cp_instr"], next_visit=ss["cp_visit"], modules=ss["cp_modules"], ) out = state.call(lambda: state.client().connect(body)) if out is not None: ss["cp_result"] = out ss["cp_result_name"] = body.display_name st.rerun() main()