/** * 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 = { '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) }