0e9b20ec37
Three strands of work that grew together across the same settings screen and
API client, so they land as one change.
Per-year preschool planning days ("studiedagar"): new table, repository
(SQLite + Postgres), service, router and migration 007, plus the hooks and
Settings UI to plan and record them per child and year. Allocation now
reserves the recorded figure for a year where one exists and only falls back
to the child-level estimate otherwise, marking that fallback as assumed.
Partner invitations: workspaces issue and list invitations, registration
accepts an invite token to join the inviter's workspace instead of creating a
personal one, and a /join/:token page plus the settings invite panel drive it.
Vacation forfeiture: tips and the dashboard now distinguish days a parent
genuinely loses from days that merely age, so the warning stops firing on
balances that were never at risk.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
78 lines
2.8 KiB
Python
78 lines
2.8 KiB
Python
"""HTTP routes for preschool planning ("studiedagar") days per year.
|
|
|
|
Routers stay thin: validate via Pydantic, call the service, map domain
|
|
errors to HTTP. Scoped per child via `WorkspaceScope.check_child`, which
|
|
404s (never 403) on a missing or foreign-workspace child id.
|
|
"""
|
|
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, HTTPException, status
|
|
|
|
from app.dependencies import PreschoolPlanningDaysServiceDep, WorkspaceScopeDep
|
|
from app.models.preschool_planning_days import (
|
|
PreschoolPlanningDaysPlannedUpdate,
|
|
PreschoolPlanningDaysResponse,
|
|
PreschoolPlanningDaysTakenUpdate,
|
|
)
|
|
from app.utils.errors import ValidationError
|
|
|
|
router = APIRouter(prefix="/api/preschool-planning-days", tags=["preschool-planning-days"])
|
|
|
|
|
|
@router.put("/{child_id}/{year}/planned", response_model=PreschoolPlanningDaysResponse)
|
|
async def upsert_planned_days(
|
|
child_id: UUID,
|
|
year: int,
|
|
payload: PreschoolPlanningDaysPlannedUpdate,
|
|
service: PreschoolPlanningDaysServiceDep,
|
|
scope: WorkspaceScopeDep,
|
|
) -> PreschoolPlanningDaysResponse:
|
|
child = await scope.check_child(child_id)
|
|
try:
|
|
entity = await service.upsert_planned(child, year, payload.planned_days)
|
|
except ValidationError as exc:
|
|
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, str(exc)) from exc
|
|
return PreschoolPlanningDaysResponse.from_entity(entity)
|
|
|
|
|
|
@router.put("/{child_id}/{year}/taken", response_model=PreschoolPlanningDaysResponse)
|
|
async def record_taken_days(
|
|
child_id: UUID,
|
|
year: int,
|
|
payload: PreschoolPlanningDaysTakenUpdate,
|
|
service: PreschoolPlanningDaysServiceDep,
|
|
scope: WorkspaceScopeDep,
|
|
) -> PreschoolPlanningDaysResponse:
|
|
child = await scope.check_child(child_id)
|
|
try:
|
|
entity = await service.record_taken(child, year, payload.taken_days)
|
|
except ValidationError as exc:
|
|
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, str(exc)) from exc
|
|
return PreschoolPlanningDaysResponse.from_entity(entity)
|
|
|
|
|
|
@router.get("/{child_id}/{year}", response_model=PreschoolPlanningDaysResponse)
|
|
async def get_year(
|
|
child_id: UUID,
|
|
year: int,
|
|
service: PreschoolPlanningDaysServiceDep,
|
|
scope: WorkspaceScopeDep,
|
|
) -> PreschoolPlanningDaysResponse:
|
|
child = await scope.check_child(child_id)
|
|
entity = await service.get_by_child_year(child_id, year)
|
|
if entity is None:
|
|
return PreschoolPlanningDaysResponse.default_for(child, year)
|
|
return PreschoolPlanningDaysResponse.from_entity(entity)
|
|
|
|
|
|
@router.get("/{child_id}", response_model=list[PreschoolPlanningDaysResponse])
|
|
async def list_years(
|
|
child_id: UUID,
|
|
service: PreschoolPlanningDaysServiceDep,
|
|
scope: WorkspaceScopeDep,
|
|
) -> list[PreschoolPlanningDaysResponse]:
|
|
await scope.check_child(child_id)
|
|
entities = await service.list_for_child(child_id)
|
|
return [PreschoolPlanningDaysResponse.from_entity(e) for e in entities]
|