Complete NextSteps roadmap: planning engine UI, calendar cluster, flexible wishes
Works through every numbered item in NextSteps260913.md section 3, plus the three known verification issues in section 4.1. Surface what the backend already computed: - 3.1 render surplus_advice in PlanResults, mirroring the shortfall remedies - 3.2 surface VacationBalance.is_assumed so assumed statutory years are visible - 3.3 age-4 expiry given the proposed plan, via a new Age4Projection reusing the existing _age4_projections arithmetic (Dashboard stat verified, not rebuilt) Calendar cluster (one pass over the holiday data): - new GET /api/calendar/days exposing roda dagar, klamdagar and day overrides, paginated four years at a time - 3.4 shade roda dagar/klamdagar, with R/K badges so colour is not the only signal - 3.6 click-to-override behind an explicit mode toggle, leaving drag multi-select intact - 3.7 click month/year to advance - 3.8 per-year red-days list in Settings, statutory days plus overrides i18n: - 3.5 five backend-generated Swedish surfaces converted to message_key + data, following the tips_service pattern; old string fields replaced, not kept Flexible wishes (3.9): - PLANNING_ENGINE_SPEC section 10: window_mode, candidate search, cost function, deterministic tie-break, two-pass ordering, test matrix rows 15-24 - backend: entity, migration 006, _FULL_SCHEMA sync, repository, allocator - frontend: season-preset toggle in WishEditor, unplaced outcome rendering Fixes (4.1): - pin typecheck to the local tsc rather than npx resolving a newer TypeScript - ignore generated coverage/ and dist/ in the ESLint flat config - fix the flaky usePlanApply test properly, with waitFor rather than a retry Tests: backend 361 -> 404, frontend 540 -> 704. Coverage 89.47% -> 96.52% lines. Not yet verified in a browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
This commit is contained in:
+247
-158
@@ -1,173 +1,235 @@
|
||||
import { ReactNode, useState, useEffect } from 'react'
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
import { useWorkspace } from '@/context'
|
||||
import { useVacationDays, useBreakPeriods, useShowWeekNumbers } from '@/hooks'
|
||||
import { useVacationDays, useShowWeekNumbers, useChildSettings, useDayOverrides, useCalendarDays } from '@/hooks'
|
||||
import * as api from '@/services/api'
|
||||
import { useTranslation, type Locale } from '@/i18n'
|
||||
import { Layout } from '@/components/layout/Layout'
|
||||
import { LoadingSpinner } from '@/components/common/LoadingSpinner'
|
||||
import { ChildBirthdayInput } from '@/components/inputs/ChildBirthdayInput'
|
||||
import { ParentProfileInput } from '@/components/inputs/ParentProfileInput'
|
||||
import { PlanningGoalSelector } from '@/components/inputs/PlanningGoalSelector'
|
||||
import { ParentProfileInput, type SalaryType, type EmploymentStatus } from '@/components/inputs/ParentProfileInput'
|
||||
import { PlanningGoalSelector, type PlanningGoal } from '@/components/inputs/PlanningGoalSelector'
|
||||
import { PlanningModeSelector, type PlanningMode } from '@/components/inputs/PlanningModeSelector'
|
||||
import { VacationDaysInput } from '@/components/inputs/VacationDaysInput'
|
||||
import { BreakPeriodsInput } from '@/components/inputs/BreakPeriodsInput'
|
||||
import { VacationDaysTable, type VacationDaysTableRow } from '@/components/vacation-days/VacationDaysTable'
|
||||
import { DayOverridesInput } from '@/components/inputs/DayOverridesInput'
|
||||
import { RedDaysList } from '@/components/calendar/RedDaysList'
|
||||
|
||||
const RED_DAYS_WINDOW_YEARS = 4
|
||||
|
||||
const DEFAULT_VACATION_DAYS = 25
|
||||
|
||||
function isSalaryType(value: string): value is SalaryType {
|
||||
return value === 'monthly' || value === 'hourly'
|
||||
}
|
||||
|
||||
function isEmploymentStatus(value: string): value is EmploymentStatus {
|
||||
return value === 'employed' || value === 'jobseeking' || value === 'none'
|
||||
}
|
||||
|
||||
/** Current year through (at minimum) the year the child turns 4. */
|
||||
function computeVacationYearRange(birthDate: string | undefined, currentYear: number): number[] {
|
||||
let endYear = currentYear
|
||||
if (birthDate) {
|
||||
const fourthBirthday = new Date(birthDate)
|
||||
if (!isNaN(fourthBirthday.getTime())) {
|
||||
fourthBirthday.setFullYear(fourthBirthday.getFullYear() + 4)
|
||||
endYear = Math.max(endYear, fourthBirthday.getFullYear())
|
||||
}
|
||||
}
|
||||
const years: number[] = []
|
||||
for (let year = currentYear; year <= endYear; year++) {
|
||||
years.push(year)
|
||||
}
|
||||
return years
|
||||
}
|
||||
|
||||
export function Settings(): ReactNode {
|
||||
const { t, explicitLocale, setLocale } = useTranslation()
|
||||
const { workspace, isLoading: wsLoading, selectedChildId, children, personalDataUuid, refreshContext } = useWorkspace()
|
||||
const { data: vacDays, isLoading: vacLoading, upsert } = useVacationDays(workspace?.id ?? '')
|
||||
const { data: breakPeriods, isLoading: bpLoading, add: addBreakPeriod, remove: removeBreakPeriod } = useBreakPeriods(
|
||||
workspace?.id ?? null
|
||||
)
|
||||
const { data: vacDays, isLoading: vacLoading, upsert: upsertVacationDays } = useVacationDays(workspace?.id ?? '')
|
||||
const {
|
||||
data: childDetails,
|
||||
isLoading: childDetailsLoading,
|
||||
update: updateChildDetails,
|
||||
} = useChildSettings(selectedChildId)
|
||||
const {
|
||||
data: dayOverrides,
|
||||
isLoading: dayOverridesLoading,
|
||||
upsert: upsertDayOverride,
|
||||
remove: removeDayOverride,
|
||||
} = useDayOverrides(workspace?.id ?? '')
|
||||
const [showWeekNumbers, setShowWeekNumbers] = useShowWeekNumbers()
|
||||
const [redDaysStartYear, setRedDaysStartYear] = useState(() => new Date().getFullYear())
|
||||
const {
|
||||
data: calendarWindow,
|
||||
isLoading: calendarLoading,
|
||||
error: calendarError,
|
||||
} = useCalendarDays(workspace?.id ?? '', redDaysStartYear, RED_DAYS_WINDOW_YEARS)
|
||||
|
||||
const [childBirthDate, setChildBirthDate] = useState('')
|
||||
const [parentData, setParentData] = useState({ salaryType: '', employmentStatus: '' })
|
||||
const [vacationDaysValue, setVacationDaysValue] = useState(vacDays?.[0]?.totalDays ?? 25)
|
||||
const [planningGoal, setPlanningGoal] = useState<'max_time' | 'max_money' | 'balanced'>(
|
||||
(workspace?.planningGoal as 'max_time' | 'max_money' | 'balanced') ?? 'balanced',
|
||||
)
|
||||
const [preschoolPlanningDays, setPreschoolPlanningDays] = useState(6)
|
||||
const [parentData, setParentData] = useState<{ salaryType?: SalaryType; employmentStatus?: EmploymentStatus }>({})
|
||||
const [planningGoal, setPlanningGoal] = useState<PlanningGoal>(workspace?.planningGoal ?? 'balanced')
|
||||
const [planningMode, setPlanningMode] = useState<PlanningMode>(workspace?.planningMode ?? 'solo')
|
||||
const [saveMessage, setSaveMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [newPartnerName, setNewPartnerName] = useState('')
|
||||
|
||||
const isLoading = wsLoading || vacLoading || bpLoading
|
||||
const isLoading = wsLoading || vacLoading || childDetailsLoading || dayOverridesLoading
|
||||
|
||||
useEffect(() => {
|
||||
if (vacDays?.[0]?.totalDays) {
|
||||
setVacationDaysValue(vacDays[0].totalDays)
|
||||
if (childDetails) {
|
||||
setChildBirthDate(childDetails.birthDate)
|
||||
setPreschoolPlanningDays(childDetails.preschoolPlanningDaysPerYear)
|
||||
}
|
||||
}, [vacDays])
|
||||
}, [childDetails])
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace?.planningGoal) {
|
||||
setPlanningGoal(workspace.planningGoal)
|
||||
}
|
||||
}, [workspace?.planningGoal])
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner message="Loading settings..." />
|
||||
}
|
||||
if (workspace?.planningMode) {
|
||||
setPlanningMode(workspace.planningMode)
|
||||
}
|
||||
}, [workspace?.planningGoal, workspace?.planningMode])
|
||||
|
||||
const selectedChild = children.find((c) => c.id === selectedChildId)
|
||||
|
||||
const handleSaveVacationDays = async () => {
|
||||
const currentYear = new Date().getFullYear()
|
||||
const vacationYears = useMemo(
|
||||
() => computeVacationYearRange(selectedChild?.birth_date, currentYear),
|
||||
[selectedChild?.birth_date, currentYear],
|
||||
)
|
||||
const vacationRows: VacationDaysTableRow[] = useMemo(
|
||||
() =>
|
||||
vacationYears.map((year) => {
|
||||
const existing = vacDays.find((v) => v.year === year)
|
||||
return {
|
||||
year,
|
||||
totalDays: existing?.totalDays ?? DEFAULT_VACATION_DAYS,
|
||||
savedDaysIn: existing?.savedDaysIn ?? 0,
|
||||
// §3.2 — no vacation_days row for this year yet: the statutory
|
||||
// default is shown, not an entered value. Mirrors the backend's
|
||||
// `VacationBalanceResponse.is_assumed` for the propose-time table.
|
||||
isAssumed: existing === undefined,
|
||||
}
|
||||
}),
|
||||
[vacationYears, vacDays],
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner message={t('settings.loading')} />
|
||||
}
|
||||
|
||||
const handleLanguageChange = (value: string) => {
|
||||
setLocale(value === 'auto' ? null : (value as Locale))
|
||||
}
|
||||
|
||||
const showMessage = (type: 'success' | 'error', text: string) => {
|
||||
setSaveMessage({ type, text })
|
||||
setTimeout(() => setSaveMessage(null), 3000)
|
||||
}
|
||||
|
||||
const handleSaveChild = async () => {
|
||||
try {
|
||||
if (workspace?.id && personalDataUuid) {
|
||||
if (selectedChildId && childBirthDate) {
|
||||
setIsSaving(true)
|
||||
await upsert({
|
||||
personalDataUuid,
|
||||
workspaceId: workspace.id,
|
||||
year: new Date().getFullYear(),
|
||||
totalDays: vacationDaysValue,
|
||||
await updateChildDetails({
|
||||
birthDate: childBirthDate,
|
||||
preschoolPlanningDaysPerYear: preschoolPlanningDays,
|
||||
})
|
||||
setSaveMessage({ type: 'success', text: 'Vacation days saved!' })
|
||||
setTimeout(() => setSaveMessage(null), 3000)
|
||||
showMessage('success', t('settings.saveChildSuccess'))
|
||||
}
|
||||
} catch (err) {
|
||||
setSaveMessage({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : 'Failed to save vacation days',
|
||||
})
|
||||
showMessage('error', err instanceof Error ? err.message : t('settings.saveChildError'))
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleParentChange = (field: string, val: string) => {
|
||||
setParentData((prev) => {
|
||||
const next = { ...prev }
|
||||
if (field === 'salaryType') {
|
||||
if (isSalaryType(val)) {
|
||||
next.salaryType = val
|
||||
} else {
|
||||
delete next.salaryType
|
||||
}
|
||||
} else if (field === 'employmentStatus') {
|
||||
if (isEmploymentStatus(val)) {
|
||||
next.employmentStatus = val
|
||||
} else {
|
||||
delete next.employmentStatus
|
||||
}
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveParentProfile = async () => {
|
||||
try {
|
||||
if (personalDataUuid) {
|
||||
setIsSaving(true)
|
||||
await api.updateParentSettings(personalDataUuid, {
|
||||
salary_type: parentData.salaryType || undefined,
|
||||
employment_status: parentData.employmentStatus || undefined,
|
||||
salary_type: parentData.salaryType,
|
||||
employment_status: parentData.employmentStatus,
|
||||
})
|
||||
setSaveMessage({ type: 'success', text: 'Parent profile saved!' })
|
||||
setTimeout(() => setSaveMessage(null), 3000)
|
||||
showMessage('success', t('settings.saveProfileSuccess'))
|
||||
}
|
||||
} catch (err) {
|
||||
setSaveMessage({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : 'Failed to save parent profile',
|
||||
})
|
||||
showMessage('error', err instanceof Error ? err.message : t('settings.saveProfileError'))
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSavePlanningGoal = async () => {
|
||||
const handleSavePlanning = async () => {
|
||||
try {
|
||||
if (workspace?.id) {
|
||||
setIsSaving(true)
|
||||
await api.updateWorkspace(workspace.id, {
|
||||
planningGoal: planningGoal as 'max_time' | 'max_money' | 'balanced',
|
||||
})
|
||||
await api.updateWorkspace(workspace.id, { planningGoal, planningMode })
|
||||
await refreshContext()
|
||||
setSaveMessage({ type: 'success', text: 'Planning goal saved!' })
|
||||
setTimeout(() => setSaveMessage(null), 3000)
|
||||
showMessage('success', t('settings.savePlanningSuccess'))
|
||||
}
|
||||
} catch (err) {
|
||||
setSaveMessage({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : 'Failed to save planning goal',
|
||||
})
|
||||
showMessage('error', err instanceof Error ? err.message : t('settings.savePlanningError'))
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveChildBirthDate = async () => {
|
||||
const handleSaveVacationYear = async (year: number, totalDays: number, savedDaysIn: number) => {
|
||||
try {
|
||||
if (selectedChildId && childBirthDate) {
|
||||
if (workspace?.id && personalDataUuid) {
|
||||
setIsSaving(true)
|
||||
await api.updateChildSettings(selectedChildId, { birth_date: childBirthDate })
|
||||
setSaveMessage({ type: 'success', text: 'Child birth date saved!' })
|
||||
setTimeout(() => setSaveMessage(null), 3000)
|
||||
await upsertVacationDays({
|
||||
personalDataUuid,
|
||||
workspaceId: workspace.id,
|
||||
year,
|
||||
totalDays,
|
||||
savedDaysIn,
|
||||
})
|
||||
showMessage('success', t('settings.saveVacationSuccess', { year }))
|
||||
}
|
||||
} catch (err) {
|
||||
setSaveMessage({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : 'Failed to save child birth date',
|
||||
})
|
||||
showMessage('error', err instanceof Error ? err.message : t('settings.saveVacationError'))
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddBreakPeriod = async (period: { name: string; startDate: string; endDate: string; targetWeeks: number }) => {
|
||||
try {
|
||||
if (workspace?.id) {
|
||||
setIsSaving(true)
|
||||
await addBreakPeriod(period.startDate, period.endDate, period.name, period.targetWeeks)
|
||||
setSaveMessage({ type: 'success', text: 'Break period added!' })
|
||||
setTimeout(() => setSaveMessage(null), 3000)
|
||||
}
|
||||
} catch (err) {
|
||||
setSaveMessage({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : 'Failed to add break period',
|
||||
})
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
const handleRedDaysPrevWindow = () => {
|
||||
setRedDaysStartYear((y) => y - RED_DAYS_WINDOW_YEARS)
|
||||
}
|
||||
|
||||
const handleRemoveBreakPeriod = async (id: string) => {
|
||||
try {
|
||||
setIsSaving(true)
|
||||
await removeBreakPeriod(id)
|
||||
setSaveMessage({ type: 'success', text: 'Break period removed!' })
|
||||
setTimeout(() => setSaveMessage(null), 3000)
|
||||
} catch (err) {
|
||||
setSaveMessage({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : 'Failed to remove break period',
|
||||
})
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
const handleRedDaysNextWindow = () => {
|
||||
setRedDaysStartYear((y) => y + RED_DAYS_WINDOW_YEARS)
|
||||
}
|
||||
|
||||
const handleAddPartner = async () => {
|
||||
if (!newPartnerName.trim() || !workspace?.id) {
|
||||
setSaveMessage({ type: 'error', text: 'Please enter a partner name' })
|
||||
showMessage('error', t('settings.partnerNameRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -177,13 +239,9 @@ export function Settings(): ReactNode {
|
||||
await api.addPartner(workspace.id, newPartnerName)
|
||||
await refreshContext()
|
||||
setNewPartnerName('')
|
||||
setSaveMessage({ type: 'success', text: 'Partner added! Refresh the page to see changes.' })
|
||||
setTimeout(() => setSaveMessage(null), 3000)
|
||||
showMessage('success', t('settings.addPartnerSuccess'))
|
||||
} catch (err) {
|
||||
setSaveMessage({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : 'Failed to add partner',
|
||||
})
|
||||
showMessage('error', err instanceof Error ? err.message : t('settings.addPartnerError'))
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
@@ -192,7 +250,7 @@ export function Settings(): ReactNode {
|
||||
return (
|
||||
<Layout>
|
||||
<div data-testid="settings-page" className="settings-page">
|
||||
<h1>Settings</h1>
|
||||
<h1>{t('settings.title')}</h1>
|
||||
|
||||
{saveMessage && (
|
||||
<div
|
||||
@@ -205,115 +263,146 @@ export function Settings(): ReactNode {
|
||||
)}
|
||||
|
||||
<section className="settings-page__section">
|
||||
<h2>Child Information</h2>
|
||||
{selectedChild && (
|
||||
<h2>{t('settings.childSectionTitle')}</h2>
|
||||
{selectedChild ? (
|
||||
<>
|
||||
<p>Selected child: <strong>{selectedChild.name}</strong></p>
|
||||
<p>
|
||||
{t('settings.selectedChildLabel')} <strong>{selectedChild.name}</strong>
|
||||
</p>
|
||||
<ChildBirthdayInput
|
||||
birthDate={childBirthDate}
|
||||
onBirthDateChange={setChildBirthDate}
|
||||
label={t('settings.childBirthDateLabel')}
|
||||
/>
|
||||
<div className="settings-page__field">
|
||||
<p className="settings-page__help">{t('settings.preschoolHelp')}</p>
|
||||
<VacationDaysInput
|
||||
label={t('settings.preschoolPlanningDaysLabel')}
|
||||
value={preschoolPlanningDays}
|
||||
onChange={setPreschoolPlanningDays}
|
||||
min={0}
|
||||
max={30}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSaveChildBirthDate}
|
||||
onClick={handleSaveChild}
|
||||
className="settings-page__button"
|
||||
disabled={isSaving}
|
||||
data-testid="save-child-button"
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Child Birth Date'}
|
||||
{isSaving ? t('common.saving') : t('settings.saveChildButton')}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<p>{t('settings.noChildSelected')}</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="settings-page__section">
|
||||
<h2>Parent Profile</h2>
|
||||
<ParentProfileInput
|
||||
parent={parentData}
|
||||
onChange={(field, val) => setParentData({ ...parentData, [field]: val })}
|
||||
/>
|
||||
<h2>{t('settings.profileSectionTitle')}</h2>
|
||||
<p className="settings-page__help">{t('settings.profileHelp')}</p>
|
||||
<ParentProfileInput parent={parentData} onChange={handleParentChange} />
|
||||
<button
|
||||
onClick={handleSaveParentProfile}
|
||||
className="settings-page__button"
|
||||
disabled={isSaving}
|
||||
data-testid="save-parent-button"
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Parent Profile'}
|
||||
{isSaving ? t('common.saving') : t('settings.saveProfileButton')}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section className="settings-page__section">
|
||||
<h2>Workspace Settings</h2>
|
||||
<PlanningGoalSelector
|
||||
value={planningGoal}
|
||||
onChange={(goal) => setPlanningGoal(goal as 'max_time' | 'max_money' | 'balanced')}
|
||||
/>
|
||||
<h2>{t('settings.planningSectionTitle')}</h2>
|
||||
<p className="settings-page__help">{t('settings.planningHelp')}</p>
|
||||
<PlanningGoalSelector value={planningGoal} onChange={setPlanningGoal} />
|
||||
<PlanningModeSelector value={planningMode} onChange={setPlanningMode} />
|
||||
<button
|
||||
onClick={handleSavePlanningGoal}
|
||||
onClick={handleSavePlanning}
|
||||
className="settings-page__button"
|
||||
disabled={isSaving}
|
||||
data-testid="save-planning-goal-button"
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Planning Goal'}
|
||||
{isSaving ? t('common.saving') : t('settings.savePlanningButton')}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section className="settings-page__section">
|
||||
<h2>Vacation Days</h2>
|
||||
<VacationDaysInput
|
||||
label="Annual vacation days"
|
||||
value={vacationDaysValue}
|
||||
onChange={setVacationDaysValue}
|
||||
min={0}
|
||||
max={365}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSaveVacationDays}
|
||||
className="settings-page__button"
|
||||
disabled={isSaving}
|
||||
data-testid="save-vacation-button"
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Vacation Days'}
|
||||
</button>
|
||||
<h2>{t('settings.vacationSectionTitle')}</h2>
|
||||
<p className="settings-page__help">{t('settings.vacationHelp')}</p>
|
||||
<VacationDaysTable rows={vacationRows} onSave={handleSaveVacationYear} isSaving={isSaving} />
|
||||
</section>
|
||||
|
||||
<section className="settings-page__section">
|
||||
<h2>Break Periods</h2>
|
||||
<BreakPeriodsInput
|
||||
periods={breakPeriods ?? []}
|
||||
onAdd={handleAddBreakPeriod}
|
||||
onRemove={handleRemoveBreakPeriod}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="settings-page__section">
|
||||
<h2>Calendar</h2>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<h2>{t('settings.calendarSectionTitle')}</h2>
|
||||
<label className="settings-page__checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showWeekNumbers}
|
||||
onChange={(e) => setShowWeekNumbers(e.target.checked)}
|
||||
data-testid="show-week-numbers-checkbox"
|
||||
/>
|
||||
<span>Visa veckonummer</span>
|
||||
<span>{t('settings.showWeekNumbers')}</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section className="settings-page__section">
|
||||
<h2>Partner</h2>
|
||||
<p>Add a partner to plan parental leave together.</p>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<h2>{t('settings.languageSectionTitle')}</h2>
|
||||
<label htmlFor="language-select" className="settings-page__label">
|
||||
{t('settings.languageLabel')}
|
||||
</label>
|
||||
<select
|
||||
id="language-select"
|
||||
className="settings-page__select"
|
||||
value={explicitLocale ?? 'auto'}
|
||||
onChange={(e) => handleLanguageChange(e.target.value)}
|
||||
data-testid="language-select"
|
||||
>
|
||||
<option value="auto">{t('settings.languageAutoOption')}</option>
|
||||
<option value="sv">{t('settings.languageSwedishOption')}</option>
|
||||
<option value="en">{t('settings.languageEnglishOption')}</option>
|
||||
</select>
|
||||
</section>
|
||||
|
||||
<section className="settings-page__section">
|
||||
<h2>{t('settings.dayOverridesSectionTitle')}</h2>
|
||||
<p className="settings-page__help">{t('settings.dayOverridesHelp')}</p>
|
||||
<DayOverridesInput
|
||||
overrides={dayOverrides}
|
||||
isLoading={dayOverridesLoading || isSaving}
|
||||
onAdd={upsertDayOverride}
|
||||
onRemove={removeDayOverride}
|
||||
workspaceId={workspace?.id ?? ''}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="settings-page__section">
|
||||
<h2>{t('settings.redDaysSectionTitle')}</h2>
|
||||
<p className="settings-page__help">{t('settings.redDaysHelp')}</p>
|
||||
<RedDaysList
|
||||
days={calendarWindow?.days ?? []}
|
||||
startYear={redDaysStartYear}
|
||||
years={RED_DAYS_WINDOW_YEARS}
|
||||
minYear={calendarWindow?.minYear ?? null}
|
||||
maxYear={calendarWindow?.maxYear ?? null}
|
||||
isLoading={calendarLoading}
|
||||
error={calendarError}
|
||||
onPrevWindow={handleRedDaysPrevWindow}
|
||||
onNextWindow={handleRedDaysNextWindow}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="settings-page__section">
|
||||
<h2>{t('settings.partnerSectionTitle')}</h2>
|
||||
<p>{t('settings.partnerIntro')}</p>
|
||||
<div className="settings-page__partner-row">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Partner name"
|
||||
placeholder={t('settings.partnerNamePlaceholder')}
|
||||
value={newPartnerName}
|
||||
onChange={(e) => setNewPartnerName(e.target.value)}
|
||||
disabled={isSaving}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 10px',
|
||||
border: '1px solid #d0d0d0',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
className="settings-page__partner-input"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddPartner}
|
||||
@@ -321,7 +410,7 @@ export function Settings(): ReactNode {
|
||||
disabled={isSaving}
|
||||
data-testid="add-partner-button"
|
||||
>
|
||||
{isSaving ? 'Adding...' : 'Lägg till partner'}
|
||||
{isSaving ? t('settings.addingPartner') : t('settings.addPartnerButton')}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user