Let a household choose to be paid for the red days

Asking for a full week off never spent anything on the Saturday, the
Sunday or a holiday, and a test now says so outright rather than leaving
it to be rediscovered. Those days are free because vacation cannot be
taken on a day that is not a working day.

Parental benefit is a payment rather than a schedule, though, so it can
be drawn on a red day to turn an unpaid day off into a paid one — at the
cost of a day the family would otherwise keep. That is now a workspace
setting, off by default, and it never reaches for a vacation day. With it
off the planner behaves exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ei7gCZzdd751uZtPzb1Pep
This commit is contained in:
2026-09-19 14:28:38 +02:00
parent 1837154225
commit b87ad0e7ad
18 changed files with 511 additions and 10 deletions
+3
View File
@@ -112,6 +112,9 @@ export const settings = {
'settings.saveVacationSuccess': 'Vacation days for {year} saved!',
'settings.selectedChildLabel': 'Selected child:',
'settings.showWeekNumbers': 'Show week numbers',
'settings.takePlOnRedDays': 'Take parental leave on red days',
'settings.takePlOnRedDaysError': 'Could not save the setting',
'settings.takePlOnRedDaysHelp': 'You get paid for Saturdays, Sundays and red days in the plan — but you spend days you would otherwise have kept, since vacation cannot be taken on these days.',
'settings.title': 'Settings',
'settings.vacationHelp': 'Enter vacation days per year. In the first year you are usually home more and have fewer days to spend on vacation, while later years have your full annual allotment. This controls how much time the planner has to work with.',
'settings.vacationSectionTitle': 'Vacation days',
+3
View File
@@ -112,6 +112,9 @@ export const settings = {
'settings.saveVacationSuccess': 'Semesterdagar för {year} sparade!',
'settings.selectedChildLabel': 'Valt barn:',
'settings.showWeekNumbers': 'Visa veckonummer',
'settings.takePlOnRedDays': 'Ta ut föräldrapenning på röda dagar',
'settings.takePlOnRedDaysError': 'Kunde inte spara inställningen',
'settings.takePlOnRedDaysHelp': 'Du får betalt för lördagar, söndagar och röda dagar i planeringen — men du förbrukar dagar du annars hade sparat, eftersom semester inte kan tas ut på dessa dagar.',
'settings.title': 'Inställningar',
'settings.vacationHelp': 'Ange semesterdagar per år. Det första året är du oftast hemma mer och har färre dagar att lägga på semester, medan kommande år har din fulla årliga tilldelning. Detta styr hur mycket tid planeraren har att arbeta med.',
'settings.vacationSectionTitle': 'Semesterdagar',
+65
View File
@@ -647,6 +647,71 @@ describe('Settings', () => {
expect(setShowWeekNumbers).toHaveBeenCalledWith(true)
})
describe('take PL on red days', () => {
it('renders unchecked by default', () => {
renderSettings()
expect(screen.getByTestId('take-pl-on-red-days-checkbox')).not.toBeChecked()
})
it('renders checked when the workspace has it enabled', () => {
vi.mocked(useWorkspace).mockReturnValue(
makeWorkspaceContextValue({
workspace: {
id: 'ws-1',
name: 'Test Workspace',
createdAt: '2026-01-01',
takePlOnRedDays: true,
},
workspaceId: 'ws-1',
children: [{ id: 'c-1', name: 'Child 1', birth_date: CHILD_BIRTH_DATE }],
selectedChildId: 'c-1',
personalDataUuid: 'pd-1',
parentName: 'Test Parent',
})
)
renderSettings()
expect(screen.getByTestId('take-pl-on-red-days-checkbox')).toBeChecked()
})
it('saves the setting and refreshes the workspace context on toggle', async () => {
vi.mocked(api.updateWorkspace).mockResolvedValue({
id: 'ws-1',
name: 'Test Workspace',
createdAt: '2026-01-01',
takePlOnRedDays: true,
})
const mockRefreshContext = vi.fn().mockResolvedValue(undefined)
vi.mocked(useWorkspace).mockReturnValue(
makeWorkspaceContextValue({
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
workspaceId: 'ws-1',
children: [{ id: 'c-1', name: 'Child 1', birth_date: CHILD_BIRTH_DATE }],
selectedChildId: 'c-1',
personalDataUuid: 'pd-1',
parentName: 'Test Parent',
refreshContext: mockRefreshContext,
})
)
renderSettings()
fireEvent.click(screen.getByTestId('take-pl-on-red-days-checkbox'))
await waitFor(() =>
expect(api.updateWorkspace).toHaveBeenCalledWith('ws-1', { takePlOnRedDays: true })
)
expect(mockRefreshContext).toHaveBeenCalled()
})
it('shows an error message when saving fails', async () => {
vi.mocked(api.updateWorkspace).mockRejectedValue(new Error('network down'))
renderSettings()
fireEvent.click(screen.getByTestId('take-pl-on-red-days-checkbox'))
expect(await screen.findByTestId('save-message')).toHaveTextContent('network down')
})
})
describe('language selector', () => {
// Node's own experimental `localStorage` global shadows jsdom's working
// implementation in this environment and throws on every call — stub a
+25
View File
@@ -144,6 +144,7 @@ export function Settings(): ReactNode {
const [memberActionError, setMemberActionError] = useState<{ uuid: string; text: string } | null>(null)
const [balanceFormUuid, setBalanceFormUuid] = useState<string | null>(null)
const [isSavingBalance, setIsSavingBalance] = useState(false)
const [isSavingRedDayPref, setIsSavingRedDayPref] = useState(false)
// Settings isn't scoped to a single child the way Onboarding/Dashboard are
// — mirrors the fallback `handleSaveBalance` below uses when saving.
@@ -361,6 +362,19 @@ export function Settings(): ReactNode {
}
}
const handleToggleTakePlOnRedDays = async (checked: boolean) => {
if (!workspace?.id) return
try {
setIsSavingRedDayPref(true)
await api.updateWorkspace(workspace.id, { takePlOnRedDays: checked })
await refreshContext()
} catch (err) {
showMessage('error', err instanceof Error ? err.message : t('settings.takePlOnRedDaysError'))
} finally {
setIsSavingRedDayPref(false)
}
}
const handleOpenBalanceForm = (uuid: string) => {
setBalanceFormUuid(uuid)
}
@@ -621,6 +635,17 @@ export function Settings(): ReactNode {
/>
<span>{t('settings.showWeekNumbers')}</span>
</label>
<label className="settings-page__checkbox-label">
<input
type="checkbox"
checked={workspace?.takePlOnRedDays ?? false}
onChange={(e) => handleToggleTakePlOnRedDays(e.target.checked)}
disabled={isSavingRedDayPref}
data-testid="take-pl-on-red-days-checkbox"
/>
<span>{t('settings.takePlOnRedDays')}</span>
</label>
<p className="settings-page__help">{t('settings.takePlOnRedDaysHelp')}</p>
</section>
<section className="settings-page__section">
+3
View File
@@ -458,6 +458,7 @@ interface RawWorkspace {
id: string
name: string
created_at: string
take_pl_on_red_days: boolean
}
function toWorkspace(raw: RawWorkspace): Workspace {
@@ -465,6 +466,7 @@ function toWorkspace(raw: RawWorkspace): Workspace {
id: raw.id,
name: raw.name,
createdAt: raw.created_at,
takePlOnRedDays: raw.take_pl_on_red_days,
}
}
@@ -716,6 +718,7 @@ export async function updateWorkspace(
method: 'PUT',
body: JSON.stringify({
name: req.name,
take_pl_on_red_days: req.takePlOnRedDays,
}),
})) as RawWorkspace
return toWorkspace(raw)
+1
View File
@@ -45,6 +45,7 @@ export interface WorkspaceCreateRequest {
export interface WorkspaceUpdateRequest {
name?: string
takePlOnRedDays?: boolean
}
// Parental leave manual entry — per-parent balances, as shown on FK's "Mina
+6
View File
@@ -5,6 +5,12 @@ export interface Workspace {
id: string
name: string
createdAt: string // ISO-8601
// Optional (rather than required) so pre-existing test literals across the
// codebase that construct a `Workspace` by hand don't all need updating —
// a real API response always includes it (see `toWorkspace` in
// src/services/api.ts). Absent/undefined means "not entered yet, treat as
// false", matching the backend column's default.
takePlOnRedDays?: boolean
}
export interface PersonalData {