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:
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user