"""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