MVP frontend+backend
This commit is contained in:
@@ -1,20 +1,33 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ReactNode, useState } from 'react'
|
||||
import { useWorkspace } from '@/context'
|
||||
import { useAnalytics, usePlannedDays, useBreakPeriods } from '@/hooks'
|
||||
import { useAnalytics, usePlannedDays, useBreakPeriods, useScraperFetch, useParentalLeave } from '@/hooks'
|
||||
import type { PlannedLeaveDay, BreakPeriod } from '@/types'
|
||||
import * as api from '@/services/api'
|
||||
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 { ParentalLeaveBreakdown } from '@/components/dashboard/ParentalLeaveBreakdown'
|
||||
import { ChildBirthdayInput } from '@/components/inputs/ChildBirthdayInput'
|
||||
import { QRCodeModal } from '@/components/modals/QRCodeModal'
|
||||
|
||||
export function Dashboard(): ReactNode {
|
||||
const { workspace, isLoading: wsLoading } = useWorkspace()
|
||||
const { data: analytics, isLoading: analyticsLoading, error: analyticsError } = useAnalytics(
|
||||
const { workspace, isLoading: wsLoading, selectedChildId, children, personalDataUuid, refreshContext } = useWorkspace()
|
||||
const { data: analytics, isLoading: analyticsLoading, error: analyticsError, refresh: refreshAnalytics } = useAnalytics(
|
||||
workspace?.id ?? null,
|
||||
'' // placeholder child ID; in real app would come from workspace context
|
||||
selectedChildId ?? ''
|
||||
)
|
||||
const { data: plannedDays, isLoading: pdLoading } = usePlannedDays(workspace?.id ?? null)
|
||||
const { data: plannedDays, isLoading: pdLoading } = usePlannedDays(selectedChildId ?? undefined)
|
||||
const { data: breakPeriods, isLoading: bpLoading } = useBreakPeriods(workspace?.id ?? null)
|
||||
const { data: parentalLeave, refresh: refreshParentalLeave } = useParentalLeave(personalDataUuid, selectedChildId ?? null)
|
||||
const scraper = useScraperFetch()
|
||||
|
||||
const [childName, setChildName] = useState('')
|
||||
const [childBirthDate, setChildBirthDate] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
const isLoading = wsLoading || analyticsLoading || pdLoading || bpLoading
|
||||
|
||||
@@ -22,16 +35,98 @@ export function Dashboard(): ReactNode {
|
||||
return <LoadingSpinner message="Loading dashboard..." />
|
||||
}
|
||||
|
||||
const handleAddChild = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSubmitError(null)
|
||||
if (!workspace || !childName || !childBirthDate) {
|
||||
setSubmitError('Please fill in all fields')
|
||||
return
|
||||
}
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await api.createChild({
|
||||
workspace_id: workspace.id,
|
||||
name: childName,
|
||||
birth_date: childBirthDate,
|
||||
})
|
||||
setChildName('')
|
||||
setChildBirthDate('')
|
||||
await refreshContext()
|
||||
} catch (err: unknown) {
|
||||
setSubmitError(err instanceof Error ? err.message : 'Failed to create child')
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFetchFK = async () => {
|
||||
setShowModal(true)
|
||||
await scraper.start(selectedChildId ?? undefined)
|
||||
}
|
||||
|
||||
const handleModalClose = () => {
|
||||
if (scraper.isRunning) return
|
||||
setShowModal(false)
|
||||
}
|
||||
|
||||
const handleFetchDone = async () => {
|
||||
setShowModal(false)
|
||||
await Promise.all([refreshAnalytics(), refreshParentalLeave()])
|
||||
}
|
||||
|
||||
const breakdown = [
|
||||
{ type: 'Vacation', count: plannedDays?.filter((d: any) => d.dayType === 'vacation').length ?? 0 },
|
||||
{ type: 'Sick Leave', count: plannedDays?.filter((d: any) => d.dayType === 'sjukpenning').length ?? 0 },
|
||||
{ type: 'Parental', count: plannedDays?.filter((d: any) => d.dayType === 'lagsta').length ?? 0 },
|
||||
{ type: 'Vacation', count: plannedDays?.filter((d: PlannedLeaveDay) => d.dayType === 'vacation').length ?? 0 },
|
||||
{ type: 'Sick Leave', count: plannedDays?.filter((d: PlannedLeaveDay) => d.dayType === 'sjukpenning').length ?? 0 },
|
||||
{ type: 'Parental', count: plannedDays?.filter((d: PlannedLeaveDay) => d.dayType === 'lagsta').length ?? 0 },
|
||||
]
|
||||
|
||||
const totalPlanned = breakdown.reduce((sum, item) => sum + item.count, 0)
|
||||
const currentWeeks = Math.floor((analytics?.combinedDaysUsed ?? 0) / 5)
|
||||
const targetWeeks = Math.floor(((analytics?.combinedDaysUsed ?? 0) + (analytics?.combinedDaysLeft ?? 0)) / 5)
|
||||
|
||||
if (children.length === 0) {
|
||||
return (
|
||||
<Layout>
|
||||
<div data-testid="dashboard" className="dashboard">
|
||||
<section className="dashboard__section">
|
||||
<h2>Lägg till barn</h2>
|
||||
<p>Du har inga barn registrerade ännu. Lägg till ditt första barn för att börja.</p>
|
||||
<form onSubmit={(e) => void handleAddChild(e)}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="child-name">Barnets namn</label>
|
||||
<input
|
||||
id="child-name"
|
||||
type="text"
|
||||
placeholder="Namn"
|
||||
value={childName}
|
||||
onChange={(e) => setChildName(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<ChildBirthdayInput
|
||||
birthDate={childBirthDate}
|
||||
onBirthDateChange={setChildBirthDate}
|
||||
label="Födelsedag"
|
||||
/>
|
||||
</div>
|
||||
{submitError && (
|
||||
<div role="alert" className="error-message">
|
||||
{submitError}
|
||||
</div>
|
||||
)}
|
||||
<button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Lägger till...' : 'Lägg till barn'}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout header={{ title: 'Översikt' }}>
|
||||
<Layout>
|
||||
<div data-testid="dashboard" className="dashboard">
|
||||
{analyticsError && (
|
||||
<div className="dashboard__error" data-testid="error-banner" role="alert">
|
||||
@@ -40,9 +135,19 @@ export function Dashboard(): ReactNode {
|
||||
)}
|
||||
|
||||
<section className="dashboard__section">
|
||||
<h2>Overview</h2>
|
||||
<button
|
||||
onClick={handleFetchFK}
|
||||
disabled={!selectedChildId}
|
||||
style={{ padding: '0.5rem 1rem', cursor: selectedChildId ? 'pointer' : 'not-allowed' }}
|
||||
>
|
||||
Hämta från Försäkringskassan
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section className="dashboard__section">
|
||||
{analytics && (
|
||||
<OverviewMetrics
|
||||
title="Översikt"
|
||||
metrics={[
|
||||
{ label: 'Days Used', value: analytics.combinedDaysUsed },
|
||||
{ label: 'Days Left', value: analytics.combinedDaysLeft },
|
||||
@@ -50,18 +155,19 @@ export function Dashboard(): ReactNode {
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<ParentalLeaveBreakdown data={parentalLeave} />
|
||||
</section>
|
||||
|
||||
<section className="dashboard__section">
|
||||
<h2>Planned Days</h2>
|
||||
<h2>Planerade dagar</h2>
|
||||
<DaysBreakdown breakdown={breakdown} />
|
||||
</section>
|
||||
|
||||
<section className="dashboard__section">
|
||||
<h2>Progress Toward Age 4</h2>
|
||||
<h2>Återstående till 4 års ålder</h2>
|
||||
<ProgressChart
|
||||
currentWeeks={Math.floor(totalPlanned / 5)}
|
||||
targetWeeks={50}
|
||||
currentWeeks={currentWeeks}
|
||||
targetWeeks={targetWeeks}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -69,7 +175,7 @@ export function Dashboard(): ReactNode {
|
||||
<section className="dashboard__section">
|
||||
<h2>Break Periods ({breakPeriods.length})</h2>
|
||||
<ul className="dashboard__break-list">
|
||||
{breakPeriods.map((period: any) => (
|
||||
{breakPeriods.map((period: BreakPeriod) => (
|
||||
<li key={period.id} className="dashboard__break-item">
|
||||
<strong>{period.name}</strong>
|
||||
<small>
|
||||
@@ -80,6 +186,19 @@ export function Dashboard(): ReactNode {
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<QRCodeModal
|
||||
isOpen={showModal}
|
||||
onClose={handleModalClose}
|
||||
onDone={handleFetchDone}
|
||||
status={scraper.pollData?.status ?? null}
|
||||
qrData={scraper.pollData?.qr_data}
|
||||
result={scraper.pollData?.result}
|
||||
message={scraper.pollData?.message}
|
||||
error={scraper.error}
|
||||
isRunning={scraper.isRunning}
|
||||
onCancel={scraper.cancel}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user