MVP frontend+backend

This commit is contained in:
2026-06-14 09:58:30 +02:00
parent d9539f23cc
commit 772d853e37
56 changed files with 5099 additions and 207 deletions
+155 -24
View File
@@ -1,6 +1,7 @@
import { ReactNode, useState } from 'react'
import { ReactNode, useState, useEffect } from 'react'
import { useWorkspace } from '@/context'
import { useVacationDays, useBreakPeriods } from '@/hooks'
import * as api from '@/services/api'
import { Layout } from '@/components/layout/Layout'
import { LoadingSpinner } from '@/components/common/LoadingSpinner'
import { ChildBirthdayInput } from '@/components/inputs/ChildBirthdayInput'
@@ -10,7 +11,7 @@ import { VacationDaysInput } from '@/components/inputs/VacationDaysInput'
import { BreakPeriodsInput } from '@/components/inputs/BreakPeriodsInput'
export function Settings(): ReactNode {
const { workspace, isLoading: wsLoading } = useWorkspace()
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
@@ -19,20 +20,38 @@ export function Settings(): ReactNode {
const [childBirthDate, setChildBirthDate] = useState('')
const [parentData, setParentData] = useState({ salaryType: '', employmentStatus: '' })
const [vacationDaysValue, setVacationDaysValue] = useState(vacDays?.[0]?.totalDays ?? 25)
const [planningGoal, setPlanningGoal] = useState(workspace?.planningGoal ?? 'balance')
const [planningGoal, setPlanningGoal] = useState<'max_time' | 'max_money' | 'balanced'>(
(workspace?.planningGoal as 'max_time' | 'max_money' | 'balanced') ?? 'balanced',
)
const [saveMessage, setSaveMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const [isSaving, setIsSaving] = useState(false)
const isLoading = wsLoading || vacLoading || bpLoading
useEffect(() => {
if (vacDays?.[0]?.totalDays) {
setVacationDaysValue(vacDays[0].totalDays)
}
}, [vacDays])
useEffect(() => {
if (workspace?.planningGoal) {
setPlanningGoal(workspace.planningGoal)
}
}, [workspace?.planningGoal])
if (isLoading) {
return <LoadingSpinner message="Loading settings..." />
}
const selectedChild = children.find((c) => c.id === selectedChildId)
const handleSaveVacationDays = async () => {
try {
if (workspace?.id) {
if (workspace?.id && personalDataUuid) {
setIsSaving(true)
await upsert({
personalDataUuid: 'current-user',
personalDataUuid,
workspaceId: workspace.id,
year: new Date().getFullYear(),
totalDays: vacationDaysValue,
@@ -40,30 +59,112 @@ export function Settings(): ReactNode {
setSaveMessage({ type: 'success', text: 'Vacation days saved!' })
setTimeout(() => setSaveMessage(null), 3000)
}
} catch {
setSaveMessage({ type: 'error', text: 'Failed to save vacation days' })
} catch (err) {
setSaveMessage({
type: 'error',
text: err instanceof Error ? err.message : 'Failed to save vacation days',
})
} finally {
setIsSaving(false)
}
}
const handleAddBreakPeriod = async (period: any) => {
const handleSaveParentProfile = async () => {
try {
if (personalDataUuid) {
setIsSaving(true)
await api.updateParentSettings(personalDataUuid, {
salary_type: parentData.salaryType || undefined,
employment_status: parentData.employmentStatus || undefined,
})
setSaveMessage({ type: 'success', text: 'Parent profile saved!' })
setTimeout(() => setSaveMessage(null), 3000)
}
} catch (err) {
setSaveMessage({
type: 'error',
text: err instanceof Error ? err.message : 'Failed to save parent profile',
})
} finally {
setIsSaving(false)
}
}
const handleSavePlanningGoal = async () => {
try {
if (workspace?.id) {
await addBreakPeriod(
period.startDate,
period.endDate,
period.name,
period.targetWeeks
)
setIsSaving(true)
await api.updateWorkspace(workspace.id, {
planningGoal: planningGoal as 'max_time' | 'max_money' | 'balanced',
})
await refreshContext()
setSaveMessage({ type: 'success', text: 'Planning goal saved!' })
setTimeout(() => setSaveMessage(null), 3000)
}
} catch (err) {
setSaveMessage({
type: 'error',
text: err instanceof Error ? err.message : 'Failed to save planning goal',
})
} finally {
setIsSaving(false)
}
}
const handleSaveChildBirthDate = async () => {
try {
if (selectedChildId && childBirthDate) {
setIsSaving(true)
await api.updateChildSettings(selectedChildId, { birth_date: childBirthDate })
setSaveMessage({ type: 'success', text: 'Child birth date saved!' })
setTimeout(() => setSaveMessage(null), 3000)
}
} catch (err) {
setSaveMessage({
type: 'error',
text: err instanceof Error ? err.message : 'Failed to save child birth date',
})
} 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 {
setSaveMessage({ type: 'error', text: 'Failed to add break period' })
} catch (err) {
setSaveMessage({
type: 'error',
text: err instanceof Error ? err.message : 'Failed to add break period',
})
} finally {
setIsSaving(false)
}
}
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)
}
}
return (
<Layout header={{ title: 'Inställningar' }}>
<Layout>
<div data-testid="settings-page" className="settings-page">
<h1>Settings</h1>
@@ -79,10 +180,23 @@ export function Settings(): ReactNode {
<section className="settings-page__section">
<h2>Child Information</h2>
<ChildBirthdayInput
birthDate={childBirthDate}
onBirthDateChange={setChildBirthDate}
/>
{selectedChild && (
<>
<p>Selected child: <strong>{selectedChild.name}</strong></p>
<ChildBirthdayInput
birthDate={childBirthDate}
onBirthDateChange={setChildBirthDate}
/>
<button
onClick={handleSaveChildBirthDate}
className="settings-page__button"
disabled={isSaving}
data-testid="save-child-button"
>
{isSaving ? 'Saving...' : 'Save Child Birth Date'}
</button>
</>
)}
</section>
<section className="settings-page__section">
@@ -91,14 +205,30 @@ export function Settings(): ReactNode {
parent={parentData}
onChange={(field, val) => setParentData({ ...parentData, [field]: val })}
/>
<button
onClick={handleSaveParentProfile}
className="settings-page__button"
disabled={isSaving}
data-testid="save-parent-button"
>
{isSaving ? 'Saving...' : 'Save Parent Profile'}
</button>
</section>
<section className="settings-page__section">
<h2>Workspace Settings</h2>
<PlanningGoalSelector
value={planningGoal}
onChange={(goal) => setPlanningGoal(goal)}
onChange={(goal) => setPlanningGoal(goal as 'max_time' | 'max_money' | 'balanced')}
/>
<button
onClick={handleSavePlanningGoal}
className="settings-page__button"
disabled={isSaving}
data-testid="save-planning-goal-button"
>
{isSaving ? 'Saving...' : 'Save Planning Goal'}
</button>
</section>
<section className="settings-page__section">
@@ -113,9 +243,10 @@ export function Settings(): ReactNode {
<button
onClick={handleSaveVacationDays}
className="settings-page__button"
disabled={isSaving}
data-testid="save-vacation-button"
>
Save Vacation Days
{isSaving ? 'Saving...' : 'Save Vacation Days'}
</button>
</section>
@@ -124,7 +255,7 @@ export function Settings(): ReactNode {
<BreakPeriodsInput
periods={breakPeriods ?? []}
onAdd={handleAddBreakPeriod}
onRemove={removeBreakPeriod}
onRemove={handleRemoveBreakPeriod}
/>
</section>
</div>