/** * Folds the wished-vs-funded/surplus/balance content that used to live only * on the Plan tab's `PlanResults.tsx` (since deleted) into Översikt, * collapsed below the fold. This is now the sole implementation of that * content — a trimmed-down component, not a shared import. * * Read-only. The calendar's `POST /api/plan/sync` (see `Calendar.tsx`) is * now the only way a plan is written — it proposes and persists in one * step, so there is no separate "propose, review, then apply" flow left * anywhere in the app, and this fold never mutates anything. It still calls * `usePlanProposal` (`POST /api/plan/propose`) to get the wished-vs-funded * figures, surplus advice and balance tables below, because that data has * no other source: there is no GET endpoint for "the current plan" shaped * like a proposal, and `/api/plan/propose` is documented read-only — * "persists nothing, safe to spam" (`backend/app/routers/plan.py`). * * Dropped relative to the old `PlanResults.tsx` (content not duplicated here * since the Hero and `Age4Bars` already cover it): the warnings section, the * age4Projections table, and the per-window explanation/remedy detail cards. * Also dropped, as of the move to `/api/plan/sync`: the apply button, * collision-confirmation UI, and rejection rendering — applying is no * longer a thing this fold does (see `frontend/src/utils/rejectionMessages.ts` * for where rejection rendering now lives, for the sync toast in * `Calendar.tsx`). */ import { ReactNode, useEffect } from 'react' import type { PersonalData, SurplusAdvice, WindowOutcome } from '@/types' import { useLeaveWishes, usePlanProposal } from '@/hooks' import { useTranslation, toTranslationParams, type TranslationKey, type TranslationParams } from '@/i18n' import { nameResolver } from '@/utils/parentNames' import { resolveWishName } from '@/utils/wishName' type Translate = (key: TranslationKey, params?: TranslationParams) => string // §9.2 surplus-advice kinds — originally mirrored the deleted `PlanResults.tsx`'s // `SURPLUS_ADVICE_KEYS`/`SURPLUS_ADVICE_BODY_KEYS`; this is now the sole copy // (see the file banner above). const SURPLUS_ADVICE_KEYS: Record = { wish_more_leave: 'planResults.surplusAdviceWishMoreLeave', shift_mix_to_fp: 'planResults.surplusAdviceShiftMixToFp', front_load_early_years: 'planResults.surplusAdviceFrontLoadEarlyYears', use_less_vacation: 'planResults.surplusAdviceUseLessVacation', } const SURPLUS_ADVICE_BODY_KEYS: Record = { 'planSurplusAdvice.wish_more_leave': 'planResults.surplusAdviceBodyWishMoreLeave', 'planSurplusAdvice.shift_mix_to_fp': 'planResults.surplusAdviceBodyShiftMixToFp', 'planSurplusAdvice.front_load_early_years': 'planResults.surplusAdviceBodyFrontLoadEarlyYears', 'planSurplusAdvice.use_less_vacation': 'planResults.surplusAdviceBodyUseLessVacation', } function nameForSafe(members: PersonalData[], t: Translate): (uuid: string) => string { const resolve = nameResolver(members) return (uuid: string) => { const name = resolve(uuid) return name === uuid ? t('dashboard.planFold.unknownParent') : name } } function renderSurplusAdviceBody(advice: SurplusAdvice, t: Translate, nameFor: (uuid: string) => string): string { const key = SURPLUS_ADVICE_BODY_KEYS[advice.messageKey] if (!key) return advice.messageKey const params = toTranslationParams(advice.data) const uuid = advice.data.parent_personal_data_uuid if (typeof uuid === 'string') { params.parent_name = nameFor(uuid) } params.days_involved = advice.daysInvolved return t(key, params) } function groupByYear(outcomes: WindowOutcome[]): Array<{ year: number; outcomes: WindowOutcome[] }> { const years = Array.from(new Set(outcomes.map((o) => o.year))).sort((a, b) => a - b) return years.map((year) => ({ year, outcomes: outcomes.filter((o) => o.year === year).sort((a, b) => a.startDate.localeCompare(b.startDate)), })) } export interface PlanFoldProps { workspaceId: string members: PersonalData[] } export function PlanFold({ workspaceId, members }: PlanFoldProps): ReactNode { const { t } = useTranslation() const { data: wishes, isLoading: wishesLoading } = useLeaveWishes(workspaceId) const { data: proposal, isLoading: isProposing, error: proposalError, propose } = usePlanProposal(workspaceId) const nameFor = nameForSafe(members, t) // Auto-propose once wishes are available — this section is read-only-by- // default (collapsed), so nothing should require the user to first find // and press a separate "suggest" button just to see it. `propose` calls // the read-only `/api/plan/propose` endpoint — it persists nothing. useEffect(() => { if (!wishesLoading && wishes.length > 0 && proposal === null && !isProposing) { void propose() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [wishesLoading, wishes.length, proposal]) return (
{t('dashboard.planFold.summary')}
{!wishesLoading && wishes.length === 0 && (

{t('dashboard.planFold.noWishesHint')}

)} {proposalError && (
{proposalError}
)} {proposal && ( <>

{t('planResults.yearOverviewTitle')}

{proposal.outcomes.length === 0 ? (

{t('planResults.noWindowsInHorizon')}

) : ( {groupByYear(proposal.outcomes).map(({ year, outcomes }) => ( ))}
{t('planResults.yearHeader')} {t('planResults.leavesHeader')}
{year} {outcomes.map((outcome) => ( {resolveWishName({ name: outcome.wishName, nameMessageKey: outcome.wishNameMessageKey }, t)} : {outcome.weeksFunded}/{outcome.weeksWished} {t('planResults.weeksUnit')} ))}
)}
{proposal.surplusAdvice.length > 0 && (

{t('planResults.surplusAdviceTitle')}

{t('planResults.surplusAdviceIntro')}

    {proposal.surplusAdvice.map((advice, idx) => { const titleKey = SURPLUS_ADVICE_KEYS[advice.kind] return (
  • {titleKey ? t(titleKey) : advice.kind}

    {renderSurplusAdviceBody(advice, t, nameFor)}

  • ) })}
)}

{t('planResults.balancesTitle')}

{proposal.poolBalances.map((pool) => ( ))}
{t('planResults.parentHeader')} {t('planResults.sjukpenningLeftHeader')} {t('planResults.lagstaLeftHeader')} {t('planResults.dubbeldagarLeftHeader')}
{nameFor(pool.parentPersonalDataUuid)} {pool.sjukpenningLeft} {pool.lagstaLeft} {pool.dubbeldagarLeft}
{proposal.vacationBalances.map((budget) => ( ))}
{t('planResults.parentHeader')} {t('planResults.yearHeader')} {t('planResults.usedHeader')} {t('planResults.totalAvailableHeader')}
{nameFor(budget.parentPersonalDataUuid)} {budget.year} {budget.alreadyUsed} {budget.totalDays + budget.savedDaysIn}
)}
) }