8e1cf3524c
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.
62 lines
3.2 KiB
TypeScript
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)
|
|
}
|