Files
F-r-ldraledigplannerare/backend/app/models/analytics.py
T
jacobh2 0e9b20ec37 Track preschool planning days, partner invites and real vacation forfeiture
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>
2026-09-20 11:22:07 +02:00

146 lines
4.3 KiB
Python

"""Pydantic response schemas for analytics endpoints."""
from datetime import date
from uuid import UUID
from pydantic import BaseModel
from app.services.analytics_service import (
Age4Projection,
ParentalLeaveYearBalance,
ParentYearBalance,
VacationYearBalance,
WorkspaceOverview,
YearTotals,
)
class VacationYearBalanceResponse(BaseModel):
year: int
earned: int
is_assumed: bool
carried_in: int
planned_use: int
left: int
carried_out: int
expiring: int
expiring_age: int
expiring_over_cap: int
@classmethod
def from_entity(cls, v: VacationYearBalance) -> "VacationYearBalanceResponse":
return cls(
year=v.year,
earned=v.earned,
is_assumed=v.is_assumed,
carried_in=v.carried_in,
planned_use=v.planned_use,
left=v.left,
carried_out=v.carried_out,
expiring=v.expiring,
expiring_age=v.expiring_age,
expiring_over_cap=v.expiring_over_cap,
)
class ParentalLeaveYearBalanceResponse(BaseModel):
year: int
sjukpenning_used: int
lagsta_used: int
sjukpenning_left: int
lagsta_left: int
@classmethod
def from_entity(cls, p: ParentalLeaveYearBalance) -> "ParentalLeaveYearBalanceResponse":
return cls(
year=p.year,
sjukpenning_used=p.sjukpenning_used,
lagsta_used=p.lagsta_used,
sjukpenning_left=p.sjukpenning_left,
lagsta_left=p.lagsta_left,
)
class ParentYearBalanceResponse(BaseModel):
personal_data_uuid: UUID
child_id: UUID
year: int
vacation: VacationYearBalanceResponse
parental_leave: ParentalLeaveYearBalanceResponse
@classmethod
def from_entity(cls, p: ParentYearBalance) -> "ParentYearBalanceResponse":
return cls(
personal_data_uuid=p.personal_data_uuid,
child_id=p.child_id,
year=p.year,
vacation=VacationYearBalanceResponse.from_entity(p.vacation),
parental_leave=ParentalLeaveYearBalanceResponse.from_entity(p.parental_leave),
)
class YearTotalsResponse(BaseModel):
year: int
pl_left: int
vacation_left: int
@classmethod
def from_entity(cls, y: YearTotals) -> "YearTotalsResponse":
return cls(year=y.year, pl_left=y.pl_left, vacation_left=y.vacation_left)
class Age4ProjectionResponse(BaseModel):
personal_data_uuid: UUID
child_id: UUID
birth_date: date
fourth_birthday: date
total_days: int
days_used: int
planned_days_before_b4: int
projected_left_at_b4: int
savable_after_b4: int
expiring_at_b4: int
savable_cap_at_b4: int
year_balances: list[ParentYearBalanceResponse]
@classmethod
def from_entity(cls, p: Age4Projection) -> "Age4ProjectionResponse":
return cls(
personal_data_uuid=p.personal_data_uuid,
child_id=p.child_id,
birth_date=p.birth_date,
fourth_birthday=p.fourth_birthday,
total_days=p.total_days,
days_used=p.days_used,
planned_days_before_b4=p.planned_days_before_b4,
projected_left_at_b4=p.projected_left_at_b4,
savable_after_b4=p.savable_after_b4,
expiring_at_b4=p.expiring_at_b4,
savable_cap_at_b4=p.savable_cap_at_b4,
year_balances=[ParentYearBalanceResponse.from_entity(yb) for yb in p.year_balances],
)
class WorkspaceOverviewResponse(BaseModel):
workspace_id: UUID
child_id: UUID
parent_projections: list[Age4ProjectionResponse]
combined_days_used: int
combined_days_left: int
combined_expiring_at_b4: int
year_totals: list[YearTotalsResponse]
@classmethod
def from_entity(cls, overview: WorkspaceOverview) -> "WorkspaceOverviewResponse":
return cls(
workspace_id=overview.workspace_id,
child_id=overview.child_id,
parent_projections=[
Age4ProjectionResponse.from_entity(p) for p in overview.parent_projections
],
combined_days_used=overview.combined_days_used,
combined_days_left=overview.combined_days_left,
combined_expiring_at_b4=overview.combined_expiring_at_b4,
year_totals=[YearTotalsResponse.from_entity(y) for y in overview.year_totals],
)