Redesign the setup flow and consolidate the CSS into one primitives layer

Onboarding asks explicitly for one or two parents and shows one card per
parent for balances; the balance form is per parent with inline
validation; the partner can be filled from the FK deduction or added
from it. Settings and Dashboard reuse the same form.

CSS: bare buttons carry no colour any more; buttons, cards, choice
cards, fields, alerts, badges, eyebrow and modal chrome are defined once
in primitives.css. Media queries live only in responsive.css. A style
guard test blocks hex, !important and stray media queries. Bilingual
fixes: calendar selects size by content, header no longer wraps. Vitest
is capped at four workers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 12:27:29 +02:00
parent e4719cb9ae
commit ae6db7e16f
105 changed files with 5683 additions and 3690 deletions
+107 -6
View File
@@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event'
import { BrowserRouter } from 'react-router-dom'
import { Dashboard } from './Dashboard'
import * as hooks from '@/hooks'
import * as api from '@/services/api'
import { useWorkspace, useAuth } from '@/context'
import {
makeAuthContextValue,
@@ -17,6 +18,8 @@ import {
makeOptimizationTipsResult,
makeLeaveWishesResult,
makePlanProposalResult,
makeParentalLeaveFetch,
makeTierData,
makeTip,
} from '../../tests/fixtures'
@@ -54,7 +57,7 @@ describe('Dashboard', () => {
vi.mocked(useAuth).mockReturnValue(makeAuthContextValue())
vi.mocked(useWorkspace).mockReturnValue(
makeWorkspaceContextValue({
workspace: { id: 'ws-1', name: 'Test Workspace', planningMode: 'solo', createdAt: '2026-01-01' },
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
workspaceId: 'ws-1',
children: [{ id: 'c-1', name: 'Child 1', birth_date: '2022-01-01' }],
selectedChildId: 'c-1',
@@ -123,12 +126,17 @@ describe('Dashboard', () => {
vi.mocked(hooks.useOptimizationTips).mockReturnValue(makeOptimizationTipsResult())
vi.mocked(hooks.useLeaveWishes).mockReturnValue(makeLeaveWishesResult())
vi.mocked(hooks.usePlanProposal).mockReturnValue(makePlanProposalResult())
// Default: nothing saved yet for whoever's balance form gets opened —
// `useLatestBalance` (a real hook, imported directly by Dashboard.tsx to
// bypass the `vi.mock('@/hooks')` above) falls back to the form's
// preset. Tests that care about a saved balance override this.
vi.mocked(api.getParentalLeave).mockResolvedValue(null)
})
it('renders loading spinner initially', () => {
vi.mocked(useWorkspace).mockReturnValue(
makeWorkspaceContextValue({
workspace: { id: 'ws-1', name: 'Test Workspace', planningMode: 'solo', createdAt: '2026-01-01' },
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
workspaceId: 'ws-1',
children: [{ id: 'c-1', name: 'Child 1', birth_date: '2022-01-01' }],
selectedChildId: 'c-1',
@@ -144,7 +152,7 @@ describe('Dashboard', () => {
it('hands off to Onboarding when there are no children yet', () => {
vi.mocked(useWorkspace).mockReturnValue(
makeWorkspaceContextValue({
workspace: { id: 'ws-1', name: 'Test Workspace', planningMode: 'solo', createdAt: '2026-01-01' },
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
workspaceId: 'ws-1',
children: [],
selectedChildId: null,
@@ -170,7 +178,7 @@ describe('Dashboard', () => {
makeWorkspaceMembersResult({ refresh: refreshMembers, data: [] }),
)
const soloWorkspace = makeWorkspaceContextValue({
workspace: { id: 'ws-1', name: 'Test Workspace', planningMode: 'couple', createdAt: '2026-01-01' },
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
workspaceId: 'ws-1',
children: [],
selectedChildId: null,
@@ -349,7 +357,7 @@ describe('Dashboard', () => {
it('disables FK fetch button when no child selected', () => {
vi.mocked(useWorkspace).mockReturnValue(
makeWorkspaceContextValue({
workspace: { id: 'ws-1', name: 'Test Workspace', planningMode: 'solo', createdAt: '2026-01-01' },
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
workspaceId: 'ws-1',
children: [{ id: 'c-1', name: 'Child 1', birth_date: '2022-01-01' }],
selectedChildId: null,
@@ -468,7 +476,7 @@ describe('Dashboard', () => {
it('disables manual fetch button when no child selected', () => {
vi.mocked(useWorkspace).mockReturnValue(
makeWorkspaceContextValue({
workspace: { id: 'ws-1', name: 'Test Workspace', planningMode: 'solo', createdAt: '2026-01-01' },
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
workspaceId: 'ws-1',
children: [{ id: 'c-1', name: 'Child 1', birth_date: '2022-01-01' }],
selectedChildId: null,
@@ -661,4 +669,97 @@ describe('Dashboard', () => {
expect(screen.getByTestId('metric-expiring-parent-pd-1')).toHaveTextContent('Alex')
expect(screen.getByTestId('metric-expiring-parent-pd-unknown')).toHaveTextContent('Förälder 2')
})
describe('manual balance entry — parent picker', () => {
it('shows a parent picker before the form when the workspace has two members', async () => {
const user = userEvent.setup()
renderWithRouter(<Dashboard />)
await user.click(screen.getByRole('button', { name: /Ange uppgifter manuellt/ }))
expect(screen.getByTestId('dashboard-balance-parent-picker')).toBeInTheDocument()
expect(screen.getByTestId('dashboard-balance-parent-pd-1')).toHaveTextContent('Alex')
expect(screen.getByTestId('dashboard-balance-parent-pd-2')).toHaveTextContent('Sam')
})
it('opens the form directly, with no picker, for a solo workspace', async () => {
vi.mocked(hooks.useWorkspaceMembers).mockReturnValue(
makeWorkspaceMembersResult({
data: [{ uuid: 'pd-1', workspaceId: 'ws-1', name: 'Alex', salaryType: 'monthly', employmentStatus: 'employed', createdAt: '2026-01-01' }],
})
)
const user = userEvent.setup()
renderWithRouter(<Dashboard />)
await user.click(screen.getByRole('button', { name: /Ange uppgifter manuellt/ }))
expect(screen.queryByTestId('dashboard-balance-parent-picker')).not.toBeInTheDocument()
expect(await screen.findByText('Dina dagar kvar')).toBeInTheDocument()
})
it('posts the chosen parent\'s uuid to saveManualFetch', async () => {
const user = userEvent.setup()
renderWithRouter(<Dashboard />)
await user.click(screen.getByRole('button', { name: /Ange uppgifter manuellt/ }))
await user.click(screen.getByTestId('dashboard-balance-parent-pd-2'))
expect(screen.queryByTestId('dashboard-balance-parent-picker')).not.toBeInTheDocument()
await user.click(await screen.findByRole('button', { name: 'Spara' }))
await waitFor(() =>
expect(api.saveManualFetch).toHaveBeenCalledWith(
expect.objectContaining({ personalDataUuid: 'pd-2', workspaceId: 'ws-1', childId: 'c-1', source: 'manual' })
)
)
})
it('shows an error when saving a manual balance fails', async () => {
const user = userEvent.setup()
vi.mocked(api.saveManualFetch).mockRejectedValue(new Error('save failed'))
renderWithRouter(<Dashboard />)
await user.click(screen.getByRole('button', { name: /Ange uppgifter manuellt/ }))
await user.click(screen.getByTestId('dashboard-balance-parent-pd-1'))
await user.click(await screen.findByRole('button', { name: 'Spara' }))
expect(await screen.findByText('save failed')).toBeInTheDocument()
})
it('prefills the manual-entry form with the parent\'s saved balance', async () => {
vi.mocked(api.getParentalLeave).mockResolvedValue(
makeParentalLeaveFetch({
personalDataUuid: 'pd-2',
childId: 'c-1',
dubbeldagarLeft: 7,
tierData: makeTierData({ sjukpenningLeft: 65, lagstaNivaLeft: 8 }),
}),
)
const user = userEvent.setup()
renderWithRouter(<Dashboard />)
await user.click(screen.getByRole('button', { name: /Ange uppgifter manuellt/ }))
await user.click(screen.getByTestId('dashboard-balance-parent-pd-2'))
expect(await screen.findByText('Sams dagar kvar')).toBeInTheDocument()
expect((screen.getByLabelText(/Sjukpenningnivå/) as HTMLInputElement).value).toBe('65')
expect((screen.getByLabelText(/Lägstanivå/) as HTMLInputElement).value).toBe('8')
expect((screen.getByLabelText(/Dubbeldagar/) as HTMLInputElement).value).toBe('7')
})
it('falls back to the preset when the parent has no saved balance yet', async () => {
vi.mocked(api.getParentalLeave).mockResolvedValue(null)
const user = userEvent.setup()
renderWithRouter(<Dashboard />)
await user.click(screen.getByRole('button', { name: /Ange uppgifter manuellt/ }))
await user.click(screen.getByTestId('dashboard-balance-parent-pd-1'))
expect(await screen.findByText('Dina dagar kvar')).toBeInTheDocument()
// Couple workspace -> 240-day preset, split 195:45.
expect((screen.getByLabelText(/Sjukpenningnivå/) as HTMLInputElement).value).toBe('195')
expect((screen.getByLabelText(/Lägstanivå/) as HTMLInputElement).value).toBe('45')
expect((screen.getByLabelText(/Dubbeldagar/) as HTMLInputElement).value).toBe('60')
})
})
})