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
@@ -0,0 +1,89 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { ParentalLeaveBreakdown } from './ParentalLeaveBreakdown'
import type { ParentalLeaveFetch } from '@/types'
describe('ParentalLeaveBreakdown', () => {
const mockData: ParentalLeaveFetch = {
id: 'pf1',
personalDataUuid: 'pd1',
workspaceId: 'ws1',
childId: 'c1',
source: 'scraped',
fetchedAt: '2026-06-13T10:00:00Z',
totalDays: 300,
daysUsed: 50,
daysLeft: 250,
reservedDaysTotal: 90,
reservedDaysLeft: 80,
transferableDaysLeft: 50,
dubbeldagarLeft: 60,
tierData: {
sjukpenningTotal: 100,
sjukpenningUsed: 20,
sjukpenningLeft: 80,
lagstaNivaTotal: 200,
lagstaNivaUsed: 30,
lagstaNivaLeft: 170,
},
createdAt: '2026-06-13T10:00:00Z',
updatedAt: '2026-06-13T10:00:00Z',
}
it('renders empty state when data is null', () => {
render(<ParentalLeaveBreakdown data={null} />)
expect(screen.getByText(/Ingen data hämtad ännu/)).toBeInTheDocument()
})
it('renders breakdown table with data', () => {
render(<ParentalLeaveBreakdown data={mockData} />)
expect(screen.getByTestId('parental-leave-breakdown')).toBeInTheDocument()
expect(screen.getByText(/Reserverade dagar/)).toBeInTheDocument()
})
it('displays reserved days', () => {
render(<ParentalLeaveBreakdown data={mockData} />)
const row = screen.getByTestId('breakdown-reserved')
expect(row).toHaveTextContent('80')
expect(row).toHaveTextContent('90')
})
it('displays transferable days', () => {
render(<ParentalLeaveBreakdown data={mockData} />)
const row = screen.getByTestId('breakdown-transferable')
expect(row).toHaveTextContent('50')
})
it('displays dubbeldagar left', () => {
render(<ParentalLeaveBreakdown data={mockData} />)
const row = screen.getByTestId('breakdown-dubbeldagar')
expect(row).toHaveTextContent('60')
})
it('displays tier data', () => {
render(<ParentalLeaveBreakdown data={mockData} />)
const sjukRow = screen.getByTestId('breakdown-sjukpenning')
expect(sjukRow).toHaveTextContent('80')
expect(sjukRow).toHaveTextContent('100')
const lagstaRow = screen.getByTestId('breakdown-lagsta-niva')
expect(lagstaRow).toHaveTextContent('170')
expect(lagstaRow).toHaveTextContent('200')
})
it('displays scraped source label', () => {
render(<ParentalLeaveBreakdown data={mockData} />)
expect(screen.getByText(/Hämtad från Försäkringskassan/)).toBeInTheDocument()
})
it('displays manual source label', () => {
const manualData = { ...mockData, source: 'manual' as const }
render(<ParentalLeaveBreakdown data={manualData} />)
expect(screen.getByText(/Manuellt angiven/)).toBeInTheDocument()
})
it('displays formatted date', () => {
render(<ParentalLeaveBreakdown data={mockData} />)
expect(screen.getByText(/2026-06-13/)).toBeInTheDocument()
})
})
@@ -0,0 +1,77 @@
import { ReactNode } from 'react'
import type { ParentalLeaveFetch } from '@/types'
export interface ParentalLeaveBreakdownProps {
data: ParentalLeaveFetch | null
}
export function ParentalLeaveBreakdown({ data }: ParentalLeaveBreakdownProps): ReactNode {
if (!data) {
return (
<div data-testid="parental-leave-breakdown" className="parental-leave-breakdown">
<p className="parental-leave-breakdown__empty">
Ingen data hämtad ännu — tryck på Hämta från Försäkringskassan.
</p>
</div>
)
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('sv-SE')
}
const sourceLabel = data.source === 'scraped' ? 'Hämtad från Försäkringskassan' : 'Manuellt angiven'
return (
<div data-testid="parental-leave-breakdown" className="parental-leave-breakdown">
<div className="parental-leave-breakdown__section">
<table className="parental-leave-breakdown__table">
<thead>
<tr>
<th>Typ</th>
<th>Återstående</th>
<th>Totalt</th>
</tr>
</thead>
<tbody>
<tr data-testid="breakdown-reserved">
<td>Reserverade dagar</td>
<td>{data.reservedDaysLeft}</td>
<td>{data.reservedDaysTotal}</td>
</tr>
<tr data-testid="breakdown-transferable">
<td>Övergripbara dagar</td>
<td>{data.transferableDaysLeft}</td>
<td>—</td>
</tr>
<tr data-testid="breakdown-dubbeldagar">
<td>Dubbeldagar kvar</td>
<td>{data.dubbeldagarLeft}</td>
<td>—</td>
</tr>
{data.tierData && (
<>
<tr data-testid="breakdown-sjukpenning">
<td>Sjukpenning</td>
<td>{data.tierData.sjukpenningLeft}</td>
<td>{data.tierData.sjukpenningTotal}</td>
</tr>
<tr data-testid="breakdown-lagsta-niva">
<td>Lägsta nivå</td>
<td>{data.tierData.lagstaNivaLeft}</td>
<td>{data.tierData.lagstaNivaTotal}</td>
</tr>
</>
)}
</tbody>
</table>
</div>
<div className="parental-leave-breakdown__provenance">
<small>
{sourceLabel} {formatDate(data.fetchedAt)}
</small>
</div>
</div>
)
}