Commit Graph

18 Commits

Author SHA1 Message Date
jacobh2 e22007dc50 Detect wish save failures directly instead of inferring them
Plan.tsx's handlers swallowed their rejections and onUpdate was typed to
return void, so WishEditor could not observe a failed save. It inferred
the outcome instead -- comparing the draft it sent against the next
wishes prop, and reading "isSaving went false without a match" as
failure. That coupled save status to refetch timing and could not tell a
network error from a slow refresh.

The handlers now return their promise, onUpdate accepts
`void | Promise<void>` so void-returning callers still typecheck, and
WishEditor awaits and catches the real outcome. The inference machinery
(sentDraftRef, prevIsSavingRef and the comparison effect) is removed
rather than left alongside, since two sources of truth can disagree.

The save/refetch/save loop guard is untouched: it lives in the draft
reseed effect, which only fills a row that has no draft yet, and saves
only ever originate from an explicit onChange. Added a comment saying so.

A failed row now keeps its edit and offers an explicit retry, rather than
requiring another edit to retrigger the debounce. Per-row failures report
inline only -- reporting them in the page banner too would double-report
the same error.

Also removes wishEditor.saveButton/savingButton, dead since the per-row
Save button was replaced by auto-save.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
2026-09-14 22:51:15 +02:00
jacobh2 8c2e5dee1b Distinguish the Dashboard and Plan age-4 figures
Both screens showed an "expiring at age 4" number under near-identical
labels, and they disagreed -- 143 on the Dashboard versus 100 on the Plan
tab for the same parent. Both were correct: the Dashboard reflects the
saved plan, the Plan tab a proposal that has not been applied and which
simulates future recurring wishes.

