LiamKhoaLe commited on
Commit
e9b7eb0
·
1 Parent(s): c6d658a

Add patient registration

Browse files
src/api/routes/user.py CHANGED
@@ -64,3 +64,74 @@ async def get_user_profile(
64
  except Exception as e:
65
  logger.error(f"Error getting user profile: {e}")
66
  raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  except Exception as e:
65
  logger.error(f"Error getting user profile: {e}")
66
  raise HTTPException(status_code=500, detail=str(e))
67
+
68
+ # -------------------- Patient APIs --------------------
69
+ from pydantic import BaseModel
70
+ from src.data.mongodb import get_patient_by_id, create_patient, update_patient_profile
71
+
72
+ class PatientCreateRequest(BaseModel):
73
+ name: str
74
+ age: int
75
+ sex: str
76
+ address: str | None = None
77
+ phone: str | None = None
78
+ email: str | None = None
79
+ medications: list[str] | None = None
80
+ past_assessment_summary: str | None = None
81
+ assigned_doctor_id: str | None = None
82
+
83
+ @router.get("/patients/{patient_id}")
84
+ async def get_patient(patient_id: str):
85
+ try:
86
+ patient = get_patient_by_id(patient_id)
87
+ if not patient:
88
+ raise HTTPException(status_code=404, detail="Patient not found")
89
+ patient["_id"] = str(patient.get("_id")) if patient.get("_id") else None
90
+ return patient
91
+ except HTTPException:
92
+ raise
93
+ except Exception as e:
94
+ logger.error(f"Error getting patient: {e}")
95
+ raise HTTPException(status_code=500, detail=str(e))
96
+
97
+ @router.post("/patients")
98
+ async def create_patient_profile(req: PatientCreateRequest):
99
+ try:
100
+ patient = create_patient(
101
+ name=req.name,
102
+ age=req.age,
103
+ sex=req.sex,
104
+ address=req.address,
105
+ phone=req.phone,
106
+ email=req.email,
107
+ medications=req.medications,
108
+ past_assessment_summary=req.past_assessment_summary,
109
+ assigned_doctor_id=req.assigned_doctor_id
110
+ )
111
+ patient["_id"] = str(patient.get("_id")) if patient.get("_id") else None
112
+ return patient
113
+ except Exception as e:
114
+ logger.error(f"Error creating patient: {e}")
115
+ raise HTTPException(status_code=500, detail=str(e))
116
+
117
+ class PatientUpdateRequest(BaseModel):
118
+ name: str | None = None
119
+ age: int | None = None
120
+ sex: str | None = None
121
+ address: str | None = None
122
+ phone: str | None = None
123
+ email: str | None = None
124
+ medications: list[str] | None = None
125
+ past_assessment_summary: str | None = None
126
+ assigned_doctor_id: str | None = None
127
+
128
+ @router.patch("/patients/{patient_id}")
129
+ async def update_patient(patient_id: str, req: PatientUpdateRequest):
130
+ try:
131
+ modified = update_patient_profile(patient_id, {k: v for k, v in req.model_dump().items() if v is not None})
132
+ if modified == 0:
133
+ return {"message": "No changes"}
134
+ return {"message": "Updated"}
135
+ except Exception as e:
136
+ logger.error(f"Error updating patient: {e}")
137
+ raise HTTPException(status_code=500, detail=str(e))
src/data/mongodb.py CHANGED
@@ -363,3 +363,58 @@ def list_patient_sessions(
363
  ) -> list[dict[str, Any]]:
364
  collection = get_collection(collection_name)
365
  return list(collection.find({"patient_id": patient_id}).sort("last_activity", DESCENDING))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  ) -> list[dict[str, Any]]:
364
  collection = get_collection(collection_name)
365
  return list(collection.find({"patient_id": patient_id}).sort("last_activity", DESCENDING))
