Lead the overview with the days about to be lost
The page opened with generic tiles and a bar reading forty-eight of ninety-six weeks, which was neither weeks nor connected to the age-four ceiling it appeared to describe. It is replaced by one headline and a bar per parent, in days, against each parent's own savable limit as the server reports it. The headline has two framings on the same arithmetic. Before anything is planned it says what there is to plan and what will be lost without a plan, because on the first day almost the whole entitlement is technically at risk and saying so bluntly reads as an accusation. Once a plan exists it warns about the days genuinely at stake. A timeline of the years to the fourth birthday answers what the next four years look like, which a month-at-a-time calendar cannot. Shortfalls, surplus advice and balances sit below the fold, read-only: applying is automatic now, so a second apply button would be a way to get two different answers. Rejection messages move to one shared place that resolves a parent's name, so an identifier can no longer surface in their place.
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
import { ReactNode, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useWorkspace } from '@/context'
|
||||
import { useAnalytics, usePlannedDays, useScraperFetch, useParentalLeave, useWorkspaceMembers } from '@/hooks'
|
||||
import {
|
||||
useAnalytics,
|
||||
usePlannedDays,
|
||||
useScraperFetch,
|
||||
useParentalLeave,
|
||||
useWorkspaceMembers,
|
||||
useOptimizationTips,
|
||||
} from '@/hooks'
|
||||
import type { PlannedLeaveDay } from '@/types'
|
||||
import * as api from '@/services/api'
|
||||
import { getLabelForType } from '@/utils/leaveTypes'
|
||||
@@ -10,14 +18,28 @@ import { Layout } from '@/components/layout/Layout'
|
||||
import { LoadingSpinner } from '@/components/common/LoadingSpinner'
|
||||
import { OverviewMetrics } from '@/components/dashboard/OverviewMetrics'
|
||||
import { DaysBreakdown } from '@/components/dashboard/DaysBreakdown'
|
||||
import { ProgressChart } from '@/components/dashboard/ProgressChart'
|
||||
import { Hero } from '@/components/dashboard/Hero'
|
||||
import { Age4Bars } from '@/components/dashboard/Age4Bars'
|
||||
import { YearTimeline } from '@/components/dashboard/YearTimeline'
|
||||
import { PlanFold } from '@/components/dashboard/PlanFold'
|
||||
import { ParentalLeaveBreakdown } from '@/components/dashboard/ParentalLeaveBreakdown'
|
||||
import { ChildBirthdayInput } from '@/components/inputs/ChildBirthdayInput'
|
||||
import { TipCard } from '@/components/inputs/TipCard'
|
||||
import { QRCodeModal } from '@/components/modals/QRCodeModal'
|
||||
import { ManualFetchModal } from '@/components/modals/ManualFetchModal'
|
||||
|
||||
/** Fallback horizon end when the analytics projection isn't available yet
|
||||
* (e.g. mid-load) — plain date arithmetic, not a business rule, so deriving
|
||||
* it here does not duplicate the age-4 savable-cap logic the backend owns. */
|
||||
function computeFourthBirthday(birthDateIso: string): string {
|
||||
const d = new Date(birthDateIso + 'T00:00:00Z')
|
||||
d.setUTCFullYear(d.getUTCFullYear() + 4)
|
||||
return d.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
export function Dashboard(): ReactNode {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { workspace, isLoading: wsLoading, selectedChildId, children, personalDataUuid, refreshContext } = useWorkspace()
|
||||
const { data: analytics, isLoading: analyticsLoading, error: analyticsError, refresh: refreshAnalytics } = useAnalytics(
|
||||
workspace?.id ?? null,
|
||||
@@ -26,6 +48,7 @@ export function Dashboard(): ReactNode {
|
||||
const { data: plannedDays, isLoading: pdLoading } = usePlannedDays(selectedChildId ?? undefined)
|
||||
const { data: parentalLeave, refresh: refreshParentalLeave } = useParentalLeave(personalDataUuid, selectedChildId ?? null)
|
||||
const { data: members, isLoading: membersLoading } = useWorkspaceMembers(workspace?.id ?? null)
|
||||
const { data: tips, dismiss: dismissTip } = useOptimizationTips(workspace?.id ?? null)
|
||||
const scraper = useScraperFetch()
|
||||
|
||||
const [childName, setChildName] = useState('')
|
||||
@@ -120,15 +143,16 @@ export function Dashboard(): ReactNode {
|
||||
await Promise.all([refreshAnalytics(), refreshParentalLeave()])
|
||||
}
|
||||
|
||||
const handleBandClick = (date: string) => {
|
||||
navigate(`/calendar?date=${date}`)
|
||||
}
|
||||
|
||||
const breakdown = [
|
||||
{ type: getLabelForType('vacation', t), count: plannedDays?.filter((d: PlannedLeaveDay) => d.dayType === 'vacation').length ?? 0 },
|
||||
{ type: getLabelForType('sjukpenning', t), count: plannedDays?.filter((d: PlannedLeaveDay) => d.dayType === 'sjukpenning').length ?? 0 },
|
||||
{ type: getLabelForType('lagsta', t), count: plannedDays?.filter((d: PlannedLeaveDay) => d.dayType === 'lagsta').length ?? 0 },
|
||||
]
|
||||
|
||||
const currentWeeks = Math.floor((analytics?.combinedDaysUsed ?? 0) / 5)
|
||||
const targetWeeks = Math.floor(((analytics?.combinedDaysUsed ?? 0) + (analytics?.combinedDaysLeft ?? 0)) / 5)
|
||||
|
||||
// Per-parent expiring-day figures are only informative once there's more
|
||||
// than one parent to compare — with a single parent they'd just repeat
|
||||
// the combined total below, so skip the breakdown in that case.
|
||||
@@ -206,6 +230,18 @@ export function Dashboard(): ReactNode {
|
||||
)
|
||||
}
|
||||
|
||||
const selectedChild = children.find((c) => c.id === selectedChildId)
|
||||
const hasPlan = (plannedDays?.length ?? 0) > 0
|
||||
const fourthBirthday =
|
||||
parentProjections[0]?.fourthBirthday ?? (selectedChild ? computeFourthBirthday(selectedChild.birth_date) : '')
|
||||
const age4Bars = parentProjections.map((projection, index) => ({
|
||||
personalDataUuid: projection.personalDataUuid,
|
||||
name: parentDisplayName(projection.personalDataUuid, index),
|
||||
projectedLeftAtB4: projection.projectedLeftAtB4,
|
||||
savableCapAtB4: projection.savableCapAtB4,
|
||||
expiringAtB4: projection.expiringAtB4,
|
||||
}))
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div data-testid="dashboard" className="dashboard">
|
||||
@@ -215,6 +251,18 @@ export function Dashboard(): ReactNode {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{analytics && selectedChild && (
|
||||
<Hero
|
||||
hasPlan={hasPlan}
|
||||
daysLeft={analytics.combinedDaysLeft}
|
||||
daysAtRisk={analytics.combinedExpiringAtB4}
|
||||
childName={selectedChild.name}
|
||||
fourthBirthday={fourthBirthday}
|
||||
/>
|
||||
)}
|
||||
|
||||
{age4Bars.length > 0 && <Age4Bars bars={age4Bars} />}
|
||||
|
||||
<section className="dashboard__section">
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button
|
||||
@@ -234,28 +282,46 @@ export function Dashboard(): ReactNode {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="dashboard__section">
|
||||
{analytics && (
|
||||
<OverviewMetrics
|
||||
title={t('dashboard.overviewTitle')}
|
||||
metrics={overviewMetrics}
|
||||
/>
|
||||
)}
|
||||
<ParentalLeaveBreakdown data={parentalLeave} />
|
||||
</section>
|
||||
|
||||
<section className="dashboard__section">
|
||||
<h2>{t('dashboard.plannedDaysTitle')}</h2>
|
||||
<DaysBreakdown breakdown={breakdown} />
|
||||
</section>
|
||||
|
||||
<section className="dashboard__section">
|
||||
<h2>{t('dashboard.remainingUntilB4Title')}</h2>
|
||||
<ProgressChart
|
||||
currentWeeks={currentWeeks}
|
||||
targetWeeks={targetWeeks}
|
||||
{selectedChild && (
|
||||
<YearTimeline
|
||||
plannedDays={plannedDays ?? []}
|
||||
startDate={selectedChild.birth_date}
|
||||
endDate={fourthBirthday}
|
||||
onBandClick={handleBandClick}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{tips && tips.length > 0 && (
|
||||
<section className="dashboard__section" data-testid="dashboard-tips">
|
||||
<h2>{t('tips.title')}</h2>
|
||||
<div className="dashboard__tips-list">
|
||||
{tips.map((tip) => (
|
||||
<TipCard key={tip.id} tip={tip} onDismiss={() => dismissTip(tip.id)} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{workspace && <PlanFold workspaceId={workspace.id} members={members} />}
|
||||
|
||||
<details className="dashboard__more-details" data-testid="dashboard-more-details">
|
||||
<summary>{t('dashboard.moreDetailsSummary')}</summary>
|
||||
|
||||
<section className="dashboard__section">
|
||||
{analytics && (
|
||||
<OverviewMetrics
|
||||
title={t('dashboard.overviewTitle')}
|
||||
metrics={overviewMetrics}
|
||||
/>
|
||||
)}
|
||||
<ParentalLeaveBreakdown data={parentalLeave} />
|
||||
</section>
|
||||
|
||||
<section className="dashboard__section">
|
||||
<h2>{t('dashboard.plannedDaysTitle')}</h2>
|
||||
<DaysBreakdown breakdown={breakdown} />
|
||||
</section>
|
||||
</details>
|
||||
|
||||
<QRCodeModal
|
||||
isOpen={showModal}
|
||||
|
||||
Reference in New Issue
Block a user