diff --git a/src/soa_builder/web/app.py b/src/soa_builder/web/app.py index bb11c74..668ac20 100644 --- a/src/soa_builder/web/app.py +++ b/src/soa_builder/web/app.py @@ -1062,7 +1062,7 @@ def _fetch_matrix(soa_id: int): cur = conn.cursor() # Epochs not part of matrix axes currently; retrieved separately where needed. cur.execute( - "SELECT id,name,label,order_index,epoch_id,description FROM visit WHERE soa_id=? ORDER BY order_index", + "SELECT id,name,label,order_index,epoch_id,description,scheduledAtId,transitionStartRule,transitionEndRule FROM visit WHERE soa_id=? ORDER BY order_index", (soa_id,), ) visits = [ @@ -1073,6 +1073,11 @@ def _fetch_matrix(soa_id: int): order_index=r[3], epoch_id=r[4], description=r[5], + scheduledAtId=( + int(r[6]) if (r[6] is not None and str(r[6]).isdigit()) else None + ), + transitionStartRule=(r[7] if r[7] else None), + transitionEndRule=(r[8] if r[8] else None), ) for r in cur.fetchall() ] @@ -3637,8 +3642,15 @@ def ui_edit(request: Request, soa_id: int): ] conn_tr.close() - # Element audit list - # element_audits = _fetch_element_audits(soa_id) -> Moved to audits.py, audits.html + # Load Timings for dropdown + conn_tm = _connect() + cur_tm = conn_tm.cursor() + cur_tm.execute( + "SELECT id,name FROM timing WHERE soa_id=? ORDER BY id", + (soa_id,), + ) + timings = [{"id": r[0], "name": r[1]} for r in cur_tm.fetchall()] + conn_tm.close() return templates.TemplateResponse( request, @@ -3673,6 +3685,7 @@ def ui_edit(request: Request, soa_id: int): # Study Cells "study_cells": study_cells, "transition_rules": transition_rules, + "timings": timings, }, ) @@ -5710,6 +5723,7 @@ def ui_set_visit_epoch( raise HTTPException(400, "Invalid epoch_id for this SOA") cur.execute("UPDATE visit SET epoch_id=? WHERE id=?", (parsed_epoch, visit_id)) conn.commit() + """ logger.info( "ui_set_visit_epoch updated visit id=%s soa_id=%s epoch_id=%s raw_val='%s' db_path=%s", visit_id, @@ -5718,6 +5732,7 @@ def ui_set_visit_epoch( raw_val, DB_PATH, ) + """ # Fetch after and record audit cur.execute( "SELECT id,name,label,order_index,epoch_id,encounter_uid,description FROM visit WHERE id=? AND soa_id=?", @@ -5749,6 +5764,259 @@ def ui_set_visit_epoch( ) +# UI endpoint for associating a Transition Start Rule with Visit/Encounter (visit.transitionStartRule) +@app.post("/ui/soa/{soa_id}/set_transition_start_rule", response_class=HTMLResponse) +def ui_set_transition_start_rule( + request: Request, + soa_id: int, + visit_id: int = Form(...), + transition_start_rule_uid: str = Form(""), +): + """Form handler for associating a Transition Start Rule with a Visit/Encounter""" + if not soa_exists(soa_id): + raise HTTPException(404, "SOA not found") + + new_uid = (transition_start_rule_uid or "").strip() or None + + conn = _connect() + cur = conn.cursor() + cur.execute( + "SELECT id,name,label,order_index,encounter_uid,description,transitionStartRule FROM visit WHERE id=? AND soa_id=?", + (visit_id, soa_id), + ) + row = cur.fetchone() + if not row: + conn.close() + raise HTTPException(404, "Visit not found") + + before = { + "id": row[0], + "name": row[1], + "label": row[2], + "order_index": row[3], + "encounter_uid": row[4], + "description": row[5], + "transitionStartRule": row[6], + } + if new_uid is not None: + cur.execute( + "SELECT 1 FROM transition_rule WHERE transition_rule_uid=? AND soa_id=?", + (new_uid, soa_id), + ) + if not cur.fetchone(): + conn.close() + raise HTTPException(400, "Invalid Transition Rule for this SOA") + + cur.execute( + "UPDATE visit SET transitionStartRule=? WHERE id=? AND soa_id=?", + (new_uid, visit_id, soa_id), + ) + conn.commit() + + cur.execute( + "SELECT id,name,label,order_index,encounter_uid,description,transitionStartRule FROM visit WHERE id=? AND soa_id=?", + ( + visit_id, + soa_id, + ), + ) + r = cur.fetchone() + after = { + "id": r[0], + "name": r[1], + "label": r[2], + "order_index": r[3], + "encounter_uid": r[4], + "description": r[5], + "transitionStartRule": r[6], + } + updated_fields = [ + f + for f in ["transitionStartRule"] + if (before.get(f) or None) != (after.get(f) or None) + ] + _record_visit_audit( + soa_id, + "update", + visit_id, + before=before, + after={**after, "updated_fields": updated_fields}, + ) + conn.close() + return HTMLResponse( + f"" + ) + + +# UI endpoint for associating a Transition End Rule with Visit/Encounter (visit.transitionEndRule) +@app.post("/ui/soa/{soa_id}/set_transition_end_rule", response_class=HTMLResponse) +def ui_set_transition_end_rule( + request: Request, + soa_id: int, + visit_id: int = Form(...), + transition_end_rule_uid: str = Form(""), +): + """Form Handler for associating a Transition End Rule with a Visit/Encounter""" + if not soa_exists(soa_id): + raise HTTPException(404, "SOA not found") + + new_uid = (transition_end_rule_uid or "").strip() or None + + conn = _connect() + cur = conn.cursor() + cur.execute( + "SELECT id,name,label,order_index,encounter_uid,description,transitionEndRule FROM visit WHERE id=? AND soa_id=?", + (visit_id, soa_id), + ) + row = cur.fetchone() + if not row: + conn.close() + raise HTTPException(404, "Visit not found") + + before = { + "id": row[0], + "name": row[1], + "label": row[2], + "order_index": row[3], + "encounter_uid": row[4], + "description": row[5], + "transitionEndRule": row[6], + } + + if new_uid is not None: + cur.execute( + "SELECT 1 FROM transition_rule WHERE transition_rule_uid=? AND soa_id=?", + (new_uid, soa_id), + ) + if not cur.fetchone(): + conn.close() + raise HTTPException(400, "Invalid Transition Rule for this SOA") + + cur.execute( + "UPDATE visit SET transitionEndRule=? WHERE id=? AND soa_id=?", + (new_uid, visit_id, soa_id), + ) + conn.commit() + + cur.execute( + "SELECT id,name,label,order_index,encounter_uid,description,transitionEndRule FROM visit WHERE id=? AND soa_id=?", + ( + visit_id, + soa_id, + ), + ) + r = cur.fetchone() + after = { + "id": r[0], + "name": r[1], + "label": r[2], + "order_index": r[3], + "encounter_uid": r[4], + "description": r[5], + "transitionEndRule": r[6], + } + updated_fields = [ + f + for f in ["transitionEndRule"] + if (before.get(f) or None) != (after.get(f) or None) + ] + _record_visit_audit( + soa_id, + "update", + visit_id, + before=before, + after={**after, "updated_fields": updated_fields}, + ) + conn.close() + return HTMLResponse( + f"" + ) + + +# UI endpoint for associating a Timing with Visit/Encounter (visit.scheduledAtId) +@app.post("/ui/soa/{soa_id}/set_timing", response_class=HTMLResponse) +def ui_set_timing( + request: Request, + soa_id: int, + visit_id: int = Form(...), + timing_id: str = Form(""), +): + """Form handler for associating a Timing with a Visit/Encounter""" + if not soa_exists(soa_id): + raise HTTPException(404, "SOA not found") + # Determing timing name + raw_val = (timing_id or "").strip() + parsed_timing: Optional[int] = None + if raw_val: + if raw_val.isdigit(): + parsed_timing = int(raw_val) + else: + raise HTTPException(400, "Invalid timing_id value") + conn = _connect() + cur = conn.cursor() + cur.execute( + "SELECT id,name,label,order_index,encounter_uid,description,scheduledAtId FROM visit WHERE id=? AND soa_id=?", + (visit_id, soa_id), + ) + row = cur.fetchone() + if not row: + conn.close() + raise HTTPException(404, "Visit not found") + before = { + "id": row[0], + "name": row[1], + "label": row[2], + "order_index": row[3], + "encounter_uid": row[4], + "description": row[5], + "scheduledAtId": row[6], + } + if parsed_timing is not None: + cur.execute( + "SELECT 1 FROM timing WHERE id=? AND soa_id=?", + (parsed_timing, soa_id), + ) + if not cur.fetchone(): + conn.close() + raise HTTPException(400, "Invalid timing_id for this SOA") + cur.execute( + "UPDATE visit SET scheduledAtId=? WHERE id=?", + (parsed_timing, visit_id), + ) + conn.commit() + # Fecth after record audit + cur.execute( + "SELECT id,name,label,order_index,encounter_uid,description,scheduledAtId FROM visit WHERE id=? AND soa_id=?", + (visit_id, soa_id), + ) + r = cur.fetchone() + after = { + "id": r[0], + "name": r[1], + "label": r[2], + "order_index": r[3], + "encounter_uid": r[4], + "description": r[5], + "scheduledAtId": r[6], + } + updated_fields = [ + f + for f in ["scheduledAtId"] + if (before.get(f) or None) != (after.get(f) or None) + ] + _record_visit_audit( + soa_id, + "update", + visit_id, + before=before, + after={**after, "updated_fields": updated_fields}, + ) + conn.close() + return HTMLResponse( + f"" + ) + + # UI endpoint for updating an Encounter/Visit @app.post("/ui/soa/{soa_id}/update_visit", response_class=HTMLResponse) def ui_update_visit( diff --git a/src/soa_builder/web/templates/edit.html b/src/soa_builder/web/templates/edit.html index e066cd0..f51f29d 100644 --- a/src/soa_builder/web/templates/edit.html +++ b/src/soa_builder/web/templates/edit.html @@ -98,6 +98,37 @@

