Spaces:
Sleeping
Sleeping
| """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"]}, | |
| ) | |
| def list_providers(active_only: bool = False, session: dict = Depends(get_current_session)): | |
| return data_manager.get_all_providers(active_only=active_only) | |
| 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 | |
| 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 | |
| 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 | |
| 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 --- | |
| 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) --- | |
| 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 | |
| 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 --- | |
| 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 | |
| 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 | |
| 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 | |