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
@@ -54,6 +54,9 @@ function FocusedReportInner({ slug }: { slug: string }) {
|
||||
const report = getReport(slug)
|
||||
// Calendar (VAT family) and param-less reports don't need a fiscal period.
|
||||
const isPeriodless = report?.params === 'calendar' || report?.params === 'none'
|
||||
// Nav-promoted pages (Momsdeklaration) drop the library chrome: no back
|
||||
// link, no shell fiscal-year selector — the view owns its period controls.
|
||||
const isStandalone = !!report?.standalone
|
||||
const reportName = report ? t(report.labelKey) : slug
|
||||
const accountFilter = searchParams.get('account')
|
||||
|
||||
@@ -68,30 +71,34 @@ function FocusedReportInner({ slug }: { slug: string }) {
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<Link
|
||||
href="/reports"
|
||||
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
{t('back_to_library')}
|
||||
</Link>
|
||||
{!isStandalone && (
|
||||
<Link
|
||||
href="/reports"
|
||||
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
{t('back_to_library')}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<PageHeader
|
||||
title={reportName}
|
||||
action={
|
||||
<FiscalYearSelector
|
||||
value={selectedPeriod || null}
|
||||
onChange={(id, period) => {
|
||||
setSelectedPeriod(id || '')
|
||||
setSelectedPeriodBounds(
|
||||
period ? { start: period.period_start, end: period.period_end } : null,
|
||||
)
|
||||
setDateRange({})
|
||||
}}
|
||||
includeAllOption={false}
|
||||
hideFuturePeriods
|
||||
onReady={() => setIsReady(true)}
|
||||
/>
|
||||
isStandalone ? undefined : (
|
||||
<FiscalYearSelector
|
||||
value={selectedPeriod || null}
|
||||
onChange={(id, period) => {
|
||||
setSelectedPeriod(id || '')
|
||||
setSelectedPeriodBounds(
|
||||
period ? { start: period.period_start, end: period.period_end } : null,
|
||||
)
|
||||
setDateRange({})
|
||||
}}
|
||||
includeAllOption={false}
|
||||
hideFuturePeriods
|
||||
onReady={() => setIsReady(true)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -174,7 +181,7 @@ function FocusedView({
|
||||
case 'balance-sheet':
|
||||
return <BalanceSheetView periodId={periodId} dateRange={dateRange} onNavigateToAccount={onNavigateToAccount} />
|
||||
case 'vat-declaration':
|
||||
return <VatDeclarationView fiscalPeriodId={periodId} fiscalPeriodBounds={periodBounds} />
|
||||
return <VatDeclarationView />
|
||||
case 'periodisk-sammanstallning':
|
||||
return <PeriodiskSammanstallningView />
|
||||
case 'ne-declaration':
|
||||
|
||||
@@ -229,7 +229,12 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
|
||||
}, [fetchStatus])
|
||||
|
||||
const handleConnect = () => {
|
||||
window.location.href = '/api/extensions/ext/skatteverket/authorize'
|
||||
// return_to brings the user back to the momsdeklaration after the BankID
|
||||
// round-trip; the authorize route's default otherwise lands on the report
|
||||
// library. The callback appends skv_connected/skv_error itself.
|
||||
window.location.href =
|
||||
'/api/extensions/ext/skatteverket/authorize?return_to=' +
|
||||
encodeURIComponent('/reports/vat-declaration')
|
||||
}
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
|
||||
+227
-126
@@ -4,14 +4,24 @@
|
||||
// Rendered by the focused /reports/[slug] route (see components/reports/FocusedReport.tsx).
|
||||
// The regulated table/figure rendering is unchanged from the original monolith.
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { AlertCircle, ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { AlertCircle, ChevronDown, ChevronRight, Percent } from 'lucide-react'
|
||||
import AgentSparkleButton from '@/components/agent/AgentSparkleButton'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { EmptyState } from '@/components/ui/empty-state'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { FiscalYearSelector } from '@/components/common/FiscalYearSelector'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import { roundOre } from '@/lib/money'
|
||||
import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver'
|
||||
@@ -1001,42 +1011,56 @@ function ReportSectionTable({
|
||||
// (räkenskapsår); undefined for monthly/quarterly (calendar periods).
|
||||
const VatDrillContext = React.createContext<{ fiscalPeriodId?: string }>({})
|
||||
|
||||
export function VatDeclarationView({
|
||||
fiscalPeriodId,
|
||||
fiscalPeriodBounds,
|
||||
}: {
|
||||
fiscalPeriodId?: string
|
||||
fiscalPeriodBounds?: { start: string; end: string } | null
|
||||
} = {}) {
|
||||
export function VatDeclarationView() {
|
||||
const currentYear = new Date().getFullYear()
|
||||
const currentMonth = new Date().getMonth() + 1
|
||||
const currentQuarter = Math.ceil(currentMonth / 3)
|
||||
|
||||
const [periodType, setPeriodType] = useState<VatPeriodType>('quarterly')
|
||||
// periodType stays null until the company's VAT settings have settled, so
|
||||
// the first (automatic) fetch runs against the configured momsperiod instead
|
||||
// of a guessed default.
|
||||
const [periodType, setPeriodType] = useState<VatPeriodType | null>(null)
|
||||
const [year, setYear] = useState(currentYear)
|
||||
const [period, setPeriod] = useState(currentQuarter)
|
||||
const [data, setData] = useState<VatDeclaration | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
// Annual VAT (helårsmoms) is reported per räkenskapsår, not per calendar
|
||||
// year — picked inline in yearly mode. Monthly/quarterly are calendar
|
||||
// periods and need no fiscal year.
|
||||
const [fiscalPeriodId, setFiscalPeriodId] = useState('')
|
||||
// Latest fetch outcome, tagged with the fetch key it was requested under.
|
||||
// loading / error / data are all derived by comparing that tag with the
|
||||
// current key, so the fetch effect never sets state synchronously.
|
||||
const [result, setResult] = useState<{
|
||||
key: string
|
||||
declaration?: VatDeclaration
|
||||
error?: string
|
||||
} | null>(null)
|
||||
const [retryKey, setRetryKey] = useState(0)
|
||||
|
||||
// Default the periodicity to the company's configured VAT reporting period
|
||||
// (moms_period in Inställningar) so the picker mirrors the setting instead of
|
||||
// always starting on quarterly. Applied once per company the first time its
|
||||
// settings load; a later manual change to the picker is preserved, and a
|
||||
// company switch re-applies the new company's setting. `useCompanySettings`
|
||||
// only refetches when the active company changes, so this never clobbers a
|
||||
// Company settings drive both the momsregistrerad gate and the default
|
||||
// periodicity (moms_period in Inställningar). Applied once per company the
|
||||
// first time its settings settle — as a render-phase adjustment, not an
|
||||
// effect. A later manual change to the picker is preserved, and a company
|
||||
// switch re-applies the new company's setting. `useCompanySettings` only
|
||||
// refetches when the active company changes, so this never clobbers a
|
||||
// manual selection mid-session.
|
||||
const { settings } = useCompanySettings()
|
||||
const appliedForCompany = useRef<string | null>(null)
|
||||
useEffect(() => {
|
||||
const momsPeriod = settings?.moms_period
|
||||
const companyId = settings?.company_id
|
||||
if (!momsPeriod || !companyId) return
|
||||
if (appliedForCompany.current === companyId) return
|
||||
appliedForCompany.current = companyId
|
||||
setPeriodType(momsPeriod)
|
||||
// `period` is reset to a sensible value by the periodType effect below.
|
||||
}, [settings])
|
||||
const { settings, isLoading: settingsLoading } = useCompanySettings()
|
||||
const [appliedCompany, setAppliedCompany] = useState<string | null>(null)
|
||||
const companyKey = settingsLoading ? null : (settings?.company_id ?? 'none')
|
||||
if (companyKey !== null && appliedCompany !== companyKey) {
|
||||
setAppliedCompany(companyKey)
|
||||
const configured = settings?.moms_period ?? 'quarterly'
|
||||
setPeriodType(configured)
|
||||
setPeriod(
|
||||
configured === 'monthly' ? currentMonth : configured === 'quarterly' ? currentQuarter : 1,
|
||||
)
|
||||
}
|
||||
|
||||
// Settings row present and the company answered "not VAT-registered" —
|
||||
// the declaration is meaningless, so the whole view is gated below.
|
||||
const notVatRegistered = !settingsLoading && settings !== null && !settings.vat_registered
|
||||
// Registered but never picked a redovisningsperiod (rare — onboarding
|
||||
// requires it, but companies created outside that flow can miss it).
|
||||
const momsPeriodMissing = settings?.vat_registered === true && !settings.moms_period
|
||||
|
||||
// Generate year options (last 5 years)
|
||||
const yearOptions = Array.from({ length: 5 }, (_, i) => currentYear - i)
|
||||
@@ -1073,24 +1097,22 @@ export function VatDeclarationView({
|
||||
}
|
||||
}
|
||||
|
||||
// Reset period when type changes
|
||||
useEffect(() => {
|
||||
if (periodType === 'monthly') {
|
||||
setPeriod(currentMonth)
|
||||
} else if (periodType === 'quarterly') {
|
||||
setPeriod(currentQuarter)
|
||||
} else {
|
||||
setPeriod(1)
|
||||
}
|
||||
}, [periodType, currentMonth, currentQuarter])
|
||||
// Switching periodicity resets the period to "now" in the new unit. Done in
|
||||
// the change handler (not an effect) so the auto-fetch below never sees an
|
||||
// inconsistent periodType/period pair.
|
||||
const handlePeriodTypeChange = (value: VatPeriodType) => {
|
||||
setPeriodType(value)
|
||||
setPeriod(value === 'monthly' ? currentMonth : value === 'quarterly' ? currentQuarter : 1)
|
||||
}
|
||||
|
||||
// Annual VAT (helårsmoms) is reported per räkenskapsår, not per calendar year.
|
||||
// For yearly we pass the selected fiscal period so the API uses its actual
|
||||
// bounds (handles extended/shortened years); monthly/quarterly stay calendar.
|
||||
const isYearly = periodType === 'yearly'
|
||||
const awaitingFiscalPeriod = isYearly && !fiscalPeriodId
|
||||
const vatQueryString = () => {
|
||||
const params = new URLSearchParams({
|
||||
periodType,
|
||||
periodType: periodType ?? 'quarterly',
|
||||
year: String(year),
|
||||
period: String(period),
|
||||
})
|
||||
@@ -1098,24 +1120,91 @@ export function VatDeclarationView({
|
||||
return params.toString()
|
||||
}
|
||||
|
||||
const fetchDeclaration = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/reports/vat-declaration?${vatQueryString()}`
|
||||
)
|
||||
const result = await res.json()
|
||||
if (result.error) {
|
||||
setError(result.error)
|
||||
} else {
|
||||
setData(result.data)
|
||||
}
|
||||
} catch {
|
||||
setError('Kunde inte hämta momsdeklaration')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
// The declaration loads as soon as the period is known — no manual "Hämta"
|
||||
// step. fetchKey is null while a prerequisite is missing (settings pending,
|
||||
// gated, or no redovisningsperiod configured); any change to it triggers a
|
||||
// refetch and stale responses are discarded.
|
||||
const fetchKey =
|
||||
periodType === null || notVatRegistered || momsPeriodMissing || awaitingFiscalPeriod
|
||||
? null
|
||||
: `${periodType}:${year}:${period}:${isYearly ? fiscalPeriodId : ''}:${retryKey}`
|
||||
|
||||
useEffect(() => {
|
||||
if (!fetchKey || periodType === null) return
|
||||
const params = new URLSearchParams({
|
||||
periodType,
|
||||
year: String(year),
|
||||
period: String(period),
|
||||
})
|
||||
if (periodType === 'yearly') params.set('fiscal_period_id', fiscalPeriodId)
|
||||
let cancelled = false
|
||||
fetch(`/api/reports/vat-declaration?${params.toString()}`)
|
||||
.then(async (res) => {
|
||||
const json = await res.json().catch(() => null)
|
||||
if (cancelled) return
|
||||
if (!res.ok || json?.error) {
|
||||
setResult({
|
||||
key: fetchKey,
|
||||
error:
|
||||
typeof json?.error === 'string' ? json.error : 'Kunde inte hämta momsdeklaration',
|
||||
})
|
||||
} else {
|
||||
setResult({ key: fetchKey, declaration: json.data })
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setResult({ key: fetchKey, error: 'Kunde inte hämta momsdeklaration' })
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [fetchKey, periodType, year, period, fiscalPeriodId])
|
||||
|
||||
// Derived fetch state: the previous declaration stays visible (dimmed)
|
||||
// while the next period loads.
|
||||
const upToDate = result !== null && result.key === fetchKey
|
||||
const data = result?.declaration ?? null
|
||||
const error = upToDate ? (result.error ?? null) : null
|
||||
const loading = fetchKey !== null && !upToDate
|
||||
|
||||
// Settings not settled yet — the picker defaults and the gate both depend
|
||||
// on them, so hold the whole view in a skeleton.
|
||||
if (settingsLoading || periodType === null) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<Skeleton className="h-5 w-32" />
|
||||
<Skeleton className="h-64" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (notVatRegistered) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Percent}
|
||||
title="Företaget är inte momsregistrerat"
|
||||
description="Momsdeklarationen bygger på företagets skatteinställningar. Om företaget är momsregistrerat anger du momsregistrering och redovisningsperiod i inställningarna, så visas deklarationen här."
|
||||
actionLabel="Öppna skatteinställningar"
|
||||
actionHref="/settings/tax"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Registered but no redovisningsperiod picked: block instead of guessing.
|
||||
// A declaration rendered (and submittable via panelen) for the wrong
|
||||
// period type is a compliance hazard, not a convenience.
|
||||
if (momsPeriodMissing) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Percent}
|
||||
title="Redovisningsperiod för moms saknas"
|
||||
description="Företaget är momsregistrerat men ingen redovisningsperiod (månad, kvartal eller helår) är vald. Ange den i skatteinställningarna så visas deklarationen för rätt period."
|
||||
actionLabel="Öppna skatteinställningar"
|
||||
actionHref="/settings/tax"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -1130,90 +1219,106 @@ export function VatDeclarationView({
|
||||
contextRef={`vat:${year}-${periodType}-${period}`}
|
||||
/>
|
||||
</ReportExportMenu>
|
||||
{/* Period selection */}
|
||||
|
||||
{/* Period selection — the declaration below follows it automatically */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Välj period</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div>
|
||||
<Label>Periodicitet</Label>
|
||||
<select
|
||||
<Select
|
||||
value={periodType}
|
||||
onChange={(e) => setPeriodType(e.target.value as VatPeriodType)}
|
||||
className="w-full mt-1 rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
onValueChange={(value) => handlePeriodTypeChange(value as VatPeriodType)}
|
||||
>
|
||||
<option value="monthly">Månadsvis</option>
|
||||
<option value="quarterly">Kvartalsvis</option>
|
||||
<option value="yearly">Årsvis</option>
|
||||
</select>
|
||||
<SelectTrigger className="mt-1 w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Månadsvis</SelectItem>
|
||||
<SelectItem value="quarterly">Kvartalsvis</SelectItem>
|
||||
<SelectItem value="yearly">Årsvis</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{isYearly ? (
|
||||
// Annual VAT covers the selected räkenskapsår — driven by the
|
||||
// fiscal-year picker on the report page, not a calendar year.
|
||||
<div>
|
||||
<Label>Räkenskapsår</Label>
|
||||
<div className="mt-1 rounded-md border border-input bg-muted/40 px-3 py-2 text-sm tabular-nums">
|
||||
{fiscalPeriodBounds
|
||||
? `${formatDate(fiscalPeriodBounds.start)} – ${formatDate(fiscalPeriodBounds.end)}`
|
||||
: '—'}
|
||||
</div>
|
||||
</div>
|
||||
// Annual VAT covers a räkenskapsår — picked here, not a
|
||||
// calendar year.
|
||||
<FiscalYearSelector
|
||||
value={fiscalPeriodId || null}
|
||||
onChange={(id) => setFiscalPeriodId(id || '')}
|
||||
includeAllOption={false}
|
||||
hideFuturePeriods
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<Label>År</Label>
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(parseInt(e.target.value))}
|
||||
className="w-full mt-1 rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
>
|
||||
{yearOptions.map((y) => (
|
||||
<option key={y} value={y}>
|
||||
{y}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select value={String(year)} onValueChange={(value) => setYear(parseInt(value))}>
|
||||
<SelectTrigger className="mt-1 w-28 tabular-nums">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{yearOptions.map((y) => (
|
||||
<SelectItem key={y} value={String(y)} className="tabular-nums">
|
||||
{y}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Period</Label>
|
||||
<select
|
||||
value={period}
|
||||
onChange={(e) => setPeriod(parseInt(e.target.value))}
|
||||
className="w-full mt-1 rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
<Select
|
||||
value={String(period)}
|
||||
onValueChange={(value) => setPeriod(parseInt(value))}
|
||||
>
|
||||
{getPeriodOptions().map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectTrigger className="mt-1 w-44">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{getPeriodOptions().map((opt) => (
|
||||
<SelectItem key={opt.value} value={String(opt.value)}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Button onClick={fetchDeclaration} disabled={loading}>
|
||||
{loading ? 'Laddar...' : 'Hämta'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{error && (
|
||||
<Card>
|
||||
<CardContent className="p-8 text-center text-destructive">
|
||||
<AlertCircle className="h-6 w-6 mx-auto mb-2" />
|
||||
{error}
|
||||
<CardContent className="flex flex-col items-center p-8 text-center">
|
||||
<AlertCircle className="mb-2 h-6 w-6 text-destructive" />
|
||||
<p className="mb-4 text-sm text-destructive">{error}</p>
|
||||
<Button variant="outline" size="sm" onClick={() => setRetryKey((k) => k + 1)}>
|
||||
Försök igen
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{data && (
|
||||
<>
|
||||
{!error && (awaitingFiscalPeriod || (loading && !data)) && (
|
||||
<Card>
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<Skeleton className="h-5 w-48" />
|
||||
<Skeleton className="h-64" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{data && !awaitingFiscalPeriod && (
|
||||
<div
|
||||
className={`space-y-4 transition-opacity duration-150 ${loading ? 'opacity-60' : ''}`}
|
||||
>
|
||||
<VatCompositionChart rutor={data.rutor} />
|
||||
|
||||
{/* Summary */}
|
||||
<Card className="border-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Momsdeklaration - {data.period.start} till {data.period.end}</CardTitle>
|
||||
@@ -1403,24 +1508,20 @@ export function VatDeclarationView({
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Skatteverket integration panel */}
|
||||
<SkatteverketPanel
|
||||
periodType={periodType}
|
||||
year={year}
|
||||
period={period}
|
||||
hasData={data !== null}
|
||||
rutor={data?.rutor ?? null}
|
||||
/>
|
||||
|
||||
{!data && !loading && !error && (
|
||||
<Card>
|
||||
<CardContent className="p-8 text-center text-muted-foreground">
|
||||
Välj period och klicka "Hämta" för att se momsdeklaration.
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Skatteverket integration panel — hidden while the räkenskapsår for
|
||||
helårsmoms is unresolved, so its actions can never target an
|
||||
unconfirmed period. */}
|
||||
{!awaitingFiscalPeriod && (
|
||||
<SkatteverketPanel
|
||||
periodType={periodType}
|
||||
year={year}
|
||||
period={period}
|
||||
hasData={data !== null}
|
||||
rutor={data?.rutor ?? null}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</VatDrillContext.Provider>
|
||||
|
||||
Reference in New Issue
Block a user