diff --git a/frontend/src/i18n/en/calendar.ts b/frontend/src/i18n/en/calendar.ts index 52298de..38195bb 100644 --- a/frontend/src/i18n/en/calendar.ts +++ b/frontend/src/i18n/en/calendar.ts @@ -80,6 +80,10 @@ export const calendar = { '{name} ({start}–{end}): none of the {weeksWished} week(s) requested could be funded — {shortfallDays} day(s) short', 'calendar.toastShortfallPartial': '{name} ({start}–{end}): only {weeksFunded} of {weeksWished} week(s) funded — {shortfallDays} day(s) short', + 'calendar.toastShortfallNoneDays': + '{name} ({start}–{end}): none of the {daysWished} day(s) requested could be funded — {shortfallDays} day(s) short', + 'calendar.toastShortfallPartialDays': + '{name} ({start}–{end}): only {daysFunded} of {daysWished} day(s) funded — {shortfallDays} day(s) short', 'calendar.undoButton': 'Undo', 'calendar.undoError': 'Failed to undo the change', 'calendar.dismissToastAria': 'Dismiss the message', diff --git a/frontend/src/i18n/sv/calendar.ts b/frontend/src/i18n/sv/calendar.ts index 0c2e9fb..ba2e208 100644 --- a/frontend/src/i18n/sv/calendar.ts +++ b/frontend/src/i18n/sv/calendar.ts @@ -80,6 +80,10 @@ export const calendar = { '{name} ({start}–{end}): inget av de {weeksWished} önskade veckorna kunde finansieras — {shortfallDays} dag(ar) saknas', 'calendar.toastShortfallPartial': '{name} ({start}–{end}): endast {weeksFunded} av {weeksWished} veckor finansierades — {shortfallDays} dag(ar) saknas', + 'calendar.toastShortfallNoneDays': + '{name} ({start}–{end}): inget av de {daysWished} önskade dagarna kunde finansieras — {shortfallDays} dag(ar) saknas', + 'calendar.toastShortfallPartialDays': + '{name} ({start}–{end}): endast {daysFunded} av {daysWished} dagar finansierades — {shortfallDays} dag(ar) saknas', 'calendar.undoButton': 'Ångra', 'calendar.undoError': 'Det gick inte att ångra ändringen', 'calendar.dismissToastAria': 'Stäng meddelandet', diff --git a/frontend/src/pages/Dashboard.test.tsx b/frontend/src/pages/Dashboard.test.tsx index 72b21f8..2c08f58 100644 --- a/frontend/src/pages/Dashboard.test.tsx +++ b/frontend/src/pages/Dashboard.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' -import { render, screen } from '@testing-library/react' +import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { BrowserRouter } from 'react-router-dom' import { Dashboard } from './Dashboard' @@ -159,6 +159,49 @@ describe('Dashboard', () => { expect(screen.queryByTestId('dashboard')).not.toBeInTheDocument() }) + it('refetches workspace members once onboarding hands off (children goes from empty to non-empty)', async () => { + // Regression: a couple's partner is added mid-onboarding via a direct + // `api.addPartner` call, invisible to this component's own + // `useWorkspaceMembers` instance (fetched once, on mount, back when + // there was only one member). `Onboarding` only ever refreshes + // `WorkspaceContext` — flipping `children` from empty to non-empty is + // the only externally-observable signal this component gets that the + // member list it already fetched might now be stale. + const refreshMembers = vi.fn().mockResolvedValue(undefined) + vi.mocked(hooks.useWorkspaceMembers).mockReturnValue( + makeWorkspaceMembersResult({ refresh: refreshMembers, data: [] }), + ) + const soloWorkspace = makeWorkspaceContextValue({ + workspace: { id: 'ws-1', name: 'Test Workspace', planningMode: 'couple', createdAt: '2026-01-01' }, + workspaceId: 'ws-1', + children: [], + selectedChildId: null, + personalDataUuid: 'pd-1', + parentName: 'Test Parent', + }) + vi.mocked(useWorkspace).mockReturnValue(soloWorkspace) + + const { rerender } = renderWithRouter() + expect(screen.getByTestId('onboarding-page')).toBeInTheDocument() + expect(refreshMembers).not.toHaveBeenCalled() + + // Onboarding finishes: `WorkspaceContext.children` flips non-empty. + vi.mocked(useWorkspace).mockReturnValue( + makeWorkspaceContextValue({ + ...soloWorkspace, + children: [{ id: 'c-1', name: 'Child 1', birth_date: '2026-01-01' }], + selectedChildId: 'c-1', + }) + ) + rerender( + + + , + ) + + await waitFor(() => expect(refreshMembers).toHaveBeenCalledTimes(1)) + }) + describe('estimated-figures badge', () => { it('is hidden once real parental-leave balances have been fetched (the default mock)', () => { renderWithRouter() diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 36d4f6f..5f050f2 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useState } from 'react' +import { ReactNode, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { useWorkspace } from '@/context' import { @@ -47,7 +47,7 @@ export function Dashboard(): ReactNode { ) const { data: plannedDays, isLoading: pdLoading } = usePlannedDays(selectedChildId ?? undefined) const { data: parentalLeave, refresh: refreshParentalLeave } = useParentalLeave(personalDataUuid, selectedChildId ?? null) - const { data: members, isLoading: membersLoading } = useWorkspaceMembers(workspace?.id ?? null) + const { data: members, isLoading: membersLoading, refresh: refreshMembers } = useWorkspaceMembers(workspace?.id ?? null) const { data: tips, dismiss: dismissTip } = useOptimizationTips(workspace?.id ?? null) const scraper = useScraperFetch() @@ -56,6 +56,23 @@ export function Dashboard(): ReactNode { const [isManualFetchSaving, setIsManualFetchSaving] = useState(false) const [manualFetchError, setManualFetchError] = useState(null) + // `useWorkspaceMembers` fetches once per `workspace.id`, which never + // changes across onboarding — so a couple's partner, added mid-wizard via + // `Onboarding.tsx`'s own direct `api.addPartner` call, is invisible to + // this hook's already-fetched `data` until something explicitly refetches + // it. `Onboarding` only refreshes `WorkspaceContext` (which flips + // `children` from empty to non-empty) when it finishes — it has no way to + // reach into this component's `useWorkspaceMembers` instance. Watching + // that same empty-to-non-empty transition here is what tells this + // component "onboarding just finished, the members list might be stale." + const wasAwaitingFirstChildRef = useRef(children.length === 0) + useEffect(() => { + if (wasAwaitingFirstChildRef.current && children.length > 0) { + void refreshMembers() + } + wasAwaitingFirstChildRef.current = children.length === 0 + }, [children.length, refreshMembers]) + const isLoading = wsLoading || analyticsLoading || pdLoading if (isLoading) {