feat(skatt): setup gates + auto-loading momsdeklaration across Skatt & bokslut tabs (#885)
* feat(skatt): setup gates + auto-loading momsdeklaration across Skatt & bokslut tabs Every tab in the Skatt & bokslut nav group now tells an unconfigured user what is missing and where to fix it, instead of dead-ending or rendering zeros: - Momsdeklaration: gates on vat_registered with a settings CTA; auto-fetches the configured period on load and on every period change (no more "Hämta" button); one period control (räkenskapsår picked inline for helårsmoms, shell selector + back link dropped via new ReportDescriptor.standalone); raw <select>s replaced with the Select primitive; banner when moms_period is missing; BankID connect returns to the page instead of the report library. - Deadlines: callout (sv+en) when no system-generated tax deadlines exist — they are derived from tax settings, so point at /settings/tax rather than presenting an empty manual todo list. - Årsbokslut: "no räkenskapsår yet" (CTA to bookkeeping settings) is now distinguished from "nothing to close yet". - Skattekonto: non-auth fetch failures render an error card with retry instead of the misleading "inget saldo hämtat ännu" empty state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatt): address bot review — res.ok guard, blocking moms_period gate, panel hidden while räkenskapsår unresolved - Auto-fetch treats non-2xx or unparsable responses as errors (with retry) instead of rendering undefined data. - momsPeriodMissing now blocks the declaration (EmptyState + settings CTA) rather than fetching a guessed quarterly period behind a banner — a declaration submittable for the wrong period type is a hazard, not a convenience. - SkatteverketPanel is not rendered while yearly mode awaits a fiscal period, so its actions can never target an unconfirmed period. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
100a4d1291
commit
9f7c842a33
@@ -15,7 +15,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { ArrowLeft, Lock } from 'lucide-react'
|
||||
import { ArrowLeft, CalendarPlus, Lock } from 'lucide-react'
|
||||
import AgentSparkleButton from '@/components/agent/AgentSparkleButton'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
@@ -55,6 +55,9 @@ export default function YearEndPage() {
|
||||
|
||||
// ---- Period selection ----
|
||||
const [periods, setPeriods] = useState<PeriodOption[] | null>(null)
|
||||
// Whether the company has ANY fiscal periods (eligible or not) — separates
|
||||
// "inget att stänga ännu" from "inget räkenskapsår har skapats".
|
||||
const [hasAnyPeriods, setHasAnyPeriods] = useState(true)
|
||||
const [periodsError, setPeriodsError] = useState<string | null>(null)
|
||||
const [selectedPeriodId, setSelectedPeriodId] = useState<string | null>(
|
||||
searchParams.get('period') ?? null,
|
||||
@@ -91,6 +94,7 @@ export default function YearEndPage() {
|
||||
eligible.sort((a, b) => a.period_start.localeCompare(b.period_start))
|
||||
if (cancelled) return
|
||||
setPeriods(eligible)
|
||||
setHasAnyPeriods((data ?? []).length > 0)
|
||||
if (!selectedPeriodId && eligible.length > 0) {
|
||||
setSelectedPeriodId(eligible[0].id)
|
||||
}
|
||||
@@ -235,11 +239,21 @@ export default function YearEndPage() {
|
||||
)}
|
||||
|
||||
{periods !== null && periods.length === 0 && (
|
||||
<EmptyState
|
||||
icon={Lock}
|
||||
title="Inga perioder att stänga"
|
||||
description="Det finns ingen öppen räkenskapsperiod vars slutdatum redan har passerat. Bokslut görs efter att periodens slutdatum är passerat."
|
||||
/>
|
||||
hasAnyPeriods ? (
|
||||
<EmptyState
|
||||
icon={Lock}
|
||||
title="Inga perioder att stänga"
|
||||
description="Det finns ingen öppen räkenskapsperiod vars slutdatum redan har passerat. Bokslut görs efter att periodens slutdatum är passerat."
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
icon={CalendarPlus}
|
||||
title="Inget räkenskapsår ännu"
|
||||
description="Bokslut görs per räkenskapsår. Skapa företagets räkenskapsår i bokföringsinställningarna för att komma igång."
|
||||
actionLabel="Öppna bokföringsinställningar"
|
||||
actionHref="/settings/bookkeeping"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{showWizard && periods && periods.length > 1 && step !== 'result' && (
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useToast } from '@/components/ui/use-toast'
|
||||
import { ToastAction } from '@/components/ui/toast'
|
||||
import { DeadlineList } from '@/components/deadlines/DeadlineList'
|
||||
import { PageHeader } from '@/components/ui/page-header'
|
||||
import { AlertTriangle, ArrowRight } from 'lucide-react'
|
||||
import { AlertTriangle, ArrowRight, CalendarClock } from 'lucide-react'
|
||||
import { useCompany } from '@/contexts/CompanyContext'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import type { Deadline } from '@/types'
|
||||
@@ -224,10 +224,33 @@ export default function DeadlinesPage() {
|
||||
)
|
||||
}
|
||||
|
||||
// Statutory deadlines (moms, arbetsgivardeklaration, F-skatt) are generated
|
||||
// from the company's tax settings — none present usually means those
|
||||
// settings were never filled in, so point there instead of letting the page
|
||||
// read as an empty manual todo list.
|
||||
const hasSystemDeadlines = deadlines.some((d) => d.source === 'system')
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<PageHeader title={t('title')} />
|
||||
|
||||
{!hasSystemDeadlines && (
|
||||
<Link href="/settings/tax" className="group block">
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg border px-4 py-3 transition-colors hover:bg-secondary/60">
|
||||
<div className="flex items-center gap-3">
|
||||
<CalendarClock className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
<p className="text-sm">
|
||||
<span className="font-medium">{t('no_system_deadlines_title')}</span>
|
||||
<span className="text-muted-foreground ml-1.5">
|
||||
{t('no_system_deadlines_description')}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:translate-x-0.5 transition-transform flex-shrink-0" />
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Overdue invoices alert */}
|
||||
{overdueInvoices.count > 0 && (
|
||||
<Link href="/invoices?status=unpaid" className="group block">
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useToast } from '@/components/ui/use-toast'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver'
|
||||
import {
|
||||
AlertCircle,
|
||||
Copy,
|
||||
ExternalLink,
|
||||
FileCheck,
|
||||
@@ -65,6 +66,7 @@ export default function SkattekontoPage() {
|
||||
const [syncing, setSyncing] = useState(false)
|
||||
const [bookingId, setBookingId] = useState<string | null>(null)
|
||||
const [notConnected, setNotConnected] = useState(false)
|
||||
const [loadError, setLoadError] = useState(false)
|
||||
const [matchOpenFor, setMatchOpenFor] = useState<StoredSkattekontoTransaction | null>(
|
||||
null,
|
||||
)
|
||||
@@ -74,6 +76,7 @@ export default function SkattekontoPage() {
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setLoadError(false)
|
||||
try {
|
||||
const [saldoRes, txRes] = await Promise.all([
|
||||
fetch('/api/extensions/ext/skatteverket/skattekonto/saldo'),
|
||||
@@ -85,6 +88,14 @@ export default function SkattekontoPage() {
|
||||
return
|
||||
}
|
||||
|
||||
// A non-auth failure must NOT fall through to the "inget saldo hämtat
|
||||
// ännu"-tomvy — that reads as "not configured" when the truth is "the
|
||||
// fetch broke". Surface it as an error with a retry instead.
|
||||
if (!saldoRes.ok) {
|
||||
setLoadError(true)
|
||||
return
|
||||
}
|
||||
|
||||
const saldoJson = (await saldoRes.json()) as SaldoEnvelope
|
||||
setSaldo(saldoJson)
|
||||
|
||||
@@ -92,6 +103,8 @@ export default function SkattekontoPage() {
|
||||
const txJson = (await txRes.json()) as TransaktionerEnvelope
|
||||
setTx(txJson.data)
|
||||
}
|
||||
} catch {
|
||||
setLoadError(true)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -246,6 +259,28 @@ export default function SkattekontoPage() {
|
||||
)
|
||||
}
|
||||
|
||||
if (loadError) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeading />
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center py-12 text-center">
|
||||
<AlertCircle className="mb-4 h-10 w-10 text-muted-foreground/40" />
|
||||
<p className="mb-1 font-medium">Kunde inte hämta skattekontot</p>
|
||||
<p className="mb-4 max-w-md text-sm text-muted-foreground">
|
||||
Något gick fel när saldo och transaktioner skulle hämtas. Försök
|
||||
igen om en stund.
|
||||
</p>
|
||||
<Button variant="outline" onClick={() => void reload()}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Försök igen
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeading
|
||||
|
||||
Reference in New Issue
Block a user