'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
)}
)
}
/**
* 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 (