The numbers are left untouched; forcing them to agree would make one of
them wrong. Instead each label now states its own basis ("om nuvarande
plan följs" / "om detta förslag tillämpas"), and the Plan tab's intro
says outright that the saved plan may differ, and why.

Tests assert the distinguishing copy on both screens so the two cannot
silently drift back to indistinguishable labels.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
2026-09-14 22:32:54 +02:00
jacobh2 dc4c0a5edd Show preset wish names in the current UI language
Preset wishes were created with `name: t(preset.labelKey)`, so the
translated string was persisted as the wish's name. A wish created while
the UI was Swedish stayed "Jul" in English forever -- the Swedish text
was the data, so no display-layer change could fix it.

Wish names are user-editable free text, so `name` cannot simply be
translated at render time. Migration 007 adds a nullable
`leave_wish.name_message_key` recording where a name came from: preset
wishes store the i18n key alongside the translated literal, and the UI
renders the key when present.

The key is cleared server-side the moment `name` actually changes,
compared against the stored value rather than trusting the caller, so no
client can forget to clear it and silently revert a user's own name to a
preset label.

An unknown key (a stale row, a renamed key) falls back to the stored
literal via an isTranslationKey guard, so a bad key can never crash the
page or leak a raw key to the user. Rows predating this column keep
displaying their stored name.

_FULL_SCHEMA in db/connection.py is updated in step with the migration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
2026-09-14 22:32:54 +02:00
jacobh2 c5481a05f0 Raise frontend branch coverage from 86.84% to 93.78%
Branches were the weakest metric and had the least headroom above the
85% floor. These are tests for decision paths that were genuinely
unexercised, not padding.

The largest gaps were systemic rather than per-file: the
`err instanceof Error ? ... : fallback` branch was untested across eight
data hooks, useVacationDays had no dedicated test file at all, and
Calendar.tsx's missing-workspace/child/personalData fallbacks were never
hit.

Left deliberately uncovered, with reasons: `?? fallback` guards whose
fallback cannot occur given how the value is constructed upstream, and
guards behind buttons that are disabled whenever the guard would matter.
Reaching those means calling handlers directly, which tests the test
rather than the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
2026-09-14 18:54:56 +02:00
jacobh2 cf378288f0 Break the Dashboard's age-4 stat down per parent
The Dashboard summed both parents' expiring days into one figure while
the Plan tab already showed them separately. Each parent's own count is
now shown alongside the combined total, which stays and is relabelled as
a sum.

The per-parent numbers were already on the wire as
parentProjections[].expiringAtB4 -- no backend change, and no arithmetic
here. Both figures are rendered exactly as the backend computed them,
since re-deriving them is what previously doubled a parent's entitlement
(87b3eb5).

A single-parent workspace shows only the combined figure, where a
breakdown would just repeat it.

nameResolver moves out of PlanResults.tsx into src/utils/parentNames.ts
so both screens resolve uuid -> name the same way. PlanResults.test.tsx
passes untouched, which is the evidence the extraction preserved
behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
2026-09-14 18:54:50 +02:00
jacobh2 f685b739f8 Add a custom bounding range for flexible wishes, and auto-save edits
Flexible wishes could only be bounded by one of four season presets, a
deliberate v1 scope cut. They can now take any month/day range, including
one that wraps the year end the way the jul preset does. The range stays
a search bound -- the engine still chooses the placement inside it at
propose time.

February is capped at 28 days rather than 29: the backend builds a
concrete date(year, month, day) per anchor year, so a Feb 29 bound would
raise in a non-leap year.

Per-row Save buttons are replaced by a debounced auto-save (800ms), which
is what prompted this: clicking save on each individual field was the
reported annoyance. A draft that has not actually changed never saves, so
a refetch cannot trigger a further save -- the save/refetch/save loop is
guarded explicitly and has a regression test. An invalid draft (bounding
range shorter than the wish) holds the edit locally and shows why rather
than saving.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
2026-09-14 18:54:43 +02:00
jacobh2 28c632ca36 Fix calendar window overshooting the cached year range
Found by probing the running API; both suites were green and missed it.

CalendarService validated only start_year against the cache, never the window
end. GET /api/calendar/days?start_year=2035&years=4 returned 200 with
end_year=2038, pulling 2036-2038 from the live holidays-library fallback in
app/utils/holidays.py. That fallback does not know the de facto free days, so
Julafton, Nyarsafton and Midsommarafton were reported as ordinary working days
(18 red days per cached year vs 13 per uncached year). Julafton being charged
as a working day was fixed once before; it had reappeared past 2035.

- clamp end_year to max_year; the response now always reports what it returned
- add strict=True to get_swedish_holidays/get_red_days plus UncachedYearError,
  checked before the memo cache, so a strict caller fails loudly rather than
  silently degrading. Non-strict default behaviour is unchanged.
- RedDaysList derived endYear locally as startYear + years - 1, independent of
  the response, so a clamped window rendered phantom empty years. It now takes
  endYear from the backend.

Tests: clamping at and before max_year (service + router), Julafton/Nyarsafton/
Midsommarafton red for every cached year, red-day count consistent across the
served range, and strict-mode raise/pass-through.

Backend 404 -> 414 tests, frontend 704 -> 705.

Known, deliberately not fixed here: AllocationInputLoader builds its holiday map
the same non-strict way over the planning horizon, so it has the same exposure
once the horizon passes 2035. Clamping would silently truncate planning and
raising would break real plans, so it needs a product decision first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
2026-09-14 02:46:47 +02:00
jacobh2 76b1e712b9 Complete NextSteps roadmap: planning engine UI, calendar cluster, flexible wishes
Works through every numbered item in NextSteps260913.md section 3, plus the
three known verification issues in section 4.1.

Surface what the backend already computed:
- 3.1 render surplus_advice in PlanResults, mirroring the shortfall remedies
- 3.2 surface VacationBalance.is_assumed so assumed statutory years are visible
- 3.3 age-4 expiry given the proposed plan, via a new Age4Projection reusing
  the existing _age4_projections arithmetic (Dashboard stat verified, not rebuilt)

Calendar cluster (one pass over the holiday data):
- new GET /api/calendar/days exposing roda dagar, klamdagar and day overrides,
  paginated four years at a time
- 3.4 shade roda dagar/klamdagar, with R/K badges so colour is not the only signal
- 3.6 click-to-override behind an explicit mode toggle, leaving drag multi-select intact
- 3.7 click month/year to advance
- 3.8 per-year red-days list in Settings, statutory days plus overrides

i18n:
- 3.5 five backend-generated Swedish surfaces converted to message_key + data,
  following the tips_service pattern; old string fields replaced, not kept

Flexible wishes (3.9):
- PLANNING_ENGINE_SPEC section 10: window_mode, candidate search, cost function,
  deterministic tie-break, two-pass ordering, test matrix rows 15-24
- backend: entity, migration 006, _FULL_SCHEMA sync, repository, allocator
- frontend: season-preset toggle in WishEditor, unplaced outcome rendering

Fixes (4.1):
- pin typecheck to the local tsc rather than npx resolving a newer TypeScript
- ignore generated coverage/ and dist/ in the ESLint flat config
- fix the flaky usePlanApply test properly, with waitFor rather than a retry

Tests: backend 361 -> 404, frontend 540 -> 704. Coverage 89.47% -> 96.52% lines.

Not yet verified in a browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
2026-09-14 02:33:03 +02:00
jacobh2 dead5e4897 Update calendar with multi-select 2026-06-14 22:20:32 +02:00
jacobh2 566595984f Styling fix and scraper bug fix 2026-06-14 18:39:55 +02:00
jacobh2 6ed2a3ae0c Fix calendar style 2026-06-14 13:05:53 +02:00
jacobh2 4924982a8a test(calendar): grid layout, week ordering, and day-cell coverage
- Add isToday test cases to CalendarDay.test.tsx
  * test --today class when isToday=true
  * test --today class not applied when isToday=false
- Create Calendar.test.tsx with full page coverage
  * Test 7 weekday headers in Monday-first Swedish order
  * Verify correct number of day cells (multiple of 7)
  * Test Sunday-starting month renders correctly (March 2026)
  * Test modal opens on current-month day click
  * Test modal doesn't open on disabled day click
  * Test current month title display
  * Test prev/next month navigation
  * Test loading state display
- Mocks useWorkspace, usePlannedDays, Layout, LoadingSpinner, PlannedDayModal
- All 19 tests pass; no coverage regression

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-14 12:51:58 +02:00
jacobh2 a2300b285d fix(calendar): Monday-first Swedish week, today highlight, default to current month
- Convert week calculation to Monday-first (startOffset = (firstDay.getDay() + 6) % 7)
- Update weekday headers to Swedish labels: [Mån, Tis, Ons, Tor, Fre, Lör, Sön]
- Change default currentMonth from June 2026 to current date (new Date())
- Add isToday prop to CalendarDay component
- Add --today modifier to highlight current date with blue circle background
- Pass computed isToday boolean to CalendarDay when date equals today's date string
- Renders correctly for Sunday-starting months (e.g., March 2026)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-14 12:51:53 +02:00
jacobh2 772d853e37 MVP frontend+backend 2026-06-14 09:58:30 +02:00
jacobh2 719da83827 Phase 3: Wire page stubs into working screens with full integration
Implemented 4 page stubs with hooks and components:
- Dashboard: displays overview metrics, planned days breakdown, progress chart, break periods
- Calendar: month-based calendar grid with event badges and navigation
- Tips: list of optimization tips with dismiss functionality
- Settings: workspace configuration with child info, parent profile, vacation days, break periods

Added utility exports:
- src/context/index.ts: re-exports useAuth, useWorkspace hooks
- src/hooks/index.ts: re-exports all domain hooks

All pages:
- Use Layout wrapper with header configuration
- Load data via hooks with loading/error states
- Render presentational components (no business logic)
- Pass callbacks for user actions
- Type-safe with full @types coverage

Frontend test coverage: 167 tests, all passing
TypeScript: zero errors, lint: zero errors

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-01 17:53:10 +02:00
jacobh2 425e29ea21 Phase 2: Build 9 presentational components with full test coverage
New components (all with .tsx + .test.tsx, 90%+ coverage):
- CalendarDay: day cell with event badges
- CalendarEvent: colored event chip with dismiss option
- DaysBreakdown: table of days by type
- ProgressChart: progress bar toward age-4 deadline
- TipCard: optimization tip card with severity badges
- VacationDaysInput: number input with min/max validation
- ParentProfileInput: dual selects for salary type & employment status
- PlanningGoalSelector: radio buttons for planning goal
- BreakPeriodsInput: list manager with inline add/edit form

All components follow existing patterns from Header/Footer:
- Typed props interfaces (exported)
- Semantic HTML with data-testid & ARIA attributes
- No business logic, purely presentational
- Comprehensive test coverage including edge cases

npm test: 22 files, 167 tests passing
npx tsc --noEmit: clean

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-01 17:50:23 +02:00
jacobh2 fbb7a04580 Phase 1: Fix TypeScript compilation — add missing API functions and fix hook signatures
- Add 8 missing API client functions (getBreakPeriods, createBreakPeriod, deleteBreakPeriod, getTips, getParentalLeave, getPlannedDays, createPlannedDay, deletePlannedDay)
- Fix useAnalytics to accept optional childId parameter
- Fix ApiError strict optional properties handling
- Export HeaderProps from Header component
- Update hook tests to use correct data structures and optional parameters

npx tsc --noEmit now passes with zero errors.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-01 17:47:17 +02:00
jacobh2 53d157535a Initial commit 2026-06-01 16:48:21 +02:00