366
+
367
+ # Patients helpers
368
+
369
+ def _generate_patient_id() -> str:
370
+ # Generate zero-padded 8-digit ID
371
+ import random
372
+ return f"{random.randint(0, 99999999):08d}"
373
+
374
+ def get_patient_by_id(patient_id: str) -> dict[str, Any] | None:
375
+ collection = get_collection(PATIENTS_COLLECTION)
376
+ return collection.find_one({"patient_id": patient_id})
377
+
378
+ def create_patient(
379
+ *,
380
+ name: str,
381
+ age: int,
382
+ sex: str,
383
+ address: str | None = None,
384
+ phone: str | None = None,
385
+ email: str | None = None,
386
+ medications: list[str] | None = None,
387
+ past_assessment_summary: str | None = None,
388
+ assigned_doctor_id: str | None = None
389
+ ) -> dict[str, Any]:
390
+ collection = get_collection(PATIENTS_COLLECTION)
391
+ now = datetime.now(timezone.utc)
392
+ # Ensure unique 8-digit id
393
+ for _ in range(10):
394
+ pid = _generate_patient_id()
395
+ if not collection.find_one({"patient_id": pid}):
396
+ break
397
+ else:
398
+ raise RuntimeError("Failed to generate unique patient ID")
399
+ doc = {
400
+ "patient_id": pid,
401
+ "name": name,
402
+ "age": age,
403
+ "sex": sex,
404
+ "address": address,
405
+ "phone": phone,
406
+ "email": email,
407
+ "medications": medications or [],
408
+ "past_assessment_summary": past_assessment_summary or "",
409
+ "assigned_doctor_id": assigned_doctor_id,
410
+ "created_at": now,
411
+ "updated_at": now
412
+ }
413
+ collection.insert_one(doc)
414
+ return doc
415
+
416
+ def update_patient_profile(patient_id: str, updates: dict[str, Any]) -> int:
417
+ collection = get_collection(PATIENTS_COLLECTION)
418
+ updates["updated_at"] = datetime.now(timezone.utc)
419
+ result = collection.update_one({"patient_id": patient_id}, {"$set": updates})
420
+ return result.modified_count
static/css/patient.css ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #0f172a; color: #e2e8f0; }
2
+ .container { max-width: 900px; margin: 40px auto; padding: 24px; background: #111827; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); }
3
+ h1 { margin-bottom: 16px; font-size: 1.6rem; }
4
+ .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
5
+ label { display: grid; gap: 6px; font-size: 0.9rem; }
6
+ input, select, textarea { padding: 10px; border-radius: 8px; border: 1px solid #334155; background: #0b1220; color: #e2e8f0; }
7
+ textarea { min-height: 100px; }
8
+ .actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 18px; }
9
+ .primary { background: #2563eb; color: #fff; border: none; padding: 10px 14px; border-radius: 8px; cursor: pointer; }
10
+ .secondary { background: transparent; color: #e2e8f0; border: 1px solid #334155; padding: 10px 14px; border-radius: 8px; cursor: pointer; }
11
+ .result { margin-top: 12px; color: #93c5fd; }
12
+ @media (max-width: 720px) { .grid { grid-template-columns: 1fr; } }
static/index.html CHANGED
@@ -38,7 +38,7 @@
38
  <div class="patient-section">
39
  <div class="patient-header">Patient</div>
40
  <div class="patient-input-group">
41
- <input type="text" id="patientIdInput" class="patient-input" placeholder="Enter 8-digit Patient ID" maxlength="8" inputmode="numeric" pattern="\\d{8}">
42
  <button class="patient-load-btn" id="loadPatientBtn" title="Load Patient">
43
  <i class="fas fa-user-injured"></i>
44
  </button>
@@ -134,7 +134,7 @@
134
  <div class="modal" id="userModal">
135
  <div class="modal-content">
136
  <div class="modal-header">
137
- <h3>User Profile</h3>
138
  <button class="modal-close" id="userModalClose">&times;</button>
139
  </div>
140
  <div class="modal-body">
@@ -226,6 +226,27 @@
226
  </div>
227
  </div>
228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  <!-- Loading overlay -->
230
  <div class="loading-overlay" id="loadingOverlay">
231
  <div class="loading-spinner">
 
38
  <div class="patient-section">
39
  <div class="patient-header">Patient</div>
40
  <div class="patient-input-group">
41
+ <input type="text" id="patientIdInput" class="patient-input" placeholder="Enter 8-digit Patient ID" maxlength="8" inputmode="numeric" pattern="\d{8}">
42
  <button class="patient-load-btn" id="loadPatientBtn" title="Load Patient">
43
  <i class="fas fa-user-injured"></i>
44
  </button>
 
134
  <div class="modal" id="userModal">
135
  <div class="modal-content">
136
  <div class="modal-header">
137
+ <h3>Doctor Profile</h3>
138
  <button class="modal-close" id="userModalClose">&times;</button>
139
  </div>
140
  <div class="modal-body">
 
226
  </div>
227
  </div>
228
 
229
+ <!-- Patient Modal -->
230
+ <div class="modal" id="patientModal">
231
+ <div class="modal-content">
232
+ <div class="modal-header">
233
+ <h3>Patient Profile</h3>
234
+ <button class="modal-close" id="patientModalClose">&times;</button>
235
+ </div>
236
+ <div class="modal-body">
237
+ <div class="patient-summary" id="patientSummary"></div>
238
+ <div class="patient-details">
239
+ <div><strong>Medications:</strong> <span id="patientMedications">-</span></div>
240
+ <div><strong>Past Assessment:</strong> <span id="patientAssessment">-</span></div>
241
+ </div>
242
+ </div>
243
+ <div class="modal-footer">
244
+ <button id="patientLogoutBtn" class="btn-danger">Log out patient</button>
245
+ <a id="patientCreateBtn" class="btn-primary" href="/static/patient.html">Create new patient</a>
246
+ </div>
247
+ </div>
248
+ </div>
249
+
250
  <!-- Loading overlay -->
251
  <div class="loading-overlay" id="loadingOverlay">
252
  <div class="loading-spinner">
static/js/app.js CHANGED
@@ -955,9 +955,62 @@ How can I assist you today?`;
955
  }
956
  }
957
 
958
- // Initialize the app when DOM is loaded
 
959
  document.addEventListener('DOMContentLoaded', () => {
960
- window.medicalChatbot = new MedicalChatbotApp();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
961
  });
962
 
963
  // Handle system theme changes
 
955
  }
956
  }
957
 
958
+ // Add near setupEventListeners inside the class methods where others are wired
959
+ // Patient modal open
960
  document.addEventListener('DOMContentLoaded', () => {
961
+ const profileBtn = document.getElementById('patientMenuBtn');
962
+ const modal = document.getElementById('patientModal');
963
+ const closeBtn = document.getElementById('patientModalClose');
964
+ const logoutBtn = document.getElementById('patientLogoutBtn');
965
+ const createBtn = document.getElementById('patientCreateBtn');
966
+
967
+ if (profileBtn && modal) {
968
+ profileBtn.addEventListener('click', async () => {
969
+ const pid = window.medicalChatbot?.currentPatientId;
970
+ if (pid) {
971
+ try {
972
+ const resp = await fetch(`/patients/${pid}`);
973
+ if (resp.ok) {
974
+ const p = await resp.json();
975
+ const name = p.name || 'Unknown';
976
+ const age = typeof p.age === 'number' ? p.age : '-';
977
+ const sex = p.sex || '-';
978
+ const meds = Array.isArray(p.medications) && p.medications.length > 0 ? p.medications.join(', ') : '-';
979
+ document.getElementById('patientSummary').textContent = `${name} — ${sex}, ${age}`;
980
+ document.getElementById('patientMedications').textContent = meds;
981
+ document.getElementById('patientAssessment').textContent = p.past_assessment_summary || '-';
982
+ }
983
+ } catch (e) {
984
+ console.error('Failed to load patient profile', e);
985
+ }
986
+ }
987
+ modal.classList.add('show');
988
+ });
989
+ }
990
+ if (closeBtn && modal) {
991
+ closeBtn.addEventListener('click', () => modal.classList.remove('show'));
992
+ modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('show'); });
993
+ }
994
+ if (logoutBtn) {
995
+ logoutBtn.addEventListener('click', () => {
996
+ if (confirm('Log out current patient?')) {
997
+ if (window.medicalChatbot) {
998
+ window.medicalChatbot.currentPatientId = null;
999
+ localStorage.removeItem('medicalChatbotPatientId');
1000
+ const status = document.getElementById('patientStatus');
1001
+ if (status) { status.textContent = 'No patient selected'; status.style.color = 'var(--text-secondary)'; }
1002
+ const input = document.getElementById('patientIdInput');
1003
+ if (input) input.value = '';
1004
+ modal.classList.remove('show');
1005
+ }
1006
+ }
1007
+ });
1008
+ }
1009
+ if (createBtn) {
1010
+ createBtn.addEventListener('click', () => {
1011
+ modal.classList.remove('show');
1012
+ });
1013
+ }
1014
  });
1015
 
1016
  // Handle system theme changes
static/js/patient.js ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const form = document.getElementById('patientForm');
3
+ const result = document.getElementById('result');
4
+ const cancelBtn = document.getElementById('cancelBtn');
5
+
6
+ cancelBtn.addEventListener('click', () => {
7
+ window.location.href = '/';
8
+ });
9
+
10
+ form.addEventListener('submit', async (e) => {
11
+ e.preventDefault();
12
+ const payload = {
13
+ name: document.getElementById('name').value.trim(),
14
+ age: parseInt(document.getElementById('age').value, 10),
15
+ sex: document.getElementById('sex').value,
16
+ address: document.getElementById('address').value.trim() || null,
17
+ phone: document.getElementById('phone').value.trim() || null,
18
+ email: document.getElementById('email').value.trim() || null,
19
+ medications: document.getElementById('medications').value.split('\n').map(s => s.trim()).filter(Boolean),
20
+ past_assessment_summary: document.getElementById('summary').value.trim() || null
21
+ };
22
+ try {
23
+ const resp = await fetch('/patients', {
24
+ method: 'POST',
25
+ headers: { 'Content-Type': 'application/json' },
26
+ body: JSON.stringify(payload)
27
+ });
28
+ if (!resp.ok) {
29
+ throw new Error(`HTTP ${resp.status}`);
30
+ }
31
+ const data = await resp.json();
32
+ const pid = data.patient_id;
33
+ result.textContent = `Created patient ${data.name} (${pid}). Redirecting...`;
34
+ localStorage.setItem('medicalChatbotPatientId', pid);
35
+ setTimeout(() => window.location.href = '/', 800);
36
+ } catch (err) {
37
+ console.error(err);
38
+ result.textContent = 'Failed to create patient. Please try again.';
39
+ result.style.color = 'crimson';
40
+ }
41
+ });
42
+ });
static/patient.html ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Create Patient</title>
7
+ <link rel="stylesheet" href="/static/css/patient.css">
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
9
+ </head>
10
+ <body>
11
+ <div class="container">
12
+ <h1>Create Patient</h1>
13
+ <form id="patientForm">
14
+ <div class="grid">
15
+ <label>Name<input type="text" id="name" required></label>
16
+ <label>Age<input type="number" id="age" min="0" max="130" required></label>
17
+ <label>Sex
18
+ <select id="sex" required>
19
+ <option value="Male">Male</option>
20
+ <option value="Female">Female</option>
21
+ <option value="Other">Other</option>
22
+ </select>
23
+ </label>
24
+ <label>Phone<input type="tel" id="phone"></label>
25
+ <label>Email<input type="email" id="email"></label>
26
+ <label>Address<input type="text" id="address"></label>
27
+ <label>Active Medications<textarea id="medications" placeholder="One per line"></textarea></label>
28
+ <label>Past Assessment Summary<textarea id="summary"></textarea></label>
29
+ </div>
30
+ <div class="actions">
31
+ <button type="button" id="cancelBtn" class="secondary">Cancel</button>
32
+ <button type="submit" class="primary">Create</button>
33
+ </div>
34
+ </form>
35
+ <div id="result" class="result"></div>
36
+ </div>
37
+ <script src="/static/js/patient.js"></script>
38
+ </body>
39
+ </html>