feat(invoices): create customer & supplier invoices in modals, matching the verifikat pattern (#861)

Invoice and supplier-invoice creation now open as pop-up dialogs on their
list pages instead of navigating to standalone form pages — the same UX as
NewJournalEntryDialog (capped-height scroll, explicit-close-only so a
half-typed invoice survives stray Escape/backdrop clicks).

- InvoiceEditor gains a `bare` variant (page chrome stripped, inline actions
  replacing the fixed mobile bar, live document-type title kept) hosted by
  the new NewInvoiceDialog (sm:max-w-5xl). Draft editing pages unchanged.
- The 2,057-line supplier form moves out of the route page into
  components/supplier-invoices/NewSupplierInvoiceForm.tsx with
  bare/inboxItemId/onCreated/onCancel props, hosted by
  NewSupplierInvoiceDialog (sm:max-w-4xl).
- Modals are URL-driven (?new=1): header buttons, empty states, command
  palette, and the reports CTA all open the same dialog; browser back
  closes it. /invoices/new and /supplier-invoices/new survive as redirects
  (bookmarks, agent intents, /expenses/new alias, inbox deep links).
- The invoice-inbox "Skapa leverantörsfaktura" action opens the modal in
  place and refreshes the inbox on success instead of navigating away.

No new i18n keys; dialog titles reuse existing strings.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-02 14:04:52 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 11126d6d56
commit 86071334cb
13 changed files with 2370 additions and 2103 deletions
+4 -2
View File
@@ -17,6 +17,8 @@ export default async function NewExpenseRedirectPage({
qs.set(key, value)
}
}
const suffix = qs.toString()
redirect(`/supplier-invoices/new${suffix ? `?${suffix}` : ''}`)
// Supplier invoice registration lives in a modal on the list page now
// (?new=1) — go there directly instead of bouncing via /supplier-invoices/new.
qs.set('new', '1')
redirect(`/supplier-invoices?${qs.toString()}`)
}
+6 -5
View File
@@ -1,8 +1,9 @@
import InvoiceEditor from '@/components/invoices/InvoiceEditor'
import { redirect } from 'next/navigation'
// The invoice creator lives in the shared <InvoiceEditor> component so the same
// form powers both creating a new invoice and editing an existing draft
// (app/(dashboard)/invoices/[id]/edit).
// Invoice creation now happens in a modal on the invoice list (issue: match
// the verifikat pattern). This route survives as a redirect so old links,
// bookmarks, and agent intents keep working. Editing drafts still has a full
// page at /invoices/[id]/edit.
export default function NewInvoicePage() {
return <InvoiceEditor mode="create" />
redirect('/invoices?new=1')
}
+25 -7
View File
@@ -2,6 +2,7 @@
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { createClient } from '@/lib/supabase/client'
import { Button } from '@/components/ui/button'
@@ -26,6 +27,7 @@ import { invoiceDisplayNumber } from '@/lib/invoices/display'
import { getDisplayTotal } from '@/lib/invoices/rounding'
import { Plus, Search, ReceiptText, Lock, Repeat } from 'lucide-react'
import { EmptyInvoices } from '@/components/ui/empty-state'
import NewInvoiceDialog from '@/components/invoices/NewInvoiceDialog'
import { useCompany } from '@/contexts/CompanyContext'
import { useCanWrite } from '@/lib/hooks/use-can-write'
import type { Invoice, InvoiceStatus } from '@/types'
@@ -69,6 +71,8 @@ function useRelativeTimeLabel() {
export default function InvoicesPage() {
const { company } = useCompany()
const { canWrite } = useCanWrite()
const router = useRouter()
const searchParams = useSearchParams()
const [invoices, setInvoices] = useState<Invoice[]>([])
const [oreRounding, setOreRounding] = useState<boolean>(true)
const [isLoading, setIsLoading] = useState(true)
@@ -79,6 +83,15 @@ export default function InvoicesPage() {
const t = useTranslations('invoices')
const getRelativeTimeLabel = useRelativeTimeLabel()
// The "Ny faktura" modal is driven by the URL (?new=1) so every entry point
// — the header button, empty states, the command palette, and the legacy
// /invoices/new redirect — opens the same dialog, and the browser back
// button closes it. No canWrite gate here: like the old /invoices/new page,
// the editor itself disables submission for viewers.
const showNewInvoice = searchParams.has('new')
const closeNewInvoice = () => router.replace('/invoices', { scroll: false })
const openNewInvoice = () => router.push('/invoices?new=1', { scroll: false })
async function fetchInvoices() {
if (!company) return
setIsLoading(true)
@@ -160,12 +173,10 @@ export default function InvoicesPage() {
</Button>
</Link>
{canWrite ? (
<Link href="/invoices/new">
<Button>
<Plus className="mr-2 h-4 w-4" />
{t('new_invoice')}
</Button>
</Link>
<Button onClick={openNewInvoice}>
<Plus className="mr-2 h-4 w-4" />
{t('new_invoice')}
</Button>
) : (
<Button
disabled
@@ -261,7 +272,7 @@ export default function InvoicesPage() {
description={t('no_search_results_description', { term: searchTerm })}
/>
) : invoices.length === 0 ? (
<EmptyInvoices />
<EmptyInvoices onAction={openNewInvoice} />
) : (
<DataListEmpty
icon={<ReceiptText className="h-6 w-6" />}
@@ -381,6 +392,13 @@ export default function InvoicesPage() {
})
)}
</DataList>
<NewInvoiceDialog
open={showNewInvoice}
onOpenChange={(open) => {
if (!open) closeNewInvoice()
}}
/>
</div>
)
}
File diff suppressed because it is too large Load Diff
+45 -9
View File
@@ -1,6 +1,7 @@
'use client'
import { useState, useEffect } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { Skeleton } from "@/components/ui/skeleton"
import { Badge } from '@/components/ui/badge'
@@ -11,6 +12,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
import { Plus, FileInput, Lock } from 'lucide-react'
import Link from 'next/link'
import { PageHeader } from '@/components/ui/page-header'
import NewSupplierInvoiceDialog from '@/components/supplier-invoices/NewSupplierInvoiceDialog'
import { useCanWrite } from '@/lib/hooks/use-can-write'
import { formatDate } from '@/lib/utils'
import type { SupplierInvoice } from '@/types'
@@ -44,10 +46,22 @@ const STATUS_LABEL_KEYS: Record<string, string> = {
export default function SupplierInvoicesPage() {
const t = useTranslations('supplier_invoices')
const { canWrite } = useCanWrite()
const router = useRouter()
const searchParams = useSearchParams()
const [invoices, setInvoices] = useState<(SupplierInvoice & { supplier?: { id: string; name: string } })[]>([])
const [isLoading, setIsLoading] = useState(true)
const [activeTab, setActiveTab] = useState('all')
// The "Registrera leverantörsfaktura" modal is driven by the URL (?new=1,
// optionally with inbox_item_id for the invoice-inbox conversion flow) so
// every entry point — the header button, the empty state, the command
// palette, and the legacy /supplier-invoices/new redirect — opens the same
// dialog, and the browser back button closes it.
const showNewInvoice = searchParams.has('new')
const inboxItemId = searchParams.get('inbox_item_id')
const closeNewInvoice = () => router.replace('/supplier-invoices', { scroll: false })
const openNewInvoice = () => router.push('/supplier-invoices?new=1', { scroll: false })
async function fetchInvoices() {
setIsLoading(true)
const res = await fetch('/api/supplier-invoices?status=all')
@@ -60,6 +74,23 @@ export default function SupplierInvoicesPage() {
fetchInvoices()
}, [])
// Mirrors the old standalone page's post-create navigation: inbox
// conversions land back in the inbox, a created invoice opens its detail
// page, and flows that end here (e.g. private expense) close the modal and
// refresh the list in place.
const handleCreated = (invoiceId?: string) => {
if (inboxItemId) {
router.push('/e/general/invoice-inbox')
return
}
if (invoiceId) {
router.push(`/supplier-invoices/${invoiceId}`)
return
}
closeNewInvoice()
fetchInvoices()
}
const filteredInvoices = invoices.filter((inv) => {
switch (activeTab) {
case 'registered': return inv.status === 'registered'
@@ -76,12 +107,10 @@ export default function SupplierInvoicesPage() {
title={t('title')}
action={
canWrite ? (
<Link href="/supplier-invoices/new">
<Button>
<Plus className="mr-2 h-4 w-4" />
{t('register_invoice')}
</Button>
</Link>
<Button onClick={openNewInvoice}>
<Plus className="mr-2 h-4 w-4" />
{t('register_invoice')}
</Button>
) : (
<Button
disabled
@@ -134,9 +163,7 @@ export default function SupplierInvoicesPage() {
}
action={
activeTab === 'all' && canWrite ? (
<Button asChild>
<Link href="/supplier-invoices/new">{t('register_invoice')}</Link>
</Button>
<Button onClick={openNewInvoice}>{t('register_invoice')}</Button>
) : undefined
}
/>
@@ -185,6 +212,15 @@ export default function SupplierInvoicesPage() {
</DataList>
</TabsContent>
</Tabs>
<NewSupplierInvoiceDialog
open={showNewInvoice}
onOpenChange={(open) => {
if (!open) closeNewInvoice()
}}
inboxItemId={inboxItemId}
onCreated={handleCreated}
/>
</div>
)
}
+1 -1
View File
@@ -244,7 +244,7 @@ export default function SupplierDetailPage() {
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-lg">{t('invoices_section_title')}</CardTitle>
<Link href="/supplier-invoices/new">
<Link href="/supplier-invoices?new=1">
<Button size="sm">
<FileText className="mr-2 h-4 w-4" />
{t('new_invoice')}
+2 -2
View File
@@ -36,10 +36,10 @@ type Entry = {
}
const ACTION_ENTRIES: Entry[] = [
{ id: 'new-invoice', label: 'Ny faktura', hint: 'Skapa & skicka faktura', icon: ReceiptText, href: '/invoices/new', keywords: 'fakturera ny invoice send create' },
{ id: 'new-invoice', label: 'Ny faktura', hint: 'Skapa & skicka faktura', icon: ReceiptText, href: '/invoices?new=1', keywords: 'fakturera ny invoice send create' },
{ id: 'book-transaction', label: 'Boka transaktion', hint: 'Gå till transaktionsinkorgen', icon: ArrowLeftRight, href: '/transactions', keywords: 'transaktion bokför kategorisera categorize' },
{ id: 'new-customer', label: 'Lägg till kund', icon: Users, href: '/customers', keywords: 'kund customer ny lägg till' },
{ id: 'new-supplier-invoice', label: 'Skapa leverantörsfaktura', icon: Wallet, href: '/supplier-invoices/new', keywords: 'leverantörsfaktura supplier invoice ny' },
{ id: 'new-supplier-invoice', label: 'Skapa leverantörsfaktura', icon: Wallet, href: '/supplier-invoices?new=1', keywords: 'leverantörsfaktura supplier invoice ny' },
{ id: 'reports', label: 'Visa resultaträkning', hint: 'Rapporter', icon: BarChart3, href: '/reports', keywords: 'rapport resultat balans report' },
]
@@ -42,6 +42,7 @@ import { CAPABILITY } from '@/lib/entitlements/keys'
import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry'
import type { InvoiceExtractionResult } from '@/types'
import BookDirectlyDialog from '@/components/extensions/general/BookDirectlyDialog'
import NewSupplierInvoiceDialog from '@/components/supplier-invoices/NewSupplierInvoiceDialog'
import BulkBookInboxDialog from '@/components/extensions/general/BulkBookInboxDialog'
import TransactionMatchPicker from '@/components/inbox/TransactionMatchPicker'
import { useAgentSheet } from '@/components/agent/AgentSheetProvider'
@@ -240,6 +241,10 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
// Match-to-bank-transaction picker (opens when user clicks "Matcha mot
// transaktion" on an unmatched inbox item).
const [matchPickerOpen, setMatchPickerOpen] = useState(false)
// "Skapa leverantörsfaktura" modal for the selected underlag — opens in
// place (instead of navigating to a form page) so the user lands right back
// here to pick the next document.
const [createSupplierInvoiceOpen, setCreateSupplierInvoiceOpen] = useState(false)
// Cash method users see "Bokför direkt" as the primary CTA; accrual users
// see "Skapa leverantörsfaktura". Defaults to 'accrual' until we've read
// the company settings so we don't flicker the CTA order on first paint.
@@ -1002,6 +1007,7 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
accountingMethod={accountingMethod}
onDelete={() => handleDelete(selected.id)}
onBookDirect={() => setBookDirectOpen(true)}
onCreateSupplierInvoice={() => setCreateSupplierInvoiceOpen(true)}
onMatchTransaction={() => setMatchPickerOpen(true)}
onUnmatchTransaction={async () => {
const targetId = selected.id
@@ -1073,6 +1079,19 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
}}
/>
)}
{selected && (
<NewSupplierInvoiceDialog
open={createSupplierInvoiceOpen}
onOpenChange={setCreateSupplierInvoiceOpen}
inboxItemId={selected.id}
onCreated={async () => {
// Stay in the inbox (the whole point of the modal): close, then
// refresh the list + the selected item so it shows as converted.
setCreateSupplierInvoiceOpen(false)
await Promise.all([fetchItems(), handleSelect(selected.id)])
}}
/>
)}
<BulkBookInboxDialog
open={bulkBookOpen}
onOpenChange={setBulkBookOpen}
@@ -1492,6 +1511,7 @@ function FieldsRail({
accountingMethod,
onDelete,
onBookDirect,
onCreateSupplierInvoice,
onMatchTransaction,
onUnmatchTransaction,
onAskAssistant,
@@ -1503,6 +1523,7 @@ function FieldsRail({
accountingMethod: AccountingMethod
onDelete: () => void
onBookDirect: () => void
onCreateSupplierInvoice: () => void
onMatchTransaction: () => void
onUnmatchTransaction: () => Promise<void>
onAskAssistant?: (transactionId: string) => void
@@ -1766,16 +1787,14 @@ function FieldsRail({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-64">
<DropdownMenuItem asChild>
<Link
href={`/supplier-invoices/new?inbox_item_id=${item.id}`}
className="flex flex-col items-start gap-1"
>
<span>Skapa leverantörsfaktura</span>
<span className="text-xs text-muted-foreground">
För leverantörsskulder du vill följa (periodisering).
</span>
</Link>
<DropdownMenuItem
onClick={onCreateSupplierInvoice}
className="flex flex-col items-start gap-1"
>
<span>Skapa leverantörsfaktura</span>
<span className="text-xs text-muted-foreground">
För leverantörsskulder du vill följa (periodisering).
</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={onBookDirect}
+29 -13
View File
@@ -67,9 +67,13 @@ export type InvoiceForEdit = Invoice & { items: InvoiceItem[] }
// `create` is the original "new invoice" flow (unchanged). `edit` pre-fills the
// form from an existing DRAFT and saves via PATCH instead of POST — no review
// dialog, no number allocation, no self-billed tab, no send/logo prompts.
export type InvoiceEditorProps =
// `bare` renders the editor without page chrome (back button, full-size
// heading, fixed mobile action bar) so it drops into NewInvoiceDialog — the
// same convention as JournalEntryForm's `bare`.
export type InvoiceEditorProps = (
| { mode?: 'create' }
| { mode: 'edit'; initial: InvoiceForEdit }
) & { bare?: boolean }
// Subset of Article fields the line picker needs to pre-fill a row.
type ArticleOption = Pick<
@@ -85,6 +89,7 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
// Edit mode pre-fills the form from an existing draft and saves via PATCH.
const isEditMode = props.mode === 'edit'
const initial = props.mode === 'edit' ? props.initial : null
const bare = props.bare === true
const router = useRouter()
const { toast } = useToast()
const { canWrite } = useCanWrite()
@@ -1105,22 +1110,29 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
? t('subtitle_delivery_note')
: t('subtitle_invoice')
// In bare (dialog) mode the dialog owns the accessible title (sr-only
// DialogTitle) and the page already has its own h1, so the visible heading
// steps down to h2 — it still tracks document type and number preview live.
const Heading = bare ? 'h2' : 'h1'
return (
<div className="space-y-8">
<div className={bare ? 'space-y-6' : 'space-y-8'}>
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => router.back()} aria-label={t('back')}>
<ArrowLeft className="h-5 w-5" />
</Button>
{!bare && (
<Button variant="ghost" size="icon" onClick={() => router.back()} aria-label={t('back')}>
<ArrowLeft className="h-5 w-5" />
</Button>
)}
<div className="flex-1 min-w-0">
<h1 className="font-display text-2xl md:text-3xl tracking-tight">
<Heading className={bare ? 'font-display text-xl tracking-tight' : 'font-display text-2xl md:text-3xl tracking-tight'}>
{titleText}
{numberPreview && !isSelfBilled && (
<span className="ml-2 text-muted-foreground tabular-nums text-xl md:text-2xl">
<span className={bare ? 'ml-2 text-muted-foreground tabular-nums text-lg' : 'ml-2 text-muted-foreground tabular-nums text-xl md:text-2xl'}>
({numberPreview})
</span>
)}
</h1>
<p className="text-muted-foreground">{subtitleText}</p>
</Heading>
{!bare && <p className="text-muted-foreground">{subtitleText}</p>}
</div>
<AgentSparkleButton
intentId="invoice.draft"
@@ -1148,7 +1160,7 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
</div>
)}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 pb-28 md:pb-0">
<form onSubmit={handleSubmit(onSubmit)} className={bare ? 'space-y-6' : 'space-y-6 pb-28 md:pb-0'}>
<div className="grid gap-6 lg:grid-cols-3 lg:items-start">
{/* Main content */}
<div className="lg:col-span-2 space-y-6">
@@ -1992,8 +2004,10 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
</CardContent>
</Card>
{/* Actions — desktop/tablet only */}
<div className="hidden md:flex md:flex-col md:gap-2">
{/* Actions — desktop/tablet only. In bare (dialog) mode the fixed
mobile bar is unusable (DialogContent's transform re-anchors
`fixed` children), so these buttons show at every width. */}
<div className={bare ? 'flex flex-col gap-2' : 'hidden md:flex md:flex-col md:gap-2'}>
<Button
type="submit"
className="w-full"
@@ -2023,7 +2037,8 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
</div>
</div>
{/* Mobile sticky total bar */}
{/* Mobile sticky total bar — page mode only (see bare note above) */}
{!bare && (
<div className="md:hidden fixed left-0 right-0 z-40 bg-card/98 backdrop-blur-sm border-t border-border/40 px-5 py-3" style={{ bottom: 'calc(4rem + env(safe-area-inset-bottom, 0px))' }}>
<div className="max-w-5xl mx-auto flex items-center justify-between gap-4">
<div>
@@ -2057,6 +2072,7 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
</div>
</div>
</div>
)}
</form>
{selectedCustomer && vatRules && (
+43
View File
@@ -0,0 +1,43 @@
'use client'
import { useTranslations } from 'next-intl'
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
import InvoiceEditor from '@/components/invoices/InvoiceEditor'
interface Props {
open: boolean
onOpenChange: (open: boolean) => void
}
/**
* "Ny faktura" as a modal — mirrors NewJournalEntryDialog. Wraps the bare
* InvoiceEditor; the editor's own review/confirm/send dialogs stack on top of
* this one, and every successful create navigates to the invoice detail page
* (unmounting the host list page and this dialog with it).
*
* The accessible title is visually hidden: the bare editor renders its own
* live heading, which tracks document type (faktura/proforma/följesedel) and
* shows the invoice-number preview — a static DialogTitle would duplicate or
* contradict it.
*/
export default function NewInvoiceDialog({ open, onOpenChange }: Props) {
const t = useTranslations('invoice_editor')
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="sm:max-w-5xl max-h-[95dvh] sm:max-h-[90vh] overflow-y-auto"
// A half-typed invoice must survive an accidental backdrop click or a
// stray Escape (nested comboboxes and date pickers portal outside the
// dialog). Closing is explicit — the header X. Same convention as
// NewJournalEntryDialog.
onEscapeKeyDown={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
>
<DialogTitle className="sr-only">{t('title_invoice')}</DialogTitle>
<InvoiceEditor mode="create" bare />
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,62 @@
'use client'
import { useTranslations } from 'next-intl'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import NewSupplierInvoiceForm from '@/components/supplier-invoices/NewSupplierInvoiceForm'
interface Props {
open: boolean
onOpenChange: (open: boolean) => void
/** Invoice-inbox item to convert; prefills the form from its AI extraction. */
inboxItemId?: string | null
/**
* Fired after a successful create. Hosts close the dialog and either
* navigate to the invoice detail (id given) or refresh their list in place.
*/
onCreated: (invoiceId?: string) => void
}
/**
* "Registrera leverantörsfaktura" as a modal — mirrors NewJournalEntryDialog.
* Wraps the bare NewSupplierInvoiceForm; the form's own review/confirm,
* supplier-create, bank-picker, and conflict dialogs stack on top of this one.
*/
export default function NewSupplierInvoiceDialog({
open,
onOpenChange,
inboxItemId,
onCreated,
}: Props) {
const t = useTranslations('supplier_invoice_editor')
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="sm:max-w-4xl max-h-[95dvh] sm:max-h-[90vh] overflow-y-auto"
// A half-typed invoice must survive an accidental backdrop click or a
// stray Escape (nested comboboxes and date pickers portal outside the
// dialog). Closing is explicit — the header X or Avbryt. Same
// convention as NewJournalEntryDialog.
onEscapeKeyDown={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle>{t('page_title')}</DialogTitle>
</DialogHeader>
<NewSupplierInvoiceForm
key={inboxItemId ?? 'fresh'}
bare
inboxItemId={inboxItemId}
onCreated={onCreated}
onCancel={() => onOpenChange(false)}
/>
</DialogContent>
</Dialog>
)
}
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -99,7 +99,7 @@ export function EmptyState({
// Preset empty states for common pages
export function EmptyInvoices() {
export function EmptyInvoices({ onAction }: { onAction?: () => void } = {}) {
const t = useTranslations('empty')
return (
<EmptyState
@@ -107,7 +107,8 @@ export function EmptyInvoices() {
title={t('preset_invoices_title')}
description={t('preset_invoices_description')}
actionLabel={t('preset_invoices_action')}
actionHref="/invoices/new"
actionHref={onAction ? undefined : '/invoices?new=1'}
onAction={onAction}
/>
)
}
@@ -186,7 +187,7 @@ export function EmptyReports() {
title={t('preset_reports_title')}
description={t('preset_reports_description')}
actionLabel={t('preset_reports_action')}
actionHref="/invoices/new"
actionHref="/invoices?new=1"
secondaryActionLabel={t('preset_reports_secondary')}
secondaryActionHref="/import"
/>