fix(year-end): typed preflight blocker codes so remediation links render (#1420)
* fix(year-end): typed preflight blocker codes so remediation links render validateYearEndReadiness emits Swedish blocker strings but the wizard's BlockerRow matched English phrases, so no remediation link ever rendered, and the voucher-gap branch pointed at /bookkeeping/voucher-gaps which only exists as an API route. Blockers now carry stable machine codes end to end (YearEndBlockerCode on YearEndValidation.blockers, mirrored additively as blockerItems on BokslutReadinessReport); errors stays the plain string mirror so the v1 compliance check and MCP tool keep their exact shapes. BlockerRow matches on code and links only to pages that exist; the voucher-gap and dead-link branches are removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(year-end): code the unbooked-transaction blockers #1414 added #1414 landed two new blockers in validateYearEndReadiness using the old errors.push style, which this branch had already renamed to a typed blockers array. Merging main left them referencing a variable that no longer exists. Converted both to the typed scheme: UNBOOKED_TRANSACTIONS (the safety guard that stops executeYearEndClosing from aborting at the step 7 lock AFTER the closing entry posted at step 4) and UNBOOKED_CHECK_FAILED (the fail-closed variant). Neither behaviour changes; both keep their Swedish wording verbatim. The MCP year_end_readiness classifier now routes on the stable YearEndBlockerCode instead of regexing the Swedish message, with the wording heuristic kept as a fallback for an unmapped or legacy English message. The public `kind` values are unchanged, so MCP consumers see the same output; both new codes map to 'unbooked_transactions' as before, since an agent reacts to "we could not tell" the same way it reacts to a real count. UNBOOKED_TRANSACTIONS gets a /transactions remediation link in the preflight step: that page is where a transaction is booked or marked private, the two remedies the message names. UNBOOKED_CHECK_FAILED gets none: the remedy is to re-run the check. --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
31fba5e8f9
commit
78a37f6396
@@ -15,6 +15,12 @@ interface PreflightStepProps {
|
||||
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 (
|
||||
@@ -48,6 +54,11 @@ export function PreflightStep({ report, isLoading, error, onContinue }: Prefligh
|
||||
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 (
|
||||
<div className="space-y-8">
|
||||
{/* Period line: muted text for the normal state, chip only when the
|
||||
@@ -66,13 +77,13 @@ export function PreflightStep({ report, isLoading, error, onContinue }: Prefligh
|
||||
)}
|
||||
</div>
|
||||
|
||||
{report.blockers.length > 0 && (
|
||||
{blockerItems.length > 0 && (
|
||||
<section>
|
||||
<SectionHead icon={<XCircle className="h-3.5 w-3.5 text-destructive" aria-hidden="true" />}>
|
||||
Måste åtgärdas innan bokslut
|
||||
</SectionHead>
|
||||
{report.blockers.map((blocker, i) => (
|
||||
<BlockerRow key={i} blocker={blocker} report={report} />
|
||||
{blockerItems.map((blocker, i) => (
|
||||
<BlockerRow key={`${blocker.code ?? 'blocker'}-${i}`} blocker={blocker} />
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
@@ -121,30 +132,40 @@ export function PreflightStep({ report, isLoading, error, onContinue }: Prefligh
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a blocker with a contextual action link when we can derive one.
|
||||
* Falls back to plain text otherwise.
|
||||
* 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, report }: { blocker: string; report: BokslutReadinessReport }) {
|
||||
function BlockerRow({ blocker }: { blocker: DisplayBlocker }) {
|
||||
let href: string | null = null
|
||||
let actionLabel: string | null = null
|
||||
|
||||
if (/draft journal entries/i.test(blocker) && report.draftCount > 0) {
|
||||
href = '/bookkeeping?status=draft'
|
||||
actionLabel = 'Visa utkast'
|
||||
} else if (/voucher gap/i.test(blocker)) {
|
||||
href = '/bookkeeping/voucher-gaps'
|
||||
actionLabel = 'Hantera nummerlucka'
|
||||
} else if (/trial balance/i.test(blocker)) {
|
||||
href = '/reports/trial-balance'
|
||||
actionLabel = 'Öppna balansrapport'
|
||||
} else if (/continuity/i.test(blocker)) {
|
||||
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 (
|
||||
<div className="flex items-baseline justify-between gap-3 border-b border-border/60 px-1 py-3 text-[13px] leading-5 last:border-b-0">
|
||||
<p className="flex-1">{blocker}</p>
|
||||
<p className="flex-1">{blocker.message}</p>
|
||||
{href && actionLabel && (
|
||||
<Link href={href} className={QUIET_LINK_CLASS}>
|
||||
{actionLabel}
|
||||
|
||||
Reference in New Issue
Block a user