Spaces:
Paused
Paused
dylanglenister commited on
Commit ·
540bab9
1
Parent(s): e6b2925
Updated patient.
Browse filesValidator 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 +20 -8
- src/api/routes/patient.py +33 -15
- src/data/repositories/patient.py +39 -42
- src/models/user.py +2 -0
- static/css/emr.css +28 -13
- static/css/patient.css +9 -2
- static/emr.html +55 -41
- static/js/app.js +15 -34
- static/js/emr.js +4 -3
- static/js/patient.js +2 -0
- static/patient.html +27 -14
schemas/patient_validator.json
CHANGED
|
@@ -6,25 +6,32 @@
|
|
| 6 |
"name",
|
| 7 |
"age",
|
| 8 |
"sex",
|
| 9 |
-
"
|
| 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": "
|
|
|
|
|
|
|
| 23 |
"description": "'description' must be an unsigned int and is required."
|
| 24 |
},
|
| 25 |
"sex": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
"bsonType": "string",
|
| 27 |
-
"description": "
|
| 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": "
|
| 43 |
-
"
|
|
|
|
| 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=["
|
| 13 |
|
| 14 |
@router.post("")
|
| 15 |
async def create_patient_profile(req: PatientCreateRequest):
|
| 16 |
try:
|
| 17 |
logger().info(f"POST /patient name={req.name}")
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
| 28 |
)
|
| 29 |
-
|
| 30 |
-
logger().info(f"Created patient {
|
| 31 |
-
return
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
| 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 |
-
|
| 45 |
-
|
| 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 |
-
) ->
|
| 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 |
-
"
|
| 75 |
-
"
|
| 76 |
-
"
|
|
|
|
| 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
|
| 85 |
-
|
| 86 |
|
| 87 |
def update_patient_profile(patient_id: str, updates: dict[str, Any]) -> int:
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 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 |
-
|
| 100 |
-
gap: 1.5rem;
|
| 101 |
}
|
| 102 |
|
| 103 |
-
.emr-
|
| 104 |
-
|
| 105 |
-
flex-direction: column;
|
| 106 |
-
gap: 0.5rem;
|
| 107 |
}
|
| 108 |
|
| 109 |
-
.emr-
|
| 110 |
-
grid-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
|
|
|
|
|
|
|
|
|
| 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) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 35 |
-
|
| 36 |
-
<
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
<
|
| 40 |
-
<
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
<
|
| 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 |
-
<
|
| 56 |
-
|
| 57 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
</div>
|
| 59 |
-
<
|
| 60 |
-
|
| 61 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 71 |
-
<
|
| 72 |
-
|
| 73 |
-
<div class="medications-
|
| 74 |
-
<
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
<
|
| 78 |
-
|
| 79 |
-
<
|
| 80 |
-
|
|
|
|
|
|
|
| 81 |
</div>
|
| 82 |
</div>
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
<
|
| 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
|
| 1216 |
-
let idMatch = p.
|
| 1217 |
-
//
|
| 1218 |
-
|
| 1219 |
-
|
| 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
|
| 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'} (${
|
| 1279 |
} else {
|
| 1280 |
status.textContent = `Patient: ${pid}`;
|
| 1281 |
}
|
|
@@ -1331,29 +1333,8 @@ How can I assist you today?`;
|
|
| 1331 |
return;
|
| 1332 |
}
|
| 1333 |
|
| 1334 |
-
//
|
| 1335 |
-
|
| 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.
|
| 1367 |
this.savePatientId();
|
| 1368 |
-
input.value = first.
|
| 1369 |
-
this.updatePatientDisplay(first.
|
| 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.
|
| 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 |
-
|
| 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.
|
| 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 |
-
<
|
| 18 |
-
<
|
| 19 |
-
|
| 20 |
-
<
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
<
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|