Update todo by human

This commit is contained in:
2026-09-21 07:30:17 +02:00
parent 782415122d
commit 4e4766d54e
2 changed files with 156 additions and 2 deletions
@@ -0,0 +1,154 @@
# Fix number-input clearing UX + drop the redundant preschool estimate field
## Context
Two problems in Settings, both reported by the user:
1. **Clearing a number input traps you at `0`.** The per-year "Planned days" input
(`Settings.tsx:186-192`) keeps its draft as a `number`, and `Number('') === 0`, so
blanking the box writes a literal `0` to the server and snaps the display to `0`.
Typing `5` then produces `05`. The same hazard exists in `VacationDaysTable` and
`PeriodPopover`. `VacationDaysInput` has the mirror bug: it silently rejects a
non-numeric value, so the field can't be blanked to retype at all.
2. **Two preschool controls look duplicated.** "Preschool planning days per year"
(the child-level estimate, `Child.preschoolPlanningDaysPerYear`) sits directly above
the "Planning days per year" heading for the per-year rows. They are not duplicates —
the estimate seeds each year row and is the allocator's fallback for years with no
saved row (`backend/app/services/allocation_service.py:545-573`, `:2985-3001`) — but
the UI gives no hint of that, so it reads as dead weight.
**Decisions taken:** remove the estimate *input* from the UI while keeping the backend
field (default `6`) as the seed/fallback; extract one shared number input and roll it out
to every numeric field that still has the `Number('') === 0` hazard.
**Out of scope (flagged, not fixed):** `backend/app/services/tips_service.py:579` passes
the *global* `settings.rules.preschool_planning_days_per_year` into
`preschool_buffer_needed` instead of the child's own value, so a customised per-child
value is ignored by that tip. Separate bug, separate change.
## 1. New shared component: `frontend/src/components/inputs/NumberField.tsx`
Lift the working pattern already used by the "Days taken" input (`Settings.tsx:199-217`).
```tsx
export interface NumberFieldProps {
value: number
onChange: (value: number) => void
label?: string
min?: number
max?: number
id?: string
className?: string
inputClassName?: string
disabled?: boolean
placeholder?: string
'data-testid'?: string
'aria-invalid'?: boolean
'aria-describedby'?: string
}
```
Behaviour contract:
- Holds an internal **string** draft. `''` is a legal transient state.
- `onChange` fires only when the draft parses to a finite number — **never on empty**, so
the previously saved value survives while the box is blank.
- No clamping while typing (clamping mid-keystroke is what makes small `max` values
unusable). Clamp to `[min, max]` on blur only, and commit the clamped value.
- On blur with an empty or unparseable draft: revert the draft to the current `value`
prop; do not call `onChange`.
- Sync the draft from the `value` prop only while the input is **not focused** (a ref
flag set on focus/blur), so a debounced server round-trip can't overwrite live typing.
- Renders `type="number" inputMode="numeric"` with `min`/`max` attributes and the existing
`field` / `field__control` BEM classes; `label` renders as `field__label` when given.
No hardcoded `id`/`data-testid` — both are props (the bug that makes `VacationDaysInput`
non-reuse-safe today).
Co-located test `NumberField.test.tsx` covering: clear-then-retype yields `5` not `05`;
empty draft fires no `onChange`; blur-empty restores the prior value; blur clamps to
`min`/`max`; external `value` change while focused does not clobber the draft.
## 2. Remove the child-level estimate input
`frontend/src/pages/Settings.tsx`
- Delete the `VacationDaysInput` block (`:562-571`) and its import (`:21`).
- Delete the `preschoolPlanningDays` state (`:252`), its prefill in the `childDetails`
effect (`:285`), and `preschoolPlanningDaysPerYear` from the `updateChildDetails` call in
`handleSaveFamily` (`:366`). The field is optional in `ChildUpdate`
(`backend/app/models/child.py:40`), so omitting it leaves the stored value untouched.
- Pass `defaultPlannedDays={childDetails?.preschoolPlanningDaysPerYear ?? 6}` to
`PreschoolPlanningYearRow` (`:583`) instead of the removed state.
- Move the `settings.preschoolHelp` paragraph (`:563`) into the per-year block, under the
heading, and drop the now-redundant `settings.preschoolPerYearHelp` line or merge the two
into one sentence.
- Retitle the section so it names the subject: `settings.preschoolPerYearTitle` →
`"Preschool planning days"` / `"Förskolans planeringsdagar"`.
`frontend/src/i18n/{en,sv}/settings.ts` — remove `settings.preschoolPlanningDaysLabel`
(`:83` in both), reword `settings.preschoolHelp` (`:82`) so it reads as the intro to the
year rows, and update `settings.preschoolPerYearTitle` (`:126`).
Delete `frontend/src/components/inputs/VacationDaysInput.tsx` and
`VacationDaysInput.test.tsx` — `Settings.tsx:564` was the only usage.
## 3. Roll `NumberField` out
| File | Inputs |
|---|---|
| `frontend/src/pages/Settings.tsx:181-195` | per-year **Planned days** (the reported bug) |
| `frontend/src/pages/Settings.tsx:199-217` | per-year **Days taken** — keep its `null` = "not recorded" sentinel; it needs `value: number \| null` handling, so either give `NumberField` an optional `nullable` mode or leave this one as-is and note why |
| `frontend/src/components/vacation-days/VacationDaysTable.tsx:174-183, 188-197` | `totalDays`, `savedDaysIn` |
| `frontend/src/components/calendar/PeriodPopover.tsx:458-470` | `lastYear` (min 2000 / max 2200 — blanking currently shows `0`) |
`frontend/src/components/modals/ParentBalanceForm.tsx` already handles empty correctly via
`parseField` (`:134`, `value={x ?? ''}`) — **leave it alone**, or migrate it only if
`NumberField` gains the nullable mode above. Do not regress its `null` semantics.
Existing `data-testid`s (`preschool-planned-{year}`, `vacation-total-days-{year}`,
`period-last-year-input`, …) must be preserved so current tests keep targeting the same
elements.
## 4. Tests to update
- `frontend/src/pages/Settings.test.tsx:416-437` — drop the `vacation-days-field` prefill
and the "change to 10 → `updateChildDetails({ preschoolPlanningDaysPerYear: 10 })`"
assertions; assert instead that the family save no longer sends that key and that
year rows still prefill from `childDetails.preschoolPlanningDaysPerYear`.
- `frontend/src/pages/Settings.test.tsx:459-500` — add a clear-then-retype case on
`preschool-planned-{year}` asserting the committed value is `5`, and that clearing alone
does not call `setPlanned`.
- `frontend/src/hooks/useChildSettings.test.ts:100-113` — unchanged (the hook still
supports the field); no edit expected, verify it still passes.
- Backend tests unchanged: no model, service, entity or migration touched.
Mock only with the typed factories in `frontend/tests/fixtures/` and `vi.mocked(fn)` —
no `as any`, no casts (CLAUDE.md anti-patterns).
## Verification
Delegate the full suites to a **Haiku** subagent, one at a time (never in parallel), with
raw verbatim output required:
```bash
# from frontend/
npm run typecheck
npm run lint # zero warnings
npm run coverage # gates: lines 96 | functions 91 | branches 92 | statements 96
```
Backend is untouched, so a backend run is only needed if something unexpected changes there.
Then a **Sonnet** subagent drives Chrome (never the coordinator) against `npm run dev`:
1. Settings → Family panel: confirm the standalone "Preschool planning days per year"
input is gone and the year rows still show the seeded value with the "assumed" badge.
2. In a **Planned days** box: select-all, delete → box goes empty (not `0`); type `5` →
shows `5`, not `05`; blur → autosaves `5`.
3. Blank a box and click away without typing → the previous value returns, nothing saved.
4. Repeat 2-3 on a `VacationDaysTable` row and on the period `lastYear` input.
5. Check the reworded section in **both Swedish and English** — the labels must not clip
(per the bilingual-CSS rule).
Finish with `graphify update .`.
+2 -2
View File
@@ -11,7 +11,7 @@ Following three have a plan (free-days-and-auto-planning-plan.md):
- On every event that updates the days off (i.e. the calendar is making some changes), that should trigger the planner to recalculate the best kinds of time-off for all days.
-> In progress
-> In progress, super long time.....
- Problem with the yearly timelines in general tab
@@ -30,7 +30,7 @@ Following three have a plan (free-days-and-auto-planning-plan.md):
## medium prio
- Update all deprecated things in the backend code.
-> fix-deprecations-backend-plan.md -> in progress
-> fix-deprecations-backend-plan.md -> DONE
## lowest prio