Files
F-r-ldraledigplannerare/frontend/src/utils/rejectionMessages.ts
T
jacobh2 8e1cf3524c Lead the overview with the days about to be lost
The page opened with generic tiles and a bar reading forty-eight of ninety-six
weeks, which was neither weeks nor connected to the age-four ceiling it appeared
to describe. It is replaced by one headline and a bar per parent, in days,
against each parent's own savable limit as the server reports it.

The headline has two framings on the same arithmetic. Before anything is
planned it says what there is to plan and what will be lost without a plan,
because on the first day almost the whole entitlement is technically at risk
and saying so bluntly reads as an accusation. Once a plan exists it warns about
the days genuinely at stake.

A timeline of the years to the fourth birthday answers what the next four years
look like, which a month-at-a-time calendar cannot. Shortfalls, surplus advice
and balances sit below the fold, read-only: applying is automatic now, so a
second apply button would be a way to get two different answers.

Rejection messages move to one shared place that resolves a parent's name, so
an identifier can no longer surface in their place.
2026-09-17 21:32:15 +02:00

62 lines
3.2 KiB
TypeScript

/**
* Single source of truth for rendering a `Rejection` (§3.5 — a backend
* `message_key` + opaque `data` bag, same idiom as `Tip`/`PlanWarning`) in
* the caller's active locale.
*
* Every rejection `message_key` `POST /api/plan/apply` and
* `POST /api/plan/sync` (`app/services/planning_service.py`) can return,
* including `insufficientVacationBalance`
* (`data: {parent_personal_data_uuid, year, needed, remaining}`).
*
* Previously duplicated between `PlanFold.tsx` and
* `frontend/src/components/planning/PlanResults.tsx` (the latter scheduled
* for deletion, and missing `insufficientVacationBalance` entirely). Moved
* here so there is exactly one copy, and so the `/api/plan/sync` toast
* (`Calendar.tsx`) can render rejections too:
*
* import { renderRejection } from '@/utils/rejectionMessages'
* renderRejection(rejection, t, members)
*
* where `t` is `useTranslation()`'s translate function and `members` is the
* workspace's `PersonalData[]` (e.g. from `useWorkspaceMembers`).
*/
import type { PersonalData, Rejection } from '@/types'
import { toTranslationParams, type TranslationKey, type TranslationParams } from '@/i18n'
import { nameResolver } from '@/utils/parentNames'
export type Translate = (key: TranslationKey, params?: TranslationParams) => string
const REJECTION_KEYS: Record<string, TranslationKey> = {
'planApply.rejection.invalidDayType': 'planResults.rejectionInvalidDayType',
'planApply.rejection.conflictingDuplicate': 'planResults.rejectionConflictingDuplicate',
'planApply.rejection.collisionsNeedReplace': 'planResults.rejectionCollisionsNeedReplace',
'planApply.rejection.dubbeldagWindowExceeded': 'planResults.rejectionDubbeldagWindowExceeded',
'planApply.rejection.dubbeldagQuotaReached': 'planResults.rejectionDubbeldagQuotaReached',
'planApply.rejection.dubbeldagInvalid': 'planResults.rejectionDubbeldagInvalid',
'planApply.rejection.dubbeldagBatchQuotaExceeded': 'planResults.rejectionDubbeldagBatchQuotaExceeded',
'planApply.rejection.outsideEligibleWindow': 'planResults.rejectionOutsideEligibleWindow',
'planApply.rejection.insufficientBalance': 'planResults.rejectionInsufficientBalance',
'planApply.rejection.insufficientVacationBalance': 'planResults.rejectionInsufficientVacationBalance',
}
/** Renders one rejection, never leaking `data.parent_personal_data_uuid` as
* a raw uuid: whenever that field is present, it's resolved to a display
* name via `members` (falling back to `planResults.unknownParent` rather
* than the uuid itself, e.g. while members are still loading or for a
* removed member) and added to the template params as `parent_name`. An
* unrecognised `messageKey` falls back to showing the raw key, same idiom
* as `TipCard.tsx`'s `TIP_TRANSLATION_KEYS`.
*/
export function renderRejection(rejection: Rejection, t: Translate, members: PersonalData[]): string {
const key = REJECTION_KEYS[rejection.messageKey]
if (!key) return rejection.messageKey
const params = toTranslationParams(rejection.data)
const uuid = rejection.data.parent_personal_data_uuid
if (typeof uuid === 'string') {
const resolve = nameResolver(members)
const name = resolve(uuid)
params.parent_name = name === uuid ? t('planResults.unknownParent') : name
}
return t(key, params)
}