diff --git a/web/src/web/__init__.py b/web/src/web/__init__.py index 4ed00d82..6be39a2e 100644 --- a/web/src/web/__init__.py +++ b/web/src/web/__init__.py @@ -1,7 +1,6 @@ from pathlib import Path from web.config import get_settings from web import ids as web_ids -from web.pages.ed import ids as ed_ids FONTS_GOOGLE = "https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;900&display=swap" FONTS_FA = "https://use.fontawesome.com/releases/v5.8.1/css/all.css" @@ -16,8 +15,6 @@ web_ids.ROOM_STORE: f"{get_settings().api_url}/baserow/rooms/", web_ids.BEDS_STORE: f"{get_settings().api_url}/baserow/beds/", web_ids.ELECTIVES_STORE: f"{get_settings().api_url}/electives/", - ed_ids.PATIENTS_STORE: f"{get_settings().api_url}/ed/individual/", - ed_ids.AGGREGATE_STORE: f"{get_settings().api_url}/ed/aggregate/", } diff --git a/web/src/web/celery_config.py b/web/src/web/celery_config.py index 61a49d13..c86b9d0a 100644 --- a/web/src/web/celery_config.py +++ b/web/src/web/celery_config.py @@ -3,9 +3,6 @@ from web import API_URLS, SITREP_DEPT2WARD_MAPPING from celery.schedules import crontab from web import ids as web_ids -from web.pages.ed import ids as ed_ids - -from web.celery_tasks import replace_alphanumeric campus_url = API_URLS.get("campus_url") @@ -67,24 +64,6 @@ ), "kwargs": {"expires": (24 * 3600) + 60}, # 24 hours + 1 minute }, - ed_ids.PATIENTS_STORE: { - "task": "web.celery_tasks.get_response", - "schedule": crontab(minute="*/15"), # ev 15 minutes - "args": ( - API_URLS[ed_ids.PATIENTS_STORE], - replace_alphanumeric(API_URLS[ed_ids.PATIENTS_STORE]), - ), - "kwargs": {"expires": (15 * 60) + 60}, # ev 16 minutes - }, - ed_ids.AGGREGATE_STORE: { - "task": "web.celery_tasks.get_response", - "schedule": crontab(minute="*/15"), # ev 15 minutes - "args": ( - API_URLS[ed_ids.AGGREGATE_STORE], - replace_alphanumeric(API_URLS[ed_ids.AGGREGATE_STORE]), - ), - "kwargs": {"expires": (15 * 60) + 60}, # ev 16 minutes - }, } diff --git a/web/src/web/layout/nav.py b/web/src/web/layout/nav.py index 0e7c1cd6..5e32e070 100644 --- a/web/src/web/layout/nav.py +++ b/web/src/web/layout/nav.py @@ -38,11 +38,6 @@ class _NavLink(NamedTuple): sitrep_icus = _NavLink( title="Critical Care", path="/sitrep/icus", icon="healthicons:critical-care-outline" ) -perrt = _NavLink(title="PERRT", path="/sitrep/perrt", icon="carbon:stethoscope") - -ed_predictor = _NavLink( - title="ED Predictor", path="/ed/table", icon="carbon:machine-learning-model" -) def create_side_navbar() -> dmc.Navbar: @@ -132,29 +127,6 @@ def create_side_nave_content() -> dmc.Stack: variant="text", target="_blank", ), - dmc.Divider( - labelPosition="left", - label=[ - DashIconify( - icon="healthicons:ambulance-outline", - width=20, - style={"marginRight": 10}, - color=dmc.theme.DEFAULT_COLORS["indigo"][5], - ), - "Emergencies", - ], - my=20, - ), - create_main_nav_link( - icon=perrt.icon, - label=perrt.title, - href=perrt.path, - ), - create_main_nav_link( - icon=ed_predictor.icon, - label=ed_predictor.title, - href=ed_predictor.path, - ), dmc.Divider( labelPosition="left", label=[ diff --git a/web/src/web/pages/ed/__init__.py b/web/src/web/pages/ed/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web/src/web/pages/ed/callbacks.py b/web/src/web/pages/ed/callbacks.py deleted file mode 100644 index 077500d6..00000000 --- a/web/src/web/pages/ed/callbacks.py +++ /dev/null @@ -1,121 +0,0 @@ -# type: ignore -from typing import Any, Dict, List - -from dash import Input, Output, callback - -from models.ed import AggregateAdmissionRow, EmergencyDepartmentPatient -from web import API_URLS -from web import ids as app_ids -from web.celery_tasks import requests_try_cache -from web.convert import parse_to_data_frame -from web.logger import logger_timeit -from web.pages.ed import ids -from web.style import colors - - -# if the time is in utc: -ts_obj = "d3.timeParse('%Y-%m-%dT%H:%M:%S%Z')(params.data.arrival_datetime)" - -cellStyle_pAdmission = { - "styleConditions": [ - { - "condition": "params.value >.75", - "style": {"backgroundColor": colors.red}, - }, - { - "condition": "params.value >.50", - "style": {"backgroundColor": colors.orange}, - }, - { - "condition": "params.value >.25", - "style": {"backgroundColor": colors.yellow}, - }, - { - "condition": "params.value <=.25", - "style": {"backgroundColor": colors.white}, - }, - ] -} -columnDefs_patients = [ - { - "headerName": "Arrived", - "field": "arrival_datetime", - "valueGetter": {"function": ts_obj}, - "valueFormatter": {"function": f"d3.timeFormat('%H:%M %a %e')({ts_obj})"}, - }, - { - "headerName": "Location", - "field": "bed", - }, - { - "headerName": "MRN", - "field": "mrn", - }, - { - "headerName": "Name", - "field": "name", - }, - { - "headerName": "Sex", - "field": "sex", - }, - { - "headerName": "DoB", - "field": "date_of_birth", - }, - { - "headerName": "P(Admission)", - "field": "admission_probability", - "valueFormatter": {"function": "d3.format(',.0%')(params.value)"}, - "cellStyle": cellStyle_pAdmission, - # "cellRenderer": "DBC_Button_Simple", - # "cellRendererParams": {"color": "success"}, - }, - { - "headerName": "Destination", - "field": "next_location", - }, -] - - -@logger_timeit() -def _get_aggregate_patients() -> list[AggregateAdmissionRow]: - url = API_URLS[ids.AGGREGATE_STORE] - data = requests_try_cache(url) - return [AggregateAdmissionRow.parse_obj(row).dict() for row in data] - - -@callback( - Output(ids.AGGREGATE_STORE, "data"), - Input(app_ids.STORE_TIMER_15M, "n_intervals"), -) -def store_aggregate_patients(n_intervals: int) -> List[Dict[str, Any]]: - if n_intervals >= 0: - return _get_aggregate_patients() - - -@logger_timeit() -def _get_individual_patients() -> list[EmergencyDepartmentPatient]: - url = API_URLS[ids.PATIENTS_STORE] - data = requests_try_cache(url) - return [EmergencyDepartmentPatient.parse_obj(row).dict() for row in data] - - -@callback( - Output(ids.PATIENTS_STORE, "data"), - Input(app_ids.STORE_TIMER_15M, "n_intervals"), -) -def store_individual_patients(n_intervals: int) -> List[Dict[str, Any]]: - if n_intervals >= 0: - return _get_individual_patients() - - -@callback( - Output(ids.PATIENTS_GRID, "rowData"), - Output(ids.PATIENTS_GRID, "columnDefs"), - Input(ids.PATIENTS_STORE, "data"), -) -def build_patients_grid(data): - df = parse_to_data_frame(data, EmergencyDepartmentPatient) - columnDefs = columnDefs_patients - return df.to_dict("records"), columnDefs diff --git a/web/src/web/pages/ed/ed.py b/web/src/web/pages/ed/ed.py deleted file mode 100644 index 9bc3a702..00000000 --- a/web/src/web/pages/ed/ed.py +++ /dev/null @@ -1,67 +0,0 @@ -import dash -import dash_ag_grid as dag -import dash_mantine_components as dmc -from dash import dcc, html - - -from web.logger import logger -from web.pages.ed import ids - -from web.pages.ed import callbacks # noqa - - -dash.register_page(__name__, path="/ed/table", name="ED") - - -logger.debug("Confirm that you have imported all the callbacks") - -grid = dag.AgGrid( - id=ids.PATIENTS_GRID, - columnSize="responsiveSizeToFit", - defaultColDef={ - # "autoSize": True, - "resizable": True, - "sortable": True, - "filter": True, - # "minWidth": 100, - # "responsiveSizeToFit": True, - # "columnSize": "sizeToFit", - }, - className="ag-theme-material", -) - -stores = html.Div( - [ - dcc.Store(id=ids.PATIENTS_STORE), - dcc.Store(id=ids.AGGREGATE_STORE), - ] -) -notifications = html.Div( - [ - # html.Div(id=ids.ACC_BED_SUBMIT_WARD_NOTIFY), - ] -) - -body = dmc.Container( - [ - dmc.Grid( - children=[ - # dmc.Col(progress, span=12), - dmc.Col(grid, span=12), - ], - ), - ], - style={"width": "100vw"}, - fluid=True, -) - - -def layout() -> dash.html.Div: - return html.Div( - children=[ - stores, - notifications, - body, - # inspector, - ] - ) diff --git a/web/src/web/pages/ed/ids.py b/web/src/web/pages/ed/ids.py deleted file mode 100644 index c3fe28c3..00000000 --- a/web/src/web/pages/ed/ids.py +++ /dev/null @@ -1,17 +0,0 @@ -from web.utils import gen_id - -# raw stores - -# controls -# CAMPUS_SELECTOR = gen_id("campus selector", __name__) - -# other -ED_TIMER = gen_id("ed timer", __name__) -PATIENTS_GRID = gen_id("patients table", __name__) -PATIENTS_STORE = gen_id("patients store", __name__) -AGGREGATE_STORE = gen_id("aggregate store", __name__) - -PROGRESS_MED = gen_id("progress med", __name__) -PROGRESS_SURG = gen_id("progress SURG", __name__) -PROGRESS_PAED = gen_id("progress PAED", __name__) -PROGRESS_HONC = gen_id("progress HONC", __name__) diff --git a/web/src/web/pages/ed/readme.md b/web/src/web/pages/ed/readme.md deleted file mode 100644 index fc60ecb5..00000000 --- a/web/src/web/pages/ed/readme.md +++ /dev/null @@ -1,44 +0,0 @@ - - -Example return from the aggregate endpoint - -```json -[ - { - "speciality": "medical", - "beds_allocated": 4, - "beds_not_allocated": 4, - "without_decision_to_admit_seventy_percent": 5, - "without_decision_to_admit_ninety_percent": 4, - "yet_to_arrive_seventy_percent": 2, - "yet_to_arrive_ninety_percent": 1 - }, - { - "speciality": "surgical", - "beds_allocated": 4, - "beds_not_allocated": 0, - "without_decision_to_admit_seventy_percent": 1, - "without_decision_to_admit_ninety_percent": 0, - "yet_to_arrive_seventy_percent": 0, - "yet_to_arrive_ninety_percent": 0 - }, - { - "speciality": "haem_onc", - "beds_allocated": 1, - "beds_not_allocated": 0, - "without_decision_to_admit_seventy_percent": 1, - "without_decision_to_admit_ninety_percent": 1, - "yet_to_arrive_seventy_percent": 0, - "yet_to_arrive_ninety_percent": 0 - }, - { - "speciality": "paediatric", - "beds_allocated": 0, - "beds_not_allocated": 0, - "without_decision_to_admit_seventy_percent": 1, - "without_decision_to_admit_ninety_percent": 0, - "yet_to_arrive_seventy_percent": 0, - "yet_to_arrive_ninety_percent": 0 - } -] -``` diff --git a/web/src/web/pages/electives/__init__.py b/web/src/web/pages/electives/__init__.py deleted file mode 100644 index f77b8ec5..00000000 --- a/web/src/web/pages/electives/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -CAMPUSES = [ - { - "value": "UNIVERSITY COLLEGE HOSPITAL CAMPUS", - "label": "UCH", - "default_dept": "UCH T03 INTENSIVE CARE", - }, - { - "value": "GRAFTON WAY BUILDING", - "label": "GWB", - "default_dept": "GWB L01 CRITICAL CARE", - }, - { - "value": "WESTMORELAND STREET", - "label": "WMS", - "default_dept": "WMS W01 CRITICAL CARE", - }, - {"value": "QUEEN SQUARE CAMPUS", "label": "NHNN", "default_dept": "NHNN C1 NCCU"}, -] diff --git a/web/src/web/pages/electives/callbacks.py b/web/src/web/pages/electives/callbacks.py deleted file mode 100644 index 94d90154..00000000 --- a/web/src/web/pages/electives/callbacks.py +++ /dev/null @@ -1,139 +0,0 @@ -from dash import Input, Output, callback - -from web.pages.electives import ids, CAMPUSES -from web.stores import ids as store_ids - -import textwrap -from datetime import datetime - - -@callback( - Output(ids.ELECTIVES_TABLE, "data"), - Output(ids.ELECTIVES_TABLE, "filter_query"), - Input(ids.CAMPUS_SELECTOR, "value"), - Input(store_ids.ELECTIVES_STORE, "data"), - Input("date_selector", "value"), - Input("pacu_selector", "value"), -) -def _store_electives( - campus: str, electives: list[dict], date: str, pacu_selection: bool -) -> tuple[list[dict], str]: - icu_cut_off = 0.5 - preassess_date_cut_off = 90 - - campus_dict = {i.get("value"): i.get("label") for i in CAMPUSES} - - # filter by campus - electives = [ - row - for row in electives - if campus_dict.get(campus, "") in row["department_name"] - ] - - # filter by surgical date - if date is not None: - electives = [ - row - for row in electives - if row["surgery_date"] >= date[0] and row["surgery_date"] <= date[1] - ] - - # add row_ids after these filters - i = 0 - for row in electives: - row["id"] = i - i += 1 - - # add front-end columns - - row["full_name"] = "{first_name} {last_name}".format(**row) - row["age_sex"] = "{age_in_years}{sex[0]}".format(**row) - - if row["pacu"] and row["icu_prob"] > icu_cut_off: - row["pacu_yn"] = "✅ BOOKED" - elif row["pacu"] and row["icu_prob"] <= icu_cut_off: - row["pacu_yn"] = "✅ BOOKED" # "✅🤷BOOKED" - elif not row["pacu"] and row["icu_prob"] > icu_cut_off: - row["pacu_yn"] = "⚠️Not booked" - else: - row["pacu_yn"] = "🏥 No" - - preassess_in_advance = ( - datetime.strptime(row["surgery_date"], "%Y-%m-%d").date() - - datetime.strptime(row["preassess_date"], "%Y-%m-%d").date() - ).days - - if preassess_in_advance <= preassess_date_cut_off and ( - row["pac_dr_review"] is not None - or row["pac_nursing_outcome"] in ("OK to proceed", "Fit for surgery", None) - ): - row["preassess_status"] = f"✅{row['preassess_date']}" - else: - row["preassess_status"] = f"⚠️{row['preassess_date']}" - - filter_query = ( - f"{{pacu_yn}} scontains {pacu_selection}" if pacu_selection is not None else "" - ) - - return electives, filter_query - - -@callback( - Output("patient_info_box", "children"), - Input(ids.ELECTIVES_TABLE, "data"), - Input(ids.ELECTIVES_TABLE, "active_cell"), - Input(store_ids.ELECTIVES_STORE, "data"), -) -def _make_info_box( - current_table: list[dict], active_cell: dict, electives: list[dict] -) -> str: - """ - Outputs text for the patient_info_box. - If no cell is selected, automatically first patient. - info_box_width is number of characters. - """ - info_box_width = 65 - - if active_cell is None: - patient_mrn = current_table[0]["primary_mrn"] - else: - patient_mrn = current_table[active_cell["row_id"]]["primary_mrn"] - pt = [row for row in electives if row["primary_mrn"] == patient_mrn][0] - - string = """FURTHER INFORMATION - Name: {first_name} {last_name}, {age_in_years}{sex[0]} - MRN: {primary_mrn} - Operation ({surgery_date}): {patient_friendly_name} - -PACU: - Booked for PACU: {pacu} - Original surgical booking destination: {booked_destination} - Destination on preassessment clinic booking: {pacdest} - Protocolised Admission: {protocolised_adm} - -PREASSESSMENT: - Preassessment note started: {preassess_date} - Nursing outcome: {pac_nursing_outcome} - Anaesthetic review: {pac_dr_review} - Nursing issues: {pac_nursing_issues} - -EPIC MEDICAL HISTORY: - {display_string} - Maximum BMI: {bmi_max_value}. - -ECHOCARDIOGRAPHY: - {first_name} has had {num_echo} echos, - of which {abnormal_echo} were flagged as abnormal. - Last echo ({last_echo_date}): {last_echo_narrative} -""".format( - **pt - ) - - return "\n".join( - [ - textwrap.fill( - x, info_box_width, initial_indent="", subsequent_indent=" " - ) - for x in string.split("\n") - ] - ) diff --git a/web/src/web/pages/electives/electives.py b/web/src/web/pages/electives/electives.py deleted file mode 100644 index c56cfbb9..00000000 --- a/web/src/web/pages/electives/electives.py +++ /dev/null @@ -1,195 +0,0 @@ -import dash -import dash_mantine_components as dmc -import json -from dash import dash_table as dtable, html -from pathlib import Path -from datetime import date, timedelta - -import web.pages.electives.callbacks # noqa -from web.pages.electives import CAMPUSES, ids -from web.style import replace_colors_in_stylesheet - - -import logging - -logger = logging.getLogger(__name__) -logger.debug("Confirm that you have imported all the callbacks") - -dash.register_page(__name__, path="/surgery/electives", name="Electives") - -with open(Path(__file__).parent / "table_style_sheet.json") as f: - table_style_sheet = json.load(f) - table_style_sheet = replace_colors_in_stylesheet(table_style_sheet) - -timers = html.Div([]) -stores = html.Div( - [ - # dcc.Store(id=ids.CENSUS_STORE), - ] -) -notifications = html.Div( - [ - # html.Div(id=ids.ACC_BED_SUBMIT_WARD_NOTIFY), - ] -) - -campus_selector = html.Div( - [ - dmc.SegmentedControl( - id=ids.CAMPUS_SELECTOR, - value=[i.get("value") for i in CAMPUSES if i.get("label") == "UCH"][0], - data=CAMPUSES, - persistence=True, - persistence_type="local", - ), - ] -) -pacu_selector = html.Div( - [ - dmc.SegmentedControl( - id="pacu_selector", - value="", - data=[ - { - "value": "", - "label": "All", - }, - { - "value": "BOOKED", - "label": "PACU", - }, - { - "value": "No", - "label": "Not PACU", - }, - ], - persistence=True, - persistence_type="local", - ), - ] -) - -date_selector = html.Div( - # dmc.Tooltip( - # label="Double-click on a date to select a single day", - # multiline=True, - # position="top", - # openDelay=500, - # children=[ - dmc.DateRangePicker( - id="date_selector", - minDate=date.today(), - maxDate=date.today() + timedelta(days=10), - allowSingleDateInRange=True, - fullWidth=True, - value=[date.today(), (date.today() + timedelta(days=3))], - ), - # ], - # ) -) - - -electives_list = dmc.Paper( - dtable.DataTable( - id=ids.ELECTIVES_TABLE, - columns=[ - {"id": "surgery_date", "name": "Date"}, - {"id": "pacu_yn", "name": "PACU"}, - {"id": "preassess_status", "name": "Preassessment"}, - {"id": "full_name", "name": "Full Name"}, - {"id": "age_sex", "name": "Age / Sex"}, - {"id": "patient_friendly_name", "name": "Operation"}, - {"id": "primary_mrn", "name": "MRN"}, - {"id": "room_name", "name": "Theatre"}, - # {"id": "abnormal_echo", "name": "abnormal_echo"}, - # { - # "id": "icu_prob", - # "name": "prediction", - # "type": "numeric", - # "format": {"specifier": ".1f"}, - # }, - ], - # data=[], - style_table={"overflowX": "scroll"}, - style_as_list_view=True, # remove col lines - style_cell={ - "fontSize": 11, - "padding": "5px", - }, - style_cell_conditional=table_style_sheet, - style_data={"color": "black", "backgroundColor": "white"}, - # striped rows - markdown_options={"html": True}, - persistence=False, - persisted_props=["data"], - sort_action="native", - filter_action="native", - filter_query="", - ), - shadow="lg", - p="md", # padding - withBorder=True, -) - -patient_info_box = dmc.Paper(dmc.Code(id="patient_info_box", block=True)) - -debug_inspector = dmc.Container( - [ - dmc.Spoiler( - children=[ - dmc.Prism( - language="json", - # id=ids.DEBUG_NODE_INSPECTOR_WARD, children="" - ) - ], - showLabel="Show more", - hideLabel="Hide", - maxHeight=100, - ) - ] -) - -inspector = html.Div( - [ - # dmc.Modal( - # id=ids.INSPECTOR_WARD_MODAL, - # centered=True, - # padding="xs", - # size="60vw", - # overflow="inside", - # overlayColor=colors.gray, - # overlayOpacity=0.5, - # transition="fade", - # transitionDuration=0, - # children=[bed_inspector], - # ) - ] -) - -body = dmc.Container( - [ - dmc.Grid( - children=[ - dmc.Col(pacu_selector, span=4), - dmc.Col(campus_selector, span=3), - dmc.Col(date_selector, span=5), - dmc.Col(electives_list, span=7), - dmc.Col(patient_info_box, span=5), - ], - ), - ], - style={"width": "100vw"}, - fluid=True, -) - - -def layout() -> dash.html.Div: - return html.Div( - children=[ - timers, - stores, - notifications, - body, - inspector, - ] - ) diff --git a/web/src/web/pages/electives/ids.py b/web/src/web/pages/electives/ids.py deleted file mode 100644 index 96f7922c..00000000 --- a/web/src/web/pages/electives/ids.py +++ /dev/null @@ -1,9 +0,0 @@ -from web.utils import gen_id - -# raw stores - -# controls -CAMPUS_SELECTOR = gen_id("campus selector", __name__) - -# other -ELECTIVES_TABLE = gen_id("electives table", __name__) diff --git a/web/src/web/pages/electives/table_style_sheet.json b/web/src/web/pages/electives/table_style_sheet.json deleted file mode 100644 index 0c441b50..00000000 --- a/web/src/web/pages/electives/table_style_sheet.json +++ /dev/null @@ -1,57 +0,0 @@ -[ - { - "if": { - "column_id": "surgery_date" - }, - "textAlign": "left", - "fontWeight": "bold" - }, - { - "if": { - "column_id": "pacu_yn" - }, - "fontWeight": "bold", - "textAlign": "left", - "width": "5vw", - "minWidth": "2vw", - "maxWidth": "5vw", - "whitespace": "normal" - }, - { - "if": { - "column_id": "age_sex" - }, - "textAlign": "centre" - }, - { - "if": { - "column_id": "primary_service" - }, - "textAlign": "left", - "width": "15vw" - }, - { - "if": { - "column_id": "patient_friendly_name" - }, - "textAlign": "left", - "height": "auto", - "whitespace": "normal", - "maxWidth": "20vw" - }, - { - "if": { - "column_id": "primary_mrn" - }, - "textAlign": "left", - "font-family": "monospace" - }, - { - "if": { - "column_id": "icu_prob" - }, - "textAlign": "centre", - "fontWeight": "italics", - "width": "2vw" - } -] diff --git a/web/src/web/pages/perrt/__init__.py b/web/src/web/pages/perrt/__init__.py deleted file mode 100644 index fb4dded8..00000000 --- a/web/src/web/pages/perrt/__init__.py +++ /dev/null @@ -1,77 +0,0 @@ -CAMPUSES = [ - { - "value": "UNIVERSITY COLLEGE HOSPITAL CAMPUS", - "label": "UCH", - "default_dept": "UCH T03 INTENSIVE CARE", - }, - { - "value": "GRAFTON WAY BUILDING", - "label": "GWB", - "default_dept": "GWB L01 CRITICAL CARE", - }, - { - "value": "WESTMORELAND STREET", - "label": "WMS", - "default_dept": "WMS W01 CRITICAL CARE", - }, - {"value": "QUEEN SQUARE CAMPUS", "label": "NHNN", "default_dept": "NHNN C1 NCCU"}, -] - -SITREP_DEPT2WARD_MAPPING: dict = { - "UCH T03 INTENSIVE CARE": "T03", - "UCH T06 SOUTH PACU": "T06", - "GWB L01 CRITICAL CARE": "GWB", - "WMS W01 CRITICAL CARE": "WMS", - "NHNN C0 NCCU": "NCCU0", - "NHNN C1 NCCU": "NCCU1", -} - -NEWS_SCORE_COLORS = { - "1": "rgb(189, 230, 175)", - "2": "rgb(189, 230, 175)", - "3": "rgb(189, 230, 175)", - "4": "rgb(189, 230, 175)", - "5": "rgb(247, 215, 172)", - "6": "rgb(247, 215, 172)", - "7": "rgb(240, 158, 158)", - "8": "rgb(240, 158, 158)", - "9": "rgb(240, 158, 158)", - "10": "rgb(240, 158, 158)", - "11": "rgb(240, 158, 158)", - "12": "rgb(240, 158, 158)", - "13": "rgb(240, 158, 158)", - "14": "rgb(240, 158, 158)", - "15": "rgb(240, 158, 158)", - "16": "rgb(240, 158, 158)", - "17": "rgb(240, 158, 158)", - "18": "rgb(240, 158, 158)", - "19": "rgb(240, 158, 158)", - "20": "rgb(240, 158, 158)", - "21": "rgb(240, 158, 158)", - "22": "rgb(240, 158, 158)", - "23": "rgb(240, 158, 158)", -} - -DISCHARGE_DECISIONS = [ - dict(label="HOLD", value="blocked", description="Not for discharge"), - dict( - label="REVIEW", - value="review", - description="Review for possible " "discharge later", - ), - dict( - label="DISCHARGE", - value="discharge", - description="Ready for " "discharge " "now", - ), - dict( - label="EXCELLENCE", - value="excellence", - description="Excellence in " "the " "End of Life " "pathway", - ), - dict( - label="BLOCKED", - value="blocked", - description="Block the bed (not " "available for " "admissions)", - ), -] diff --git a/web/src/web/pages/perrt/callbacks/__init__.py b/web/src/web/pages/perrt/callbacks/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web/src/web/pages/perrt/callbacks/cytoscape.py b/web/src/web/pages/perrt/callbacks/cytoscape.py deleted file mode 100644 index f3c04708..00000000 --- a/web/src/web/pages/perrt/callbacks/cytoscape.py +++ /dev/null @@ -1,413 +0,0 @@ -import pandas as pd -import warnings -from dash import Input, Output, callback -from datetime import datetime -from typing import Tuple - -from models.census import CensusRow -from web.config import get_settings -from web.pages.perrt import CAMPUSES, ids -from web.stores import ids as store_ids -from web.celery_tasks import requests_try_cache - -DEBUG = True - - -@callback( - Output(ids.DEPTS_OPEN_STORE, "data"), - Input(ids.CAMPUS_SELECTOR, "value"), - Input(store_ids.DEPT_STORE, "data"), -) -def _store_depts(campus: str, depts: list[dict]) -> list[dict]: - """Need a list of departments for this building""" - try: - these_depts = [dept for dept in depts if dept.get("location_name") == campus] - except TypeError as e: - print(e) - warnings.warn(f"No departments found at {campus} campus") - these_depts = [] - return these_depts - - -@callback( - Output(ids.DEPTS_OPEN_STORE_NAMES, "data"), - Input(ids.DEPTS_OPEN_STORE, "data"), -) -def _dept_open_store_names(depts_open: list[dict]) -> list[str]: - return [i.get("department", {}) for i in depts_open] - - -@callback( - Output(ids.ROOMS_OPEN_STORE, "data"), - Input(ids.DEPTS_OPEN_STORE, "data"), - Input(store_ids.ROOM_STORE, "data"), -) -def _store_rooms( - depts: list[dict], - rooms: list[dict], -) -> list[dict]: - """Need a list of rooms for this building""" - dfdepts = pd.DataFrame.from_records(depts) - dfdepts = dfdepts[["department", "hl7_department"]] - dfrooms = pd.DataFrame.from_records(rooms) - # default inner join drops rooms not in the selected departments - dfrooms = dfrooms.merge(dfdepts, on="department") - # drop closed rooms - dfrooms = dfrooms.loc[~dfrooms["closed"], :] - - return dfrooms.to_dict(orient="records") # type: ignore - - -@callback( - Output(ids.BEDS_STORE, "data"), - Input(ids.DEPTS_OPEN_STORE, "data"), - Input(ids.ROOMS_OPEN_STORE, "data"), - Input(store_ids.BEDS_STORE, "data"), -) -def _store_beds( - depts: list[dict], - rooms: list[dict], - beds: list[dict], -) -> list[dict]: - """ - Return a list of beds using the local filtered versions of depts/rooms - - generate the floor_index from the bed_number to permit appropriate sorting - """ - - bedsdf = pd.DataFrame.from_records(beds) - dfdepts = pd.DataFrame.from_records(depts) - dfrooms = pd.DataFrame.from_records(rooms) - - dfdepts = dfdepts[["department", "floor_order"]] - - # drop beds where rooms are closed - # look for bays where all beds are closed - dft = bedsdf.groupby("hl7_room")["closed"].all() - dft = pd.DataFrame(dft).reset_index() - dft.rename(columns={"closed": "closed_all_beds"}, inplace=True) - dfrooms = dfrooms.merge(dft, on="hl7_room") - - # now close a room if any of the following are true - dfrooms["closed"] = dfrooms["closed"] | dfrooms["closed_all_beds"] - dfrooms.drop(columns=["closed_all_beds"], inplace=True) - # drop closed rooms - dfrooms = dfrooms.loc[~dfrooms["closed"], :] - dfrooms = dfrooms[["hl7_room", "is_sideroom"]] - - # inner join to drop rooms without beds - bedsdf = bedsdf.merge(dfrooms, on="hl7_room", how="inner") - # inner join to drop closed_perm_01 - bedsdf = bedsdf.merge(dfdepts, on="department", how="inner") - - bedsdf = bedsdf[bedsdf["bed_number"] != -1] - bedsdf = bedsdf[~bedsdf["closed"]] - - def _gen_floor_indices(df: pd.DataFrame) -> pd.DataFrame: - # now generate floor_y_index - df.sort_values( - ["floor", "floor_order", "department", "bed_number"], inplace=True - ) - floor_depts = df[["floor", "floor_order", "department"]].drop_duplicates() - floor_depts.sort_values(["floor", "floor_order"], inplace=True) - floor_depts["floor_y_index"] = floor_depts.reset_index().index + 1 - df = df.merge(floor_depts, how="left") - - # create a floor x_index by sorting and ranking within floor_y_index - df.sort_values(["floor_y_index", "bed_number"], inplace=True) - df["floor_x_index"] = df.groupby("floor_y_index")["bed_number"].rank( - method="first", na_option="keep" - ) - df.sort_values(["location_string"], inplace=True) - return df - - bedsdf = _gen_floor_indices(bedsdf) - - res: list[dict] = bedsdf.to_dict(orient="records") - return res - - -@callback( - Output(ids.CENSUS_STORE, "data"), - Input(ids.CAMPUS_SELECTOR, "value"), - Input(ids.DEPTS_OPEN_STORE_NAMES, "data"), -) -def _store_census( - campus: str, - depts_open_names: list[str], -) -> list[dict]: - """ - Store CensusRow as list of dictionaries after filtering out closed - departments for that building - Args: - campus: one of UCH/WMS/GWB/NHNN - depts_open_names: list of departments that are open - - Returns: - Filtered list of CensusRow dictionaries - - """ - campus_short_name = next( - i.get("label") for i in CAMPUSES if i.get("value") == campus - ) - - # Drop in replacement for requests.get that uses the redis cache - # response = requests.get( - # f"{get_settings().api_url}/census/campus/", - # params={"campuses": campus_short_name}, - # ) - url = f"{get_settings().api_url}/census/campus/" - params = {"campuses": campus_short_name} - data = requests_try_cache(url, params=params) - - res = [CensusRow.parse_obj(row).dict() for row in data] - res = [row for row in res if row.get("department") in depts_open_names] - return res - - -@callback( - Output(ids.NEWS_STORE, "data"), - Input(ids.CENSUS_STORE, "data"), - prevent_initial_callback=True, -) -def _store_news(census: list[dict]) -> list[dict]: - """ - Use the census store to provide the CSNs to query additional data - Args: - census: - - Returns: - NEWS score for each patient in the CENSUS - """ - csn_list = [i.get("encounter") for i in census if i.get("occupied")] # type: ignore - - url = f"{get_settings().api_url}/perrt/vitals/wide" - params = {"encounter_ids": csn_list} - data = requests_try_cache(url, params=params) - - newsdf = pd.DataFrame.from_records(data) - # TODO: simpplify: you just want the most recent and highest NEWS score - # and its timestamp - - news: list[dict] = newsdf.to_dict(orient="records") - return news - - -@callback( - Output(ids.PREDICTIONS_STORE, "data"), - Input(ids.CENSUS_STORE, "data"), -) -def _store_predictions(census: list[dict]) -> dict: - """ - Use the census store to provide the CSNs to query admission prediction data - Args: - census: - - Returns: - Admission prediction data for each patient in the CENSUS,if it exists, - NULL otherwise - """ - hv_id_list = [i.get("ovl_hv_id") for i in census if i.get("occupied")] - url = f"{get_settings().api_url}/perrt/icu_admission_prediction" - params = {"hospital_visit_ids": hv_id_list} # type: ignore - predictions = requests_try_cache(url, params=params) - - return predictions # type: ignore - - -@callback( - [ - Output(ids.DEPT_SELECTOR, "data"), - Output(ids.DEPT_SELECTOR, "value"), - ], - Input(ids.DEPTS_OPEN_STORE_NAMES, "data"), - Input(ids.CAMPUS_SELECTOR, "value"), -) -def _dept_select_control(depts: list[str], campus: str) -> Tuple[list[str], str]: - """Populate select input with data (dept name) and default value""" - default = [i.get("default_dept", "") for i in CAMPUSES if i.get("value") == campus][ - 0 - ] - return depts, default - - -def _make_elements( # noqa: C901 - census: list[dict], - depts: list[dict], - beds: list[dict], - news: list[dict], - predictions: dict, -) -> list[dict]: - """ - Logic to create elements for cyto map - - Args: - census: list of bed status objects with occupancy (census) - depts: list of dept objects - beds: list of bed objects (from baserow) - - Returns: - list of elements for cytoscape map - """ - - # Start with an empty list of elements to populate - elements = list() - - # define the 'height' of the map - y_index_max = max([bed.get("floor_y_index", -1) for bed in beds]) - census_lookup = {i.get("location_string"): i for i in census} - news_lookup = {i.get("encounter"): i for i in news} - - # create beds - for bed in beds: - department = bed.get("department") - location_string = bed.get("location_string") - - occupied = census_lookup.get(location_string, {}).get("occupied", False) - encounter = census_lookup.get(location_string, {}).get("encounter", "") - hospital_visit_id = census_lookup.get(location_string, {}).get( - "ovl_hv_id", None - ) - news_wide: dict = news_lookup.get(encounter, {}) # type: ignore - - # Hospital_visit_ids are integers, but the dictionary uses strings for keys. - # Lookup using a string to be safe - # Don't get the prediction if the bed isn't occupied - admission_prediction = ( - predictions.get(str(hospital_visit_id), None) if occupied else None - ) - - def _max_news_wide(row: dict) -> int: - if not row: - return -1 - scale_1_max = row.get("news_scale_1_max", -1) - scale_2_max = row.get("news_scale_2_max", -1) - max_news: int = max( - i for i in [-1, scale_1_max, scale_2_max] if i is not None - ) - - return max_news - - data = dict( - id=location_string, - bed_number=bed.get("bed_number"), - bed_index=bed.get("bed_index"), - department=department, - floor=bed.get("floor"), - entity="bed", - parent=bed.get("department"), - bed=bed, - census=census_lookup.get(location_string, {}), - closed=bed.get("closed"), - blocked=bed.get("blocked"), - occupied=occupied, - encounter=encounter, - news=news_wide, - news_max=_max_news_wide(news_wide), - admission_prediction=admission_prediction, - ) - position = dict( - x=bed.get("floor_x_index", -1) * 40, - y=(y_index_max - bed.get("floor_y_index", -1)) * 60, - ) - elements.append( - dict( - data=data, - position=position, - grabbable=True, - selectable=True, - locked=False, - ) - ) - - for dept in depts: - dept_name = dept.get("department") - data = dict( - id=dept_name, - label=dept_name, - entity="department", - dept=dept, - parent=dept.get("location_name"), - ) - elements.append( - dict( - data=data, - grabbable=True, - selectable=True, - locked=False, - selected=False, - ) - ) - - # Sort elements by floor/dept/bed_number: NB: make a tuple for the sort - # https://stackoverflow.com/a/33893264/992999 - elements = sorted( - elements, - key=lambda e: ( - e.get("data").get("entity", ""), # type: ignore - e.get("data").get("department", ""), # type: ignore - e.get("data").get("bed_number", ""), # type: ignore - ), - ) # type: ignore - return elements - - -@callback( - Output(ids.CYTO_CAMPUS, "elements"), - Input(ids.CENSUS_STORE, "data"), - Input(ids.DEPTS_OPEN_STORE, "data"), - Input(ids.BEDS_STORE, "data"), - Input(ids.NEWS_STORE, "data"), - Input(ids.PREDICTIONS_STORE, "data"), - prevent_initial_call=True, -) -def _prepare_cyto_elements_campus( - census: list[dict], - depts: list[dict], - beds: list[dict], - news: list[dict], - predictions: dict, -) -> list[dict]: - """ - Build the element list from pts/beds/rooms/depts for the map - """ - elements = _make_elements(census, depts, beds, news, predictions) - return elements - - -def format_census(census: dict) -> dict: - """Given a census object return a suitably formatted dictionary""" - mrn = census.get("mrn", "") - encounter = str(census.get("encounter", "")) - lastname = census.get("lastname", "").upper() - firstname = census.get("firstname", "").title() - initials = ( - f"{census.get('firstname', '?')[0]}" - f"" - f"" - f"" - f"{census.get('lastname', '?')[0]}" - ) - date_of_birth = census.get("date_of_birth", "1900-01-01") # default dob - dob = datetime.fromisoformat(date_of_birth) - dob_fshort = datetime.strftime(dob, "%d-%m-%Y") - dob_flong = datetime.strftime(dob, "%d %b %Y") - age = int((datetime.utcnow() - dob).days / 365.25) - sex = census.get("sex") - if sex is None: - sex = "" - else: - sex = "M" if sex.lower() == "m" else "F" - - return dict( - mrn=mrn, - encounter=encounter, - lastname=lastname, - firstname=firstname, - initials=initials, - dob=dob, - dob_fshort=dob_fshort, - dob_flong=dob_flong, - age=age, - sex=sex, - demographic_slug=f"{firstname} {lastname} | {age}{sex} | MRN {mrn}", - ) diff --git a/web/src/web/pages/perrt/callbacks/discharges.py b/web/src/web/pages/perrt/callbacks/discharges.py deleted file mode 100644 index 456cc582..00000000 --- a/web/src/web/pages/perrt/callbacks/discharges.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Module to manage the CRUD of discharge status -""" -import requests -from pydantic import BaseModel -from typing import Tuple - -from models.beds import DischargeStatus -from web.config import get_settings -from web.convert import parse_to_data_frame - - -def post_discharge_status(csn: int, status: str) -> Tuple[int, DischargeStatus]: - status = status.lower() - response = requests.post( - url=f"{get_settings().api_url}/baserow/discharge_status", - params={"csn": csn, "status": status}, # type: ignore - ) - return response.status_code, DischargeStatus.parse_obj(response.json()) - - -def _most_recent_row_only( - rows: list[dict], groupby_col: str, timestamp_col: str, data_model: BaseModel -) -> list[dict]: - df = parse_to_data_frame(rows, data_model) - # remove duplicates here - df = df.sort_values(timestamp_col, ascending=False) - df = df.groupby(groupby_col).head(1) - return df.to_dict(orient="records") # type: ignore diff --git a/web/src/web/pages/perrt/callbacks/inspector.py b/web/src/web/pages/perrt/callbacks/inspector.py deleted file mode 100644 index 55de5096..00000000 --- a/web/src/web/pages/perrt/callbacks/inspector.py +++ /dev/null @@ -1,392 +0,0 @@ -import dash -import dash_mantine_components as dmc -import json -from dash import Input, Output, State, callback, callback_context -from dash_iconify import DashIconify -from typing import Any, Tuple -from datetime import datetime - -from web.pages.perrt import DISCHARGE_DECISIONS, ids -from web.pages.perrt.callbacks.cytoscape import format_census -from web.pages.perrt.callbacks.discharges import post_discharge_status -from web.style import colors - -DEBUG = True - - -def _format_tapnode(data: dict | None) -> str: - """JSON dump of data from node (for debugging inspector""" - if data: - # remove the style part of tapNode for readabilty - data.pop("style", None) - return json.dumps(data, indent=4) - - -def _create_accordion_item(control: Any, panel: Any) -> Any: - return [dmc.AccordionControl(control), dmc.AccordionPanel(panel)] - - -@callback( - [ - Output(ids.SIDEBAR_CONTENT, "hidden"), - Output(ids.INSPECTOR_CAMPUS_ACCORDION, "value"), - Output(ids.SIDEBAR_TITLE, "children"), - ], - Input(ids.CYTO_CAMPUS, "selectedNodeData"), - prevent_initial_callback=True, -) -def update_patient_sidebar(nodes: list[dict]) -> Tuple[bool, list[str], dmc.Group]: - """ - Open modal - prepare modal title - define which accordion item is open - """ - click_title = dmc.Group( - [ - DashIconify( - icon="material-symbols:left-click-rounded", - color=colors.indigo, - width=30, - ), - dmc.Text("Click on a bed for more information", weight=500), - ] - ) - - if not nodes or len(nodes) != 1: - return True, [], dmc.Group(click_title) - - data = nodes[0] - if data.get("entity") != "bed": - return True, ["bed"], dmc.Group(click_title) - - bed = data.get("bed") # type: ignore - bed_color = colors.orange if data.get("occupied") else colors.gray - bed_number = bed.get("bed_number") # type: ignore - department = bed.get("department") # type: ignore - - bed_title = dmc.Group( - [ - DashIconify( - icon="carbon:hospital-bed", - color=bed_color, - width=30, - ), - dmc.Text(f"BED {bed_number}", weight=500), - dmc.Text(f"{department}", color=colors.gray), - ], - pb=20, - ) - - return False, ["bed"], bed_title - - -@callback( - Output(ids.ACCORDION_ITEM_PERRT, "children"), - Input(ids.CYTO_CAMPUS, "selectedNodeData"), - prevent_initial_call=True, -) -def perrt_accordion_item( - nodes: list[dict], -) -> Tuple[dmc.AccordionControl, dmc.AccordionPanel]: - """Prepare content for PERRT accordion item""" - control, panel = None, None - if not nodes or len(nodes) != 1: - return dmc.AccordionControl(control), dmc.AccordionPanel(panel) - - data = nodes[0] - if data.get("entity") != "bed": - return dmc.AccordionControl(control), dmc.AccordionPanel(panel) - - news = data.get("news", {}) - admission_prediction = data.get("admission_prediction", {}) - - if admission_prediction: - pred_text = dmc.Text("ICU admission probability") - pred_content = dmc.Group( - dmc.Badge( - f"{round(100 * admission_prediction)}%", - color="orange", - variant="filled", - ) - ) - else: - pred_text = dmc.Text("ICU admission probability not available") - pred_content = dmc.Group() - - if news: - news_text = dmc.Text("Highest/lowest vitals within the last 6 hours") - news_content = dmc.Group( - [ - dmc.Stack( - [ - dmc.Badge("HR", color=colors.indigo, variant="outline"), - dmc.Badge( - news.get("pulse_max"), color=colors.indigo, variant="filled" - ), - dmc.Badge( - news.get("pulse_min"), color=colors.indigo, variant="filled" - ), - ] - ), - dmc.Stack( - [ - dmc.Badge("RR", color=colors.indigo, variant="outline"), - dmc.Badge( - news.get("resp_max"), color=colors.indigo, variant="filled" - ), - dmc.Badge( - news.get("resp_min"), color=colors.indigo, variant="filled" - ), - ] - ), - dmc.Stack( - [ - dmc.Badge("BP", color=colors.indigo, variant="outline"), - dmc.Badge( - news.get("bp_max"), color=colors.indigo, variant="filled" - ), - dmc.Badge( - news.get("bp_min"), color=colors.indigo, variant="filled" - ), - ] - ), - dmc.Stack( - [ - dmc.Badge("SpO2", color=colors.indigo, variant="outline"), - dmc.Badge( - news.get("spo2_max"), color=colors.indigo, variant="filled" - ), - dmc.Badge( - news.get("spo2_min"), color=colors.indigo, variant="filled" - ), - ] - ), - dmc.Stack( - [ - dmc.Badge("Temp", color=colors.indigo, variant="outline"), - dmc.Badge( - round(news.get("temp_max"), 1), - color=colors.indigo, - variant="filled", - ), - dmc.Badge( - round(news.get("temp_min"), 1), - color=colors.indigo, - variant="filled", - ), - ] - ), - dmc.Stack( - [ - dmc.Badge("AVPU", color=colors.indigo, variant="outline"), - dmc.Badge( - news.get("avpu_max"), color=colors.indigo, variant="filled" - ), - dmc.Badge( - news.get("avpu_min"), color=colors.indigo, variant="filled" - ), - ] - ), - ] - ) - - else: - news_content = dmc.Group() - news_text = dmc.Text("Vitals not available") - - control = dmc.Group( - [ - DashIconify( - icon="carbon:activity", - width=20, - ), - dmc.Text("NEWS and vitals"), - ] - ) - panel = dmc.Grid( - [ - dmc.Col( - [ - pred_text, - pred_content, - news_text, - news_content, - ], - span=12, - ), - ] - ) - - return dmc.AccordionControl(control), dmc.AccordionPanel(panel) - - -@callback( - Output(ids.ACC_BED_DECISION_TEXT, "children"), - Input(ids.ACC_BED_STATUS_CAMPUS, "value"), -) -def update_decision_description(value: str) -> str: - description = [ - i.get("description", "") for i in DISCHARGE_DECISIONS if i.get("label") == value - ] - if description: - return description[0] - else: - return "Choose one" - - -@callback( - [ - Output(ids.ACC_BED_SUBMIT_CAMPUS_NOTIFY, "children"), - Output(ids.ACC_BED_SUBMIT_CAMPUS, "disabled"), - Output(ids.ACC_BED_SUBMIT_STORE, "data"), - ], - Input(ids.ACC_BED_SUBMIT_CAMPUS, "n_clicks"), - Input(ids.ACC_BED_STATUS_CAMPUS, "value"), - State(ids.ACC_BED_SUBMIT_CAMPUS, "disabled"), - State(ids.CYTO_CAMPUS, "tapNode"), - prevent_initial_call=True, -) -def submit_discharge_status( - _: int, - value: str, - disabled: bool, - node: dict, -) -> Tuple[dmc.Notification, bool, dict]: - """Handle the submission of new info""" - - msg = "" - data = node.get("data", {}) - status = value.lower() - response_status = -1 - response_dict = {} - - if callback_context.triggered_id == ids.ACC_BED_STATUS_CAMPUS: - bed_status_control_value = data.get("dc_status", "").upper() - disabled = True if bed_status_control_value == status else False - show = False - elif callback_context.triggered_id == ids.ACC_BED_SUBMIT_CAMPUS: - if status != "blocked": - encounter = int(data.get("encounter", -1)) - response_status, response_json = post_discharge_status( - csn=encounter, status=value - ) - response_dict = response_json.dict() - if response_status == 200: - msg = "Updated discharge status: OK" - disabled = True - else: - msg = "Uh-oh: Unable to save discharge status - try again?" - disabled = False - - show = True - else: - disabled = False - show = False - - if show: - show_arg = "show" if show else "hide" - - bed_submit_dict = dict( - msg=msg, - status=status.lower(), - id=data.get("id"), - response_json=response_dict, - response_status=response_status, - ) - - notificaton = dmc.Notification( - title="Saving discharge status", - id="_submit_discharge_status_notification_NOT_IN_USE", - action=show_arg, - message=msg, - icon=DashIconify(icon="ic:round-celebration"), - ) - - return notificaton, disabled, bed_submit_dict - - else: - return dash.no_update, disabled, dash.no_update - - -@callback( - Output(ids.ACCORDION_ITEM_PATIENT, "children"), - Input(ids.CYTO_CAMPUS, "tapNode"), -) -def patient_accordion_item( - node: dict, -) -> Tuple[dmc.AccordionControl, dmc.AccordionPanel]: - """Prepare content for bed accordion item""" - if not node: - control, panel = None, None - return dmc.AccordionControl(control), dmc.AccordionPanel(panel) - - data = node.get("data", {}) - census = data.get("census", {}) - occupied = census.get("occupied", False) - sex_icon = "carbon:person" - control_text = "Unoccupied" - if census and occupied: - censusf = format_census(census) - sex = censusf.get("sex", "") - if sex: - sex_icon = ( - "carbon:gender-male" if sex.lower() == "m" else "carbon:gender-female" - ) - control_text = censusf.get("demographic_slug", "Uh-oh! No patient data?") - - control = dmc.Group( - [ - DashIconify( - icon=sex_icon, - width=20, - ), - dmc.Text(control_text), - ] - ) - - try: - hospital_admit = census.get("hv_admission_dt") - hospital_los = int((datetime.utcnow() - hospital_admit).days) - text_los = ( - f"Day {hospital_los}" - f"(Hospital admission: {datetime.strftime(hospital_admit, '%d %b %Y')})" - ) - - except TypeError: - text_los = "Hospital length of stay unknown" - - panel = dmc.Group([dmc.Text(text_los)]) - - return dmc.AccordionControl(control), dmc.AccordionPanel(panel) - - -@callback( - Output(ids.ACCORDION_ITEM_DEBUG, "children"), - Input(ids.CYTO_CAMPUS, "tapNode"), -) -def debug_accordion_item(node: dict) -> Tuple[dmc.AccordionControl, dmc.AccordionPanel]: - """Prepare content for debug accordion item""" - title = dmc.Group( - [ - DashIconify( - icon="carbon:debug", - width=20, - ), - dmc.Text("Developer and debug inspector"), - ] - ) - control = dmc.AccordionControl(title) - panel = dmc.AccordionPanel( - dmc.Spoiler( - children=[ - dmc.Prism( - language="json", - children=_format_tapnode(node), - ) - ], - showLabel="Show more", - hideLabel="Hide", - maxHeight=200, - ) - ) - return control, panel diff --git a/web/src/web/pages/perrt/callbacks/widgets.py b/web/src/web/pages/perrt/callbacks/widgets.py deleted file mode 100644 index 1081db31..00000000 --- a/web/src/web/pages/perrt/callbacks/widgets.py +++ /dev/null @@ -1,85 +0,0 @@ -from dash import Input, Output, callback - -from web.pages.perrt import ids -from web.style import colors - -DEBUG = True - - -def _progress_bar_bed_count(elements: list[dict]) -> list[dict]: - """Given elements from a cytoscape bed map then prepare sections for - progress bar""" - beds = [ - ele.get("data", {}) - for ele in elements - if ele.get("data", {}).get("entity") == "bed" - ] - - # TODO: replace with total capacity from department sum - N = len(beds) - occupied = len([i for i in beds if i.get("occupied")]) - blocked = len([i for i in beds if i.get("blocked")]) - news = [i.get("news_max", -1) for i in beds if i.get("occupied")] - news_miss = len([i for i in news if i == -1]) - news_low = len([i for i in news if 0 <= i <= 4]) - news_medium = len([i for i in news if 5 <= i <= 6]) - news_high = len([i for i in news if i >= 7]) - empty = N - occupied - blocked - - # Adjust colors and labels based on size - def _make_progress_label(val: int, N: int, label: str) -> str: - if val == 0: - return "" - elif val / N < 0.2: - return f"{val}" - else: - return f"{val} {label}" - - empty_label = _make_progress_label(empty, N, "empty") - news_miss_label = _make_progress_label(news_miss, N, "unrecorded") - news_low_label = _make_progress_label(news_low, N, "Low risk") - news_medium_label = _make_progress_label(news_medium, N, "Medium risk") - news_high_label = _make_progress_label(news_high, N, "High risk") - empty_colour = colors.silver - - return [ - dict( - value=news_low / N * 100, - color=colors.olive, - label=news_low_label, - tooltip=f"{news_low} low risk patients", - ), - dict( - value=news_miss / N * 100, - color=colors.indigo, - label=news_miss_label, - tooltip=f"{news_miss} patients without recent NEWS", - ), - dict( - value=news_medium / N * 100, - color="#F5C487", - label=news_medium_label, - tooltip=f"{news_medium} medium risk patients", - ), - dict( - value=news_high / N * 100, - color="#EC9078", - label=news_high_label, - tooltip=f"{news_high} high risk patients", - ), - dict( - value=empty / N * 100, - color=empty_colour, - label=empty_label, - tooltip=f"{empty} empty beds", - ), - ] - - -@callback( - Output(ids.PROGRESS_CAMPUS, "sections"), - Input(ids.CYTO_CAMPUS, "elements"), - prevent_initial_call=True, -) -def progress_bar_campus(elements: list[dict]) -> list[dict]: - return _progress_bar_bed_count(elements) diff --git a/web/src/web/pages/perrt/campus.py b/web/src/web/pages/perrt/campus.py deleted file mode 100644 index 95e5186f..00000000 --- a/web/src/web/pages/perrt/campus.py +++ /dev/null @@ -1,182 +0,0 @@ -import dash -import dash_cytoscape as cyto -import dash_mantine_components as dmc -import json -from dash import dcc, html -from pathlib import Path - -# noqa suppresses black errors when linting since you need this import for -# access to callbacks -import web.pages.perrt.callbacks.cytoscape # noqa -import web.pages.perrt.callbacks.inspector # noqa -import web.pages.perrt.callbacks.widgets # noqa -from web.pages.perrt import CAMPUSES, ids -from web.style import replace_colors_in_stylesheet - -dash.register_page(__name__, path="/sitrep/perrt", name="PERRT") - -with open(Path(__file__).parent / "cyto_style_sheet.json") as f: - cyto_style_sheet = json.load(f) - cyto_style_sheet = replace_colors_in_stylesheet(cyto_style_sheet) - -timers = html.Div([]) -stores = html.Div( - [ - dcc.Store(id=ids.CENSUS_STORE), - dcc.Store(id=ids.DEPTS_OPEN_STORE), - dcc.Store(id=ids.ROOMS_OPEN_STORE), - dcc.Store(id=ids.BEDS_STORE), - dcc.Store(id=ids.NEWS_STORE), - dcc.Store(id=ids.DEPTS_OPEN_STORE_NAMES), - dcc.Store(id=ids.ACC_BED_SUBMIT_STORE), - dcc.Store(id=ids.PREDICTIONS_STORE), - ] -) - -notifications = html.Div( - [ - html.Div(id=ids.ACC_BED_SUBMIT_CAMPUS_NOTIFY), - ] -) - -campus_selector = dmc.Container( - [ - dmc.SegmentedControl( - id=ids.CAMPUS_SELECTOR, - value=[i.get("value") for i in CAMPUSES if i.get("label") == "UCH"][0], - data=CAMPUSES, - persistence=True, - persistence_type="local", - ), - ] -) - -dept_selector = dmc.Container( - [ - dmc.Select( - label="Select a ward", - placeholder="ward", - id=ids.DEPT_SELECTOR, - ), - ] -) - -campus_status = dmc.Paper( - [ - dmc.Progress( - id=ids.PROGRESS_CAMPUS, - size=20, - radius="md", - # style={"font-size": "10px", "font-weight": 300}, - ) - ], -) - -campus_cyto = dmc.Paper( - [ - cyto.Cytoscape( - id=ids.CYTO_CAMPUS, - style={ - # "width": "70vw", - "height": "75vh", - "z-index": 999, - }, - layout={ - "name": "preset", - "animate": True, - "fit": True, - "padding": 10, - }, - stylesheet=cyto_style_sheet, - responsive=True, - userPanningEnabled=True, - userZoomingEnabled=True, - ) - ], - shadow="lg", - radius="lg", - p="md", # padding - withBorder=True, - # style={"width": "90vw"}, -) - -debug_inspector = dmc.Container( - [ - dmc.Spoiler( - children=[ - dmc.Prism( - language="json", id=ids.DEBUG_NODE_INSPECTOR_CAMPUS, children="" - ) - ], - showLabel="Show more", - hideLabel="Hide", - maxHeight=100, - ) - ] -) - -bed_inspector = html.Div( - [ - dmc.AccordionMultiple( - id=ids.INSPECTOR_CAMPUS_ACCORDION, - children=[ - dmc.AccordionItem(id=ids.ACCORDION_ITEM_PATIENT, value="patient"), - dmc.AccordionItem(id=ids.ACCORDION_ITEM_PERRT, value="bed"), - dmc.AccordionItem(id=ids.ACCORDION_ITEM_DEBUG, value="debug"), - ], - chevronPosition="left", - variant="separated", - transitionDuration=0, - ) - ] -) - - -sidebar_title = html.Div(id=ids.SIDEBAR_TITLE) -sidebar_content = html.Div(id=ids.SIDEBAR_CONTENT, children=bed_inspector) - -sidebar = html.Div( - children=[ - sidebar_title, - sidebar_content, - ] -) - -patient_sidebar = dmc.Container( - dmc.Paper(shadow="lg", radius="lg", p="xs", withBorder=True, children=[sidebar]) -) - -body = dmc.Container( - [ - dmc.Grid( - [ - # dmc.Col(dept_selector, span=6), - dmc.Col(campus_selector, offset=9, span=3), - dmc.Col(campus_status, span=12), - # nested grid - dmc.Col( - dmc.Grid( - [ - dmc.Col(campus_cyto, span=12), - ] - ), - span=9, - ), - dmc.Col(dmc.Grid([dmc.Col(patient_sidebar)]), span=3), - ] - ) - ], - style={"width": "90vw"}, - fluid=True, -) - - -def layout() -> dash.html.Div: - return html.Div( - children=[ - timers, - stores, - notifications, - body, - ] - ) diff --git a/web/src/web/pages/perrt/cyto_style_sheet.json b/web/src/web/pages/perrt/cyto_style_sheet.json deleted file mode 100644 index 784ac73c..00000000 --- a/web/src/web/pages/perrt/cyto_style_sheet.json +++ /dev/null @@ -1,101 +0,0 @@ -[ - { - "selector": "node", - "style": { - "text-halign": "center", - "text-valign": "center", - "width": 36, - "height": 36 - } - }, - { - "selector": "[entity='department']", - "style": { - "label": "data(label)", - "color": "black", - "background-opacity": 0.1, - "background-color": "gray", - "border-color": "black", - "border-width": 0, - "text-halign": "left", - "text-margin-x": -5 - } - }, - { - "selector": "[entity='bed']", - "style": { - "shape": "ellipse", - "label": "data(bed_number)", - "color": "gray", - "border-color": "olive", - "border-width": 2, - "background-color": "olive", - "background-opacity": 0 - } - }, - { - "selector": "[?closed]", - "style": { - "background-color": "white", - "color": "gray" - } - }, - { - "selector": "[?blocked]", - "style": { - "background-color": "gray" - } - }, - { - "selector": "[?occupied]", - "style": { - "border-width": 0, - "background-color": "indigo", - "background-opacity": 1 - } - }, - { - "selector": "[?occupied][news_max>=0]", - "style": { - "background-color": "olive", - "background-opacity": 0.5, - "color": "white" - } - }, - { - "selector": "[?occupied][news_max>4]", - "style": { - "background-color": "#F5C487", - "background-opacity": 1, - "color": "white" - } - }, - { - "selector": "[?occupied][news_max>6]", - "style": { - "background-color": "#EC9078", - "background-opacity": 1, - "color": "white" - } - }, - { - "selector": "[?occupied][news_max=-1]", - "style": { - "background-color": "indigo", - "background-opacity": 1, - "color": "white" - } - }, - - { - "selector": ":selected:", - "style": { - "width": 36, - "height": 36, - "text-margin-y": 2, - "border-width": 3, - "border-color": "black", - "color": "black" - } - } -] diff --git a/web/src/web/pages/perrt/ids.py b/web/src/web/pages/perrt/ids.py deleted file mode 100644 index dd3a0a79..00000000 --- a/web/src/web/pages/perrt/ids.py +++ /dev/null @@ -1,43 +0,0 @@ -from web.utils import gen_id - -# raw stores -CENSUS_STORE = gen_id("census store", __name__) -BEDS_STORE = gen_id("beds store", __name__) -NEWS_STORE = gen_id("news store", __name__) -PREDICTIONS_STORE = gen_id("predictions store", __name__) - -# derived stores -DEPTS_OPEN_STORE = gen_id("open depts store", __name__) -ROOMS_OPEN_STORE = gen_id("open rooms store", __name__) -DEPTS_OPEN_STORE_NAMES = gen_id("open depts store names", __name__) - -# controls -CAMPUS_SELECTOR = gen_id("campus selector", __name__) -DEPT_SELECTOR = gen_id("dept selector", __name__) -LAYOUT_SELECTOR = gen_id("layout selector", __name__) -BED_SELECTOR_CAMPUS = gen_id("bed selector campus", __name__) - -# content -CYTO_CAMPUS = gen_id("cyto campus", __name__) -PROGRESS_CAMPUS = gen_id("progress campus", __name__) - -# inspector -SIDEBAR_TITLE = gen_id("sidebar title", __name__) -SIDEBAR_CONTENT = gen_id("sidebar content", __name__) - -INSPECTOR_CAMPUS_MODAL = gen_id("inspector campus modal", __name__) -INSPECTOR_CAMPUS_ACCORDION = gen_id("campus accordion", __name__) - -ACCORDION_ITEM_PERRT = gen_id("accordion bed", __name__) -ACC_BED_DECISION_TEXT = gen_id("bed decision text", __name__) -ACC_BED_STATUS_CAMPUS = gen_id("bed status campus", __name__) -ACC_BED_SUBMIT_CAMPUS = gen_id("bed submit campus", __name__) -ACC_BED_SUBMIT_CAMPUS_NOTIFY = gen_id("bed submit campus notify", __name__) -ACC_BED_SUBMIT_STORE = gen_id("bed submit campus store", __name__) - -ACCORDION_ITEM_PATIENT = gen_id("accordion patient", __name__) -ACCORDION_ITEM_DEBUG = gen_id("accordion debug", __name__) - - -# other -DEBUG_NODE_INSPECTOR_CAMPUS = gen_id("debug inspect node campus", __name__)