fix(vat): keep the RC-basis worklist visible until every voucher is fixed (#1164)
Correcting a single voucher cleared the momsdeklaration's RC_BASIS_MISSING error and the whole per-voucher worklist with it: the check tested mere presence of ruta 20-24 basis, the stepper re-derived its landing step and yanked the user to Granska mid-work, and the remounted checks card never refetched gaps once the aggregate check stopped firing. The declaration then claimed "klart" while the remaining vouchers still under-reported rutor 20-24 (FK004). - Make RC_BASIS_MISSING/RC_OUTPUT_MISSING proportional: compare reported basis against the basis the per-rate output boxes imply (moms/sats), with a 0.5% + 1 kr tolerance for per-voucher ore rounding. - Fetch the rc-basis-gaps worklist once per period, ungated from the aggregate check, so remaining rows survive remounts. - Latch the automatic stepper landing once per period so a refetch after a korrigering cannot navigate the user off Kontrollera. - Resolve rc-basis-gaps against the rakenskapsar (fiscal_period_id) for helarsmoms, matching the declaration totals; a calendar span hid gap vouchers in the tail of an extended first year. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
98e1a48c2b
commit
98886e68d8
@@ -107,10 +107,9 @@ export function VatChecksCard({
|
||||
// Gap fetch tagged with the key it was requested under: loading is derived
|
||||
// by comparing tags, so the effect never sets state synchronously. Fixed
|
||||
// rows are removed via removedIds (the fetch itself is not re-run after a
|
||||
// korrigering: the period key is unchanged). The key is period-only, NOT
|
||||
// gated on RC_BASIS_MISSING: the aggregate check clears as soon as ONE
|
||||
// voucher is corrected, and the remaining unfixed rows must survive the
|
||||
// declaration refetch that follows each korrigering.
|
||||
// korrigering: the period key is unchanged). The key is period-only so the
|
||||
// list survives the declaration refetch that follows each korrigering, and
|
||||
// survives a remount even when the aggregate check no longer fires.
|
||||
const gapsKey = `${periodType}:${year}:${period}:${fiscalPeriodId ?? ''}`
|
||||
const [gapsResult, setGapsResult] = useState<{
|
||||
key: string
|
||||
@@ -145,25 +144,26 @@ export function VatChecksCard({
|
||||
const gapsFetched = gapsResult !== null && gapsResult.key === gapsKey
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch once per period, and only while the aggregate check flags a gap:
|
||||
// the fetched list then outlives the check, which clears after the first
|
||||
// correction even though other vouchers may remain broken.
|
||||
if (!hasRcBasisGaps || gapsFetched) return
|
||||
// Fetch once per period, NOT gated on the aggregate check: the check
|
||||
// compares period totals and can clear (or never fire) while individual
|
||||
// vouchers still miss their basis pair. Gating the fetch on it meant a
|
||||
// remount after the first korrigering showed "inga fel" with the
|
||||
// remaining broken vouchers silently hidden.
|
||||
if (gapsFetched) return
|
||||
let cancelled = false
|
||||
const params = new URLSearchParams({
|
||||
periodType,
|
||||
year: String(year),
|
||||
period: String(period),
|
||||
})
|
||||
// Forward-compatible: the route reads calendar params today, but yearly
|
||||
// (helårsmoms) declarations resolve against the räkenskapsår.
|
||||
// Yearly (helårsmoms) resolves against the räkenskapsår server-side.
|
||||
if (fiscalPeriodId) params.set('fiscal_period_id', fiscalPeriodId)
|
||||
fetch(`/api/reports/vat-declaration/rc-basis-gaps?${params.toString()}`)
|
||||
.then(async (r) => {
|
||||
const j = await r.json().catch(() => null)
|
||||
if (cancelled) return
|
||||
// A failed fetch must not masquerade as "no gaps found": the check
|
||||
// above says there ARE gaps, so an empty list here would mislead.
|
||||
// A failed fetch must not masquerade as "no gaps found": an empty
|
||||
// list would mislead whenever gaps actually exist.
|
||||
if (!r.ok || j?.error) setGapsResult({ key: gapsKey, gaps: [], failed: true })
|
||||
else setGapsResult({ key: gapsKey, gaps: j?.data?.gaps || [] })
|
||||
})
|
||||
@@ -173,7 +173,7 @@ export function VatChecksCard({
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [hasRcBasisGaps, gapsFetched, gapsKey, periodType, year, period, fiscalPeriodId])
|
||||
}, [gapsFetched, gapsKey, periodType, year, period, fiscalPeriodId])
|
||||
|
||||
const gaps = gapsFetched
|
||||
? gapsResult.gaps.filter((g) => !removedIds.has(g.entryId))
|
||||
|
||||
@@ -1627,6 +1627,16 @@ export function VatDeclarationView() {
|
||||
const checksBlocked = checks.some((c) => c.status === 'ERROR')
|
||||
const errorCount = checks.filter((c) => c.status === 'ERROR').length
|
||||
const warningCount = checks.filter((c) => c.status === 'WARNING').length
|
||||
// Latch the automatic landing step once per period, as a render-phase
|
||||
// adjustment when the period's declaration first settles. Deriving it live
|
||||
// from checksBlocked navigated the user away mid-work: the refetch after a
|
||||
// korrigering can clear the aggregate error while the Kontrollera worklist
|
||||
// still holds broken vouchers, and the view would jump to Granska under
|
||||
// their feet. The period-change effect below resets chosenStep to null,
|
||||
// which re-arms this latch for the next period.
|
||||
if (chosenStep === null && upToDate && data && !error) {
|
||||
setChosenStep(checksBlocked ? 1 : 2)
|
||||
}
|
||||
const activeStep = chosenStep ?? (checksBlocked ? 1 : 2)
|
||||
|
||||
// Settings not settled yet — the picker defaults and the gate both depend
|
||||
|
||||
Reference in New Issue
Block a user