diff --git a/components/reports/ReportExportMenu.tsx b/components/reports/ReportExportMenu.tsx index 2e9b153f..3903b6bc 100644 --- a/components/reports/ReportExportMenu.tsx +++ b/components/reports/ReportExportMenu.tsx @@ -30,10 +30,13 @@ export function ReportExportMenu({ items, children, size = 'sm', + variant = 'outline', }: { items?: ReportExportItem[] children?: React.ReactNode size?: 'default' | 'sm' + /** Trigger style; the VAT Stegen page uses the primary pill. */ + variant?: 'outline' | 'default' }) { const t = useTranslations('reports') const hasItems = !!items && items.length > 0 @@ -44,7 +47,7 @@ export function ReportExportMenu({ {hasItems && ( - diff --git a/components/reports/SkatteverketPanel.tsx b/components/reports/SkatteverketPanel.tsx index 20c2bd32..ea321465 100644 --- a/components/reports/SkatteverketPanel.tsx +++ b/components/reports/SkatteverketPanel.tsx @@ -2,7 +2,6 @@ import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' import React, { useState, useEffect, useCallback } from 'react' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' @@ -678,12 +677,12 @@ function SkatteverketPanelInner({ if (loading) { return ( - - +
+
- - +
+
) } @@ -693,14 +692,14 @@ function SkatteverketPanelInner({ // trial sees the upsell instead of action buttons that would 403. if (!hasSkvCapability) { return ( - - - +
+
+

Skicka direkt till Skatteverket (valfritt) - - - +

+
+

Med ett abonnemang kan du ansluta med BankID och skicka deklarationen direkt härifrån, samt validera, spara utkast och signera. @@ -708,8 +707,8 @@ function SkatteverketPanelInner({ Direktinlämning till Skatteverket kräver ett abonnemang. - - +

+
) } @@ -719,14 +718,14 @@ function SkatteverketPanelInner({ // directly from Accounted, so frame it that way. if (!status?.connected) { return ( - - - +
+
+

Skicka direkt till Skatteverket (valfritt) - - - +

+
+
{notice?.kind === 'error' && (
Anslut med BankID - - +
+
) } @@ -754,13 +753,13 @@ function SkatteverketPanelInner({ const hasErrors = kontroller.some(k => k.status === 'ERROR') return ( - - +
+
- +

Skicka direkt till Skatteverket - +

@@ -902,8 +901,8 @@ function SkatteverketPanelInner({
- - +
+
{/* In-flight status for overflow-menu actions: their menu closes on select, so this row is the only visible sign of work. */} {actionLoading && ACTION_IN_FLIGHT_LABELS[actionLoading] && ( @@ -1067,8 +1066,8 @@ function SkatteverketPanelInner({ Valideringsfelen ovan måste åtgärdas innan deklarationen kan lämnas in.

)} - +
- +
) } diff --git a/components/reports/VatChecksCard.tsx b/components/reports/VatChecksCard.tsx index a82693b8..8ce361f7 100644 --- a/components/reports/VatChecksCard.tsx +++ b/components/reports/VatChecksCard.tsx @@ -2,8 +2,6 @@ import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' import { @@ -39,6 +37,8 @@ import type { RcBasisGap } from '@/lib/reports/rc-basis-gaps' import type { VatPeriodType } from '@/types' import { formatDate } from '@/lib/utils' import { useCanWrite } from '@/lib/hooks/use-can-write' +import { QUIET_LINK_CLASS } from '@/components/ui/dry-table' +import { cn } from '@/lib/utils' function formatAmount(amount: number): string { return amount.toLocaleString('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) @@ -102,8 +102,6 @@ export function VatChecksCard({ const { canWrite } = useCanWrite() const { dialogProps, confirm } = useDestructiveConfirm() - const errorCount = checks.filter((c) => c.status === 'ERROR').length - const warningCount = checks.length - errorCount const hasRcBasisGaps = checks.some((c) => c.code === 'RC_BASIS_MISSING') // Gap fetch tagged with the key it was requested under: loading is derived @@ -298,24 +296,7 @@ export function VatChecksCard({ const visibleGaps = showAll ? gaps : gaps.slice(0, GAP_PREVIEW_COUNT) return ( - - -
- Kontroll av underlaget - {errorCount > 0 ? ( - - {errorCount} {errorCount === 1 ? 'fel' : 'fel'} - - ) : warningCount > 0 ? ( - - {warningCount} {warningCount === 1 ? 'varning' : 'varningar'} - - ) : ( - Inga anmärkningar - )} -
-
- +
{checks.length === 0 && (
@@ -324,23 +305,19 @@ export function VatChecksCard({ )} {checks.length > 0 && ( -
+
{checks.map((c, i) => (
{c.status === 'ERROR' ? ( - +
@@ -461,21 +438,20 @@ export function VatChecksCard({ {formatAmount(gap.expectedBasisAmount)} kr - + + )} )}
)} - - +
) } diff --git a/components/reports/views/index.tsx b/components/reports/views/index.tsx index 405b1226..c7cf0080 100644 --- a/components/reports/views/index.tsx +++ b/components/reports/views/index.tsx @@ -11,8 +11,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' -import { AlertCircle, ChevronDown, ChevronRight, ExternalLink, FileCode, FileDown, Percent } from 'lucide-react' -import AgentSparkleButton from '@/components/agent/AgentSparkleButton' +import { AlertCircle, Check, ChevronDown, ChevronRight, ExternalLink, FileCode, FileDown, Percent } from 'lucide-react' import { Skeleton } from '@/components/ui/skeleton' import { EmptyState } from '@/components/ui/empty-state' import { @@ -23,7 +22,7 @@ import { SelectValue, } from '@/components/ui/select' import { FiscalYearSelector } from '@/components/common/FiscalYearSelector' -import { formatDate } from '@/lib/utils' +import { cn, formatDate } from '@/lib/utils' import { roundOre } from '@/lib/money' import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver' import { AccountNumber } from '@/components/ui/account-number' @@ -1085,12 +1084,15 @@ const SKATTEVERKET_MOMS_URL = */ function VatManualFilingCard({ xmlHref, pdfHref }: { xmlHref: string; pdfHref: string }) { return ( - - - Lämna in själv (utan anslutning) - - -

+

+
+

+ Lämna in själv, med fil +

+
+
+
+

Du behöver inte vara ansluten till Skatteverket för att lämna in.

    @@ -1123,8 +1125,8 @@ function VatManualFilingCard({ xmlHref, pdfHref }: { xmlHref: string; pdfHref: s
- - +
+
) } @@ -1144,6 +1146,7 @@ function VatBookingCard({ period, fiscalPeriodId, checksBlocked, + onStatus, }: { periodType: VatPeriodType year: number @@ -1155,6 +1158,8 @@ function VatBookingCard({ * accounts the settlement clears), but the user should know before filing. */ checksBlocked?: boolean + /** Lets the surrounding stepper mirror the booking state on its dot. */ + onStatus?: (status: 'booked' | 'draft' | 'none') => void }) { const { canWrite } = useCanWrite() const [dialogOpen, setDialogOpen] = useState(false) @@ -1199,6 +1204,12 @@ function VatBookingCard({ const booked = proposal?.existing_entries.find((e) => e.status === 'posted') const draft = booked ? undefined : proposal?.existing_entries.find((e) => e.status === 'draft') + const bookingStatus = booked ? 'booked' : draft ? 'draft' : 'none' + useEffect(() => { + if (upToDate && proposal) onStatus?.(bookingStatus) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [upToDate, bookingStatus]) + // FormLine amounts are input strings; the proposal's numbers are already // öre-rounded server-side, so this is display formatting, not money math. const initialLines: FormLine[] = (proposal?.lines ?? []).map((l) => ({ @@ -1209,20 +1220,16 @@ function VatBookingCard({ })) return ( - - - Bokför momsrapporten - - -

+

+

Skapa ett verifikat som nollställer periodens momskonton och bokför momsen att betala eller få tillbaka på redovisningskontot. Du granskar förslaget och kan ändra raderna innan verifikatet bokförs.

{booked && ( -
- +
+
+ ) +} + + +/** The Stegen header (concept Moms C): the filing pipeline as a clickable + * horizontal stepper with honest per-step status subs. Statutory surface, + * Swedish in both locales like the rest of the declaration. */ +function VatStepper({ + active, + onSelect, + errorCount, + warningCount, + ruta49, + bookingStatus, +}: { + active: number + onSelect: (step: number) => void + errorCount: number + warningCount: number + ruta49: number + bookingStatus: 'booked' | 'draft' | 'none' | null +}) { + const steps = [ + { + n: 1, + label: 'Kontrollera', + sub: + errorCount > 0 + ? `${errorCount} fel` + : warningCount > 0 + ? `${warningCount} ${warningCount === 1 ? 'varning' : 'varningar'}` + : 'klart', + done: errorCount === 0, + warn: errorCount > 0, + }, + { + n: 2, + label: 'Granska', + sub: + ruta49 > 0 + ? `${formatAmount(ruta49)} kr att betala` + : ruta49 < 0 + ? `${formatAmount(Math.abs(ruta49))} kr att återfå` + : 'ingen moms', + done: false, + warn: false, + }, + { + n: 3, + label: 'Bokför', + sub: + bookingStatus === 'booked' + ? 'bokförd' + : bookingStatus === 'draft' + ? 'utkast finns' + : 'mot 2650', + done: bookingStatus === 'booked', + warn: false, + }, + { n: 4, label: 'Lämna in', sub: 'till Skatteverket', done: false, warn: false }, + ] + + return ( +
+ {steps.map((step, i) => ( + + {i > 0 && + ))} +
) } @@ -1358,6 +1477,11 @@ export function VatDeclarationView() { error?: string } | null>(null) const [retryKey, setRetryKey] = useState(0) + // Stegen: which of the four pipeline steps is open. null = automatic + // (errors land on Kontrollera, otherwise Granska). A period switch resets + // to automatic so stale step choices never survive a context change. + const [chosenStep, setChosenStep] = useState(null) + const [bookingStatus, setBookingStatus] = useState<'booked' | 'draft' | 'none' | null>(null) // Company settings drive both the momsregistrerad gate and the default // periodicity (moms_period in Inställningar). Applied once per company the @@ -1452,6 +1576,11 @@ export function VatDeclarationView() { ? null : `${periodType}:${year}:${period}:${isYearly ? fiscalPeriodId : ''}:${retryKey}` + useEffect(() => { + setChosenStep(null) + setBookingStatus(null) + }, [periodType, year, period, fiscalPeriodId]) + useEffect(() => { if (!fetchKey || periodType === null) return const params = new URLSearchParams({ @@ -1495,6 +1624,9 @@ export function VatDeclarationView() { // but concern manual filers just as much. const checks = data ? runVatDeclarationChecks(data.rutor) : [] 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 + const activeStep = chosenStep ?? (checksBlocked ? 1 : 2) // Settings not settled yet — the picker defaults and the gate both depend // on them, so hold the whole view in a skeleton. @@ -1542,28 +1674,20 @@ export function VatDeclarationView() { {/* XML and PDF live in "Lämna in" below: they are filing artifacts, not report exports, and one home avoids two competing download surfaces. */} - - + /> - {/* Period selection — the declaration below follows it automatically */} - - -
-
- + {/* Period selection — the declaration below follows it automatically. + Flat toolbar row (concept language), far right like a context picker. */} +
-
{isYearly ? ( // Annual VAT covers a räkenskapsår: picked here, not a // calendar year. @@ -1587,10 +1710,8 @@ export function VatDeclarationView() { /> ) : ( <> -
- -
-
- -
)} -
- - +
{error && ( @@ -1651,14 +1766,21 @@ export function VatDeclarationView() {
- {/* The page follows the filing pipeline: kontrollera, granska, - bokför, lämna in. The checks come first because their errors - invalidate everything below them. */} -
-

- 1 · Kontrollera underlaget -

- + + {activeStep === 1 && ( +
+ setRetryKey((k) => k + 1)} /> -
- -
-

- 2 · Granska deklarationen -

- - -
- Momsdeklaration - {data.period.start} till {data.period.end} -
- 0 - ? 'warning' - : data.rutor.ruta49 < 0 - ? 'success' - : 'secondary' - } - > - {data.rutor.ruta49 > 0 - ? 'Att betala' - : data.rutor.ruta49 < 0 - ? 'Att återfå' - : 'Ingen moms'} - - {data.rutor.ruta49 !== 0 && ( - - {formatAmount(Math.abs(data.rutor.ruta49))} kr - - )} -
-
-
- -
- Baserat på {data.invoiceCount} fakturor och {data.transactionCount} transaktioner +
+
+
+ )} -
+ {activeStep === 2 && ( +
+
+
+

+ Momsdeklaration · {data.period.start} till {data.period.end} +

+ + {data.invoiceCount} fakturor · {data.transactionCount} transaktioner + +
+
+
{/* Utgående moms */}
-

+

Utgående moms (försäljning)

@@ -1790,7 +1893,7 @@ export function VatDeclarationView() { {(data.rutor.ruta20 > 0 || data.rutor.ruta21 > 0 || data.rutor.ruta22 > 0 || data.rutor.ruta23 > 0 || data.rutor.ruta24 > 0 || data.rutor.ruta30 > 0 || data.rutor.ruta31 > 0 || data.rutor.ruta32 > 0) && ( <> -

+

Omvänd skattskyldighet (inköp)

@@ -1811,7 +1914,7 @@ export function VatDeclarationView() { {/* Moms vid import */} {(data.rutor.ruta50 > 0 || data.rutor.ruta60 > 0 || data.rutor.ruta61 > 0 || data.rutor.ruta62 > 0) && ( <> -

+

Moms vid import

@@ -1825,10 +1928,11 @@ export function VatDeclarationView() { )} - + +
{/* Ingående moms */}
-

+

Ingående moms (avdragsgill)

@@ -1868,63 +1972,77 @@ export function VatDeclarationView() {
+
- {/* Net result: the tables' sum row. The headline amount lives in - the card header next to the status badge. */} -
-
-
- 49 - - {data.rutor.ruta49 >= 0 ? 'Moms att betala' : 'Moms att återfå'} - -
- 0 - ? 'text-warning' - : data.rutor.ruta49 < 0 - ? 'text-success' - : '' - }`} - > - {formatAmount(Math.abs(data.rutor.ruta49))} kr - -
+ {/* Ruta 49 as the emphasized document foot (concept skv-foot). */} +
+ 49 + + {data.rutor.ruta49 >= 0 ? 'Moms att betala' : 'Moms att återfå'} + + + {formatAmount(Math.abs(data.rutor.ruta49))} kr + +
+
+
+
- - -
+
+ )} -
-

- 3 · Bokför momsen -

- + -
+
+ +
+ + )} -
-

- 4 · Lämna in -

- + + -
+ + )}
)} - {/* Skatteverket integration panel — hidden while the räkenskapsår for - helårsmoms is unresolved, so its actions can never target an - unconfirmed period. */} - {!awaitingFiscalPeriod && ( + {/* Skatteverket integration panel for the no-data states (fetch error, + empty period): with data it lives inside steg 4. Hidden while the + räkenskapsår for helårsmoms is unresolved, so its actions can never + target an unconfirmed period. */} + {!awaitingFiscalPeriod && !data && (