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:
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user