From 4bc2093e5179a0aeebf6f35779ea05791ea9a768 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:59:06 +0200 Subject: [PATCH] polish(vat): title-row Exportera, fused period chip, chip classification, calm SKV status (#1181) Founder feedback on the live momsdeklaration (2026-07-25): - The black Exportera now sits on the title row like every other page: standalone report pages render their own PageHeader (FocusedReport passes the title and skips its own), and the period chips get their own row below. - Year + quarter/month fuse into ONE chip ("Kvartal 3 2026") listing five years reverse-chronologically with month-span annotations; cadence stays behind the Period chip. Yearly keeps FyPicker, which is already a fused rakenskapsar chip. - The RC-basis worklist's Leverantorstyp/Typ av inkop selects (old boxy style with labels) become ContextPicker chips, in the toolbar and in each expanded row. - SkatteverketPanel connection status per the locked conventions: the contradictory "Ansluten" + "Session utgangen" badge cluster becomes muted "Ansluten" text for the normal state and one attn sentence with an embedded "Fornya med BankID" action for the expired session. Verified via sandbox screenshots (Playwright against dev). Co-authored-by: Claude Fable 5 --- DECISIONS.md | 1 + components/reports/FocusedReport.tsx | 22 ++- components/reports/SkatteverketPanel.tsx | 38 ++-- components/reports/VatChecksCard.tsx | 213 ++++++++++------------- components/reports/views/index.tsx | 199 +++++++++++---------- 5 files changed, 231 insertions(+), 242 deletions(-) diff --git a/DECISIONS.md b/DECISIONS.md index 55345b76..38d39000 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -371,3 +371,4 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-07-25] Removed invented 6-month minimum for first räkenskapsår: BFL 3 kap 3 § sets no floor (Bolagsverket: "hur kort som helst", max 18 months); the check only existed for isFirstPeriod, exactly the case the law exempts, and blocked a customer shortening an autumn-registered first year to Dec 31. [2026-07-25] Article EUR-price support bug: root cause was the edit dialog omitting currency from initialData (form defaulted SEK and PATCHed it back) plus kr-hardcoded formatCurrency calls; export gets a Valuta column + suffix-free decimalColumn instead of extending CURRENCY_FORMAT, importer Valuta detection deferred as follow-up to keep the diff scoped. [2026-07-25] Reinstated article deactivation as an explicit PATCH active-toggle button on the detail page (support: odinaero.se) instead of reverting DELETE to soft-delete: 8a9a930f intentionally made DELETE hard-delete for unused articles, but that left invoice-referenced articles (ARTICLE_IN_USE) with no retire path; the old deactivate i18n keys were still in messages/ and are reused. +[2026-07-25] Momsdeklaration period selection fused into one chip (year+quarter/month, reverse-chron, 5 years) instead of separate year and period pickers; cadence stays behind the Period chip. Standalone report pages now render their own PageHeader so Exportera sits on the title row per convention 9. diff --git a/components/reports/FocusedReport.tsx b/components/reports/FocusedReport.tsx index 8d6da661..7c7b56ca 100644 --- a/components/reports/FocusedReport.tsx +++ b/components/reports/FocusedReport.tsx @@ -113,10 +113,13 @@ function FocusedReportInner({ )} - { @@ -132,9 +135,9 @@ function FocusedReportInner({ initialPeriods={initialPeriods} initialCompanyId={initialCompanyId} /> - ) - } - /> + } + /> + )} {DATE_RANGE_SLUGS.has(slug) && selectedPeriodBounds && ( case 'vat-declaration': - return + return case 'periodisk-sammanstallning': return case 'ne-declaration': diff --git a/components/reports/SkatteverketPanel.tsx b/components/reports/SkatteverketPanel.tsx index 064bf156..39da0669 100644 --- a/components/reports/SkatteverketPanel.tsx +++ b/components/reports/SkatteverketPanel.tsx @@ -3,7 +3,6 @@ import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' import React, { useState, useEffect, useCallback } from 'react' import { Button } from '@/components/ui/button' -import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { DropdownMenu, @@ -49,8 +48,8 @@ interface SkatteverketStatus { * Codes from /lib/api-client.ts's SkatteverketAuthError that mean "the user * needs to reconnect with BankID before this action can succeed". When the API * returns one of these codes we flip the local status.expired flag so the - * "Session utgången" badge + "Förnya session" button surface, even if the - * upstream /status endpoint hasn't reflected the change yet. + * expired-session attn line with its "Förnya med BankID" action surfaces, + * even if the upstream /status endpoint hasn't reflected the change yet. */ const AUTH_RECONNECT_CODES = new Set([ 'NOT_CONNECTED', @@ -761,21 +760,12 @@ function SkatteverketPanelInner({ Skicka direkt till Skatteverket
- - - Ansluten - - {status.expired && ( - <> - - - Session utgången - - - + {/* Connected is the normal state here (the not-connected branch is + a different section): muted text, never a chip. The expired + session is the one exception and gets the attn sentence below + instead of a badge-and-button cluster. */} + {!status.expired && ( + Ansluten )} {/* Read-only lookups and recovery actions live in the overflow menu: the visible surface stays the forward path (validera, @@ -901,6 +891,18 @@ function SkatteverketPanelInner({
+ {status.expired && ( +

+ Sessionen mot Skatteverket har gått ut.{' '} + +

+ )}
{/* In-flight status for overflow-menu actions: their menu closes on diff --git a/components/reports/VatChecksCard.tsx b/components/reports/VatChecksCard.tsx index 9fba9167..930efe4c 100644 --- a/components/reports/VatChecksCard.tsx +++ b/components/reports/VatChecksCard.tsx @@ -3,14 +3,7 @@ import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { Button } from '@/components/ui/button' -import { Label } from '@/components/ui/label' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' +import { ContextPicker } from '@/components/common/ContextPicker' import { DataList, DataListMeta, @@ -71,6 +64,27 @@ const SUPPLY_LABELS: Record = { service: 'tjänst', goods: 'vara', } +/** Chip labels (sentence case); SUPPLY_LABELS stays lowercase for dialog prose. */ +const SUPPLY_DISPLAY: Record = { + service: 'Tjänst', + goods: 'Vara', +} + +const SUPPLIER_ITEMS = [ + { id: 'eu_business', label: 'EU-leverantör' }, + { id: 'non_eu_business', label: 'Utanför EU' }, + { id: 'swedish_business', label: 'Svensk omvänd skattskyldighet' }, +] + +// Non-EU + goods is import VAT (ruta 50/60-62), not reverse charge: the +// goods choice disappears entirely for non-EU suppliers. +const supplyItemsFor = (supplierType: SupplierType) => + supplierType === 'non_eu_business' + ? [{ id: 'service', label: 'Tjänst' }] + : [ + { id: 'service', label: 'Tjänst' }, + { id: 'goods', label: 'Vara' }, + ] /** How many gap rows render before the "Visa alla" toggle. */ const GAP_PREVIEW_COUNT = 8 @@ -355,54 +369,38 @@ export function VatChecksCard({

) : ( <> -
-
- - -
-
- - -
+ {/* Classification chips (house context-picker style), then the + bulk action: the chip values ("EU-leverantör", "Tjänst") + are self-describing, so no field labels. */} +
+ { + const supplierType = value as SupplierType + setSharedSel((prev) => ({ + supplierType, + // Non-EU + goods is import VAT, not reverse charge: + // coerce back to service so an invalid combo can't + // be mass-applied. + supplyType: + supplierType === 'non_eu_business' ? 'service' : prev.supplyType, + })) + }} + triggerLabel={SUPPLIER_LABELS[sharedSel.supplierType]} + ariaLabel="Leverantörstyp" + disabled={busy} + /> + + setSharedSel((prev) => ({ ...prev, supplyType: value as SupplyType })) + } + triggerLabel={SUPPLY_DISPLAY[sharedSel.supplyType]} + ariaLabel="Typ av inköp" + disabled={busy} + />