0ef5593388
* refactor(reports): restructure declaration pages around the filing pipeline Momsdeklaration (/reports/vat-declaration) becomes the four-step flow the user actually runs: kontrollera, granska, bokfoer, laemna in. - NEW VatChecksCard, mounted first and ungated: the local pre-flight checks and the RC-basis-gap worklist used to render inside SkatteverketPanel BELOW the filing CTAs, and vanished entirely for free-tier or not-connected users, exactly the manual filers who must not file a declaration the checks would have blocked. - The gap worklist scales: compact DataList rows (first 8 + visa alla), visible shared classification selects, per-row overrides, bulk "Korrigera alla" behind a confirm dialog with serial progress, and an in-page declaration refetch replacing "Ladda om sidan". The list outlives the aggregate RC_BASIS_MISSING check so remaining rows never vanish after the first fix. - Summary card: status Badge + font-display headline amount instead of a Badge carrying the number; sanctioned h3 section heads; Table primitive; NEW import block (rutor 50/60-62) and the utgaende sum now includes 60-62 so it matches ruta 49 arithmetic; drill-downs preserved. - SkatteverketPanel stops being an API console: three forward buttons (Validera, Spara utkast, Laas och signera), six lookup/recovery actions in an overflow menu with two-line descriptions, destructive confirms on radera/koppla bort, one truthful notice slot (not-found is info, never green), visible disabled-reasons instead of title attrs, signing link as a real anchor plus auto re-check of inlaemning on tab refocus, and per-period state reset so a Q1 signing link can never show Q4's kvittens. - VatCompositionChart deleted (decorative donut mixing in/out VAT); export menu keeps xlsx only, XML/PDF live in the Laemna in card. - Sibling declaration pages adopt the same grammar: PS gets shadcn selects, auto-fetch with stale-discard, refresh button, envelope-parse fix (message_sv never existed); NE/INK2 auto-fetch on fiscal-year change (kills stale-year data), shared keyboard-accessible DeclarationRutaRow (fixes the expense sign bug), whole-krona amounts matching filed SRU values, neutral info notes instead of bg-primary/10, Skeleton loading, accessible download errors. UI-only: no API, schema, or dependency changes. All strings hardcoded Swedish (statutory surface). Adversarially reviewed (19-agent pass); all 10 confirmed findings fixed in this commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reports): address CodeRabbit review on #992 - formatWholeKronor truncates instead of rounds: NE/INK2 SRU generators drop oere with Math.trunc, and the UI must show the filed figures. - SkatteverketPanel disconnect surfaces non-ok responses instead of silently stopping the spinner. - VatChecksCard distinguishes a failed rc-basis-gaps fetch from a real zero-gap result: destructive note + retry instead of the benign 'Inga verifikationer hittades'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
120 lines
4.0 KiB
TypeScript
120 lines
4.0 KiB
TypeScript
'use client'
|
||
|
||
import { useId, useState } from 'react'
|
||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||
import { AccountNumber } from '@/components/ui/account-number'
|
||
|
||
export interface DeclarationAccountLine {
|
||
accountNumber: string
|
||
accountName: string
|
||
amount: number
|
||
}
|
||
|
||
/**
|
||
* Default formatter: whole kronor, matching the filed SRU values. Truncation,
|
||
* not rounding: SFL "öretal faller bort" and the NE/INK2 SRU generators drop
|
||
* öre with Math.trunc, so the UI must agree with the filed figures. Callers
|
||
* that need öre pass their own formatter.
|
||
*/
|
||
export function formatWholeKronor(n: number): string {
|
||
// + 0 normalizes -0 (Math.trunc(-0.3) is -0, which sv-SE renders "−0").
|
||
return `${(Math.trunc(n) + 0).toLocaleString('sv-SE')} kr`
|
||
}
|
||
|
||
/**
|
||
* One expandable declaration row (NE-bilaga, INK2): ruta code chip, label,
|
||
* signed amount, and a per-account breakdown behind a keyboard-accessible
|
||
* toggle. Replaces the copy-pasted <tr onClick> rows that had no keyboard
|
||
* path, no aria-expanded, and double-encoded signs.
|
||
*
|
||
* `amount` is the SIGNED display value: expense callers pass the negated
|
||
* value instead of an isExpense flag, so a credit-balance expense renders
|
||
* with its true sign. Composes inside the Table primitive's TableBody.
|
||
*/
|
||
export function DeclarationRutaRow({
|
||
code,
|
||
label,
|
||
amount,
|
||
accounts = [],
|
||
hideWhenZero = true,
|
||
formatAmount = formatWholeKronor,
|
||
}: {
|
||
code: string
|
||
label: string
|
||
amount: number
|
||
accounts?: DeclarationAccountLine[]
|
||
hideWhenZero?: boolean
|
||
formatAmount?: (n: number) => string
|
||
}) {
|
||
const [expanded, setExpanded] = useState(false)
|
||
const panelId = useId()
|
||
|
||
if (amount === 0 && accounts.length === 0 && hideWhenZero) return null
|
||
|
||
const hasAccounts = accounts.length > 0
|
||
|
||
return (
|
||
<>
|
||
<tr
|
||
className={`border-b ${hasAccounts ? 'cursor-pointer hover:bg-muted/50 transition-colors' : ''}`}
|
||
onClick={() => hasAccounts && setExpanded((v) => !v)}
|
||
>
|
||
<td className="py-2">
|
||
{hasAccounts && (
|
||
<button
|
||
type="button"
|
||
aria-expanded={expanded}
|
||
aria-controls={panelId}
|
||
aria-label={expanded ? `Dölj konton för ${code}` : `Visa konton för ${code}`}
|
||
className="mr-1 inline-flex h-6 w-6 items-center justify-center rounded align-middle hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
setExpanded((v) => !v)
|
||
}}
|
||
>
|
||
{expanded ? (
|
||
<ChevronDown className="h-4 w-4" />
|
||
) : (
|
||
<ChevronRight className="h-4 w-4" />
|
||
)}
|
||
</button>
|
||
)}
|
||
<span className="font-mono text-xs bg-muted px-1 rounded mr-2">{code}</span>
|
||
{label}
|
||
{hasAccounts && (
|
||
<span className="text-xs text-muted-foreground ml-2">
|
||
({accounts.length} konton)
|
||
</span>
|
||
)}
|
||
</td>
|
||
<td className="py-2 text-right tabular-nums">{formatAmount(amount)}</td>
|
||
</tr>
|
||
{expanded && hasAccounts && (
|
||
<tr id={panelId}>
|
||
<td colSpan={2} className="py-2 pl-8 bg-muted/20">
|
||
<table className="w-full text-xs">
|
||
<tbody>
|
||
{accounts.map((acc) => (
|
||
<tr key={acc.accountNumber}>
|
||
<td className="py-1">
|
||
<AccountNumber
|
||
number={acc.accountNumber}
|
||
name={acc.accountName}
|
||
size="sm"
|
||
/>
|
||
</td>
|
||
<td className="py-1">{acc.accountName}</td>
|
||
<td className="py-1 text-right tabular-nums">
|
||
{formatAmount(acc.amount)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</>
|
||
)
|
||
}
|