Beijuka commited on
Commit
f563461
Β·
verified Β·
1 Parent(s): 7df7676

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README_APPS.md +58 -0
  2. app.py +9 -6
  3. app_local.py +1062 -0
README_APPS.md ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Breast Cancer Data Collection Tool
2
+
3
+ This repository contains a Streamlit application for collecting breast cancer treatment data.
4
+
5
+ ## Files Overview
6
+
7
+ ### App Versions
8
+
9
+ - **`app.py`** - Production version for Hugging Face Spaces deployment
10
+ - Uses `/tmp/data` directory for file storage (required for Hugging Face)
11
+ - Configured with Hugging Face metadata
12
+ - Includes patient ID sanitization for safe file paths
13
+
14
+ - **`app_local.py`** - Local development version
15
+ - Uses local `data/` directory for file storage
16
+ - Optimized for local development and testing
17
+ - Full functionality without cloud deployment constraints
18
+
19
+ ### Supporting Files
20
+
21
+ - **`requirements.txt`** - Python dependencies
22
+ - **`districts.txt`** - Uganda districts list
23
+ - **`Dockerfile`** - Container configuration for Hugging Face Spaces
24
+ - **`.github/workflows/deploy.yml`** - CI/CD pipeline for automatic deployment
25
+
26
+ ## Quick Start
27
+
28
+ ### Local Development
29
+ ```bash
30
+ # Install dependencies
31
+ pip install -r requirements.txt
32
+
33
+ # Run the local version
34
+ streamlit run app_local.py
35
+ ```
36
+
37
+ ### Hugging Face Spaces Deployment
38
+ The main `app.py` is automatically deployed via GitHub Actions to Hugging Face Spaces when changes are pushed to the main branch.
39
+
40
+ ## Features
41
+
42
+ - **Baseline Data Collection**: Patient demographics, diagnosis, and treatment planning
43
+ - **Treatment Cycle Management**: Progressive cycle data entry with dynamic medication tracking
44
+ - **Data Validation**: Comprehensive form validation and error handling
45
+ - **JSON Export**: Structured data storage for analysis
46
+ - **Patient ID Support**: Handles special characters safely (e.g., "1275/17")
47
+
48
+ ## Data Storage
49
+
50
+ - **Local**: Data saved to `./data/patient_[sanitized_id]/` directory
51
+ - **Hugging Face**: Data saved to `/tmp/data/patient_[sanitized_id]/` directory
52
+ - **Format**: JSON files with baseline data and treatment cycles
53
+
54
+ ## Development Notes
55
+
56
+ Patient IDs with special characters (like "1275/17") are automatically sanitized for file paths while preserving the original ID in the data.
57
+
58
+ Both versions include the same functionality - the only difference is the data storage location to accommodate different deployment environments.
app.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
- Breast Cancer Data Collection Tool
3
- =====================================
4
  Data capture tool for analysis of adherence patterns to chemotherapy and their
5
  association with recurrence-free survival among breast cancer patients at UCI.
6
 
@@ -8,6 +8,9 @@ Study Period: 2016 - 2018
8
  Author: Data Collection Team
9
  Date: October 2025
10
 
 
 
 
11
  title: Breast Cancer Data Collection Tool
12
  emoji: πŸŽ—οΈ
13
  colorFrom: pink
@@ -562,7 +565,7 @@ def render_cycle_1_form(patient_id: str) -> Dict:
562
  return {
563
  "cycle_number": 1,
564
  "patient_id": patient_id,
565
- "regimen_prescribed": regimen_prescribed if not regimen_prescribed.startswith("-- Select") else None,
566
  "prescription_date": prescription_date.strftime("%Y-%m-%d"),
567
  "medications": medications,
568
  "chemo_received_date": chemo_received_date.strftime("%Y-%m-%d"),
@@ -847,8 +850,8 @@ def render_linear_baseline_form(districts: List[str]) -> Dict:
847
  "marital_status": marital_status,
848
  "income_source": income_source,
849
  "income_other": income_other if income_source == "Other" else None,
850
- "district": district if not district.startswith("-- Select") else None,
851
- "initial_diagnosis": initial_diagnosis if not initial_diagnosis.startswith("-- Select") else None,
852
  "immunohisto_present": immunohisto_present,
853
  "immunohisto_results": immunohisto_results if immunohisto_present == "Yes" else [],
854
  "immunohisto_other": immunohisto_other if "Other" in immunohisto_results else None,
@@ -862,7 +865,7 @@ def render_linear_baseline_form(districts: List[str]) -> Dict:
862
  "other_specify": commodities_other if other_comorbidities else None
863
  },
864
  "chemo_cycles_prescribed": chemo_cycles,
865
- "regimen_prescribed": regimen_prescribed if not regimen_prescribed.startswith("-- Select") else None,
866
  "treatment_started": treatment_started,
867
  "treatment_not_started_reason": treatment_not_started_reason if treatment_started == "No" else None
868
  }
 
