refactor: plan timeframes use a per-day-of-week picker (like the V2 tariff)

The timeframes model was a coarse weekday/weekend split, which couldn't express
"open Saturdays" or different rules on a specific day — and it didn't match the
V2 tariff, which already has a proper per-day-of-week picker (Hën–Die).

Replace PlanTimeframes { weekday, weekend } with { days[], fromMin, toMin }: the
allowed window applies only on the selected days (0=Sun..6=Sat; empty = every
day); on unselected days the subscriber parks free. A "night plan, free
weekends" is just days [Mon..Fri] with a 20:00→08:00 window — the exact case
from before, now expressible alongside any other day combination.

outOfWindowGap reworked to the days model (per-day membership test instead of
the weekend helper); the plans editor reuses the tariff composer's Mon-first
checkbox row and the shared tariff.dow0..6 labels. No production plans carry
timeframes yet (feature shipped today), so the shape changed directly with no
migration. Unit tests updated + extended (Saturday-only, every-day, weekday
night); 81 shared tests pass. Build+lint 12/12.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-20 18:43:21 +02:00
parent 21bd0f6227
commit e0e218fa61
8 changed files with 114 additions and 98 deletions
+6 -3
View File
@@ -92,9 +92,12 @@ subscriber may park (e.g. weekday allowed 20:00→08:00, weekend all-day). Inste
out-of-window scans, the system **charges the out-of-window minutes at the normal transient
[[tariff]]** — the subscriber becomes a transient customer for the time outside their window:
- `PlanTimeframes` = per day-type `DayWindow` ({ allDay | fromMin, toMin } minutes-of-local-midnight;
`toMin ≤ fromMin` wraps past midnight for a night window) + `graceMin` + the site `tz` (frozen in
the plan version, like a V2 tariff's tz). null timeframes = 24/7, no charge ever.
- `PlanTimeframes` = `{ days[], fromMin, toMin, graceMin?, tz }`. The allowed window
`[fromMin, toMin)` (minutes-of-local-midnight; `toMin ≤ fromMin` wraps past midnight for a night
window) applies ONLY on the selected **`days`** (0=Sun..6=Sat — the **same per-day-of-week picker as
the V2 [[tariff]]**, Hën–Die; empty = every day). On a day NOT in the set the subscriber parks free.
`tz` is frozen in the plan version (like a V2 tariff's tz). null timeframes = 24/7, no charge ever.
*(A "night plan, free weekends" is just `days:[Mon..Fri], 20:00→08:00`.)*
- `outOfWindowGap(timeframes, tz, at, edge)` (pure, tz-aware, unit-tested in `@parking/shared`)
returns the `[start, end]` portion outside the window. **Early entry**: gap = arrival → next
window-open (a 09:00 arrival to a 20:00 window owes 09:00→20:00, capped by the tariff's daily cap).