Files
accounted/components/reports/DeclarationRutaRow.tsx
T
Jakob Wennberg 0ef5593388 refactor(reports): restructure declaration pages around the filing pipeline (#992)
* 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>
2026-07-11 23:22:52 +02:00

120 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)}
</>
)
}