1
  """
2
+ Breast Cancer Data Collection Tool - Hugging Face Spaces Version
3
+ ===============================================================
4
  Data capture tool for analysis of adherence patterns to chemotherapy and their
5
  association with recurrence-free survival among breast cancer patients at UCI.
6
 
 
8
  Author: Data Collection Team
9
  Date: October 2025
10
 
11
+ This version is configured for deployment on Hugging Face Spaces and uses /tmp/data
12
+ for file storage to avoid permission issues. For local development, use app_local.py
13
+
14
  title: Breast Cancer Data Collection Tool
15
  emoji: πŸŽ—οΈ
16
  colorFrom: pink
 
565
  return {
566
  "cycle_number": 1,
567
  "patient_id": patient_id,
568
+ "regimen_prescribed": regimen_prescribed if regimen_prescribed and not regimen_prescribed.startswith("-- Select") else None,
569
  "prescription_date": prescription_date.strftime("%Y-%m-%d"),
570
  "medications": medications,
571
  "chemo_received_date": chemo_received_date.strftime("%Y-%m-%d"),
 
850
  "marital_status": marital_status,
851
  "income_source": income_source,
852
  "income_other": income_other if income_source == "Other" else None,
853
+ "district": district if district and not district.startswith("-- Select") else None,
854
+ "initial_diagnosis": initial_diagnosis if initial_diagnosis and not initial_diagnosis.startswith("-- Select") else None,
855
  "immunohisto_present": immunohisto_present,
856
  "immunohisto_results": immunohisto_results if immunohisto_present == "Yes" else [],
857
  "immunohisto_other": immunohisto_other if "Other" in immunohisto_results else None,
 
865
  "other_specify": commodities_other if other_comorbidities else None
866
  },
867
  "chemo_cycles_prescribed": chemo_cycles,
868
+ "regimen_prescribed": regimen_prescribed if regimen_prescribed and not regimen_prescribed.startswith("-- Select") else None,
869
  "treatment_started": treatment_started,
870
  "treatment_not_started_reason": treatment_not_started_reason if treatment_started == "No" else None
871
  }
app_local.py ADDED
@@ -0,0 +1,1062 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Breast Cancer Data Collection Tool - Local Development Version
3
+ =============================================================
4
+ Data capture tool for analysis of adherence patterns to chemotherapy and their
5
+ association with recurrence-free survival among breast cancer patients at UCI.
6
+
7
+ Study Period: 2016 - 2018
8
+ Author: Data Collection Team
9
+ Date: October 2025
10
+
11
+ This version is configured for local development and uses the local 'data' directory
12
+ for file storage. For Hugging Face Spaces deployment, use app.py
13
+
14
+ Usage: streamlit run app_local.py
15
+ """
16
+
17
+ import streamlit as st
18
+ from datetime import datetime, date
19
+ import pandas as pd
20
+ import json
21
+ import os
22
+ import time
23
+ from typing import Dict, List, Optional
24
+
25
+
26
+ # =========================================
27
+ def render_linear_baseline_form(districts: List[str]) -> Dict:
28
+ """Render the linear baseline form following exact PDF numbering"""
29
+ st.header("Section 1: Baseline Data (Collected at First Visit Only)")
30
+ st.markdown("---")
31
+
32
+ # 1. Patient ID=========================================
33
+ # CONFIGURATION AND CONSTANTS
34
+ # ========================================================================================
35
+
36
+ class Config:
37
+ """Application configuration constants"""
38
+ STUDY_START_DATE = date(2016, 1, 1)
39
+ STUDY_END_DATE = date(2018, 12, 31)
40
+ DEFAULT_DATA_DIR = "data"
41
+
42
+ # Form options
43
+ EDUCATION_OPTIONS = ["None", "Primary", "Secondary", "Tertiary", "Not captured"]
44
+ MARITAL_OPTIONS = ["Single", "Married", "Divorced", "Widowed", "Not captured"]
45
+ INCOME_OPTIONS = ["Farmer", "Business", "Professional", "Unemployed", "Other"]
46
+ DIAGNOSIS_OPTIONS = [
47
+ "Invasive ductal carcinoma",
48
+ "Moderately differentiated invasive ductal carcinoma",
49
+ "Moderately differentiated ductal carcinoma",
50
+ "Ductal carcinoma in situ",
51
+ "Infiltrating carcinoma",
52
+ "Poorly differentiated adenocarcinoma",
53
+ "Invasive adenocarcinoma",
54
+ "Invasive lobular carcinoma",
55
+ "Other"
56
+ ]
57
+ STAGE_OPTIONS = ["Stage 0", "Stage I", "Stage II", "Stage III", "Stage IV"]
58
+
59
+ # Predefined medication options
60
+ MEDICATION_OPTIONS = [
61
+ "Adriamycin",
62
+ "Cyclophosphamide",
63
+ "Doxorubicin",
64
+ "Dexamethasone",
65
+ "5-fluorouracil",
66
+ "Ondansetron",
67
+ "Ranitidine",
68
+ "Metoclopramide",
69
+ "Plasil",
70
+ "Ifosfamide",
71
+ "Mesna",
72
+ "Paclitaxel",
73
+ "Epirubicin",
74
+ "Carboplatin",
75
+ "Capecitabine (Xeloda)",
76
+ "Docetaxel",
77
+ "Epirubicin",
78
+ "Promethazine",
79
+ "Docetaxel",
80
+ "Tamoxifen",
81
+ "Anastrazole",
82
+ "Ifosfamide",
83
+ "Mesra",
84
+ "Promethazine"
85
+ ]
86
+
87
+ IMMUNOHISTO_OPTIONS = [
88
+ "ER-positive (ER+)",
89
+ "PR-positive (PR+)",
90
+ "HR-positive (HR+)",
91
+ "HR-negative (HR-)",
92
+ "HER2-positive (HER2+)",
93
+ "ER-negative (ER-)",
94
+ "HER2-negative (HER2-)"
95
+ ]
96
+
97
+ REGIMEN_OPTIONS = [
98
+ "AC (Doxorubicin + Cyclophosphamide)",
99
+ "AC-T (Doxorubicin + Cyclophosphamide + Paclitaxel)",
100
+ "CMF (Cyclophosphamide + Methotrexate + 5-Fluorouracil)",
101
+ "FAC (5-Fluorouracil + Doxorubicin + Cyclophosphamide)",
102
+ "FEC (5-Fluorouracil + Epirubicin + Cyclophosphamide)",
103
+ "TC (Docetaxel + Cyclophosphamide)",
104
+ "TCH (Docetaxel + Carboplatin + Trastuzumab)",
105
+ "TAC (Docetaxel + Doxorubicin + Cyclophosphamide)",
106
+ "EC-T (Epirubicin + Cyclophosphamide + Paclitaxel)",
107
+ "Capecitabine (Xeloda) monotherapy",
108
+ "Tamoxifen monotherapy",
109
+ "Other"
110
+ ]
111
+ SIDE_EFFECTS_OPTIONS = ["Nausea", "Fatigue", "Vomiting", "Neuropathy", "None", "Other"]
112
+ CONDITION_OPTIONS = ["Better", "Weaker", "Other"]
113
+
114
+
115
+ # ========================================================================================
116
+ # UTILITY FUNCTIONS
117
+ # ========================================================================================
118
+
119
+ def sanitize_patient_id(patient_id: str) -> str:
120
+ """Sanitize patient ID for safe use in file paths"""
121
+ # Replace forward slashes and other problematic characters with underscores
122
+ return patient_id.replace("/", "_").replace("\\", "_").replace(":", "_").replace("*", "_").replace("?", "_").replace('"', "_").replace("<", "_").replace(">", "_").replace("|", "_")
123
+
124
+ def load_uganda_districts() -> List[str]:
125
+ """Load Uganda districts from districts.txt file"""
126
+ try:
127
+ with open('districts.txt', 'r', encoding='utf-8') as f:
128
+ districts = [line.strip() for line in f.readlines() if line.strip()]
129
+ return sorted(districts)
130
+ except FileNotFoundError:
131
+ st.error("⚠️ districts.txt file not found. Please ensure the file exists in the same directory.")
132
+ return ["File not found - Please check districts.txt"]
133
+ except Exception as e:
134
+ st.error(f"⚠️ Error loading districts: {str(e)}")
135
+ return ["Error loading districts"]
136
+
137
+
138
+ def initialize_session_state() -> None:
139
+ """Initialize Streamlit session state variables"""
140
+ if 'patient_data' not in st.session_state:
141
+ st.session_state.patient_data = {}
142
+ if 'data_directory' not in st.session_state:
143
+ st.session_state.data_directory = Config.DEFAULT_DATA_DIR
144
+ if 'current_patient_id' not in st.session_state:
145
+ st.session_state.current_patient_id = None
146
+ if 'baseline_completed' not in st.session_state:
147
+ st.session_state.baseline_completed = False
148
+ if 'current_cycle' not in st.session_state:
149
+ st.session_state.current_cycle = 0
150
+
151
+
152
+ def clear_form_fields() -> None:
153
+ """Clear all form input fields"""
154
+ # Clear text inputs
155
+ for key in ['patient_id', 'income_other', 'immunohisto_other_specify', 'commodities_other', 'treatment_not_started_reason']:
156
+ if key in st.session_state:
157
+ del st.session_state[key]
158
+
159
+ # Clear numeric inputs
160
+ for key in ['age_input', 'chemo_cycles']:
161
+ if key in st.session_state:
162
+ del st.session_state[key]
163
+
164
+ # Clear radio button selections
165
+ for key in ['education_level', 'marital_status', 'income_source', 'immunohisto_present', 'disease_stage', 'treatment_started']:
166
+ if key in st.session_state:
167
+ del st.session_state[key]
168
+
169
+ # Clear dropdown selections
170
+ for key in ['district', 'initial_diagnosis', 'regimen_prescribed']:
171
+ if key in st.session_state:
172
+ del st.session_state[key]
173
+
174
+ # Clear checkboxes
175
+ checkbox_keys = ['diabetes', 'hypertension', 'hiv', 'none_captured', 'other_comorbidities',
176
+ 'er_positive', 'er_negative', 'pr_positive', 'hr_positive', 'hr_negative',
177
+ 'her2_positive', 'her2_negative', 'immunohisto_other_check']
178
+ for key in checkbox_keys:
179
+ if key in st.session_state:
180
+ del st.session_state[key]
181
+
182
+ # Reset date to default
183
+ if 'date_admitted' in st.session_state:
184
+ del st.session_state['date_admitted']
185
+
186
+
187
+ def validate_form_data(form_data: Dict) -> Optional[str]:
188
+ """Validate form data and return error message if any"""
189
+ patient_id = form_data.get("patient_id", "")
190
+ age = form_data.get("age", 0)
191
+
192
+ if not patient_id or patient_id.strip() == "":
193
+ return "Patient ID is required!"
194
+
195
+ if age <= 0:
196
+ return "Please enter a valid age!"
197
+
198
+ if len(patient_id.strip()) < 3:
199
+ return "Patient ID must be at least 3 characters long!"
200
+
201
+ # Check required radio button selections
202
+ if form_data.get("education_level") is None:
203
+ return "Please select an education level!"
204
+
205
+ if form_data.get("marital_status") is None:
206
+ return "Please select a marital status!"
207
+
208
+ if form_data.get("income_source") is None:
209
+ return "Please select an income source!"
210
+
211
+ # Check dropdown selections (placeholder check)
212
+ if form_data.get("district", "").startswith("-- Select"):
213
+ return "Please select a district!"
214
+
215
+ if form_data.get("initial_diagnosis", "").startswith("-- Select"):
216
+ return "Please select an initial diagnosis!"
217
+
218
+ if form_data.get("immunohisto_present") is None:
219
+ return "Please select if immunohistochemistry results are present!"
220
+
221
+ if form_data.get("disease_stage") is None:
222
+ return "Please select a disease stage!"
223
+
224
+ # Check chemotherapy fields
225
+ if form_data.get("chemo_cycles_prescribed", 0) <= 0:
226
+ return "Please enter the number of chemotherapy cycles prescribed!"
227
+
228
+ if form_data.get("regimen_prescribed", "").startswith("-- Select"):
229
+ return "Please select a prescribed regimen!"
230
+
231
+ if form_data.get("treatment_started") is None:
232
+ return "Please select if the patient started treatment!"
233
+
234
+ return None
235
+
236
+
237
+ def save_patient_data(data: Dict, data_type: str = "baseline") -> tuple[bool, str]:
238
+ """Save patient data to JSON file"""
239
+ try:
240
+ # Create directory structure
241
+ base_dir = os.path.expanduser(st.session_state.data_directory)
242
+ sanitized_patient_id = sanitize_patient_id(data['patient_id'])
243
+ patient_folder = os.path.join(base_dir, f"patient_{sanitized_patient_id}")
244
+
245
+ os.makedirs(base_dir, exist_ok=True)
246
+ os.makedirs(patient_folder, exist_ok=True)
247
+
248
+ # Main patient file
249
+ main_filename = os.path.join(patient_folder, f"patient_{sanitized_patient_id}.json")
250
+
251
+ # Load existing data or create new structure
252
+ if os.path.exists(main_filename):
253
+ with open(main_filename, "r") as f:
254
+ patient_data = json.load(f)
255
+ else:
256
+ patient_data = {
257
+ "patient_id": data['patient_id'],
258
+ "baseline_data": {},
259
+ "treatment_cycles": []
260
+ }
261
+
262
+ if data_type == "baseline":
263
+ patient_data["baseline_data"] = data
264
+ elif data_type == "cycle":
265
+ # Add or update cycle data
266
+ cycle_num = data.get("cycle_number", 1)
267
+ # Check if cycle already exists
268
+ existing_cycle = next((c for c in patient_data["treatment_cycles"] if c.get("cycle_number") == cycle_num), None)
269
+ if existing_cycle:
270
+ # Update existing cycle
271
+ existing_cycle.update(data)
272
+ else:
273
+ # Add new cycle
274
+ patient_data["treatment_cycles"].append(data)
275
+
276
+ # Save updated data
277
+ with open(main_filename, "w") as f:
278
+ json.dump(patient_data, f, indent=4, default=str)
279
+
280
+ return True, main_filename
281
+
282
+ except Exception as e:
283
+ return False, str(e)
284
+
285
+
286
+ def load_patient_data(patient_id: str) -> Dict:
287
+ """Load patient data from JSON file"""
288
+ try:
289
+ base_dir = os.path.expanduser(st.session_state.data_directory)
290
+ sanitized_patient_id = sanitize_patient_id(patient_id)
291
+ patient_folder = os.path.join(base_dir, f"patient_{sanitized_patient_id}")
292
+ filename = os.path.join(patient_folder, f"patient_{sanitized_patient_id}.json")
293
+
294
+ if os.path.exists(filename):
295
+ with open(filename, "r") as f:
296
+ return json.load(f)
297
+ else:
298
+ return {
299
+ "patient_id": patient_id,
300
+ "baseline_data": {},
301
+ "treatment_cycles": []
302
+ }
303
+ except Exception as e:
304
+ return {
305
+ "patient_id": patient_id,
306
+ "baseline_data": {},
307
+ "treatment_cycles": []
308
+ }
309
+
310
+
311
+ def render_dynamic_medications(cycle_num: int) -> List[Dict]:
312
+ """Render dynamic medication input fields with dropdown selection"""
313
+ st.markdown("**Medications & Dosages:**")
314
+
315
+ # Initialize medications in session state
316
+ med_key = f"cycle_{cycle_num}_medications"
317
+ if med_key not in st.session_state:
318
+ st.session_state[med_key] = [{"name": "", "dose": "", "unit": "mg"}]
319
+
320
+ medications = []
321
+
322
+ for i, med in enumerate(st.session_state[med_key]):
323
+ col1, col2, col3, col4 = st.columns([3, 2, 1, 1])
324
+
325
+ with col1:
326
+ # Medication dropdown with search
327
+ medication_options = ["-- Select Medication --"] + Config.MEDICATION_OPTIONS + ["Other (specify)"]
328
+ selected_med = st.selectbox(
329
+ f"Medication {i+1}:",
330
+ medication_options,
331
+ index=0 if not med.get("name") else (
332
+ medication_options.index(med.get("name")) if med.get("name") in medication_options else 0
333
+ ),
334
+ key=f"med_name_{cycle_num}_{i}"
335
+ )
336
+
337
+ # If "Other" is selected, show text input
338
+ if selected_med == "Other (specify)":
339
+ med_name = st.text_input(
340
+ "Specify medication:",
341
+ value=med.get("custom_name", ""),
342
+ key=f"med_custom_{cycle_num}_{i}",
343
+ placeholder="Enter medication name"
344
+ )
345
+ else:
346
+ med_name = selected_med if not selected_med.startswith("-- Select") else ""
347
+
348
+ with col2:
349
+ med_dose = st.text_input(
350
+ f"Dose:",
351
+ value=med.get("dose", ""),
352
+ key=f"med_dose_{cycle_num}_{i}",
353
+ placeholder="e.g., 60, 1.5"
354
+ )
355
+
356
+ with col3:
357
+ med_unit = st.selectbox(
358
+ f"Unit:",
359
+ ["mg", "mg/m2", "g", "mL", "tabs", "IU"],
360
+ index=0 if med.get("unit", "mg") == "mg" else ["mg", "mg/m2", "g", "mL", "tabs", "IU"].index(med.get("unit", "mg")),
361
+ key=f"med_unit_{cycle_num}_{i}"
362
+ )
363
+
364
+ with col4:
365
+ if len(st.session_state[med_key]) > 1:
366
+ if st.button("πŸ—‘οΈ", key=f"remove_med_{cycle_num}_{i}", help="Remove medication"):
367
+ st.session_state[med_key].pop(i)
368
+ st.rerun()
369
+
370
+ # Only add to medications list if a medication is actually selected
371
+ if med_name and not med_name.startswith("-- Select"):
372
+ medications.append({
373
+ "name": med_name,
374
+ "dose": med_dose,
375
+ "unit": med_unit
376
+ })
377
+
378
+ # Add medication button
379
+ if st.button("βž• Add Medication", key=f"add_med_{cycle_num}"):
380
+ st.session_state[med_key].append({"name": "", "dose": "", "unit": "mg"})
381
+ st.rerun()
382
+
383
+ return medications
384
+
385
+
386
+ def render_cycle_1_form(patient_id: str) -> Dict:
387
+ """Render Chemotherapy Treatment Cycle 1 form"""
388
+ st.header("Chemotherapy Treatment Cycle 1")
389
+ st.markdown("---")
390
+
391
+ # Prescribed regimen for this cycle
392
+ regimen_options = ["-- Select Regimen --"] + Config.REGIMEN_OPTIONS
393
+ regimen_prescribed = st.selectbox(
394
+ "Prescribed regimen for this cycle:",
395
+ regimen_options,
396
+ key="cycle1_regimen_prescribed",
397
+ index=None,
398
+ help="Select the chemotherapy regimen prescribed for this treatment cycle"
399
+ )
400
+
401
+ # Prescription date
402
+ prescription_date = st.date_input(
403
+ "Chemotherapy prescription date:",
404
+ value=date(2017, 7, 1),
405
+ min_value=Config.STUDY_START_DATE,
406
+ max_value=Config.STUDY_END_DATE,
407
+ key="cycle1_prescription_date"
408
+ )
409
+
410
+ # Dynamic medications
411
+ medications = render_dynamic_medications(1)
412
+
413
+ # Date chemotherapy received
414
+ chemo_received_date = st.date_input(
415
+ "Date chemotherapy received:",
416
+ value=date(2017, 7, 1),
417
+ min_value=Config.STUDY_START_DATE,
418
+ max_value=Config.STUDY_END_DATE,
419
+ key="cycle1_chemo_received_date"
420
+ )
421
+
422
+ # Laboratory values
423
+ st.markdown("**Laboratory Results:**")
424
+ col1, col2, col3 = st.columns(3)
425
+
426
+ with col1:
427
+ wbc = st.number_input(
428
+ "Total WBC:",
429
+ min_value=0.0,
430
+ max_value=50000.0,
431
+ step=100.0,
432
+ key="cycle1_wbc",
433
+ help="White Blood Cell count"
434
+ )
435
+
436
+ with col2:
437
+ hemoglobin = st.number_input(
438
+ "Hemoglobin:",
439
+ min_value=0.0,
440
+ max_value=25.0,
441
+ step=0.1,
442
+ key="cycle1_hemoglobin",
443
+ help="Hemoglobin level (g/dL)"
444
+ )
445
+
446
+ with col3:
447
+ platelets = st.number_input(
448
+ "Platelets:",
449
+ min_value=0,
450
+ max_value=1000000,
451
+ step=1000,
452
+ key="cycle1_platelets",
453
+ help="Platelet count"
454
+ )
455
+
456
+ # Was chemotherapy received on prescription day?
457
+ st.markdown("**Was chemotherapy received on the day of prescription?**")
458
+ chemo_on_prescription_day = st.radio(
459
+ "Select option:",
460
+ ["Yes", "No"],
461
+ key="cycle1_chemo_on_prescription_day",
462
+ horizontal=True,
463
+ label_visibility="collapsed",
464
+ index=None
465
+ )
466
+
467
+ # Conditional reason if No
468
+ chemo_delay_reason = ""
469
+ if chemo_on_prescription_day == "No":
470
+ chemo_delay_reason = st.text_input(
471
+ "If No, Why?:",
472
+ key="cycle1_chemo_delay_reason",
473
+ placeholder="Please specify reason..."
474
+ )
475
+
476
+ # Side effects
477
+ st.markdown("**Documented side effects post treatment:**")
478
+ side_effects_present = st.radio(
479
+ "Are there documented side effects?",
480
+ ["Yes", "No"],
481
+ key="cycle1_side_effects_present",
482
+ horizontal=True,
483
+ index=None
484
+ )
485
+
486
+ # Side effects checkboxes
487
+ side_effects = []
488
+ side_effects_other = ""
489
+ if side_effects_present == "Yes":
490
+ st.markdown("Select side effects:")
491
+ col1, col2, col3, col4, col5, col6 = st.columns(6)
492
+
493
+ with col1:
494
+ if st.checkbox("Nausea", key="cycle1_nausea"):
495
+ side_effects.append("Nausea")
496
+ with col2:
497
+ if st.checkbox("Fatigue", key="cycle1_fatigue"):
498
+ side_effects.append("Fatigue")
499
+ with col3:
500
+ if st.checkbox("Vomiting", key="cycle1_vomiting"):
501
+ side_effects.append("Vomiting")
502
+ with col4:
503
+ if st.checkbox("Neuropathy", key="cycle1_neuropathy"):
504
+ side_effects.append("Neuropathy")
505
+ with col5:
506
+ if st.checkbox("None", key="cycle1_none_side_effects"):
507
+ side_effects.append("None")
508
+ with col6:
509
+ if st.checkbox("Other", key="cycle1_other_side_effects"):
510
+ side_effects.append("Other")
511
+ side_effects_other = st.text_input(
512
+ "Specify other side effects:",
513
+ key="cycle1_side_effects_other",
514
+ placeholder="Please specify..."
515
+ )
516
+
517
+ # Patient condition
518
+ st.markdown("**What is the general condition of the patient at the time of the clinic visit?**")
519
+ patient_condition = st.radio(
520
+ "Select condition:",
521
+ Config.CONDITION_OPTIONS,
522
+ key="cycle1_patient_condition",
523
+ horizontal=True,
524
+ label_visibility="collapsed",
525
+ index=None
526
+ )
527
+
528
+ # Conditional input for "Other" condition
529
+ condition_other = ""
530
+ if patient_condition == "Other":
531
+ condition_other = st.text_input(
532
+ "Specify other condition:",
533
+ key="cycle1_condition_other",
534
+ placeholder="Please specify..."
535
+ )
536
+
537
+ # Hospitalization
538
+ st.markdown("**Was there any hospitalization between this cycle and the previous cycle?**")
539
+ hospitalization = st.radio(
540
+ "Select option:",
541
+ ["Yes", "No"],
542
+ key="cycle1_hospitalization",
543
+ horizontal=True,
544
+ label_visibility="collapsed",
545
+ index=None
546
+ )
547
+
548
+ # Conditional reason for hospitalization
549
+ hospitalization_reason = ""
550
+ if hospitalization == "Yes":
551
+ hospitalization_reason = st.text_input(
552
+ "If yes, specify the reason:",
553
+ key="cycle1_hospitalization_reason",
554
+ placeholder="Please specify reason..."
555
+ )
556
+
557
+ # Return cycle data
558
+ return {
559
+ "cycle_number": 1,
560
+ "patient_id": patient_id,
561
+ "regimen_prescribed": regimen_prescribed if regimen_prescribed and not regimen_prescribed.startswith("-- Select") else None,
562
+ "prescription_date": prescription_date.strftime("%Y-%m-%d"),
563
+ "medications": medications,
564
+ "chemo_received_date": chemo_received_date.strftime("%Y-%m-%d"),
565
+ "laboratory": {
566
+ "wbc": wbc,
567
+ "hemoglobin": hemoglobin,
568
+ "platelets": platelets
569
+ },
570
+ "chemo_on_prescription_day": chemo_on_prescription_day,
571
+ "chemo_delay_reason": chemo_delay_reason if chemo_on_prescription_day == "No" else None,
572
+ "side_effects_present": side_effects_present,
573
+ "side_effects": side_effects if side_effects_present == "Yes" else [],
574
+ "side_effects_other": side_effects_other if "Other" in side_effects else None,
575
+ "patient_condition": patient_condition,
576
+ "condition_other": condition_other if patient_condition == "Other" else None,
577
+ "hospitalization": hospitalization,
578
+ "hospitalization_reason": hospitalization_reason if hospitalization == "Yes" else None
579
+ }
580
+
581
+
582
+ # ========================================================================================
583
+ # UI COMPONENTS
584
+ # ========================================================================================
585
+
586
+ def render_page_header() -> None:
587
+ """Render the main page header and description"""
588
+ st.set_page_config(page_title="Breast Cancer Data Collection", layout="centered")
589
+ st.title("Data Capture Tool for Analysis of Adherence Patterns to Chemotherapy")
590
+ st.markdown("---")
591
+
592
+ def render_data_storage_config() -> None:
593
+ """Render data storage configuration in sidebar"""
594
+ with st.sidebar:
595
+ st.header("βš™οΈ Settings")
596
+ data_dir = st.text_input(
597
+ "Data Storage Directory",
598
+ value=st.session_state.data_directory,
599
+ help="Enter the path where you want to store the data files"
600
+ )
601
+ if data_dir != st.session_state.data_directory:
602
+ st.session_state.data_directory = data_dir
603
+
604
+ st.markdown("---")
605
+ st.markdown("**Quick Stats**")
606
+ if os.path.exists(st.session_state.data_directory):
607
+ try:
608
+ patient_folders = [d for d in os.listdir(st.session_state.data_directory)
609
+ if d.startswith('patient_') and os.path.isdir(os.path.join(st.session_state.data_directory, d))]
610
+ st.metric("Patients Recorded", len(patient_folders))
611
+ except:
612
+ st.metric("Patients Recorded", "0")
613
+ else:
614
+ st.metric("Patients Recorded", "0")
615
+
616
+
617
+ def render_linear_baseline_form(districts: List[str]) -> Dict:
618
+ """Render the linear baseline form following exact PDF numbering"""
619
+ st.header("Section 1: Baseline Data (Collected at First Visit Only)")
620
+ st.markdown("---")
621
+
622
+ # 1. Patient ID
623
+ patient_id = st.text_input(
624
+ "1. Patient ID:",
625
+ help="Enter unique patient identifier",
626
+ key="patient_id",
627
+ placeholder="e.g., WMJ11"
628
+ )
629
+
630
+ # 2. Age and Date Admitted (on same line conceptually but separate inputs)
631
+ col1, col2 = st.columns(2)
632
+ with col1:
633
+ age = st.number_input(
634
+ "2. Age (years):",
635
+ min_value=0,
636
+ max_value=120,
637
+ step=1,
638
+ key="age_input"
639
+ )
640
+
641
+ with col2:
642
+ date_admitted = st.date_input(
643
+ "Date Admitted:",
644
+ value=date(2017, 1, 1),
645
+ min_value=Config.STUDY_START_DATE,
646
+ max_value=Config.STUDY_END_DATE,
647
+ key="date_admitted",
648
+ help="Select date between 2016-2018"
649
+ )
650
+
651
+ # 3. Education Level
652
+ st.markdown("**3. Highest level of education:**")
653
+ education_level = st.radio(
654
+ "Select education level:",
655
+ Config.EDUCATION_OPTIONS,
656
+ key="education_level",
657
+ horizontal=True,
658
+ label_visibility="collapsed",
659
+ index=None # No default selection
660
+ )
661
+
662
+ # 4. Marital Status
663
+ st.markdown("**4. Current marital status:**")
664
+ marital_status = st.radio(
665
+ "Select marital status:",
666
+ Config.MARITAL_OPTIONS,
667
+ key="marital_status",
668
+ horizontal=True,
669
+ label_visibility="collapsed",
670
+ index=None # No default selection
671
+ )
672
+
673
+ # 5. Income Source
674
+ st.markdown("**5. Main source of income:**")
675
+ income_source = st.radio(
676
+ "Select income source:",
677
+ Config.INCOME_OPTIONS,
678
+ key="income_source",
679
+ horizontal=True,
680
+ label_visibility="collapsed",
681
+ index=None # No default selection
682
+ )
683
+
684
+ # Conditional input for "Other" income source
685
+ income_other = ""
686
+ if income_source == "Other":
687
+ income_other = st.text_input(
688
+ "Specify other source of income:",
689
+ key="income_other",
690
+ placeholder="Please specify..."
691
+ )
692
+
693
+ # 6. District of residence
694
+ district_options = ["-- Select District --"] + districts
695
+ district = st.selectbox(
696
+ "6. District of residence:",
697
+ options=district_options,
698
+ key="district",
699
+ help=f"Select from {len(districts)} Uganda districts",
700
+ index=0 # Default to placeholder
701
+ )
702
+
703
+ # 7. Initial diagnosis
704
+ diagnosis_options = ["-- Select Initial Diagnosis --"] + Config.DIAGNOSIS_OPTIONS
705
+ initial_diagnosis = st.selectbox(
706
+ "7. Initial diagnosis:",
707
+ diagnosis_options,
708
+ key="initial_diagnosis",
709
+ index=0 # Default to placeholder
710
+ )
711
+
712
+ # 8. Immunohistochemistry results
713
+ st.markdown("**8. Immunohistochemistry results present:**")
714
+ immunohisto_present = st.radio(
715
+ "Select option:",
716
+ ["Yes", "No"],
717
+ key="immunohisto_present",
718
+ horizontal=True,
719
+ label_visibility="collapsed",
720
+ index=None # No default selection
721
+ )
722
+
723
+ # Conditional multiselect for immunohistochemistry results
724
+ immunohisto_results = []
725
+ immunohisto_other = ""
726
+ if immunohisto_present == "Yes":
727
+ st.markdown("**Select immunohistochemistry results (you can select multiple):**")
728
+
729
+ # Create checkboxes in a grid layout
730
+ col1, col2, col3, col4 = st.columns(4)
731
+
732
+ with col1:
733
+ if st.checkbox("ER-positive (ER+)", key="er_positive"):
734
+ immunohisto_results.append("ER-positive (ER+)")
735
+ if st.checkbox("ER-negative (ER-)", key="er_negative"):
736
+ immunohisto_results.append("ER-negative (ER-)")
737
+
738
+ with col2:
739
+ if st.checkbox("PR-positive (PR+)", key="pr_positive"):
740
+ immunohisto_results.append("PR-positive (PR+)")
741
+ if st.checkbox("HR-positive (HR+)", key="hr_positive"):
742
+ immunohisto_results.append("HR-positive (HR+)")
743
+
744
+ with col3:
745
+ if st.checkbox("HR-negative (HR-)", key="hr_negative"):
746
+ immunohisto_results.append("HR-negative (HR-)")
747
+ if st.checkbox("HER2-positive (HER2+)", key="her2_positive"):
748
+ immunohisto_results.append("HER2-positive (HER2+)")
749
+
750
+ with col4:
751
+ if st.checkbox("HER2-negative (HER2-)", key="her2_negative"):
752
+ immunohisto_results.append("HER2-negative (HER2-)")
753
+ if st.checkbox("Other", key="immunohisto_other_check"):
754
+ immunohisto_results.append("Other")
755
+ immunohisto_other = st.text_input(
756
+ "Specify other results:",
757
+ key="immunohisto_other_specify",
758
+ placeholder="Please specify..."
759
+ )
760
+
761
+ # 9. Disease stage
762
+ st.markdown("**9. Disease stage at first diagnosis:**")
763
+ disease_stage = st.radio(
764
+ "Select stage:",
765
+ Config.STAGE_OPTIONS,
766
+ key="disease_stage",
767
+ horizontal=True,
768
+ label_visibility="collapsed",
769
+ index=None # No default selection
770
+ )
771
+
772
+ # 10. Comorbidities
773
+ st.markdown("**10. List of other comorbidities:**")
774
+
775
+ # Create checkboxes in horizontal layout
776
+ col1, col2, col3, col4, col5 = st.columns(5)
777
+
778
+ with col1:
779
+ diabetes = st.checkbox("Diabetes", key="diabetes")
780
+ with col2:
781
+ hypertension = st.checkbox("Hypertension", key="hypertension")
782
+ with col3:
783
+ hiv = st.checkbox("HIV", key="hiv")
784
+ with col4:
785
+ none_captured = st.checkbox("None captured", key="none_captured")
786
+ with col5:
787
+ other_comorbidities = st.checkbox("Other", key="other_comorbidities")
788
+
789
+ # Conditional input for "Other" comorbidities
790
+ commodities_other = ""
791
+ if other_comorbidities:
792
+ commodities_other = st.text_input(
793
+ "Specify other comorbidities:",
794
+ key="commodities_other",
795
+ placeholder="Please specify..."
796
+ )
797
+
798
+ # 11. Chemotherapy cycles prescribed
799
+ chemo_cycles = st.number_input(
800
+ "11. How many chemotherapy cycles have been prescribed:",
801
+ min_value=0,
802
+ max_value=50,
803
+ step=1,
804
+ key="chemo_cycles",
805
+ help="Enter the number of prescribed cycles"
806
+ )
807
+
808
+ # 12. Regimen prescribed
809
+ regimen_options = ["-- Select Regimen --"] + Config.REGIMEN_OPTIONS
810
+ regimen_prescribed = st.selectbox(
811
+ "12. Which regimen was prescribed:",
812
+ regimen_options,
813
+ key="regimen_prescribed",
814
+ index=0 # Default to placeholder
815
+ )
816
+
817
+ # 13. Treatment start status
818
+ st.markdown("**13. Did the patient start treatment:**")
819
+ treatment_started = st.radio(
820
+ "Select option:",
821
+ ["Yes", "No"],
822
+ key="treatment_started",
823
+ horizontal=True,
824
+ label_visibility="collapsed",
825
+ index=None # No default selection
826
+ )
827
+
828
+ # Conditional input for "No" treatment start
829
+ treatment_not_started_reason = ""
830
+ if treatment_started == "No":
831
+ treatment_not_started_reason = st.text_input(
832
+ "If No, why?:",
833
+ key="treatment_not_started_reason",
834
+ placeholder="Please specify reason..."
835
+ )
836
+
837
+ # Return all collected data
838
+ return {
839
+ "patient_id": patient_id,
840
+ "age": age,
841
+ "date_admitted": date_admitted.strftime("%Y-%m-%d"),
842
+ "education_level": education_level,
843
+ "marital_status": marital_status,
844
+ "income_source": income_source,
845
+ "income_other": income_other if income_source == "Other" else None,
846
+ "district": district if district and not district.startswith("-- Select") else None,
847
+ "initial_diagnosis": initial_diagnosis if initial_diagnosis and not initial_diagnosis.startswith("-- Select") else None,
848
+ "immunohisto_present": immunohisto_present,
849
+ "immunohisto_results": immunohisto_results if immunohisto_present == "Yes" else [],
850
+ "immunohisto_other": immunohisto_other if "Other" in immunohisto_results else None,
851
+ "disease_stage": disease_stage,
852
+ "comorbidities": {
853
+ "diabetes": diabetes,
854
+ "hypertension": hypertension,
855
+ "hiv": hiv,
856
+ "none_captured": none_captured,
857
+ "other": other_comorbidities,
858
+ "other_specify": commodities_other if other_comorbidities else None
859
+ },
860
+ "chemo_cycles_prescribed": chemo_cycles,
861
+ "regimen_prescribed": regimen_prescribed if regimen_prescribed and not regimen_prescribed.startswith("-- Select") else None,
862
+ "treatment_started": treatment_started,
863
+ "treatment_not_started_reason": treatment_not_started_reason if treatment_started == "No" else None
864
+ }
865
+
866
+
867
+ def render_cycle_actions(cycle_data, cycle_number):
868
+ """Render action buttons for cycle forms"""
869
+ st.markdown("---")
870
+ col1, col2, col3 = st.columns([2, 2, 1])
871
+
872
+ with col1:
873
+ if st.button("πŸ’Ύ Save Cycle", type="primary", use_container_width=True):
874
+ if validate_cycle_data(cycle_data):
875
+ # Save cycle data
876
+ success, result = save_patient_data(cycle_data, data_type='cycle')
877
+ if success:
878
+ st.success(f"βœ… Treatment Cycle {cycle_number} data saved successfully!")
879
+ # Reset current cycle to go back to cycle management
880
+ st.session_state.current_cycle = 0
881
+ time.sleep(1)
882
+ st.rerun()
883
+ else:
884
+ st.error(f"❌ Failed to save cycle data: {result}")
885
+ else:
886
+ st.error("❌ Please fill in all required fields before saving.")
887
+
888
+ with col2:
889
+ if st.button("🚫 Cancel Cycle", use_container_width=True):
890
+ st.session_state.current_cycle = 0
891
+ st.rerun()
892
+
893
+ with col3:
894
+ if st.button("πŸ—‘οΈ", help="Clear form", use_container_width=True):
895
+ clear_form_fields()
896
+ st.rerun()
897
+
898
+
899
+ def validate_cycle_data(cycle_data):
900
+ """Validate cycle form data"""
901
+ if not cycle_data:
902
+ return False
903
+
904
+ # Check required fields for cycle 1
905
+ required_fields = ['regimen_prescribed', 'prescription_date']
906
+
907
+ for field in required_fields:
908
+ if field not in cycle_data or not cycle_data[field]:
909
+ return False
910
+
911
+ # Validate medications
912
+ medications = cycle_data.get('medications', [])
913
+ if not medications:
914
+ return False
915
+
916
+ for med in medications:
917
+ if not med.get('name') or not med.get('dose'):
918
+ return False
919
+
920
+ return True
921
+
922
+
923
+ def render_form_actions(form_data: Dict) -> None:
924
+ """Render form submission and action buttons"""
925
+ st.markdown("---")
926
+
927
+ col1, col2 = st.columns([3, 1])
928
+
929
+ with col1:
930
+ if st.button("πŸ’Ύ Save Baseline Data", type="primary", use_container_width=True):
931
+ # Validate form data
932
+ error_msg = validate_form_data(form_data)
933
+
934
+ if error_msg:
935
+ st.error(f"❌ {error_msg}")
936
+ else:
937
+ # Save data
938
+ success, result = save_patient_data(form_data, "baseline")
939
+
940
+ if success:
941
+ st.success(f"βœ… Baseline data for Patient {form_data['patient_id']} saved successfully!")
942
+ st.success(f"πŸ“ File saved to: {result}")
943
+
944
+ # Update session state
945
+ st.session_state.patient_data = form_data
946
+ st.session_state.current_patient_id = form_data['patient_id']
947
+ st.session_state.baseline_completed = True
948
+ st.session_state.current_cycle = 0
949
+
950
+ # Show success metrics
951
+ with st.expander("πŸ“‹ Saved Data Summary", expanded=True):
952
+ col_a, col_b = st.columns(2)
953
+ with col_a:
954
+ st.metric("Patient ID", form_data['patient_id'])
955
+ st.metric("Age", form_data['age'])
956
+ st.metric("District", form_data['district'] or "Not selected")
957
+ with col_b:
958
+ st.metric("Education", form_data['education_level'] or "Not selected")
959
+ st.metric("Diagnosis", form_data['initial_diagnosis'] or "Not selected")
960
+ st.metric("Stage", form_data['disease_stage'] or "Not selected")
961
+
962
+ st.rerun() # Refresh to show cycle options
963
+ else:
964
+ st.error(f"❌ Error saving data: {result}")
965
+
966
+ with col2:
967
+ if st.button("πŸ”„ Clear Form", use_container_width=True):
968
+ clear_form_fields()
969
+ st.rerun()
970
+
971
+
972
+ def render_footer() -> None:
973
+ """Render application footer"""
974
+ st.markdown("---")
975
+ st.info(" **Note**: Please ensure all information is accurate before submission. Use 'Not captured' for missing information. Data will be exported to Excel/CSV for analysis.")
976
+
977
+
978
+ # ========================================================================================
979
+ # MAIN APPLICATION
980
+ # ========================================================================================
981
+
982
+ def main():
983
+ """Main application function"""
984
+ # Initialize session state
985
+ initialize_session_state()
986
+
987
+ # Render page header
988
+ render_page_header()
989
+
990
+ # Render sidebar configuration
991
+ render_data_storage_config()
992
+
993
+ # Load districts data
994
+ uganda_districts = load_uganda_districts()
995
+
996
+ # Check if baseline is completed
997
+ if not st.session_state.baseline_completed:
998
+ # Show baseline form
999
+ form_data = render_linear_baseline_form(uganda_districts)
1000
+ render_form_actions(form_data)
1001
+ else:
1002
+ # Baseline completed - show cycle management
1003
+ st.success(f"βœ… Baseline data completed for Patient {st.session_state.current_patient_id}")
1004
+
1005
+ # Load patient data to check existing cycles
1006
+ patient_data = load_patient_data(st.session_state.current_patient_id)
1007
+ existing_cycles = len(patient_data.get("treatment_cycles", []))
1008
+
1009
+ st.markdown("---")
1010
+ st.subheader("πŸ”„ Treatment Cycles Management")
1011
+
1012
+ col1, col2, col3 = st.columns([2, 2, 1])
1013
+
1014
+ with col1:
1015
+ st.metric("Patient ID", st.session_state.current_patient_id)
1016
+ with col2:
1017
+ st.metric("Completed Cycles", existing_cycles)
1018
+ with col3:
1019
+ if st.button("πŸ”„ New Patient", help="Start with a new patient"):
1020
+ # Reset session state
1021
+ st.session_state.baseline_completed = False
1022
+ st.session_state.current_patient_id = None
1023
+ st.session_state.current_cycle = 0
1024
+ clear_form_fields()
1025
+ st.rerun()
1026
+
1027
+ # Show cycle addition buttons
1028
+ next_cycle = existing_cycles + 1
1029
+
1030
+ if next_cycle == 1:
1031
+ st.markdown("### Ready to add Treatment Cycle 1")
1032
+ if st.button(f"πŸ“‹ Add Treatment Cycle 1", type="primary", use_container_width=True):
1033
+ st.session_state.current_cycle = 1
1034
+ st.rerun()
1035
+ else:
1036
+ st.markdown(f"### Ready to add Treatment Cycle {next_cycle}")
1037
+ col_a, col_b = st.columns(2)
1038
+ with col_a:
1039
+ if st.button(f"πŸ“‹ Add Treatment Cycle {next_cycle}", type="primary", use_container_width=True):
1040
+ st.session_state.current_cycle = next_cycle
1041
+ st.rerun()
1042
+ with col_b:
1043
+ if st.button("βœ… Complete Treatment", use_container_width=True):
1044
+ st.session_state.baseline_completed = False
1045
+ st.session_state.current_patient_id = None
1046
+ st.session_state.current_cycle = 0
1047
+ st.success("πŸŽ‰ Treatment cycles completed! Ready for new patient.")
1048
+ st.rerun()
1049
+
1050
+ # Show cycle form if current_cycle is set
1051
+ if st.session_state.current_cycle > 0:
1052
+ if st.session_state.current_cycle == 1:
1053
+ cycle_data = render_cycle_1_form(st.session_state.current_patient_id)
1054
+ render_cycle_actions(cycle_data, 1)
1055
+ # TODO: Add cycle 2+ forms here
1056
+
1057
+ # Footer
1058
+ render_footer()
1059
+
1060
+
1061
+ if __name__ == "__main__":
1062
+ main()