fix(arsredovisning): unblock the signing flow, accept foreign parent org nr, explain Fortnox underlag failures (#1738)
Batch from a real migration walkthrough (Fortnox -> Accounted, 2026-08-20): - Årsredovisning: the "Låst version" select was empty with no explanation because the only version was a draft and "Lås version för underskrift" is disabled while the four Lagstadgade upplysningar checkboxes and the content confirmation count as blockers. The select is now disabled with a hint that names the blocker count and links to Fullständighetskontroll, the four AR-NOTE-*-UNCONFIRMED issues carry remediation text, the lock button explains why it is grey, and "Markera som signerad" says what it still needs (locked version, bevisreferens, date). - Moderföretagets org.nr accepts a foreign registration identifier (CHE-123.456.789, HRB 12345, 923 609 016); personnummer shapes stay out. - Fortnox underlag discovery: log status, body and Fortnox's message on failure, show the message in the UI, treat a 400 with behörighet/scope text as scopes-required, and fall back to an unfiltered voucherfileconnections list when the financialyear filter answers 400. - Kontomapping: the Momskod column had min-w only; table-fixed collapsed it and its selects overflowed into Konfidens. Real w-72 now. - SIE import warnings pluralise correctly for one skipped voucher; the Verifikationsserie option says the source series is preserved. 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
5a8dd21931
commit
e733ab7c43
@@ -48,6 +48,8 @@ interface AnnualReportStudioProps {
|
||||
hasUnsavedNarrative: boolean
|
||||
narrativeRevision: string | null
|
||||
onVersionsChanged?: (versions: AnnualReportVersionSummary[]) => void
|
||||
/** Blocking-issue count once the compliance check has loaded, null while loading. */
|
||||
onBlockingCountChanged?: (count: number | null) => void
|
||||
}
|
||||
|
||||
type NullableBoolean = boolean | null
|
||||
@@ -91,6 +93,7 @@ export function AnnualReportStudio({
|
||||
hasUnsavedNarrative,
|
||||
narrativeRevision,
|
||||
onVersionsChanged,
|
||||
onBlockingCountChanged,
|
||||
}: AnnualReportStudioProps) {
|
||||
const t = useTranslations('annualReportStudio')
|
||||
const { toast } = useToast()
|
||||
@@ -143,6 +146,11 @@ export function AnnualReportStudio({
|
||||
() => compliance?.validation.issues.filter((issue) => issue.severity === 'error') ?? [],
|
||||
[compliance],
|
||||
)
|
||||
// The signature section further down explains why "Låst version" is empty
|
||||
// in terms of this count, so it must not read 0 while we are still loading.
|
||||
useEffect(() => {
|
||||
onBlockingCountChanged?.(compliance ? blockingIssues.length : null)
|
||||
}, [compliance, blockingIssues.length, onBlockingCountChanged])
|
||||
const digitalOnlyIssues = useMemo(() => {
|
||||
const generalCodes = new Set(compliance?.validation.issues.map((issue) => issue.code) ?? [])
|
||||
return (
|
||||
@@ -496,7 +504,7 @@ export function AnnualReportStudio({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<section id="ar-fullstandighetskontroll" className="scroll-mt-24">
|
||||
<div className="mb-1 flex items-center gap-2 px-1">
|
||||
<h3 className="font-sans text-xs font-medium uppercase tracking-wider text-muted-foreground">{t('checks_title')}</h3>
|
||||
<div className="h-px flex-1 bg-border/60" />
|
||||
@@ -561,6 +569,13 @@ export function AnnualReportStudio({
|
||||
{t('lock_version')}
|
||||
</Button>
|
||||
</div>
|
||||
{(blockingIssues.length > 0 || hasUnsavedNarrative) && (
|
||||
<p className="text-right text-xs leading-5 text-muted-foreground">
|
||||
{hasUnsavedNarrative
|
||||
? t('lock_hint_unsaved')
|
||||
: t('lock_hint_blocked', { count: blockingIssues.length })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1069,6 +1069,7 @@ function OptionsStep({
|
||||
onStart: () => void
|
||||
onBack: () => void
|
||||
}) {
|
||||
const t = useTranslations('extensions')
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
|
||||
const toggleOption = (key: keyof MigrationOptions) => {
|
||||
@@ -1155,7 +1156,7 @@ function OptionsStep({
|
||||
<div className="flex items-center gap-3 border-t border-border py-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium">Verifikationsserie</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">Serie för importerade verifikationer</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">{t('ext_arcim_option_series_help')}</p>
|
||||
</div>
|
||||
<Input
|
||||
className="w-16 text-center"
|
||||
@@ -1597,6 +1598,13 @@ function DocumentImportFollowUp({
|
||||
? t('ext_arcim_documents_discovery_error')
|
||||
: t('ext_arcim_documents_import_error')}
|
||||
</p>
|
||||
{state.problem?.providerMessage && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('ext_arcim_documents_provider_message', {
|
||||
message: state.problem.providerMessage,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{state.problem?.requestId && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('ext_arcim_documents_error_reference', {
|
||||
|
||||
@@ -63,6 +63,8 @@ export interface ArcimDocumentImportProblem {
|
||||
requestId: string | null
|
||||
reconnectRequired: boolean
|
||||
message?: string
|
||||
/** What the source system (Fortnox/Bokio) itself answered, when known. */
|
||||
providerMessage?: string
|
||||
}
|
||||
|
||||
export function documentOAuthProblemFromReason(
|
||||
@@ -211,16 +213,25 @@ function problemFromPayload(payload: unknown): ArcimDocumentImportProblem {
|
||||
const error = (payload as { error?: unknown } | null)?.error
|
||||
const structured =
|
||||
error && typeof error === 'object'
|
||||
? (error as { code?: unknown; requestId?: unknown })
|
||||
? (error as { code?: unknown; requestId?: unknown; details?: unknown })
|
||||
: null
|
||||
const code = typeof structured?.code === 'string' ? structured.code : null
|
||||
const requestId =
|
||||
typeof structured?.requestId === 'string' ? structured.requestId : null
|
||||
const details =
|
||||
structured?.details && typeof structured.details === 'object'
|
||||
? (structured.details as { providerMessage?: unknown })
|
||||
: null
|
||||
const providerMessage =
|
||||
typeof details?.providerMessage === 'string' && details.providerMessage.trim()
|
||||
? details.providerMessage.trim()
|
||||
: null
|
||||
|
||||
return {
|
||||
code,
|
||||
requestId,
|
||||
reconnectRequired: code === PROVIDER_DOCUMENT_SCOPES_REQUIRED,
|
||||
...(providerMessage ? { providerMessage } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -248,7 +248,10 @@ export default function AccountMappingStep({
|
||||
<TableHead className="w-64 max-w-64">Källnamn</TableHead>
|
||||
<TableHead className="w-12"></TableHead>
|
||||
<TableHead className="w-64">Målkonto</TableHead>
|
||||
<TableHead className="min-w-72">{t('vat_treatment_column')}</TableHead>
|
||||
{/* table-fixed sizes columns from the header's width only:
|
||||
min-w collapsed this column to nothing on laptop widths
|
||||
and its selects overflowed into Konfidens (2026-08-20). */}
|
||||
<TableHead className="w-72">{t('vat_treatment_column')}</TableHead>
|
||||
<TableHead className="w-24">Konfidens</TableHead>
|
||||
<TableHead className="sticky right-0 z-20 w-32 min-w-32 border-l border-border bg-background text-right">
|
||||
{t('vat_treatment_confirm')}
|
||||
|
||||
Reference in New Issue
Block a user