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>
This commit is contained in:
@@ -58,7 +58,16 @@ class VacationYearBalance:
|
||||
planned_use: int # committed 'vacation' planned_leave_day rows in this year (see PlanningService._vacation_days_left)
|
||||
left: int # max(earned + carried_in - planned_use, 0)
|
||||
carried_out: int # days saved forward into next year's carried_in
|
||||
expiring: int # days that hit the 5-year (rules.vacation_carry_forward_years) cap this year — LOST if not used
|
||||
# EVERY day lost at this year's end, from both causes combined — the sum
|
||||
# of `expiring_age` and `expiring_over_cap` below. Never a subset: a UI
|
||||
# showing only `expiring` must not silently under-report a forfeiture.
|
||||
expiring: int
|
||||
# subset of `expiring`: bank entries that hit the 5-year
|
||||
# (rules.vacation_carry_forward_years) age cap this year.
|
||||
expiring_age: int
|
||||
# subset of `expiring`: days above (earned - floor) that couldn't be
|
||||
# banked — includes an unused floor itself when usage is low.
|
||||
expiring_over_cap: int
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -94,7 +103,17 @@ class ParentYearBalance:
|
||||
- A bank entry in its final valid year (earned exactly
|
||||
`vacation_carry_forward_years` years ago) that still has an unconsumed
|
||||
balance drops out instead of carrying forward, and is reported as
|
||||
`expiring` for that year.
|
||||
`expiring_age` for that year.
|
||||
- Separately, whatever of `left` cannot be absorbed by `carried_out`
|
||||
(days above `earned - floor` with nowhere to bank — including an
|
||||
unused floor itself, when usage is low) is `expiring_over_cap` for
|
||||
that year.
|
||||
- `expiring` is `expiring_age + expiring_over_cap`: EVERY vacation day
|
||||
lost at this year's end, however it was lost. Earlier versions of this
|
||||
field counted only the age-cap portion, so a family could lose days
|
||||
purely to the over-cap cause (e.g. earned 30, floor 20, used 15 →
|
||||
5 days over-cap lost) while the dashboard showed "Expiring 0" — that
|
||||
silent gap is why the breakdown exists now.
|
||||
|
||||
`saved_days_in` on a `vacation_days` row is normally only used to *seed*
|
||||
the bank for the first year of the series (year-earned = year − 1, since
|
||||
@@ -416,7 +435,7 @@ class AnalyticsService:
|
||||
# Entries still valid to carry into next year vs. ones in their
|
||||
# final valid year (expire now if any balance survived).
|
||||
old_carry_forward = sum(c for ye, c in bank if ye >= year - (carry_years - 1))
|
||||
expiring = sum(c for ye, c in bank if ye == year - carry_years)
|
||||
expiring_age_raw = sum(c for ye, c in bank if ye == year - carry_years)
|
||||
bank = [e for e in bank if e[0] >= year - (carry_years - 1)]
|
||||
|
||||
current_year_saveable = max(earned - floor, 0)
|
||||
@@ -425,6 +444,18 @@ class AnalyticsService:
|
||||
if new_entry_amount > 0:
|
||||
bank.append([year, new_entry_amount])
|
||||
|
||||
# `left` minus what actually got banked is EVERY day lost this
|
||||
# year — from both causes. `expiring_age_raw` (bank entries that
|
||||
# just hit the age cap) is a subset of that total by construction
|
||||
# (those days are inside `left` and excluded from `carried_out`
|
||||
# via `old_carry_forward`), but clamp it defensively so the two
|
||||
# portions can never sum to more than `total_lost` in an edge
|
||||
# case (e.g. `left` itself clamped to 0 by the `max(..., 0)`
|
||||
# above, when planned_use outstrips earned + carried_in).
|
||||
total_lost = max(left - carried_out, 0)
|
||||
expiring_age = min(expiring_age_raw, total_lost)
|
||||
expiring_over_cap = total_lost - expiring_age
|
||||
|
||||
result.append(
|
||||
VacationYearBalance(
|
||||
year=year,
|
||||
@@ -434,7 +465,9 @@ class AnalyticsService:
|
||||
planned_use=planned_use,
|
||||
left=left,
|
||||
carried_out=carried_out,
|
||||
expiring=expiring,
|
||||
expiring=total_lost,
|
||||
expiring_age=expiring_age,
|
||||
expiring_over_cap=expiring_over_cap,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user