Redesign the setup flow and consolidate the CSS into one primitives layer
Onboarding asks explicitly for one or two parents and shows one card per parent for balances; the balance form is per parent with inline validation; the partner can be filled from the FK deduction or added from it. Settings and Dashboard reuse the same form. CSS: bare buttons carry no colour any more; buttons, cards, choice cards, fields, alerts, badges, eyebrow and modal chrome are defined once in primitives.css. Media queries live only in responsive.css. A style guard test blocks hex, !important and stray media queries. Bilingual fixes: calendar selects size by content, header no longer wraps. Vitest is capped at four workers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -9,10 +9,15 @@ import {
|
||||
useWorkspaceMembers,
|
||||
useOptimizationTips,
|
||||
} from '@/hooks'
|
||||
// Imported directly (not via the `@/hooks` barrel) so this page's tests can
|
||||
// drive it through a mocked `api.getParentalLeave` — see the hook's doc
|
||||
// comment for why.
|
||||
import { useLatestBalance } from '@/hooks/useLatestBalance'
|
||||
import type { PlannedLeaveDay } from '@/types'
|
||||
import * as api from '@/services/api'
|
||||
import { getLabelForType } from '@/utils/leaveTypes'
|
||||
import { nameResolver } from '@/utils/parentNames'
|
||||
import { isSelf, orderSelfFirst } from '@/utils/members'
|
||||
import { useTranslation } from '@/i18n'
|
||||
import { Layout } from '@/components/layout/Layout'
|
||||
import { LoadingSpinner } from '@/components/common/LoadingSpinner'
|
||||
@@ -25,7 +30,7 @@ import { PlanFold } from '@/components/dashboard/PlanFold'
|
||||
import { ParentalLeaveBreakdown } from '@/components/dashboard/ParentalLeaveBreakdown'
|
||||
import { TipCard } from '@/components/inputs/TipCard'
|
||||
import { QRCodeModal } from '@/components/modals/QRCodeModal'
|
||||
import { ManualFetchModal } from '@/components/modals/ManualFetchModal'
|
||||
import { ParentBalanceForm } from '@/components/modals/ParentBalanceForm'
|
||||
import { Onboarding } from '@/pages/Onboarding'
|
||||
|
||||
/** Fallback horizon end when the analytics projection isn't available yet
|
||||
@@ -52,9 +57,19 @@ export function Dashboard(): ReactNode {
|
||||
const scraper = useScraperFetch()
|
||||
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [showBalancePicker, setShowBalancePicker] = useState(false)
|
||||
const [showManualFetchModal, setShowManualFetchModal] = useState(false)
|
||||
const [manualFetchTargetUuid, setManualFetchTargetUuid] = useState<string | null>(null)
|
||||
const [isManualFetchSaving, setIsManualFetchSaving] = useState(false)
|
||||
const [manualFetchError, setManualFetchError] = useState<string | null>(null)
|
||||
|
||||
// Reopening the balance form for a parent should show what was actually
|
||||
// saved for them, not always the 195/45/60 preset — `manualFetchTargetUuid`
|
||||
// is null except while the form is open/opening, so this no-ops the rest
|
||||
// of the time.
|
||||
const {
|
||||
initial: manualFetchInitial,
|
||||
isLoading: manualFetchInitialLoading,
|
||||
} = useLatestBalance(manualFetchTargetUuid, selectedChildId ?? null)
|
||||
|
||||
// `useWorkspaceMembers` fetches once per `workspace.id`, which never
|
||||
// changes across onboarding — so a couple's partner, added mid-wizard via
|
||||
@@ -85,43 +100,65 @@ export function Dashboard(): ReactNode {
|
||||
}
|
||||
|
||||
const handleSaveManualFetch = async (data: {
|
||||
totalDays: number
|
||||
daysUsed: number
|
||||
reservedDaysLeft: number
|
||||
transferableDaysLeft: number
|
||||
sjukpenningLeft: number
|
||||
lagstaNivaLeft: number
|
||||
dubbeldagarLeft: number
|
||||
sjukpenningUsed: number
|
||||
lagstaNivaUsed: number
|
||||
reservedDaysLeft: number | null
|
||||
}) => {
|
||||
if (!workspace?.id || !selectedChildId || !personalDataUuid) return
|
||||
// `manualFetchTargetUuid` is always set to a real uuid by
|
||||
// `handleOpenManualFetch`/`handlePickManualFetchTarget` before the form
|
||||
// is ever shown — there is no path that calls this with it unset.
|
||||
const targetUuid = manualFetchTargetUuid
|
||||
if (!workspace?.id || !selectedChildId || !targetUuid) return
|
||||
|
||||
// No try/catch here on purpose — a rejection propagates back to
|
||||
// `ParentBalanceForm`'s own `onSave` handler, which owns showing it
|
||||
// (inline under the offending field for a mapped 422, or its own top
|
||||
// banner otherwise). See the audit note on `ParentBalanceForm.tsx`.
|
||||
try {
|
||||
setIsManualFetchSaving(true)
|
||||
setManualFetchError(null)
|
||||
|
||||
await api.saveManualFetch({
|
||||
personalDataUuid,
|
||||
personalDataUuid: targetUuid,
|
||||
workspaceId: workspace.id,
|
||||
childId: selectedChildId,
|
||||
totalDays: data.totalDays,
|
||||
daysUsed: data.daysUsed,
|
||||
reservedDaysLeft: data.reservedDaysLeft,
|
||||
transferableDaysLeft: data.transferableDaysLeft,
|
||||
sjukpenningLeft: data.sjukpenningLeft,
|
||||
lagstaNivaLeft: data.lagstaNivaLeft,
|
||||
dubbeldagarLeft: data.dubbeldagarLeft,
|
||||
sjukpenningUsed: data.sjukpenningUsed,
|
||||
lagstaNivaUsed: data.lagstaNivaUsed,
|
||||
reservedDaysLeft: data.reservedDaysLeft,
|
||||
source: 'manual',
|
||||
})
|
||||
|
||||
await refreshParentalLeave()
|
||||
await refreshAnalytics()
|
||||
setShowManualFetchModal(false)
|
||||
} catch (err) {
|
||||
setManualFetchError(err instanceof Error ? err.message : t('dashboard.saveManualFetchError'))
|
||||
// Reset so the next open of this same target refetches instead of
|
||||
// reusing the (now stale) balance `useLatestBalance` already cached.
|
||||
setManualFetchTargetUuid(null)
|
||||
} finally {
|
||||
setIsManualFetchSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
/** "Ange manuellt" — for a solo workspace this opens the form straight for
|
||||
* self; for a couple it shows a small parent picker first (see
|
||||
* `dashboard-balance-parent-picker` below) since either parent's balance
|
||||
* can be entered from here. */
|
||||
const handleOpenManualFetch = () => {
|
||||
if (members.length >= 2) {
|
||||
setShowBalancePicker(true)
|
||||
return
|
||||
}
|
||||
setManualFetchTargetUuid(personalDataUuid)
|
||||
setShowManualFetchModal(true)
|
||||
}
|
||||
|
||||
const handlePickManualFetchTarget = (uuid: string) => {
|
||||
setManualFetchTargetUuid(uuid)
|
||||
setShowBalancePicker(false)
|
||||
setShowManualFetchModal(true)
|
||||
}
|
||||
|
||||
const handleModalClose = () => {
|
||||
if (scraper.isRunning) return
|
||||
setShowModal(false)
|
||||
@@ -173,6 +210,11 @@ export function Dashboard(): ReactNode {
|
||||
label: t('dashboard.expiringAtB4'),
|
||||
value: analytics.combinedExpiringAtB4,
|
||||
testId: 'metric-expiring-combined',
|
||||
// Only actually a distinct "all parents" summary when there was
|
||||
// a per-parent breakdown above it to summarize — for a single
|
||||
// parent it's just this workspace's one figure, so it renders as
|
||||
// an ordinary grid cell instead of a footer row.
|
||||
isSummary: showPerParentExpiring,
|
||||
},
|
||||
]
|
||||
: []
|
||||
@@ -202,7 +244,7 @@ export function Dashboard(): ReactNode {
|
||||
<Layout>
|
||||
<div data-testid="dashboard" className="dashboard">
|
||||
{analyticsError && (
|
||||
<div className="dashboard__error" data-testid="error-banner" role="alert">
|
||||
<div className="alert alert--error" data-testid="error-banner" role="alert">
|
||||
{analyticsError}
|
||||
</div>
|
||||
)}
|
||||
@@ -219,7 +261,7 @@ export function Dashboard(): ReactNode {
|
||||
|
||||
{!parentalLeave && (
|
||||
<p
|
||||
className="dashboard__estimate-badge"
|
||||
className="badge badge--warning dashboard__estimate-badge"
|
||||
data-testid="dashboard-estimate-badge"
|
||||
title={t('dashboard.estimatedBadgeCaption')}
|
||||
>
|
||||
@@ -230,18 +272,39 @@ export function Dashboard(): ReactNode {
|
||||
{age4Bars.length > 0 && <Age4Bars bars={age4Bars} />}
|
||||
|
||||
<section className="dashboard__section dashboard__fetch-actions">
|
||||
<button onClick={handleFetchFK} disabled={!selectedChildId}>
|
||||
<button type="button" className="btn btn--primary" onClick={handleFetchFK} disabled={!selectedChildId}>
|
||||
{t('dashboard.fetchFromFk')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowManualFetchModal(true)}
|
||||
type="button"
|
||||
onClick={handleOpenManualFetch}
|
||||
disabled={!selectedChildId}
|
||||
className="button--secondary"
|
||||
className="btn btn--secondary"
|
||||
>
|
||||
{t('dashboard.enterManually')}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{showBalancePicker && (
|
||||
<div className="card dashboard__balance-picker" data-testid="dashboard-balance-parent-picker">
|
||||
<p className="dashboard__balance-picker-label">{t('dashboard.balancePickerPrompt')}</p>
|
||||
<div className="dashboard__balance-picker-actions">
|
||||
{orderSelfFirst(members, personalDataUuid).map((member) => (
|
||||
<button
|
||||
key={member.uuid}
|
||||
type="button"
|
||||
className="btn btn--secondary"
|
||||
onClick={() => handlePickManualFetchTarget(member.uuid)}
|
||||
data-testid={`dashboard-balance-parent-${member.uuid}`}
|
||||
>
|
||||
{member.name}
|
||||
{isSelf(member, personalDataUuid) && ` ${t('settings.youBadge')}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedChild && (
|
||||
<YearTimeline
|
||||
plannedDays={plannedDays ?? []}
|
||||
@@ -296,13 +359,27 @@ export function Dashboard(): ReactNode {
|
||||
onCancel={scraper.cancel}
|
||||
/>
|
||||
|
||||
<ManualFetchModal
|
||||
isOpen={showManualFetchModal}
|
||||
onSave={handleSaveManualFetch}
|
||||
onClose={() => setShowManualFetchModal(false)}
|
||||
isSaving={isManualFetchSaving}
|
||||
error={manualFetchError}
|
||||
/>
|
||||
{showManualFetchModal && manualFetchInitialLoading && (
|
||||
<div className="modal__overlay" data-testid="dashboard-balance-loading">
|
||||
<LoadingSpinner message={t('parentBalance.loadingBalance')} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showManualFetchModal && !manualFetchInitialLoading && (
|
||||
<ParentBalanceForm
|
||||
parentName={members.find((m) => m.uuid === manualFetchTargetUuid)?.name ?? t('calendar.you')}
|
||||
isSelf={manualFetchTargetUuid === personalDataUuid}
|
||||
totalDays={members.length >= 2 ? 240 : 480}
|
||||
initial={manualFetchInitial}
|
||||
onSave={handleSaveManualFetch}
|
||||
onClose={() => {
|
||||
setShowManualFetchModal(false)
|
||||
setManualFetchTargetUuid(null)
|
||||
}}
|
||||
isSaving={isManualFetchSaving}
|
||||
error={null}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user