feat(invoices): implement öresavrundning logic and next invoice numbe… (#429)

* feat(invoices): implement öresavrundning logic and next invoice number preview

- Added `getDisplayTotal` utility to handle rounding for SEK invoices based on company settings.
- Updated `InvoicesPage` to utilize the new rounding logic when displaying totals.
- Introduced `peek_next_invoice_number` function to allow previewing the next invoice number without consuming the sequence.
- Modified invoice number generation to remove the year prefix and prevent truncation of numbers exceeding three digits.
- Enhanced tests for invoice number generation and rounding functionality to ensure correctness.
- Updated PDF template to reflect new rounding logic for totals and display appropriate values.
- Adjusted company switcher to hide options in sandbox mode.
- Improved error handling and logging in sandbox seeding process.

* fix(skatteverket): remove unused scope labels from SCOPE_LABELS and DEFAULT_SCOPES
This commit is contained in:
Mattsson
2026-05-10 14:21:26 +02:00
committed by GitHub
parent c110d3ef99
commit 0ee5219b6c
19 changed files with 733 additions and 106 deletions
+15 -12
View File
@@ -9,7 +9,7 @@ import { switchCompany } from '@/lib/company/actions'
import { Check, ChevronsUpDown, Plus, Loader2 } from 'lucide-react'
export default function CompanySwitcher() {
const { company, companies } = useCompany()
const { company, companies, isSandbox } = useCompany()
const [open, setOpen] = useState(false)
const [isPending, setIsPending] = useState(false)
const triggerRef = useRef<HTMLButtonElement>(null)
@@ -106,8 +106,9 @@ export default function CompanySwitcher() {
const hasMultiple = companies.length > 1
// No companies yet — show a direct "Lägg till företag" link instead of
// the switcher so the user can still create one.
// the switcher so the user can still create one. Hidden in sandbox mode.
if (!company && companies.length === 0) {
if (isSandbox) return null
return (
<Link
href="/select-company"
@@ -187,16 +188,18 @@ export default function CompanySwitcher() {
</>
)}
<div className={cn(companies.length > 0 && 'border-t border-border/40 mt-1 pt-1', 'px-1')}>
<Link
href="/select-company"
onClick={() => setOpen(false)}
className="flex items-center gap-2 px-2.5 py-2 text-[13px] text-muted-foreground hover:text-foreground hover:bg-muted/40 rounded-md transition-colors md:whitespace-nowrap"
>
<Plus className="h-3.5 w-3.5" />
Lägg till företag
</Link>
</div>
{!isSandbox && (
<div className={cn(companies.length > 0 && 'border-t border-border/40 mt-1 pt-1', 'px-1')}>
<Link
href="/select-company"
onClick={() => setOpen(false)}
className="flex items-center gap-2 px-2.5 py-2 text-[13px] text-muted-foreground hover:text-foreground hover:bg-muted/40 rounded-md transition-colors md:whitespace-nowrap"
>
<Plus className="h-3.5 w-3.5" />
Lägg till företag
</Link>
</div>
)}
</div>,
document.body
)}
@@ -25,6 +25,9 @@ interface InvoiceReviewContentProps {
yourReference?: string
ourReference?: string
notes?: string
/** The invoice number that will be assigned on confirm. Null when unknown
* (e.g. delivery notes use a different sequence) or unfetched. */
numberPreview?: string | null
}
export function InvoiceReviewContent({
@@ -39,6 +42,7 @@ export function InvoiceReviewContent({
yourReference,
ourReference,
notes,
numberPreview,
}: InvoiceReviewContentProps) {
const customerTypeLabel: Record<string, string> = {
individual: 'Privatperson',
@@ -60,6 +64,12 @@ export function InvoiceReviewContent({
return (
<div className="space-y-4">
{numberPreview && (
<div className="text-sm text-muted-foreground">
Tilldelas fakturanummer{' '}
<span className="font-medium tabular-nums text-foreground">{numberPreview}</span>
</div>
)}
{/* Customer info */}
<div className="bg-muted rounded-lg p-3 sm:p-4 flex flex-col sm:flex-row sm:items-center gap-2 sm:justify-between">
<div className="min-w-0">
+48 -42
View File
@@ -734,55 +734,61 @@ export function AGIPanel(props: AGIPanelProps) {
)}
{!readOnly && !isSigned && (
<>
{/* Direct AGI submission to Skatteverket is paused while the
APIGW subscription is sorted out at SKV's end. Users still
generate and download the AGI XML from the salary run page
and upload it manually via Mina Sidor. Re-enable the three
buttons below once the subscription is in place. */}
<div className="rounded-md border border-amber-300 bg-amber-50 p-3 dark:border-amber-900/40 dark:bg-amber-900/20">
<p className="text-sm font-medium">
Direktinlämning till Skatteverket är pausad
</p>
<p className="mt-1 text-xs text-muted-foreground">
Ladda ner AGI-filen ovan och lämna in den manuellt via Mina Sidor
hos Skatteverket. Direktinlämning aktiveras igen när vår
anslutning hos Skatteverket är klar.
</p>
</div>
<div className="flex flex-wrap gap-2">
<Button
size="sm"
variant="outline"
onClick={handleSubmit}
disabled
title="Direktinlämning till Skatteverket är pausad"
>
<div className="flex flex-wrap gap-2">
<Button
size="sm"
variant="outline"
onClick={handleSubmit}
disabled={actionLoading === 'submit'}
>
{actionLoading === 'submit' ? (
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
) : (
<Send className="mr-1.5 h-3.5 w-3.5" />
Skicka in underlag
</Button>
<Button
size="sm"
onClick={handleCreateSigningLink}
disabled
title="Direktinlämning till Skatteverket är pausad"
>
)}
Skicka in underlag
</Button>
<Button
size="sm"
onClick={handleCreateSigningLink}
disabled={actionLoading === 'granskning' || !underlagSubmitted}
>
{actionLoading === 'granskning' ? (
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
) : (
<Lock className="mr-1.5 h-3.5 w-3.5" />
Skapa signeringslänk
</Button>
)}
Skapa signeringslänk
</Button>
<Button
size="sm"
variant="ghost"
onClick={handleCheckSubmitted}
disabled={actionLoading === 'check'}
>
{actionLoading === 'check' ? (
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
) : (
<Download className="mr-1.5 h-3.5 w-3.5" />
)}
Hämta kvittens
</Button>
{awaitingSigning && (
<Button
size="sm"
variant="ghost"
onClick={handleCheckSubmitted}
disabled
title="Direktinlämning till Skatteverket är pausad"
onClick={handleUnlock}
disabled={actionLoading === 'unlock'}
>
<Download className="mr-1.5 h-3.5 w-3.5" />
Hämta kvittens
{actionLoading === 'unlock' ? (
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
) : (
<Unlock className="mr-1.5 h-3.5 w-3.5" />
)}
Lås upp
</Button>
</div>
</>
)}
</div>
)}
</CardContent>
</Card>
@@ -24,8 +24,6 @@ type Status =
const SCOPE_LABELS: Record<string, string> = {
momsdeklaration: 'Momsdeklaration',
inkforetag: 'Företagsinformation',
ska: 'Skatteinformation',
skahmst: 'Hemortskommun',
skattekonto: 'Skattekonto',
agd: 'Arbetsgivardeklaration',
}