dylanglenister commited on
Commit
540bab9
·
1 Parent(s): e6b2925

Updated patient.

Browse files

Validator has been finalised, all associated files have been updated to match.
Now stores patient ethnicity. Uses mongodb id as patient id (removed redundancy).

schemas/patient_validator.json CHANGED
@@ -6,25 +6,32 @@
6
  "name",
7
  "age",
8
  "sex",
9
- "assigned_doctor_id",
10
  "created_at",
11
  "updated_at"
12
  ],
13
  "properties": {
14
- "assigned_doctor_id": {
15
- "bsonType": ""
16
- },
17
  "name": {
18
  "bsonType": "string",
19
  "description": "'name' must be a string is required."
20
  },
21
  "age": {
22
- "bsonType": "uint",
 
 
23
  "description": "'description' must be an unsigned int and is required."
24
  },
25
  "sex": {
 
 
 
 
 
 
 
 
26
  "bsonType": "string",
27
- "description": "sex must be string and is required."
28
  },
29
  "address": {
30
  "bsonType": "string",
@@ -39,13 +46,18 @@
39
  "description": "'email' must be a string and is optional."
40
  },
41
  "medications": {
42
- "bsonType": "string",
43
- "description": "'medication' must be a and is optional."
 
44
  },
45
  "past_assessment_summary": {
46
  "bsonType": "string",
47
  "description": "'past_assessment_summary' must be a string and is optional."
48
  },
 
 
 
 
49
  "created_at": {
50
  "bsonType": "date",
51
  "description": "'created_at' must be a date and is required."
 
6
  "name",
7
  "age",
8
  "sex",
9
+ "ethnicity",
10
  "created_at",
11
  "updated_at"
12
  ],
13
  "properties": {
 
 
 
14
  "name": {
15
  "bsonType": "string",
16
  "description": "'name' must be a string is required."
17
  },
18
  "age": {
19
+ "bsonType": "int",
20
+ "minimum": 0,
21
+ "maximum": 200,
22
  "description": "'description' must be an unsigned int and is required."
23
  },
24
  "sex": {
25
+ "enum": [
26
+ "Male",
27
+ "Female",
28
+ "Intersex"
29
+ ],
30
+ "description": "'sex' must be one of: ['Male', 'Female', 'Intersex'] and is required."
31
+ },
32
+ "ethnicity": {
33
  "bsonType": "string",
34
+ "description": "'ethnicity' must be a string and is required"
35
  },
36
  "address": {
37
  "bsonType": "string",
 
46
  "description": "'email' must be a string and is optional."
47
  },
48
  "medications": {
49
+ "bsonType": "array",
50
+ "items": { "bsonType": "string" },
51
+ "description": "'medication' must be an array of strings and is optional."
52
  },
53
  "past_assessment_summary": {
54
  "bsonType": "string",
55
  "description": "'past_assessment_summary' must be a string and is optional."
56
  },
57
+ "assigned_doctor_id": {
58
+ "bsonType": "string",
59
+ "description": "'assigned_docter_id' must be a string and is optional."
60
+ },
61
  "created_at": {
62
  "bsonType": "date",
63
  "description": "'created_at' must be a date and is required."
src/api/routes/patient.py CHANGED
@@ -1,5 +1,7 @@
1
  # api/routes/patient.py
2
 
 
 
3
  from fastapi import APIRouter, HTTPException
4
 
5
  from src.data.repositories.patient import (create_patient, get_patient_by_id,
@@ -9,26 +11,27 @@ from src.data.repositories.session import list_patient_sessions
9
  from src.models.user import PatientCreateRequest, PatientUpdateRequest
10
  from src.utils.logger import logger
11
 
12
- router = APIRouter(prefix="/patient", tags=["Patients"])
13
 
14
  @router.post("")
15
  async def create_patient_profile(req: PatientCreateRequest):
16
  try:
17
  logger().info(f"POST /patient name={req.name}")
18
- patient = create_patient(
19
- name=req.name,
20
- age=req.age,
21
- sex=req.sex,
22
- address=req.address,
23
- phone=req.phone,
24
- email=req.email,
25
- medications=req.medications,
26
- past_assessment_summary=req.past_assessment_summary,
27
- assigned_doctor_id=req.assigned_doctor_id
 
28
  )
29
- patient["_id"] = str(patient.get("_id")) if patient.get("_id") else None
30
- logger().info(f"Created patient {patient.get('name')} id={patient.get('patient_id')}")
31
- return patient
32
  except Exception as e:
33
  logger().error(f"Error creating patient: {e}")
34
  raise HTTPException(status_code=500, detail=str(e))
@@ -48,10 +51,19 @@ async def search_patients_route(q: str, limit: int = 20):
48
  async def get_patient(patient_id: str):
49
  try:
50
  logger().info(f"GET /patient/{patient_id}")
 
 
 
 
 
 
 
51
  patient = get_patient_by_id(patient_id)
52
  if not patient:
53
  raise HTTPException(status_code=404, detail="Patient not found")
54
- patient["_id"] = str(patient.get("_id")) if patient.get("_id") else None
 
 
55
  return patient
56
  except HTTPException:
57
  raise
@@ -62,12 +74,18 @@ async def get_patient(patient_id: str):
62
  @router.patch("/{patient_id}")
63
  async def update_patient(patient_id: str, req: PatientUpdateRequest):
64
  try:
 
 
 
 
65
  payload = {k: v for k, v in req.model_dump().items() if v is not None}
66
  logger().info(f"PATCH /patient/{patient_id} fields={list(payload.keys())}")
67
  modified = update_patient_profile(patient_id, payload)
68
  if modified == 0:
69
  return {"message": "No changes"}
70
  return {"message": "Updated"}
 
 
71
  except Exception as e:
72
  logger().error(f"Error updating patient: {e}")
73
  raise HTTPException(status_code=500, detail=str(e))
 
1
  # api/routes/patient.py
2
 
3
+ from bson import ObjectId
4
+ from bson.errors import InvalidId
5
  from fastapi import APIRouter, HTTPException
6
 
7
  from src.data.repositories.patient import (create_patient, get_patient_by_id,
 
11
  from src.models.user import PatientCreateRequest, PatientUpdateRequest
12
  from src.utils.logger import logger
13
 
14
+ router = APIRouter(prefix="/patient", tags=["Patient"])
15
 
16
  @router.post("")
17
  async def create_patient_profile(req: PatientCreateRequest):
18
  try:
19
  logger().info(f"POST /patient name={req.name}")
20
+ patient_id = create_patient(
21
+ req.name,
22
+ req.age,
23
+ req.sex,
24
+ req.ethnicity,
25
+ req.address,
26
+ req.phone,
27
+ req.email,
28
+ req.medications,
29
+ req.past_assessment_summary,
30
+ req.assigned_doctor_id
31
  )
32
+ #patient_id["_id"] = str(patient_id.get("_id")) if patient_id.get("_id") else None
33
+ logger().info(f"Created patient {req.name} id={patient_id}")
34
+ return { "patient_id": patient_id }
35
  except Exception as e:
36
  logger().error(f"Error creating patient: {e}")
37
  raise HTTPException(status_code=500, detail=str(e))
 
51
  async def get_patient(patient_id: str):
52
  try:
53
  logger().info(f"GET /patient/{patient_id}")
54
+ try:
55
+ # Validate ObjectId format
56
+ if not ObjectId.is_valid(patient_id):
57
+ raise HTTPException(status_code=400, detail="Invalid patient ID format")
58
+ except InvalidId:
59
+ raise HTTPException(status_code=400, detail="Invalid patient ID format")
60
+
61
  patient = get_patient_by_id(patient_id)
62
  if not patient:
63
  raise HTTPException(status_code=404, detail="Patient not found")
64
+
65
+ # Convert ObjectId to string for JSON response
66
+ patient["_id"] = str(patient["_id"])
67
  return patient
68
  except HTTPException:
69
  raise
 
74
  @router.patch("/{patient_id}")
75
  async def update_patient(patient_id: str, req: PatientUpdateRequest):
76
  try:
77
+ # Validate ObjectId format
78
+ if not ObjectId.is_valid(patient_id):
79
+ raise HTTPException(status_code=400, detail="Invalid patient ID format")
80
+
81
  payload = {k: v for k, v in req.model_dump().items() if v is not None}
82
  logger().info(f"PATCH /patient/{patient_id} fields={list(payload.keys())}")
83
  modified = update_patient_profile(patient_id, payload)
84
  if modified == 0:
85
  return {"message": "No changes"}
86
  return {"message": "Updated"}
87
+ except HTTPException:
88
+ raise
89
  except Exception as e:
90
  logger().error(f"Error updating patient: {e}")
91
  raise HTTPException(status_code=500, detail=str(e))
src/data/repositories/patient.py CHANGED
@@ -5,23 +5,25 @@ A patient is a person who has been assigned to a doctor for treatment.
5
 
6
  ## Fields
7
  _id: index
8
- name:
9
- age:
10
- sex:
11
- address:
12
- phone:
13
- email:
14
- medications:
15
- past_assessment_summary:
16
- assigned_doctor_id:
17
- created_at:
18
- updated_at:
 
19
  """
20
 
21
  import re
22
  from datetime import datetime, timezone
23
  from typing import Any
24
 
 
25
  from pymongo import ASCENDING
26
  from pymongo.errors import (ConnectionFailure, DuplicateKeyError,
27
  OperationFailure, PyMongoError)
@@ -32,64 +34,59 @@ from src.utils.logger import logger
32
  PATIENTS_COLLECTION = "patients"
33
 
34
  def create():
 
35
  create_collection(PATIENTS_COLLECTION, "schemas/patient_validator.json")
36
-
37
- def _generate_patient_id() -> str:
38
- """Generate zero-padded 8-digit ID"""
39
- import random
40
- return f"{random.randint(0, 99999999):08d}"
41
-
42
 
43
  def get_patient_by_id(patient_id: str) -> dict[str, Any] | None:
44
- collection = get_collection(PATIENTS_COLLECTION)
45
- return collection.find_one({"patient_id": patient_id})
46
-
 
 
 
 
47
 
48
  def create_patient(
49
- *,
50
  name: str,
51
  age: int,
52
  sex: str,
 
53
  address: str | None = None,
54
  phone: str | None = None,
55
  email: str | None = None,
56
  medications: list[str] | None = None,
57
  past_assessment_summary: str | None = None,
58
  assigned_doctor_id: str | None = None
59
- ) -> dict[str, Any]:
60
  collection = get_collection(PATIENTS_COLLECTION)
61
  now = datetime.now(timezone.utc)
62
- # Ensure unique 8-digit id
63
- for _ in range(10):
64
- pid = _generate_patient_id()
65
- if not collection.find_one({"patient_id": pid}):
66
- break
67
- else:
68
- raise RuntimeError("Failed to generate unique patient ID")
69
  doc = {
70
- "patient_id": pid,
71
  "name": name,
72
  "age": age,
73
  "sex": sex,
74
- "address": address,
75
- "phone": phone,
76
- "email": email,
 
77
  "medications": medications or [],
78
  "past_assessment_summary": past_assessment_summary or "",
79
- "assigned_doctor_id": assigned_doctor_id,
80
  "created_at": now,
81
  "updated_at": now
82
  }
83
- collection.insert_one(doc)
84
- return doc
85
-
86
 
87
  def update_patient_profile(patient_id: str, updates: dict[str, Any]) -> int:
88
- collection = get_collection(PATIENTS_COLLECTION)
89
- updates["updated_at"] = datetime.now(timezone.utc)
90
- result = collection.update_one({"patient_id": patient_id}, {"$set": updates})
91
- return result.modified_count
92
-
 
 
 
93
 
94
  def search_patients(query: str, limit: int = 10) -> list[dict[str, Any]]:
95
  """Search patients by name (case-insensitive starts-with/contains) or partial patient_id."""
 
5
 
6
  ## Fields
7
  _id: index
8
+ name: The name of the patient
9
+ age: How old the patient is
10
+ sex: Male or female
11
+ ethnicity: Geneological information
12
+ address: Where they live
13
+ phone: What their phone number is
14
+ email: What their email address it
15
+ medications: Any medications they are currently taking
16
+ past_assessment_summary: Summarisation of past assessments
17
+ assigned_doctor_id: The id of the account assigned to this patient
18
+ created_at: The timestamp when the patient was created
19
+ updated_at: The timestamp when the patient data was last modified
20
  """
21
 
22
  import re
23
  from datetime import datetime, timezone
24
  from typing import Any
25
 
26
+ from bson import ObjectId
27
  from pymongo import ASCENDING
28
  from pymongo.errors import (ConnectionFailure, DuplicateKeyError,
29
  OperationFailure, PyMongoError)
 
34
  PATIENTS_COLLECTION = "patients"
35
 
36
  def create():
37
+ #get_collection(PATIENTS_COLLECTION).drop()
38
  create_collection(PATIENTS_COLLECTION, "schemas/patient_validator.json")
39
+ get_collection(PATIENTS_COLLECTION).create_index("assigned_doctor_id")
 
 
 
 
 
40
 
41
  def get_patient_by_id(patient_id: str) -> dict[str, Any] | None:
42
+ logger().info(f"Searching for patient with id '{patient_id}'")
43
+ try:
44
+ collection = get_collection(PATIENTS_COLLECTION)
45
+ return collection.find_one({"_id": ObjectId(patient_id)})
46
+ except Exception as e:
47
+ logger().error(f"Error in get_patient_by_id: {e}")
48
+ return None
49
 
50
  def create_patient(
 
51
  name: str,
52
  age: int,
53
  sex: str,
54
+ ethnicity: str,
55
  address: str | None = None,
56
  phone: str | None = None,
57
  email: str | None = None,
58
  medications: list[str] | None = None,
59
  past_assessment_summary: str | None = None,
60
  assigned_doctor_id: str | None = None
61
+ ) -> str:
62
  collection = get_collection(PATIENTS_COLLECTION)
63
  now = datetime.now(timezone.utc)
 
 
 
 
 
 
 
64
  doc = {
 
65
  "name": name,
66
  "age": age,
67
  "sex": sex,
68
+ "ethnicity": ethnicity,
69
+ "address": address or "",
70
+ "phone": phone or "",
71
+ "email": email or "",
72
  "medications": medications or [],
73
  "past_assessment_summary": past_assessment_summary or "",
74
+ "assigned_doctor_id": assigned_doctor_id or "",
75
  "created_at": now,
76
  "updated_at": now
77
  }
78
+ result = collection.insert_one(doc)
79
+ return str(result.inserted_id)
 
80
 
81
  def update_patient_profile(patient_id: str, updates: dict[str, Any]) -> int:
82
+ try:
83
+ collection = get_collection(PATIENTS_COLLECTION)
84
+ updates["updated_at"] = datetime.now(timezone.utc)
85
+ result = collection.update_one({"_id": ObjectId(patient_id)}, {"$set": updates})
86
+ return result.modified_count
87
+ except Exception as e:
88
+ logger().error(f"Error in update_patient_profile: {e}")
89
+ return 0
90
 
91
  def search_patients(query: str, limit: int = 10) -> list[dict[str, Any]]:
92
  """Search patients by name (case-insensitive starts-with/contains) or partial patient_id."""
src/models/user.py CHANGED
@@ -12,6 +12,7 @@ class PatientCreateRequest(BaseModel):
12
  name: str
13
  age: int
14
  sex: str
 
15
  address: str | None = None
16
  phone: str | None = None
17
  email: str | None = None
@@ -23,6 +24,7 @@ class PatientUpdateRequest(BaseModel):
23
  name: str | None = None
24
  age: int | None = None
25
  sex: str | None = None
 
26
  address: str | None = None
27
  phone: str | None = None
28
  email: str | None = None
 
12
  name: str
13
  age: int
14
  sex: str
15
+ ethnicity: str
16
  address: str | None = None
17
  phone: str | None = None
18
  email: str | None = None
 
24
  name: str | None = None
25
  age: int | None = None
26
  sex: str | None = None
27
+ ethnicity: str | None = None
28
  address: str | None = None
29
  phone: str | None = None
30
  email: str | None = None
static/css/emr.css CHANGED
@@ -95,19 +95,27 @@
95
  }
96
 
97
  .emr-grid {
 
 
 
 
 
 
98
  display: grid;
99
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
100
- gap: 1.5rem;
101
  }
102
 
103
- .emr-field {
104
- display: flex;
105
- flex-direction: column;
106
- gap: 0.5rem;
107
  }
108
 
109
- .emr-field.full-width {
110
- grid-column: 1 / -1;
 
 
 
 
 
111
  }
112
 
113
  .emr-field label {
@@ -300,30 +308,37 @@
300
  align-items: stretch;
301
  text-align: center;
302
  }
303
-
304
  .patient-info-header {
305
  align-items: center;
306
  text-align: center;
307
  }
308
-
309
  .emr-main {
310
  padding: 1rem;
311
  }
312
-
313
  .emr-grid {
314
  grid-template-columns: 1fr;
315
  }
316
-
317
  .emr-actions {
318
  flex-direction: column;
319
  }
320
-
321
  .add-medication {
322
  flex-direction: column;
323
  align-items: stretch;
324
  }
325
  }
326
 
 
 
 
 
 
 
 
327
  /* Dark Theme Adjustments */
328
  [data-theme="dark"] .emr-field input,
329
  [data-theme="dark"] .emr-field select,
 
95
  }
96
 
97
  .emr-grid {
98
+ display: flex;
99
+ flex-direction: column;
100
+ gap: 16px;
101
+ }
102
+
103
+ .emr-grid .row {
104
  display: grid;
105
+ gap: 16px;
 
106
  }
107
 
108
+ .emr-grid .row:not(.contact-info) {
109
+ grid-template-columns: repeat(2, 1fr);
 
 
110
  }
111
 
112
+ .emr-grid .row.contact-info {
113
+ grid-template-columns: repeat(3, 1fr);
114
+ }
115
+
116
+ .emr-field {
117
+ display: grid;
118
+ gap: 6px;
119
  }
120
 
121
  .emr-field label {
 
308
  align-items: stretch;
309
  text-align: center;
310
  }
311
+
312
  .patient-info-header {
313
  align-items: center;
314
  text-align: center;
315
  }
316
+
317
  .emr-main {
318
  padding: 1rem;
319
  }
320
+
321
  .emr-grid {
322
  grid-template-columns: 1fr;
323
  }
324
+
325
  .emr-actions {
326
  flex-direction: column;
327
  }
328
+
329
  .add-medication {
330
  flex-direction: column;
331
  align-items: stretch;
332
  }
333
  }
334
 
335
+ @media (max-width: 720px) {
336
+ .emr-grid .row,
337
+ .emr-grid .row.contact-info {
338
+ grid-template-columns: 1fr;
339
+ }
340
+ }
341
+
342
  /* Dark Theme Adjustments */
343
  [data-theme="dark"] .emr-field input,
344
  [data-theme="dark"] .emr-field select,
static/css/patient.css CHANGED
@@ -3,7 +3,10 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxyge
3
  .back-link { display:inline-flex; align-items:center; gap:8px; color:#93c5fd; text-decoration:none; margin-bottom:12px; }
4
  .back-link:hover { text-decoration:underline; }
5
  h1 { margin-bottom: 16px; font-size: 1.6rem; }
6
- .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
 
 
 
7
  label { display: grid; gap: 6px; font-size: 0.9rem; }
8
  input, select, textarea { padding: 10px; border-radius: 8px; border: 1px solid #334155; background: #0b1220; color: #e2e8f0; }
9
  textarea { min-height: 100px; }
@@ -11,7 +14,11 @@ textarea { min-height: 100px; }
11
  .primary { background: #2563eb; color: #fff; border: none; padding: 10px 14px; border-radius: 8px; cursor: pointer; }
12
  .secondary { background: transparent; color: #e2e8f0; border: 1px solid #334155; padding: 10px 14px; border-radius: 8px; cursor: pointer; }
13
  .result { margin-top: 12px; color: #93c5fd; }
14
- @media (max-width: 720px) { .grid { grid-template-columns: 1fr; } }
 
 
 
 
15
  .modal { position: fixed; inset:0; display:none; align-items:center; justify-content:center; background: rgba(0,0,0,0.45); z-index: 3000; }
16
  .modal.show { display:flex; }
17
  .modal-content { background:#111827; color:#e2e8f0; width: 520px; max-width: 92vw; border-radius: 12px; overflow:hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.4); }
 
3
  .back-link { display:inline-flex; align-items:center; gap:8px; color:#93c5fd; text-decoration:none; margin-bottom:12px; }
4
  .back-link:hover { text-decoration:underline; }
5
  h1 { margin-bottom: 16px; font-size: 1.6rem; }
6
+ .grid { display: flex; flex-direction: column; gap: 16px; }
7
+ .grid .row { display: grid; gap: 16px; }
8
+ .grid .row:not(.contact-info) { grid-template-columns: repeat(2, 1fr); }
9
+ .grid .row.contact-info { grid-template-columns: repeat(3, 1fr); }
10
  label { display: grid; gap: 6px; font-size: 0.9rem; }
11
  input, select, textarea { padding: 10px; border-radius: 8px; border: 1px solid #334155; background: #0b1220; color: #e2e8f0; }
12
  textarea { min-height: 100px; }
 
14
  .primary { background: #2563eb; color: #fff; border: none; padding: 10px 14px; border-radius: 8px; cursor: pointer; }
15
  .secondary { background: transparent; color: #e2e8f0; border: 1px solid #334155; padding: 10px 14px; border-radius: 8px; cursor: pointer; }
16
  .result { margin-top: 12px; color: #93c5fd; }
17
+ @media (max-width: 720px) {
18
+ .grid .row, .grid .row.contact-info {
19
+ grid-template-columns: 1fr;
20
+ }
21
+ }
22
  .modal { position: fixed; inset:0; display:none; align-items:center; justify-content:center; background: rgba(0,0,0,0.45); z-index: 3000; }
23
  .modal.show { display:flex; }
24
  .modal-content { background:#111827; color:#e2e8f0; width: 520px; max-width: 92vw; border-radius: 12px; overflow:hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.4); }
static/emr.html CHANGED
@@ -31,34 +31,46 @@
31
  <section class="emr-section">
32
  <h2><i class="fas fa-user"></i> Patient Overview</h2>
33
  <div class="emr-grid">
34
- <div class="emr-field">
35
- <label>Full Name</label>
36
- <input type="text" id="patientNameInput" placeholder="Enter patient name">
37
- </div>
38
- <div class="emr-field">
39
- <label>Age</label>
40
- <input type="number" id="patientAgeInput" placeholder="Enter age" min="0" max="150">
41
- </div>
42
- <div class="emr-field">
43
- <label>Sex</label>
44
- <select id="patientSexInput">
45
- <option value="">Select sex</option>
46
- <option value="Male">Male</option>
47
- <option value="Female">Female</option>
48
- <option value="Other">Other</option>
49
- </select>
50
- </div>
51
- <div class="emr-field">
52
- <label>Phone</label>
53
- <input type="tel" id="patientPhoneInput" placeholder="Enter phone number">
54
  </div>
55
- <div class="emr-field">
56
- <label>Email</label>
57
- <input type="email" id="patientEmailInput" placeholder="Enter email address">
 
 
 
 
 
 
 
 
 
 
 
58
  </div>
59
- <div class="emr-field full-width">
60
- <label>Address</label>
61
- <textarea id="patientAddressInput" placeholder="Enter full address" rows="3"></textarea>
 
 
 
 
 
 
 
 
 
 
 
62
  </div>
63
  </div>
64
  </section>
@@ -67,23 +79,25 @@
67
  <section class="emr-section">
68
  <h2><i class="fas fa-pills"></i> Medical Information</h2>
69
  <div class="emr-grid">
70
- <div class="emr-field full-width">
71
- <label>Current Medications</label>
72
- <div class="medications-container">
73
- <div class="medications-list" id="medicationsList">
74
- <!-- Medications will be added here dynamically -->
75
- </div>
76
- <div class="add-medication">
77
- <input type="text" id="newMedicationInput" placeholder="Add new medication">
78
- <button id="addMedicationBtn" class="btn-secondary">
79
- <i class="fas fa-plus"></i> Add
80
- </button>
 
 
81
  </div>
82
  </div>
83
- </div>
84
- <div class="emr-field full-width">
85
- <label>Past Assessment Summary</label>
86
- <textarea id="pastAssessmentInput" placeholder="Enter past assessment summary" rows="4"></textarea>
87
  </div>
88
  </div>
89
  </section>
 
31
  <section class="emr-section">
32
  <h2><i class="fas fa-user"></i> Patient Overview</h2>
33
  <div class="emr-grid">
34
+ <!-- Row 1: Name and Age -->
35
+ <div class="row">
36
+ <div class="emr-field">
37
+ <label>Full Name</label>
38
+ <input type="text" id="patientNameInput" placeholder="Enter patient name">
39
+ </div>
40
+ <div class="emr-field">
41
+ <label>Age</label>
42
+ <input type="number" id="patientAgeInput" placeholder="Enter age" min="0" max="150">
43
+ </div>
 
 
 
 
 
 
 
 
 
 
44
  </div>
45
+ <!-- Row 2: Sex and Ethnicity -->
46
+ <div class="row">
47
+ <div class="emr-field">
48
+ <label>Sex</label>
49
+ <select id="patientSexInput">
50
+ <option value="Male">Male</option>
51
+ <option value="Female">Female</option>
52
+ <option value="Intersex">Intersex</option>
53
+ </select>
54
+ </div>
55
+ <div class="emr-field">
56
+ <label>Ethnicity</label>
57
+ <input type="ethnicity" id="patientEthnicityInput" placeholder="Enter ethnicity">
58
+ </div>
59
  </div>
60
+ <!-- Row 3: Contact Information -->
61
+ <div class="row contact-info">
62
+ <div class="emr-field">
63
+ <label>Phone</label>
64
+ <input type="tel" id="patientPhoneInput" placeholder="Enter phone number">
65
+ </div>
66
+ <div class="emr-field">
67
+ <label>Email</label>
68
+ <input type="email" id="patientEmailInput" placeholder="Enter email address">
69
+ </div>
70
+ <div class="emr-field">
71
+ <label>Address</label>
72
+ <input type="text" id="patientAddressInput" placeholder="Enter full address">
73
+ </div>
74
  </div>
75
  </div>
76
  </section>
 
79
  <section class="emr-section">
80
  <h2><i class="fas fa-pills"></i> Medical Information</h2>
81
  <div class="emr-grid">
82
+ <div class="row">
83
+ <div class="emr-field">
84
+ <label>Current Medications</label>
85
+ <div class="medications-container">
86
+ <div class="medications-list" id="medicationsList">
87
+ <!-- Medications will be added here dynamically -->
88
+ </div>
89
+ <div class="add-medication">
90
+ <input type="text" id="newMedicationInput" placeholder="Add new medication">
91
+ <button id="addMedicationBtn" class="btn-secondary">
92
+ <i class="fas fa-plus"></i> Add
93
+ </button>
94
+ </div>
95
  </div>
96
  </div>
97
+ <div class="emr-field">
98
+ <label>Past Assessment Summary</label>
99
+ <textarea id="pastAssessmentInput" placeholder="Enter past assessment summary" rows="4"></textarea>
100
+ </div>
101
  </div>
102
  </div>
103
  </section>
static/js/app.js CHANGED
@@ -1212,12 +1212,14 @@ How can I assist you today?`;
1212
  return storedPatients.filter(p => {
1213
  // Check name match (case-insensitive contains)
1214
  const nameMatch = p.name.toLowerCase().includes(query.toLowerCase());
1215
- // Check patient_id match
1216
- let idMatch = p.patient_id.includes(query);
1217
- // Special handling for numeric queries - check if patient_id starts with the query
1218
- if (/^\d+$/.test(query)) {
1219
- idMatch = p.patient_id.startsWith(query) || p.patient_id.includes(query);
1220
- }
 
 
1221
  return nameMatch || idMatch;
1222
  });
1223
  } catch (e) {
@@ -1263,7 +1265,7 @@ How can I assist you today?`;
1263
  }
1264
  async loadSavedPatientId() {
1265
  const pid = localStorage.getItem('medicalChatbotPatientId');
1266
- if (pid && /^\d{8}$/.test(pid)) {
1267
  this.currentPatientId = pid;
1268
  const status = document.getElementById('patientStatus');
1269
  const actions = document.getElementById('patientActions');
@@ -1275,7 +1277,7 @@ How can I assist you today?`;
1275
  const resp = await fetch(`/patient/${pid}`);
1276
  if (resp.ok) {
1277
  const patient = await resp.json();
1278
- status.textContent = `Patient: ${patient.name || 'Unknown'} (${pid})`;
1279
  } else {
1280
  status.textContent = `Patient: ${pid}`;
1281
  }
@@ -1331,29 +1333,8 @@ How can I assist you today?`;
1331
  return;
1332
  }
1333
 
1334
- // If it's a complete 8-digit ID, use it directly
1335
- if (/^\d{8}$/.test(value)) {
1336
- console.log('[DEBUG] Valid 8-digit ID provided');
1337
- this.currentPatientId = value;
1338
- this.savePatientId();
1339
- // Try to get patient name for display
1340
- try {
1341
- const resp = await fetch(`/patient/${value}`);
1342
- if (resp.ok) {
1343
- const patient = await resp.json();
1344
- this.updatePatientDisplay(value, patient.name || 'Unknown');
1345
- } else {
1346
- this.updatePatientDisplay(value);
1347
- }
1348
- } catch (e) {
1349
- this.updatePatientDisplay(value);
1350
- }
1351
- await this.fetchAndRenderPatientSessions();
1352
- return;
1353
- }
1354
-
1355
- // Otherwise, search for patient by name or partial ID
1356
- console.log('[DEBUG] Searching for patient by name/partial ID');
1357
  try {
1358
  const resp = await fetch(`/patient/search?q=${encodeURIComponent(value)}&limit=1`);
1359
  console.log('[DEBUG] Search response status:', resp.status);
@@ -1363,10 +1344,10 @@ How can I assist you today?`;
1363
  const first = (data.results || [])[0];
1364
  if (first) {
1365
  console.log('[DEBUG] Found patient, setting as current:', first);
1366
- this.currentPatientId = first.patient_id;
1367
  this.savePatientId();
1368
- input.value = first.patient_id;
1369
- this.updatePatientDisplay(first.patient_id, first.name || 'Unknown');
1370
  await this.fetchAndRenderPatientSessions();
1371
  return;
1372
  }
 
1212
  return storedPatients.filter(p => {
1213
  // Check name match (case-insensitive contains)
1214
  const nameMatch = p.name.toLowerCase().includes(query.toLowerCase());
1215
+ // Check _id match
1216
+ let idMatch = p._id.includes(query);
1217
+ //// Check patient_id match
1218
+ //let idMatch = p.patient_id.includes(query);
1219
+ //// Special handling for numeric queries - check if patient_id starts with the query
1220
+ //if (/^\d+$/.test(query)) {
1221
+ // idMatch = p.patient_id.startsWith(query) || p.patient_id.includes(query);
1222
+ //}
1223
  return nameMatch || idMatch;
1224
  });
1225
  } catch (e) {
 
1265
  }
1266
  async loadSavedPatientId() {
1267
  const pid = localStorage.getItem('medicalChatbotPatientId');
1268
+ if (pid) {
1269
  this.currentPatientId = pid;
1270
  const status = document.getElementById('patientStatus');
1271
  const actions = document.getElementById('patientActions');
 
1277
  const resp = await fetch(`/patient/${pid}`);
1278
  if (resp.ok) {
1279
  const patient = await resp.json();
1280
+ status.textContent = `Patient: ${patient.name || 'Unknown'} (${patient._id})`;
1281
  } else {
1282
  status.textContent = `Patient: ${pid}`;
1283
  }
 
1333
  return;
1334
  }
1335
 
1336
+ // Search for patient by name or ID
1337
+ console.log('[DEBUG] Searching for patient');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1338
  try {
1339
  const resp = await fetch(`/patient/search?q=${encodeURIComponent(value)}&limit=1`);
1340
  console.log('[DEBUG] Search response status:', resp.status);
 
1344
  const first = (data.results || [])[0];
1345
  if (first) {
1346
  console.log('[DEBUG] Found patient, setting as current:', first);
1347
+ this.currentPatientId = first._id;
1348
  this.savePatientId();
1349
+ input.value = first._id;
1350
+ this.updatePatientDisplay(first._id, first.name || 'Unknown');
1351
  await this.fetchAndRenderPatientSessions();
1352
  return;
1353
  }
static/js/emr.js CHANGED
@@ -85,12 +85,13 @@ class PatientEMR {
85
 
86
  // Update header
87
  document.getElementById('patientName').textContent = this.patientData.name || 'Unknown';
88
- document.getElementById('patientId').textContent = `ID: ${this.patientData.patient_id}`;
89
 
90
  // Populate form fields
91
  document.getElementById('patientNameInput').value = this.patientData.name || '';
92
  document.getElementById('patientAgeInput').value = this.patientData.age || '';
93
  document.getElementById('patientSexInput').value = this.patientData.sex || '';
 
94
  document.getElementById('patientPhoneInput').value = this.patientData.phone || '';
95
  document.getElementById('patientEmailInput').value = this.patientData.email || '';
96
  document.getElementById('patientAddressInput').value = this.patientData.address || '';
@@ -235,7 +236,7 @@ class PatientEMR {
235
  }
236
 
237
  const exportData = {
238
- patient_id: this.patientData.patient_id,
239
  name: this.patientData.name,
240
  age: this.patientData.age,
241
  sex: this.patientData.sex,
@@ -253,7 +254,7 @@ class PatientEMR {
253
  const url = URL.createObjectURL(blob);
254
  const a = document.createElement('a');
255
  a.href = url;
256
- a.download = `patient-${this.patientData.patient_id}-emr.json`;
257
  document.body.appendChild(a);
258
  a.click();
259
  document.body.removeChild(a);
 
85
 
86
  // Update header
87
  document.getElementById('patientName').textContent = this.patientData.name || 'Unknown';
88
+ document.getElementById('patientId').textContent = `ID: ${this.patientData._id}`;
89
 
90
  // Populate form fields
91
  document.getElementById('patientNameInput').value = this.patientData.name || '';
92
  document.getElementById('patientAgeInput').value = this.patientData.age || '';
93
  document.getElementById('patientSexInput').value = this.patientData.sex || '';
94
+ document.getElementById('patientEthnicityInput').value = this.patientData.ethnicity || '';
95
  document.getElementById('patientPhoneInput').value = this.patientData.phone || '';
96
  document.getElementById('patientEmailInput').value = this.patientData.email || '';
97
  document.getElementById('patientAddressInput').value = this.patientData.address || '';
 
236
  }
237
 
238
  const exportData = {
239
+ _id: this.patientData._id,
240
  name: this.patientData.name,
241
  age: this.patientData.age,
242
  sex: this.patientData.sex,
 
254
  const url = URL.createObjectURL(blob);
255
  const a = document.createElement('a');
256
  a.href = url;
257
+ a.download = `patient-${this.patientData._id}-emr.json`;
258
  document.body.appendChild(a);
259
  a.click();
260
  document.body.removeChild(a);
static/js/patient.js CHANGED
@@ -28,6 +28,7 @@ document.addEventListener('DOMContentLoaded', () => {
28
  document.getElementById('name').value = data.name || '';
29
  document.getElementById('age').value = data.age ?? '';
30
  document.getElementById('sex').value = data.sex || 'Other';
 
31
  document.getElementById('address').value = data.address || '';
32
  document.getElementById('phone').value = data.phone || '';
33
  document.getElementById('email').value = data.email || '';
@@ -64,6 +65,7 @@ document.addEventListener('DOMContentLoaded', () => {
64
  name: document.getElementById('name').value.trim(),
65
  age: parseInt(document.getElementById('age').value, 10),
66
  sex: document.getElementById('sex').value,
 
67
  address: document.getElementById('address').value.trim() || null,
68
  phone: document.getElementById('phone').value.trim() || null,
69
  email: document.getElementById('email').value.trim() || null,
 
28
  document.getElementById('name').value = data.name || '';
29
  document.getElementById('age').value = data.age ?? '';
30
  document.getElementById('sex').value = data.sex || 'Other';
31
+ document.getElementById('ethnicity').value = data.ethnicity || '';
32
  document.getElementById('address').value = data.address || '';
33
  document.getElementById('phone').value = data.phone || '';
34
  document.getElementById('email').value = data.email || '';
 
65
  name: document.getElementById('name').value.trim(),
66
  age: parseInt(document.getElementById('age').value, 10),
67
  sex: document.getElementById('sex').value,
68
+ ethnicity: document.getElementById('ethnicity').value,
69
  address: document.getElementById('address').value.trim() || null,
70
  phone: document.getElementById('phone').value.trim() || null,
71
  email: document.getElementById('email').value.trim() || null,
static/patient.html CHANGED
@@ -14,20 +14,33 @@
14
  <h1>Create Patient</h1>
15
  <form id="patientForm">
16
  <div class="grid">
17
- <label>Name<input type="text" id="name" required></label>
18
- <label>Age<input type="number" id="age" min="0" max="130" required></label>
19
- <label>Sex
20
- <select id="sex" required>
21
- <option value="Male">Male</option>
22
- <option value="Female">Female</option>
23
- <option value="Other">Other</option>
24
- </select>
25
- </label>
26
- <label>Phone<input type="tel" id="phone"></label>
27
- <label>Email<input type="email" id="email"></label>
28
- <label>Address<input type="text" id="address"></label>
29
- <label>Active Medications<textarea id="medications" placeholder="One per line"></textarea></label>
30
- <label>Past Assessment Summary<textarea id="summary"></textarea></label>
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  </div>
32
  <div class="actions">
33
  <button type="button" id="cancelBtn" class="secondary">Cancel</button>
 
14
  <h1>Create Patient</h1>
15
  <form id="patientForm">
16
  <div class="grid">
17
+ <!-- Row 1: Name and Age -->
18
+ <div class="row">
19
+ <label>Name<input type="text" id="name" required></label>
20
+ <label>Age<input type="number" id="age" min="0" max="130" required></label>
21
+ </div>
22
+ <!-- Row 2: Sex and Ethnicity -->
23
+ <div class="row">
24
+ <label>Sex
25
+ <select id="sex" required>
26
+ <option value="Male">Male</option>
27
+ <option value="Female">Female</option>
28
+ <option value="Intersex">Intersex</option>
29
+ </select>
30
+ </label>
31
+ <label>Ethnicity<input type="ethnicity" id="ethnicity"></label>
32
+ </div>
33
+ <!-- Row 3: Contact Information -->
34
+ <div class="row contact-info">
35
+ <label>Phone<input type="tel" id="phone"></label>
36
+ <label>Email<input type="email" id="email"></label>
37
+ <label>Address<input type="text" id="address"></label>
38
+ </div>
39
+ <!-- Row 4: Medical Information -->
40
+ <div class="row">
41
+ <label>Active Medications<textarea id="medications" placeholder="One per line"></textarea></label>
42
+ <label>Past Assessment Summary<textarea id="summary"></textarea></label>
43
+ </div>
44
  </div>
45
  <div class="actions">
46
  <button type="button" id="cancelBtn" class="secondary">Cancel</button>