1a69221040
Every funded day the allocator places already carries a machine-readable `reason` code — the constants block in allocation_service.py even says it exists "so the UI can explain itself" — but no user has ever seen one. The calendar showed what was decided, never why, which is the one question this product exists to answer. Clicking a planned day now also raises the existing calendar toast with a plain-Swedish (and English) sentence for that date and parent: a pinned day says the user chose it, an allocator-placed day explains the placement, and anything unrecognised — or a session where no proposal has landed yet — shows nothing rather than a raw code. The reason is deliberately not persisted: it is stripped at apply and `planned_leave_day` has no column for it, so the source is the live in-memory proposal, merged last-writer-wins from Calendar's own sync response and PlanSyncProvider's latest. Before the first sync of a session there is simply no explanation to show. Also extends the backend's field-parity walk to ProposedDay, which it did not cover — the reason could have been dropped from the wire without a single test failing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
37 lines
1.7 KiB
TypeScript
37 lines
1.7 KiB
TypeScript
/**
|
|
* Maps an allocator `reason` code (`ProposedDay.reason` — the
|
|
* `REASON_*` constants in `backend/app/services/allocation_service.py:62-83`,
|
|
* §3.5, "machine-readable so the UI can explain itself") to the
|
|
* `planReasons.<code>` translation key that explains it in the calendar
|
|
* toast (see `Calendar.tsx`'s `handleBandClick`).
|
|
*
|
|
* Follows `frontend/src/utils/rejectionMessages.ts`'s precedent: an
|
|
* exhaustive `Record` from the backend-defined code to a `TranslationKey`,
|
|
* so an unrecognised or empty reason returns `null` rather than ever
|
|
* putting a raw code on screen. Every one of the allocator's 10 reason
|
|
* codes has an entry here even though only the non-free ones are reachable
|
|
* in practice (a free day renders no band, so it can never be clicked) —
|
|
* kept exhaustive so the map can't quietly fall behind the backend's list.
|
|
*/
|
|
import type { TranslationKey } from '@/i18n'
|
|
|
|
const REASON_KEYS: Record<string, TranslationKey> = {
|
|
free_weekend: 'planReasons.free_weekend',
|
|
free_red_day: 'planReasons.free_red_day',
|
|
vacation_wrap_red_day: 'planReasons.vacation_wrap_red_day',
|
|
vacation_wrap_weekend: 'planReasons.vacation_wrap_weekend',
|
|
fp_interior: 'planReasons.fp_interior',
|
|
fp_lagsta_stretch: 'planReasons.fp_lagsta_stretch',
|
|
swap_first_half: 'planReasons.swap_first_half',
|
|
swap_second_half: 'planReasons.swap_second_half',
|
|
dubbeldag: 'planReasons.dubbeldag',
|
|
fp_red_day: 'planReasons.fp_red_day',
|
|
}
|
|
|
|
/** Empty or unrecognised `reason` -> `null`; callers (`Calendar.tsx`) render
|
|
* nothing rather than a raw code or a generic "unknown" string. */
|
|
export function reasonMessageKey(reason: string): TranslationKey | null {
|
|
if (!reason) return null
|
|
return REASON_KEYS[reason] ?? null
|
|
}
|