Editing SoA {{ soa_id }}

{% endif %} + {% if timings %} +
+ + +
+ {% endif %} + {% if transition_rules %} +
+ + +
+
+ + +
+ {% endif %}
diff --git a/src/usdm/generate_encounters.py b/src/usdm/generate_encounters.py index c3969e0..67d00a0 100644 --- a/src/usdm/generate_encounters.py +++ b/src/usdm/generate_encounters.py @@ -20,6 +20,75 @@ def _nz(s: Optional[str]) -> Optional[str]: return s or None +def _get_timing_name(soa_id: int, timing_id: Optional[int]) -> str: + conn = _connect() + cur = conn.cursor() + cur.execute( + "SELECT timing_uid FROM timing WHERE id=? AND soa_id=?", + ( + timing_id, + soa_id, + ), + ) + row = cur.fetchone() + conn.close() + timing_uid = row[0] if (row and row[0] is not None) else None + + return timing_uid + + +def _get_transition_start_rule( + soa_id: int, transition_rule_uid: Optional[str] +) -> Optional[Dict[str, Any]]: + if not transition_rule_uid: + return None + conn = _connect() + cur = conn.cursor() + cur.execute( + "SELECT tr.name, tr.label, tr.description, tr.text FROM transition_rule tr WHERE soa_id=? AND transition_rule_uid=?", + (soa_id, transition_rule_uid), + ) + row = cur.fetchone() + conn.close() + if not row: + return None + return { + "id": transition_rule_uid, + "extensionAttributes": [], + "name": row[0] or None, + "label": row[1] or None, + "description": row[2] or None, + "text": row[3] or None, + "instanceType": "TransitionRule", + } + + +def _get_transition_end_rule( + soa_id: int, transition_rule_uid: Optional[str] +) -> Optional[Dict[str, Any]]: + if not transition_rule_uid: + return None + conn = _connect() + cur = conn.cursor() + cur.execute( + "SELECT tr.name, tr.label, tr.description, tr.text FROM transition_rule tr WHERE soa_id=? AND transition_rule_uid=?", + (soa_id, transition_rule_uid), + ) + row = cur.fetchone() + conn.close() + if not row: + return None + return { + "id": transition_rule_uid, + "extensionAttributes": [], + "name": row[0] or None, + "label": row[1] or None, + "description": row[2] or None, + "text": row[3] or None, + "instanceType": "TransitionRule", + } + + def _get_type_code_tuple(soa_id: int, code_uid: str) -> Tuple[str, str, str, str]: conn = _connect() cur = conn.cursor() @@ -102,7 +171,7 @@ def build_usdm_encounters(soa_id: int) -> List[Dict[str, Any]]: conn = _connect() cur = conn.cursor() cur.execute( - "SELECT name,label,order_index,encounter_uid,description,type,environmentalSettings FROM visit WHERE soa_id=?", + "SELECT name,label,order_index,encounter_uid,description,type,environmentalSettings,scheduledAtId,transitionStartRule,transitionEndRule FROM visit WHERE soa_id=?", (soa_id,), ) rows = cur.fetchall() @@ -123,6 +192,9 @@ def build_usdm_encounters(soa_id: int) -> List[Dict[str, Any]]: description, type, environmentalSettings, + scheduledAtId, + transition_start_rule_uid, + transition_end_rule_uid, ) = ( r[0], r[1], @@ -131,6 +203,9 @@ def build_usdm_encounters(soa_id: int) -> List[Dict[str, Any]]: r[4], r[5], r[6], + r[7], + r[8], + r[9], ) eid = encounter_uid t_code, t_decode, t_codeSystem, t_codeSystemVersion = _get_type_code_tuple( @@ -143,6 +218,23 @@ def build_usdm_encounters(soa_id: int) -> List[Dict[str, Any]]: prev_id = id_by_index.get(i - 1) next_id = id_by_index.get(i + 1) + timing_uid = _get_timing_name( + soa_id, + ( + int(scheduledAtId) + if (scheduledAtId is not None and str(scheduledAtId).isdigit()) + else None + ), + ) + + transition_start_rule_obj = _get_transition_start_rule( + soa_id, transition_start_rule_uid + ) + + transition_end_rule_obj = _get_transition_end_rule( + soa_id, transition_end_rule_uid + ) + encounter = { "id": eid, "extensionAttributes": [], @@ -160,7 +252,7 @@ def build_usdm_encounters(soa_id: int) -> List[Dict[str, Any]]: }, "previousId": prev_id, "nextId": next_id, - "scheduledAt": "", + "scheduledAt": timing_uid, "environmentSettings": [ { "id": environmentalSettings, @@ -173,8 +265,8 @@ def build_usdm_encounters(soa_id: int) -> List[Dict[str, Any]]: }, ], "contactModes": [], - "transitionStartRule": {}, - "transitionEndRule": {}, + "transitionStartRule": transition_start_rule_obj or {}, + "transitionEndRule": transition_end_rule_obj or {}, "notes": [], "instanceType": "Encounter", }