diff --git a/patient-care-app/backend/app/database.py b/patient-care-app/backend/app/database.py index 1e1d449..e95a351 100644 --- a/patient-care-app/backend/app/database.py +++ b/patient-care-app/backend/app/database.py @@ -89,6 +89,24 @@ def init_db(): ) """) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS treatment_plans ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + patient_id INTEGER NOT NULL, + provider_id INTEGER NOT NULL, + name TEXT NOT NULL, + description TEXT, + goals TEXT, + notes TEXT, + status TEXT DEFAULT 'active' CHECK(status IN ('active', 'completed', 'discontinued')), + is_active BOOLEAN DEFAULT 1, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (patient_id) REFERENCES patients(id), + FOREIGN KEY (provider_id) REFERENCES providers(id) + ) + """) + cursor.execute(""" CREATE TABLE IF NOT EXISTS audit_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/patient-care-app/backend/app/main.py b/patient-care-app/backend/app/main.py index f6c89c1..7df196e 100644 --- a/patient-care-app/backend/app/main.py +++ b/patient-care-app/backend/app/main.py @@ -2,7 +2,7 @@ from app.config import settings from app.database import init_db from app.middleware.error_handler import global_exception_handler -from app.routers import auth, providers, patients, visits, treatments, audit +from app.routers import auth, providers, patients, visits, treatments, treatment_plans, audit # Validate configuration on startup settings.validate() @@ -25,6 +25,7 @@ app.include_router(patients.router) app.include_router(visits.router) app.include_router(treatments.router) +app.include_router(treatment_plans.router) app.include_router(audit.router) diff --git a/patient-care-app/backend/app/routers/treatment_plans.py b/patient-care-app/backend/app/routers/treatment_plans.py new file mode 100644 index 0000000..f48771d --- /dev/null +++ b/patient-care-app/backend/app/routers/treatment_plans.py @@ -0,0 +1,138 @@ +from datetime import datetime, timezone +from fastapi import APIRouter, Depends, HTTPException, Request, status +from app.database import get_db_connection +from app.schemas.treatment_plan import TreatmentPlanCreate, TreatmentPlanUpdate, TreatmentPlanResponse +from app.auth.rbac import require_permission + +router = APIRouter(prefix="/treatment-plans", tags=["treatment-plans"]) + + +@router.post("/", response_model=TreatmentPlanResponse, status_code=status.HTTP_201_CREATED) +async def create_treatment_plan( + request: Request, + plan_data: TreatmentPlanCreate, + current_user: dict = Depends(require_permission("treatments:create")), +): + now = datetime.now(timezone.utc).isoformat() + conn = get_db_connection() + try: + patient = conn.execute("SELECT id FROM patients WHERE id = ? AND is_active = 1", (plan_data.patient_id,)).fetchone() + if not patient: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Patient not found") + + cursor = conn.execute( + """INSERT INTO treatment_plans (patient_id, provider_id, name, description, goals, notes, status, is_active, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?)""", + ( + plan_data.patient_id, current_user["id"], plan_data.name, + plan_data.description, plan_data.goals, plan_data.notes, + plan_data.status or "active", now, now, + ), + ) + conn.commit() + plan_id = cursor.lastrowid + + row = conn.execute("SELECT * FROM treatment_plans WHERE id = ?", (plan_id,)).fetchone() + return _row_to_response(row) + finally: + conn.close() + + +@router.get("/", response_model=list[TreatmentPlanResponse]) +async def list_treatment_plans( + request: Request, + patient_id: int = None, + status_filter: str = None, + current_user: dict = Depends(require_permission("treatments:read")), +): + conn = get_db_connection() + try: + query = "SELECT * FROM treatment_plans WHERE is_active = 1" + params = [] + if patient_id: + query += " AND patient_id = ?" + params.append(patient_id) + if status_filter: + query += " AND status = ?" + params.append(status_filter) + query += " ORDER BY id DESC" + + rows = conn.execute(query, params).fetchall() + return [_row_to_response(row) for row in rows] + finally: + conn.close() + + +@router.get("/{plan_id}", response_model=TreatmentPlanResponse) +async def get_treatment_plan( + plan_id: int, + request: Request, + current_user: dict = Depends(require_permission("treatments:read")), +): + conn = get_db_connection() + try: + row = conn.execute("SELECT * FROM treatment_plans WHERE id = ? AND is_active = 1", (plan_id,)).fetchone() + if not row: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Treatment plan not found") + + return _row_to_response(row) + finally: + conn.close() + + +@router.put("/{plan_id}", response_model=TreatmentPlanResponse) +async def update_treatment_plan( + plan_id: int, + request: Request, + plan_data: TreatmentPlanUpdate, + current_user: dict = Depends(require_permission("treatments:update")), +): + conn = get_db_connection() + try: + row = conn.execute("SELECT * FROM treatment_plans WHERE id = ? AND is_active = 1", (plan_id,)).fetchone() + if not row: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Treatment plan not found") + + update_data = plan_data.model_dump(exclude_unset=True) + if not update_data: + return _row_to_response(row) + + update_data["updated_at"] = datetime.now(timezone.utc).isoformat() + + set_clause = ", ".join(f"{k} = ?" for k in update_data) + values = list(update_data.values()) + [plan_id] + conn.execute(f"UPDATE treatment_plans SET {set_clause} WHERE id = ?", values) + conn.commit() + + row = conn.execute("SELECT * FROM treatment_plans WHERE id = ?", (plan_id,)).fetchone() + return _row_to_response(row) + finally: + conn.close() + + +@router.delete("/{plan_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_treatment_plan( + plan_id: int, + request: Request, + current_user: dict = Depends(require_permission("treatments:delete")), +): + conn = get_db_connection() + try: + row = conn.execute("SELECT * FROM treatment_plans WHERE id = ? AND is_active = 1", (plan_id,)).fetchone() + if not row: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Treatment plan not found") + + now = datetime.now(timezone.utc).isoformat() + conn.execute("UPDATE treatment_plans SET is_active = 0, updated_at = ? WHERE id = ?", (now, plan_id)) + conn.commit() + finally: + conn.close() + + +def _row_to_response(row) -> TreatmentPlanResponse: + return TreatmentPlanResponse( + id=row["id"], patient_id=row["patient_id"], provider_id=row["provider_id"], + name=row["name"], description=row["description"], goals=row["goals"], + notes=row["notes"], status=row["status"], is_active=row["is_active"], + created_at=row["created_at"], updated_at=row["updated_at"], + ) diff --git a/patient-care-app/backend/app/schemas/treatment_plan.py b/patient-care-app/backend/app/schemas/treatment_plan.py new file mode 100644 index 0000000..069c32c --- /dev/null +++ b/patient-care-app/backend/app/schemas/treatment_plan.py @@ -0,0 +1,33 @@ +from pydantic import BaseModel, Field +from typing import Optional + + +class TreatmentPlanCreate(BaseModel): + patient_id: int = Field(..., gt=0) + name: str = Field(..., min_length=1, max_length=200) + description: Optional[str] = Field(None, max_length=2000) + goals: Optional[str] = Field(None, max_length=2000) + notes: Optional[str] = Field(None, max_length=2000) + status: Optional[str] = Field("active", pattern=r"^(active|completed|discontinued)$") + + +class TreatmentPlanUpdate(BaseModel): + name: Optional[str] = Field(None, min_length=1, max_length=200) + description: Optional[str] = Field(None, max_length=2000) + goals: Optional[str] = Field(None, max_length=2000) + notes: Optional[str] = Field(None, max_length=2000) + status: Optional[str] = Field(None, pattern=r"^(active|completed|discontinued)$") + + +class TreatmentPlanResponse(BaseModel): + id: int + patient_id: int + provider_id: int + name: str + description: Optional[str] = None + goals: Optional[str] = None + notes: Optional[str] = None + status: str + is_active: bool + created_at: str + updated_at: str diff --git a/patient-care-app/backend/seed_data.py b/patient-care-app/backend/seed_data.py index 8bfd19c..5f25535 100644 --- a/patient-care-app/backend/seed_data.py +++ b/patient-care-app/backend/seed_data.py @@ -37,7 +37,7 @@ def seed(): now = datetime.now(timezone.utc).isoformat() # Clear existing data - for table in ["treatments", "visits", "patients", "audit_logs", "providers"]: + for table in ["treatment_plans", "treatments", "visits", "patients", "audit_logs", "providers"]: conn.execute(f"DELETE FROM {table}") # --- Providers --- @@ -126,6 +126,21 @@ def seed(): tstatus, now, now), ) + # --- Treatment Plans (PHI stored as plaintext — no encrypt_phi) --- + treatment_plans_data = [ + (patient_ids[0], provider_ids[1], "Cardiac Care Plan", "Comprehensive management of stable angina with medication therapy and lifestyle modifications", "Reduce chest pain frequency to less than once per week; improve exercise tolerance to 30 minutes daily", "Patient is motivated and compliant. Consider cardiac rehab referral if symptoms persist after 3 months.", "active"), + (patient_ids[2], provider_ids[1], "COPD Management Plan", "Long-term management of COPD with bronchodilator therapy and pulmonary rehabilitation", "Maintain FEV1 above 70% predicted; reduce exacerbation frequency to less than 2 per year", "Patient has history of smoking (quit 2019). Monitor for depression secondary to chronic illness.", "active"), + (patient_ids[4], provider_ids[1], "Diabetes Type 2 Management", "Integrated diabetes management including glycemic control, cardiovascular risk reduction, and renal protection", "Maintain A1C below 7.0%; blood pressure below 130/80; annual eye and foot exams", "Patient managing well on current regimen. Wife assists with meal planning. Review insulin initiation if A1C rises above 7.5%.", "active"), + (patient_ids[3], provider_ids[1], "Migraine Prevention Protocol", "Preventive and abortive migraine management with lifestyle trigger identification", "Reduce migraine frequency from 3/month to less than 1/month within 8 weeks", "Patient keeps a headache diary. Oral contraceptive use may be contributing factor — coordinate with OB/GYN.", "active"), + ] + + for pid, prov_id, name, description, goals, notes, plan_status in treatment_plans_data: + conn.execute( + """INSERT INTO treatment_plans (patient_id, provider_id, name, description, goals, notes, status, is_active, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?)""", + (pid, prov_id, name, description, goals, notes, plan_status, now, now), + ) + conn.commit() conn.close()