scheduler / api /providers.py
umangchaudhry's picture
Upload 31 files
0d04b76 verified
Raw
History Blame Contribute Delete
11.7 kB
"""Provider CRUD routes."""
from datetime import date as date_cls
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Any
from core import data_manager
from core.scheduling_engine import normalize_time_str
from api.deps import get_current_session, require_admin_session
router = APIRouter(prefix="/api/providers", tags=["providers"])
# --- Pydantic models ---
class WorkHours(BaseModel):
start: str
end: str
class ProviderCreate(BaseModel):
name: str
email: str = ""
password: str = ""
address: str = ""
category: str = "provider" # provider | nurse
work_hours: WorkHours
lunch_break: WorkHours
service_levels: list[Any] = []
class ProviderUpdate(BaseModel):
name: str | None = None
email: str | None = None
password: str | None = None
address: str | None = None
category: str | None = None
work_hours: WorkHours | None = None
lunch_break: WorkHours | None = None
service_levels: list[Any] | None = None
class RecurringDaysOff(BaseModel):
recurring_days_off: list[str]
class DaysOffEntry(BaseModel):
date: str
all_day: bool = True
start_time: str | None = None
end_time: str | None = None
title: str = "Unavailable"
class OnCallEntry(BaseModel):
date: str
def _normalize_hours(work_hours: WorkHours, lunch: WorkHours) -> tuple[dict, dict]:
fields = {
"Work Hours Start": work_hours.start,
"Work Hours End": work_hours.end,
"Lunch Start": lunch.start,
"Lunch End": lunch.end,
}
out = {}
for label, value in fields.items():
n = normalize_time_str(value)
if n is None:
raise HTTPException(status_code=400, detail=f"{label} is not a valid time (HH:MM): {value!r}")
out[label] = n
return (
{"start": out["Work Hours Start"], "end": out["Work Hours End"]},
{"start": out["Lunch Start"], "end": out["Lunch End"]},
)
@router.get("")
def list_providers(active_only: bool = False, session: dict = Depends(get_current_session)):
return data_manager.get_all_providers(active_only=active_only)
@router.get("/{provider_id}")
def get_provider(provider_id: str, session: dict = Depends(get_current_session)):
p = data_manager.get_provider_by_id(provider_id)
if not p:
raise HTTPException(status_code=404, detail="Provider not found")
# Provider users can only access their own record
if session.get("user_role") == "provider" and session.get("provider_id") != provider_id:
raise HTTPException(status_code=403, detail="Forbidden")
return p
@router.post("")
def create_provider(req: ProviderCreate, session: dict = Depends(require_admin_session)):
if not req.name:
raise HTTPException(status_code=400, detail="Provider name is required")
wh, lb = _normalize_hours(req.work_hours, req.lunch_break)
provider_data = {
"name": req.name,
"email": req.email,
"password": req.password,
"address": req.address,
"category": req.category,
"work_hours": wh,
"lunch_break": lb,
"service_levels": req.service_levels,
}
new_prov = data_manager.add_provider(provider_data)
data_manager.log_action(
session["current_user"], "ADD_PROVIDER",
f"Added provider: {req.name} ({new_prov['id']})",
)
return new_prov
@router.put("/{provider_id}")
def update_provider(provider_id: str, req: ProviderUpdate, session: dict = Depends(get_current_session)):
# Providers may only edit themselves
if session.get("user_role") == "provider" and session.get("provider_id") != provider_id:
raise HTTPException(status_code=403, detail="Forbidden")
existing = data_manager.get_provider_by_id(provider_id)
if not existing:
raise HTTPException(status_code=404, detail="Provider not found")
updates: dict = {}
if req.name is not None:
updates["name"] = req.name
if req.email is not None:
updates["email"] = req.email
if req.password is not None:
updates["password"] = req.password
if req.address is not None:
updates["address"] = req.address
if req.category is not None:
updates["category"] = req.category
if req.service_levels is not None:
updates["service_levels"] = req.service_levels
if req.work_hours is not None and req.lunch_break is not None:
wh, lb = _normalize_hours(req.work_hours, req.lunch_break)
updates["work_hours"] = wh
updates["lunch_break"] = lb
elif req.work_hours is not None:
# normalize alone
s = normalize_time_str(req.work_hours.start)
e = normalize_time_str(req.work_hours.end)
if not s or not e:
raise HTTPException(status_code=400, detail="Invalid work hours")
updates["work_hours"] = {"start": s, "end": e}
elif req.lunch_break is not None:
s = normalize_time_str(req.lunch_break.start)
e = normalize_time_str(req.lunch_break.end)
if not s or not e:
raise HTTPException(status_code=400, detail="Invalid lunch break")
updates["lunch_break"] = {"start": s, "end": e}
updated = data_manager.update_provider(provider_id, updates)
data_manager.log_action(
session["current_user"], "UPDATE_PROVIDER",
f"Updated provider: {updated['name']} ({provider_id})",
)
return updated
@router.delete("/{provider_id}")
def delete_provider(provider_id: str, session: dict = Depends(require_admin_session)):
p = data_manager.get_provider_by_id(provider_id)
if not p:
raise HTTPException(status_code=404, detail="Provider not found")
ok = data_manager.delete_provider(provider_id)
if not ok:
raise HTTPException(status_code=500, detail="Delete failed")
data_manager.log_action(
session["current_user"], "DELETE_PROVIDER",
f"Deleted provider: {p['name']} ({provider_id})",
)
return {"ok": True}
# --- Recurring days off ---
@router.put("/{provider_id}/recurring-days-off")
def update_recurring_days_off(provider_id: str, req: RecurringDaysOff, session: dict = Depends(get_current_session)):
if session.get("user_role") == "provider" and session.get("provider_id") != provider_id:
raise HTTPException(status_code=403, detail="Forbidden")
p = data_manager.get_provider_by_id(provider_id)
if not p:
raise HTTPException(status_code=404, detail="Provider not found")
updated = data_manager.update_provider(provider_id, {"recurring_days_off": req.recurring_days_off})
data_manager.log_action(
session["current_user"], "UPDATE_PROVIDER",
f"Updated standing days off for {p['name']}: {', '.join(req.recurring_days_off) or 'None'}",
)
return updated
# --- Days off (unavailable blocks) ---
@router.post("/{provider_id}/days-off")
def add_days_off(provider_id: str, req: DaysOffEntry, session: dict = Depends(get_current_session)):
if session.get("user_role") == "provider" and session.get("provider_id") != provider_id:
raise HTTPException(status_code=403, detail="Forbidden")
p = data_manager.get_provider_by_id(provider_id)
if not p:
raise HTTPException(status_code=404, detail="Provider not found")
is_all_day = req.all_day
new_entry = {
"date": req.date,
"all_day": is_all_day,
"start_time": None if is_all_day else req.start_time,
"end_time": None if is_all_day else req.end_time,
"title": req.title or "Unavailable",
}
current = p.get("days_off", []) or []
current.append(new_entry)
updated = data_manager.update_provider(provider_id, {"days_off": current})
detail = f"all day on {req.date}" if is_all_day else f"{req.start_time}{req.end_time} on {req.date}"
data_manager.log_action(
session["current_user"], "UPDATE_PROVIDER",
f"Added unavailable block for {p['name']}: {req.title} ({detail})",
)
return updated
@router.delete("/{provider_id}/days-off/{index}")
def remove_days_off(provider_id: str, index: int, session: dict = Depends(get_current_session)):
if session.get("user_role") == "provider" and session.get("provider_id") != provider_id:
raise HTTPException(status_code=403, detail="Forbidden")
p = data_manager.get_provider_by_id(provider_id)
if not p:
raise HTTPException(status_code=404, detail="Provider not found")
current = p.get("days_off", []) or []
if index < 0 or index >= len(current):
raise HTTPException(status_code=400, detail="Invalid index")
removed = current.pop(index)
updated = data_manager.update_provider(provider_id, {"days_off": current})
label = removed if isinstance(removed, str) else f"{removed.get('date')} {removed.get('title','')}"
data_manager.log_action(
session["current_user"], "UPDATE_PROVIDER",
f"Removed unavailable block from {p['name']}: {label}",
)
return updated
# --- On-Call ---
@router.post("/{provider_id}/on-call")
def add_on_call(provider_id: str, req: OnCallEntry, session: dict = Depends(get_current_session)):
if session.get("user_role") == "provider" and session.get("provider_id") != provider_id:
raise HTTPException(status_code=403, detail="Forbidden")
p = data_manager.get_provider_by_id(provider_id)
if not p:
raise HTTPException(status_code=404, detail="Provider not found")
current = sorted(p.get("on_call", []) or [])
if req.date in current:
return {"ok": True, "duplicate": True, "provider": p}
current = sorted(current + [req.date])
updated = data_manager.update_provider(provider_id, {"on_call": current})
data_manager.log_action(
session["current_user"], "UPDATE_PROVIDER",
f"Added on-call day for {p['name']}: {req.date}",
)
return updated
@router.delete("/{provider_id}/on-call/{date}")
def remove_on_call(provider_id: str, date: str, session: dict = Depends(get_current_session)):
if session.get("user_role") == "provider" and session.get("provider_id") != provider_id:
raise HTTPException(status_code=403, detail="Forbidden")
p = data_manager.get_provider_by_id(provider_id)
if not p:
raise HTTPException(status_code=404, detail="Provider not found")
current = [d for d in (p.get("on_call", []) or []) if d != date]
updated = data_manager.update_provider(provider_id, {"on_call": current})
data_manager.log_action(
session["current_user"], "UPDATE_PROVIDER",
f"Removed on-call day for {p['name']}: {date}",
)
return updated
class OnCallToggle(BaseModel):
date: str
on_call: bool
@router.post("/{provider_id}/on-call/toggle")
def toggle_on_call(provider_id: str, req: OnCallToggle, session: dict = Depends(get_current_session)):
"""Toggle on-call status for a specific date. Convenience endpoint for the UI switch."""
if session.get("user_role") == "provider" and session.get("provider_id") != provider_id:
raise HTTPException(status_code=403, detail="Forbidden")
p = data_manager.get_provider_by_id(provider_id)
if not p:
raise HTTPException(status_code=404, detail="Provider not found")
current = set(p.get("on_call", []) or [])
if req.on_call:
current.add(req.date)
action_word = "Enabled"
else:
current.discard(req.date)
action_word = "Disabled"
new_list = sorted(current)
updated = data_manager.update_provider(provider_id, {"on_call": new_list})
data_manager.log_action(
session["current_user"], "UPDATE_PROVIDER",
f"{action_word} on-call for {p['name']}: {req.date}",
)
return updated