Lead the overview with the days about to be lost
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.
This commit is contained in:
@@ -14,11 +14,22 @@ import {
|
||||
makeScraperFetchResult,
|
||||
makeWorkspaceMembersResult,
|
||||
makePlannedLeaveDay,
|
||||
makeOptimizationTipsResult,
|
||||
makeLeaveWishesResult,
|
||||
makePlanProposalResult,
|
||||
makePlanApplyResult,
|
||||
makeTip,
|
||||
} from '../../tests/fixtures'
|
||||
|
||||
const mockNavigate = vi.fn()
|
||||
|
||||
vi.mock('@/hooks')
|
||||
vi.mock('@/context')
|
||||
vi.mock('@/services/api')
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom')
|
||||
return { ...actual, useNavigate: () => mockNavigate }
|
||||
})
|
||||
|
||||
// Layout pulls in useAuth/useNavigate, which need a full AuthProvider/Router tree
|
||||
// to work. Dashboard.tsx only cares that its content renders inside Layout, so
|
||||
@@ -103,6 +114,10 @@ describe('Dashboard', () => {
|
||||
],
|
||||
})
|
||||
)
|
||||
vi.mocked(hooks.useOptimizationTips).mockReturnValue(makeOptimizationTipsResult())
|
||||
vi.mocked(hooks.useLeaveWishes).mockReturnValue(makeLeaveWishesResult())
|
||||
vi.mocked(hooks.usePlanProposal).mockReturnValue(makePlanProposalResult())
|
||||
vi.mocked(hooks.usePlanApply).mockReturnValue(makePlanApplyResult())
|
||||
})
|
||||
|
||||
it('renders loading spinner initially', () => {
|
||||
@@ -137,8 +152,120 @@ describe('Dashboard', () => {
|
||||
expect(screen.getByLabelText(/Barnets namn/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows overview metrics when children exist', () => {
|
||||
describe('Hero', () => {
|
||||
it('uses the forward-looking framing when there are no planned days yet', () => {
|
||||
vi.mocked(hooks.usePlannedDays).mockReturnValue(makePlannedDaysResult({ data: [] }))
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByTestId('dashboard-hero-message')).toHaveTextContent(
|
||||
'Du har 270 dagar att planera. Utan en plan försvinner 100 av dem när Child 1 fyller 4.',
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the warning framing once days are planned, with the 4th-birthday date', () => {
|
||||
// selectedChild birth_date is 2022-01-01 and there is no parent
|
||||
// projection in this test's default mock, so the 4th birthday is
|
||||
// derived client-side as plain date arithmetic (not a business rule).
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByTestId('dashboard-hero-message')).toHaveTextContent(
|
||||
'100 dagar riskerar att försvinna när Child 1 fyller 4 (1 januari 2026).',
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the backend-provided 4th birthday when a parent projection is available', () => {
|
||||
vi.mocked(hooks.useAnalytics).mockReturnValue(
|
||||
makeAnalyticsResult({
|
||||
data: {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
parentProjections: [
|
||||
{ personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2029-06-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 60, savableCapAtB4: 96 },
|
||||
],
|
||||
combinedDaysUsed: 30,
|
||||
combinedDaysLeft: 210,
|
||||
combinedExpiringAtB4: 60,
|
||||
},
|
||||
})
|
||||
)
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByTestId('dashboard-hero-message')).toHaveTextContent('1 juni 2029')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Age4Bars', () => {
|
||||
it('renders each parent bar using the backend-resolved savableCapAtB4, never re-derived', () => {
|
||||
vi.mocked(hooks.useAnalytics).mockReturnValue(
|
||||
makeAnalyticsResult({
|
||||
data: {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
parentProjections: [
|
||||
{ personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 40, savableAfterB4: 40, expiringAtB4: 0, savableCapAtB4: 48 },
|
||||
{ personalDataUuid: 'pd-2', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 1, plannedDaysBeforeB4: 0, projectedLeftAtB4: 239, savableAfterB4: 48, expiringAtB4: 191, savableCapAtB4: 48 },
|
||||
],
|
||||
combinedDaysUsed: 31,
|
||||
combinedDaysLeft: 279,
|
||||
combinedExpiringAtB4: 191,
|
||||
},
|
||||
})
|
||||
)
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByTestId('age4-bar-savable-pd-1')).toHaveTextContent('40 av 48 dagar kan sparas')
|
||||
expect(screen.getByTestId('age4-bar-pd-1')).toHaveTextContent('Alex')
|
||||
expect(screen.getByTestId('age4-bar-expiring-pd-2')).toHaveTextContent('191 dagar förfaller')
|
||||
expect(screen.getByTestId('age4-bar-pd-2')).toHaveTextContent('Sam')
|
||||
})
|
||||
|
||||
it('is not rendered when there are no parent projections yet', () => {
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.queryByTestId('age4-bars')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('YearTimeline', () => {
|
||||
it('renders one row per year from the child\'s birth to the 4th birthday', () => {
|
||||
renderWithRouter(<Dashboard />)
|
||||
for (const year of [2022, 2023, 2024, 2025, 2026]) {
|
||||
expect(screen.getByTestId(`year-timeline-row-${year}`)).toBeInTheDocument()
|
||||
}
|
||||
})
|
||||
|
||||
it('navigates to the calendar at the clicked band\'s date', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(hooks.usePlannedDays).mockReturnValue(
|
||||
makePlannedDaysResult({
|
||||
data: [makePlannedLeaveDay({ id: '1', date: '2024-06-15', dayType: 'vacation' })],
|
||||
})
|
||||
)
|
||||
renderWithRouter(<Dashboard />)
|
||||
await user.click(screen.getByTestId('year-timeline-band-2024-0'))
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/calendar?date=2024-06-15')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tips', () => {
|
||||
it('is hidden entirely when there are no tips', () => {
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.queryByTestId('dashboard-tips')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders tip cards when tips exist', () => {
|
||||
vi.mocked(hooks.useOptimizationTips).mockReturnValue(
|
||||
makeOptimizationTipsResult({ data: [makeTip({ id: 'tip-1', messageKey: 'tips.use_vacation_before_expiry' })] })
|
||||
)
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByTestId('dashboard-tips')).toBeInTheDocument()
|
||||
expect(screen.getAllByTestId('tip-card')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders the folded plan-proposal section', () => {
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByTestId('plan-fold')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows overview metrics inside the collapsed "more details" section', () => {
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByTestId('dashboard-more-details')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('overview-metrics')).toBeInTheDocument()
|
||||
expect(screen.getByText(/Använda dagar/)).toBeInTheDocument()
|
||||
})
|
||||
@@ -185,16 +312,10 @@ describe('Dashboard', () => {
|
||||
expect(screen.getByTestId('days-breakdown')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows progress chart with real data', () => {
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByTestId('progress-chart')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows Swedish section headings', () => {
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByTestId('metrics-title')).toHaveTextContent(/Översikt/)
|
||||
expect(screen.getAllByText(/Planerade dagar/)[0]).toBeInTheDocument()
|
||||
expect(screen.getByText(/Återstående till 4 års ålder/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// The combined expiring-at-4 figure reflects the currently saved plan, while
|
||||
@@ -215,8 +336,8 @@ describe('Dashboard', () => {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
parentProjections: [
|
||||
{ personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 60 },
|
||||
{ personalDataUuid: 'pd-2', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 20, plannedDaysBeforeB4: 5, projectedLeftAtB4: 210, savableAfterB4: 96, expiringAtB4: 44 },
|
||||
{ personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 60, savableCapAtB4: 96 },
|
||||
{ personalDataUuid: 'pd-2', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 20, plannedDaysBeforeB4: 5, projectedLeftAtB4: 210, savableAfterB4: 96, expiringAtB4: 44, savableCapAtB4: 96 },
|
||||
],
|
||||
combinedDaysUsed: 50,
|
||||
combinedDaysLeft: 190,
|
||||
@@ -515,7 +636,7 @@ describe('Dashboard', () => {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
parentProjections: [
|
||||
{ personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 104 },
|
||||
{ personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 104, savableCapAtB4: 96 },
|
||||
],
|
||||
combinedDaysUsed: 30,
|
||||
combinedDaysLeft: 210,
|
||||
@@ -536,8 +657,8 @@ describe('Dashboard', () => {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
parentProjections: [
|
||||
{ personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 60 },
|
||||
{ personalDataUuid: 'pd-2', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 20, plannedDaysBeforeB4: 5, projectedLeftAtB4: 210, savableAfterB4: 96, expiringAtB4: 44 },
|
||||
{ personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 60, savableCapAtB4: 96 },
|
||||
{ personalDataUuid: 'pd-2', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 20, plannedDaysBeforeB4: 5, projectedLeftAtB4: 210, savableAfterB4: 96, expiringAtB4: 44, savableCapAtB4: 96 },
|
||||
],
|
||||
combinedDaysUsed: 50,
|
||||
combinedDaysLeft: 190,
|
||||
@@ -570,8 +691,8 @@ describe('Dashboard', () => {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
parentProjections: [
|
||||
{ personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 60 },
|
||||
{ personalDataUuid: 'pd-2', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 20, plannedDaysBeforeB4: 5, projectedLeftAtB4: 210, savableAfterB4: 96, expiringAtB4: 44 },
|
||||
{ personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 60, savableCapAtB4: 96 },
|
||||
{ personalDataUuid: 'pd-2', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 20, plannedDaysBeforeB4: 5, projectedLeftAtB4: 210, savableAfterB4: 96, expiringAtB4: 44, savableCapAtB4: 96 },
|
||||
],
|
||||
combinedDaysUsed: 50,
|
||||
combinedDaysLeft: 190,
|
||||
@@ -592,8 +713,8 @@ describe('Dashboard', () => {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
parentProjections: [
|
||||
{ personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 60 },
|
||||
{ personalDataUuid: 'pd-unknown', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 20, plannedDaysBeforeB4: 5, projectedLeftAtB4: 210, savableAfterB4: 96, expiringAtB4: 44 },
|
||||
{ personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 60, savableCapAtB4: 96 },
|
||||
{ personalDataUuid: 'pd-unknown', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 20, plannedDaysBeforeB4: 5, projectedLeftAtB4: 210, savableAfterB4: 96, expiringAtB4: 44, savableCapAtB4: 96 },
|
||||
],
|
||||
combinedDaysUsed: 50,
|
||||
combinedDaysLeft: 190,
|
||||
|
||||
Reference in New Issue
Block a user