'use client' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { QUIET_LINK_CLASS } from '@/components/ui/dry-table' import { AlertTriangle, Info, XCircle } from 'lucide-react' import Link from 'next/link' import type { BokslutReadinessReport } from '@/lib/bokslut/readiness-aggregator' interface PreflightStepProps { report: BokslutReadinessReport | null isLoading: boolean error: string | null onContinue: () => void } /** A blocker as rendered: code is null for legacy responses without codes. */ interface DisplayBlocker { code: string | null message: string } /** Sans eyebrow section head with a trailing hairline (house idiom). */ function SectionHead({ icon, children }: { icon?: React.ReactNode; children: React.ReactNode }) { return (
{icon}

{children}

) } export function PreflightStep({ report, isLoading, error, onContinue }: PreflightStepProps) { if (isLoading) { return (
) } if (error) { return

{error}

} if (!report) { return null } // A response cached from before blockerItems shipped only has the plain // strings: fall back so blockers never disappear, just without links. const blockerItems: DisplayBlocker[] = report.blockerItems ?? report.blockers.map((message) => ({ code: null, message })) return (
{/* Period line: muted text for the normal state, chip only when the period deviates (design.md: chips mark exceptions). */}

{report.period.name}

{report.period.period_start} till {report.period.period_end}
{report.ready ? ( Redo för bokslut ) : ( Inte redo )}
{blockerItems.length > 0 && (
{blockerItems.map((blocker, i) => ( ))}
)} {report.warnings.length > 0 && (
{report.warnings.map((warning, i) => (

{warning}

))}
)} {report.reminders.length > 0 && (
{report.reminders.map((reminder) => (

{reminder.message}

{reminder.href && ( Öppna )}
))}
)}
) } /** * Renders a blocker with a contextual action link derived from its stable * machine code (YearEndBlockerCode). Codes without an existing remediation * page (voucher gaps, sequence counter, period state) render as plain text: * a link must never point at a page that does not exist. UNBOOKED_CHECK_FAILED * is deliberately link-less too: the remedy is to re-run the check, not to * visit a page. */ function BlockerRow({ blocker }: { blocker: DisplayBlocker }) { let href: string | null = null let actionLabel: string | null = null if (blocker.code === 'DRAFT_ENTRIES') { // The verifikat list has its own Utkast tab; it does not read a status // query param, so the link goes to the plain list. href = '/bookkeeping' actionLabel = 'Visa utkast' } else if (blocker.code === 'UNBOOKED_TRANSACTIONS') { // Transaktionslistan is where an unbooked transaction is either booked or // marked private, the two remedies the message names. href = '/transactions' actionLabel = 'Visa transaktioner' } else if (blocker.code === 'TRIAL_BALANCE_UNBALANCED') { href = '/reports/trial-balance' actionLabel = 'Öppna saldobalansen' } else if (blocker.code === 'CONTINUITY_MISMATCH') { // Saldobalansen lists ingående och utgående saldo per konto: the closest // existing surface for reviewing IB against prior-year UB. href = '/reports/trial-balance' actionLabel = 'Granska ingående balans' } return (

{blocker.message}

{href && actionLabel && ( {actionLabel} )}
) }