From d63d2aecf0e1fc774c9c5642cc8f284da134edb0 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:42:56 +0200 Subject: [PATCH] feat: UI slop cleanup, invoice icon/header polish + year-end in Rapporter, journal-list DataList refactor (#847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI cleanup: removed AI-slop (redundant suppliers subtitle, decorative Sparkles glyph), decluttered the article-detail header (single status badge + muted type · #number), standardized the invoice icon Receipt→ReceiptText (no $ in a SEK app), and matched ReportExportMenu trigger size to the primary CTA on list pages. Bookkeeping: surfaced year-end closing in Rapporter (catalog descriptor) and dropped the redundant header button; refactored JournalEntryList to DataList primitives + chunked /api/documents/counts in 50-ID batches (large pages previously 400'd); added optional fraction-digit overrides to formatCurrency. The fiscal-year lock indicator is preserved as a labeled Låst/Stängt badge in FiscalYearSelector. All PR-bot findings triaged as false positives (unused import, formatCurrency öre, lock indicator) or intentional design (year-end placement, empty-state messaging). CI green. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/(dashboard)/articles/[id]/page.tsx | 10 +- app/(dashboard)/articles/page.tsx | 1 + app/(dashboard)/bookkeeping/page.tsx | 41 +- app/(dashboard)/customers/[id]/page.tsx | 6 +- app/(dashboard)/customers/page.tsx | 1 + app/(dashboard)/invoices/page.tsx | 6 +- app/(dashboard)/pending/page.tsx | 10 +- app/(dashboard)/suppliers/page.tsx | 4 +- components/bookkeeping/JournalEntryList.tsx | 999 +++++++++--------- components/common/CommandPalette.tsx | 4 +- components/dashboard/AttGoraSection.tsx | 4 +- components/dashboard/DashboardNav.tsx | 4 +- components/reports/ReportExportMenu.tsx | 4 +- .../transactions/BankSyncSinceLastVisit.tsx | 3 +- components/ui/empty-state.tsx | 4 +- lib/reports/catalog.ts | 11 + lib/utils.ts | 10 +- messages/en.json | 3 +- messages/sv.json | 3 +- 19 files changed, 569 insertions(+), 559 deletions(-) diff --git a/app/(dashboard)/articles/[id]/page.tsx b/app/(dashboard)/articles/[id]/page.tsx index 39461828..9d56907e 100644 --- a/app/(dashboard)/articles/[id]/page.tsx +++ b/app/(dashboard)/articles/[id]/page.tsx @@ -191,15 +191,13 @@ export default function ArticleDetailPage({

{article.name}

- {t(ARTICLE_TYPE_KEY[article.type])} - {article.article_number && ( - - {article.article_number} - - )} {article.active ? t('status_active') : t('status_inactive')} + + {t(ARTICLE_TYPE_KEY[article.type])} + {article.article_number ? ` · #${article.article_number}` : ''} +
diff --git a/app/(dashboard)/articles/page.tsx b/app/(dashboard)/articles/page.tsx index fd8c7823..8c9652fb 100644 --- a/app/(dashboard)/articles/page.tsx +++ b/app/(dashboard)/articles/page.tsx @@ -240,6 +240,7 @@ function ArticlesPageInner() { action={
- - -
+ } /> diff --git a/app/(dashboard)/customers/[id]/page.tsx b/app/(dashboard)/customers/[id]/page.tsx index 7c8824d3..a6fb60db 100644 --- a/app/(dashboard)/customers/[id]/page.tsx +++ b/app/(dashboard)/customers/[id]/page.tsx @@ -23,7 +23,7 @@ import { Edit2, Trash2, Loader2, - Receipt, + ReceiptText, Lock, } from 'lucide-react' import { useCanWrite } from '@/lib/hooks/use-can-write' @@ -313,7 +313,7 @@ export default function CustomerDetailPage({
- + {t('invoice_count', { count: customer.invoices?.length || 0 })}
@@ -336,7 +336,7 @@ export default function CustomerDetailPage({ - + {t('section_invoices')} {customer.invoices?.length > 0 && ( {customer.invoices.length} diff --git a/app/(dashboard)/customers/page.tsx b/app/(dashboard)/customers/page.tsx index 226bc466..1f343695 100644 --- a/app/(dashboard)/customers/page.tsx +++ b/app/(dashboard)/customers/page.tsx @@ -244,6 +244,7 @@ function CustomersPageInner() { action={
} + icon={} title={t('no_search_results_title')} description={t('no_search_results_description', { term: searchTerm })} /> @@ -264,7 +264,7 @@ export default function InvoicesPage() { ) : ( } + icon={} title={t('no_category_title')} description={t('no_category_description')} /> diff --git a/app/(dashboard)/pending/page.tsx b/app/(dashboard)/pending/page.tsx index 5d8cbc58..b757c2d2 100644 --- a/app/(dashboard)/pending/page.tsx +++ b/app/(dashboard)/pending/page.tsx @@ -43,7 +43,7 @@ import { ClipboardCheck, ArrowLeftRight, Users, - Receipt, + ReceiptText, Bot, BookOpen, ChevronDown, @@ -63,14 +63,14 @@ import { MatchTransactionInvoicePreview } from '@/components/bookkeeping/MatchTr const OPERATION_LABEL_KEYS: Record = { categorize_transaction: { labelKey: 'type_categorize_transaction', icon: ArrowLeftRight, variant: 'default' }, create_customer: { labelKey: 'type_create_customer', icon: Users, variant: 'secondary' }, - create_invoice: { labelKey: 'type_create_invoice', icon: Receipt, variant: 'outline' }, + create_invoice: { labelKey: 'type_create_invoice', icon: ReceiptText, variant: 'outline' }, create_transaction: { labelKey: 'type_create_transaction', icon: ArrowLeftRight, variant: 'secondary' }, create_voucher: { labelKey: 'type_create_voucher', icon: BookOpen, variant: 'outline' }, correct_entry: { labelKey: 'type_correct_entry', icon: BookOpen, variant: 'outline' }, reverse_entry: { labelKey: 'type_reverse_entry', icon: BookOpen, variant: 'outline' }, - mark_invoice_paid: { labelKey: 'type_mark_invoice_paid', icon: Receipt, variant: 'default' }, - send_invoice: { labelKey: 'type_send_invoice', icon: Receipt, variant: 'outline' }, - mark_invoice_sent: { labelKey: 'type_mark_invoice_sent', icon: Receipt, variant: 'outline' }, + mark_invoice_paid: { labelKey: 'type_mark_invoice_paid', icon: ReceiptText, variant: 'default' }, + send_invoice: { labelKey: 'type_send_invoice', icon: ReceiptText, variant: 'outline' }, + mark_invoice_sent: { labelKey: 'type_mark_invoice_sent', icon: ReceiptText, variant: 'outline' }, match_transaction_invoice: { labelKey: 'type_match_transaction_invoice', icon: ArrowLeftRight, variant: 'secondary' }, } diff --git a/app/(dashboard)/suppliers/page.tsx b/app/(dashboard)/suppliers/page.tsx index 881d0cdf..a7198279 100644 --- a/app/(dashboard)/suppliers/page.tsx +++ b/app/(dashboard)/suppliers/page.tsx @@ -116,12 +116,10 @@ export default function SuppliersPage() {

{t('title')}

-

- {t('subtitle')} -

(null) const [periodHydrated, setPeriodHydrated] = useState(false) - const [periods, setPeriods] = useState([]) const [filterOpen, setFilterOpen] = useState(false) const [dateFrom, setDateFrom] = useState('') const [dateTo, setDateTo] = useState('') @@ -164,13 +169,31 @@ export default function JournalEntryList() { } const fetchAttachmentCounts = useCallback(async (entryIds: string[]) => { - if (entryIds.length === 0) return + if (entryIds.length === 0) { + setAttachmentCounts({}) + return + } + // The counts route caps each request at 50 IDs, so a large page ("Alla", or + // 100/page) must be split into chunks and merged. Without this the whole + // request 400s and every document-requiring row falsely shows the + // missing-underlag warning until it's expanded. + const COUNTS_BATCH_SIZE = 50 + const batches: string[][] = [] + for (let i = 0; i < entryIds.length; i += COUNTS_BATCH_SIZE) { + batches.push(entryIds.slice(i, i + COUNTS_BATCH_SIZE)) + } try { - const res = await fetch( - `/api/documents/counts?journal_entry_ids=${entryIds.join(',')}` + const results = await Promise.all( + batches.map(async (batch) => { + const res = await fetch( + `/api/documents/counts?journal_entry_ids=${batch.join(',')}` + ) + if (!res.ok) return {} as Record + const { data } = await res.json() + return (data || {}) as Record + }) ) - const { data } = await res.json() - setAttachmentCounts(data || {}) + setAttachmentCounts(Object.assign({}, ...results)) } catch { // Non-critical — silently ignore } @@ -230,7 +253,6 @@ export default function JournalEntryList() { // gates that first fetch so the list loads already scoped to the resolved year. useEffect(() => { if (!company?.id) { - setPeriods([]) setPeriodId(null) setPeriodHydrated(true) return @@ -248,7 +270,6 @@ export default function JournalEntryList() { // Non-critical — fall through with an empty list (scope stays "all years"). } if (cancelled) return - setPeriods(fetched) const stored = typeof window !== 'undefined' @@ -544,28 +565,25 @@ export default function JournalEntryList() { // Count of active dialog filters, shown as a badge on the Filtrera button so // the user can tell the list is scoped without opening the dialog. Sort order // is a view preference (always set), not a filter, so it is excluded. - const activeFilterCount = - (periodId ? 1 : 0) + + // Filters that live inside the Filtrera dialog. The fiscal-year scope is its + // own control now, so it no longer counts toward the dialog badge (it would + // otherwise always read "1" for the default year). + const dialogFilterCount = (seriesFilter !== 'all' ? 1 : 0) + (dateFrom || dateTo ? 1 : 0) + (showMissingOnly ? 1 : 0) + // Year scope included — drives empty-state messaging + keeping the bar mounted. + const activeFilterCount = (periodId ? 1 : 0) + dialogFilterCount // When any filter or search is active we keep the filter bar mounted even // with zero results, so the user can edit or clear their query. The pristine // "no entries yet" state below only applies to an untouched, empty ledger. const hasActiveFilters = Boolean(search) || activeFilterCount > 0 - // Resolve the active fiscal-year scope for the bar chip. "All years" (periodId - // null) renders immediately; a specific period waits until its name resolves - // from the fetched list (scopeLabel stays null meanwhile, so the chip never - // flashes the wrong scope). Surfacing this keeps the period visible per BFL - // without the user having to open the filter dialog. - const activePeriod = periodId ? periods.find((p) => p.id === periodId) ?? null : null - const scopeLabel = periodId ? activePeriod?.name ?? null : t('scope_all_years') - - // Apply a fiscal-year selection from the dialog. The FiscalYearSelector - // persists the choice to localStorage itself; here we only mirror it into - // local state and reset pagination. + // Apply a fiscal-year selection. The FiscalYearSelector (now an inline control + // in the toolbar, not buried in the filter dialog) persists the choice to + // localStorage itself; here we only mirror it into local state and reset + // pagination. const handlePeriodChange = (next: string | null) => { setPeriodId(next) setPage(0) @@ -620,24 +638,44 @@ export default function JournalEntryList() { // Verifikat/Utkast toggle stays reachable. if (!loading && entries.length === 0 && !hasActiveFilters && listMode === 'committed' && draftCount === 0) { return ( - - -
- -
-

{t('empty_title')}

-

- {t('empty_description')} -

-
-
+ + } + title={t('empty_title')} + description={t('empty_description')} + /> + ) } return (
- {/* Search (always visible) + filter dialog for everything else */} -
+ {/* Control bar: view toggle + search + filters + active fiscal-year scope + on one aligned row (wraps on narrow screens) rather than four stacked rows. */} +
+ {/* Verifikat vs Utkast. Drafts live in their own view with a count badge so + they don't sink to the last page of the committed list. */} +
+ + +
0 - ? t('filter_with_count', { count: activeFilterCount }) + dialogFilterCount > 0 + ? t('filter_with_count', { count: dialogFilterCount }) : t('filter') } > {t('filter')} - {activeFilterCount > 0 && ( + {dialogFilterCount > 0 && ( - {activeFilterCount} + {dialogFilterCount} )} @@ -692,17 +730,6 @@ export default function JournalEntryList() {
- {/* Räkenskapsår */} -
- - -
- {/* Sortering */}
@@ -840,152 +867,121 @@ export default function JournalEntryList() { -
- - {/* Verifikat vs Utkast. Drafts live in their own view with a count badge so - they don't sink to the last page of the committed list. */} -
-
- - -
-
- - {/* Active fiscal-year scope — visible without opening the filter dialog so - the user always sees which räkenskapsår the ledger is scoped to (BFL - period-correctness). Clicking it opens the dialog to change the scope. */} - {listMode === 'committed' && periodHydrated && scopeLabel && ( -
- {t('scope_label')} - -
- )} - - {/* Batch-mark "Inget underlag krävs": select-all + contextual action bar */} - {(eligibleEntries.length > 0 || selectedIds.size > 0) && ( -
-
- + {t('scope_label')} + -
- {selectedIds.size > 0 ? ( -
- setBatchReason(e.target.value)} - placeholder={t('no_doc_required_reason_placeholder')} - list="batch-no-doc-suggestions" - maxLength={200} - className="h-8 text-xs sm:w-56" - disabled={batchSubmitting} - /> - - -
- - -
-
- ) : ( - // Filter-scoped: mark every missing-doc verifikat matching the active - // filters across all pages — scales to a post-import flood. - - )} -
- )} + )} +
{loading ? ( - - - -

{t('loading')}

-
-
+ + + ) : filteredEntries.length === 0 ? ( // Empty placeholder, scoped to the situation: an empty drafts view, a // filtered committed view with no matches, or a committed view with no // posted entries yet (but drafts exist — hence we got here, not the // pristine early return above). - - -
- {listMode === 'drafts' || !hasActiveFilters ? ( - + + ) : ( - - )} -
-

- {listMode === 'drafts' + + ) + } + title={ + listMode === 'drafts' ? t('empty_drafts_title') : hasActiveFilters ? t('no_results_title') - : t('empty_title')} -

-

- {listMode === 'drafts' + : t('empty_title') + } + description={ + listMode === 'drafts' ? t('empty_drafts_description') : hasActiveFilters ? t('no_results_description') - : t('empty_description')} -

-
-
+ : t('empty_description') + } + /> + ) : ( -
+ + {/* Batch-mark "Inget underlag krävs": select-all + contextual action bar, + rendered as the list header so it reads as part of the ledger rather + than a detached box above it. */} + {(eligibleEntries.length > 0 || selectedIds.size > 0) && ( + +
+ + +
+ {selectedIds.size > 0 ? ( +
+ setBatchReason(e.target.value)} + placeholder={t('no_doc_required_reason_placeholder')} + list="batch-no-doc-suggestions" + maxLength={200} + className="h-8 text-xs sm:w-56" + disabled={batchSubmitting} + /> + + +
+ + +
+
+ ) : ( + // Filter-scoped: mark every missing-doc verifikat matching the active + // filters across all pages — scales to a post-import flood. + + )} +
+ )} {filteredEntries.map((entry) => { const isExpanded = expandedId === entry.id const lines = (entry.lines || []) as JournalEntryLine[] @@ -994,55 +990,205 @@ export default function JournalEntryList() { const selectable = canWrite && isEligibleForExempt(entry) return ( - -
- {selectable && ( -
e.stopPropagation()}> + toggleExpand(entry.id)} + leading={ + selectable ? ( +
e.stopPropagation()}> toggleSelect(entry.id)} aria-label={t('batch_select_row')} />
+ ) : undefined + } + expandedContent={ + isExpanded ? ( + <> + {lines.length === 0 ? ( +

{t('no_lines')}

+ ) : ( +
+ + + + {t('account_column')} + {t('description_column')} + {t('debit')} + {t('credit')} + + + + {lines + .slice() + .sort((a, b) => a.sort_order - b.sort_order) + .map((line) => { + const accountName = getAccountDescription(line.account_number)?.name + const desc = line.line_description + const showDesc = desc + && desc.toLowerCase() !== accountName?.toLowerCase() + && desc.toLowerCase() !== entry.description?.toLowerCase() + const debit = Number(line.debit_amount) || 0 + const credit = Number(line.credit_amount) || 0 + const fx = line.currency && line.currency !== 'SEK' && line.amount_in_currency != null + ? `${Number(line.amount_in_currency).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} ${line.currency}` + : null + return ( + + + + + + {showDesc ? desc : ''} + + + {debit > 0 ? debit.toLocaleString('sv-SE', { minimumFractionDigits: 2 }) : ''} + {debit > 0 && fx && ( + {fx} + )} + + + {credit > 0 ? credit.toLocaleString('sv-SE', { minimumFractionDigits: 2 }) : ''} + {credit > 0 && fx && ( + {fx} + )} + + + ) + })} + + + + {t('sum_label')} + + {lines.reduce((sum, l) => sum + (Number(l.debit_amount) || 0), 0).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} + + + {lines.reduce((sum, l) => sum + (Number(l.credit_amount) || 0), 0).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} + + + +
+
+ )} + + {entry.notes && ( +

+ {entry.notes} +

+ )} + + handleAttachmentCountChange(entry.id, c)} + /> + + {entry.status === 'posted' && NEEDS_ATTACHMENT.has(entry.source_type) && ( + { + setNoDocRequired((prev) => { + const next = new Map(prev) + if (exempted) next.set(entry.id, reason ?? null) + else next.delete(entry.id) + return next + }) + }} + /> + )} + +
+ {entry.status === 'draft' && ( + + )} + + {entry.status === 'posted' && entry.source_type !== 'storno' && entry.source_type !== 'correction' && ( + + )} + {canWrite && entry.status === 'posted' && entry.source_type !== 'storno' && entry.source_type !== 'correction' && ( + + )} + +
+ + ) : undefined + } + > + {/* Desktop: single row */} +
+ {isExpanded ? ( + + ) : ( + )} - - {attachmentCounts[entry.id] ? ( - - ) : ( - NEEDS_ATTACHMENT.has(entry.source_type) && entry.status === 'posted' && ( - noDocRequired.has(entry.id) ? ( - - - - ) : ( - - - - ) - ) - )} -
- {/* Mobile: two rows */} -
-
- {isExpanded ? ( - - ) : ( - - )} - e.stopPropagation()} - > - {formatVoucher(entry)} - - - {formatDate(entry.entry_date)} - - {entry.out_of_period && ( - - {t('out_of_period_label')} - - )} - {(entry.status === 'reversed' || entry.status === 'draft' || entry.source_type === 'storno' || entry.source_type === 'correction') && ( - - )} - + {/* Fixed-width attachment slot keeps the copy icon in a stable + column and the right edge aligned whether a row has a + paperclip, a warning, or nothing. */} + + {attachmentCounts[entry.id] ? ( - {attachmentCounts[entry.id] ? ( -
+
+ {/* Mobile: two rows */} +
+
+ {isExpanded ? ( + + ) : ( + + )} + e.stopPropagation()} + > + {formatVoucher(entry)} + + + {formatDate(entry.entry_date)} + + {entry.out_of_period && ( + + {t('out_of_period_label')} + + )} + {(entry.status === 'reversed' || entry.status === 'draft' || entry.source_type === 'storno' || entry.source_type === 'correction') && ( + + )} + + + {attachmentCounts[entry.id] ? ( + + ) : ( + NEEDS_ATTACHMENT.has(entry.source_type) && entry.status === 'posted' && ( + noDocRequired.has(entry.id) ? ( + + + + ) : ( + + - - ) : ( - NEEDS_ATTACHMENT.has(entry.source_type) && entry.status === 'posted' && ( - noDocRequired.has(entry.id) ? ( - - - - ) : ( - - - - ) ) - )} - -
-
-

{entry.description}

- - {formatCurrency(voucherTotal)} - -
+ ) + )} + +
+
+

{entry.description}

+ + {formatCurrency(voucherTotal, 'SEK', { minimumFractionDigits: 2 })} +
-
- - {isExpanded && ( - - {lines.length === 0 ? ( -

{t('no_lines')}

- ) : ( -
- - - - {t('account_column')} - {t('description_column')} - {t('debit')} - {t('credit')} - - - - {lines - .slice() - .sort((a, b) => a.sort_order - b.sort_order) - .map((line) => { - const accountName = getAccountDescription(line.account_number)?.name - const desc = line.line_description - const showDesc = desc - && desc.toLowerCase() !== accountName?.toLowerCase() - && desc.toLowerCase() !== entry.description?.toLowerCase() - const debit = Number(line.debit_amount) || 0 - const credit = Number(line.credit_amount) || 0 - const fx = line.currency && line.currency !== 'SEK' && line.amount_in_currency != null - ? `${Number(line.amount_in_currency).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} ${line.currency}` - : null - return ( - - - - - - {showDesc ? desc : ''} - - - {debit > 0 ? debit.toLocaleString('sv-SE', { minimumFractionDigits: 2 }) : ''} - {debit > 0 && fx && ( - {fx} - )} - - - {credit > 0 ? credit.toLocaleString('sv-SE', { minimumFractionDigits: 2 }) : ''} - {credit > 0 && fx && ( - {fx} - )} - - - ) - })} - - - - {t('sum_label')} - - {lines.reduce((sum, l) => sum + (Number(l.debit_amount) || 0), 0).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} - - - {lines.reduce((sum, l) => sum + (Number(l.credit_amount) || 0), 0).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} - - - -
-
- )} - - {entry.notes && ( -

- {entry.notes} -

- )} - - handleAttachmentCountChange(entry.id, c)} - /> - - {entry.status === 'posted' && NEEDS_ATTACHMENT.has(entry.source_type) && ( - { - setNoDocRequired((prev) => { - const next = new Map(prev) - if (exempted) next.set(entry.id, reason ?? null) - else next.delete(entry.id) - return next - }) - }} - /> - )} - -
- {entry.status === 'draft' && ( - - )} - - {entry.status === 'posted' && entry.source_type !== 'storno' && entry.source_type !== 'correction' && ( - - )} - {canWrite && entry.status === 'posted' && entry.source_type !== 'storno' && entry.source_type !== 'correction' && ( - - )} - -
-
- )} - + ) })} -
+
)} {/* Filter-scoped bulk "Inget underlag krävs" confirmation */} diff --git a/components/common/CommandPalette.tsx b/components/common/CommandPalette.tsx index 06858d38..c135834f 100644 --- a/components/common/CommandPalette.tsx +++ b/components/common/CommandPalette.tsx @@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useRouter } from 'next/navigation' import * as DialogPrimitive from '@radix-ui/react-dialog' import { - Receipt, + ReceiptText, ArrowLeftRight, Users, Wallet, @@ -35,7 +35,7 @@ type Entry = { } const ACTION_ENTRIES: Entry[] = [ - { id: 'new-invoice', label: 'Ny faktura', hint: 'Skapa & skicka faktura', icon: Receipt, href: '/invoices/new', keywords: 'fakturera ny invoice send create' }, + { id: 'new-invoice', label: 'Ny faktura', hint: 'Skapa & skicka faktura', icon: ReceiptText, href: '/invoices/new', 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' }, diff --git a/components/dashboard/AttGoraSection.tsx b/components/dashboard/AttGoraSection.tsx index a66ed540..de242bc1 100644 --- a/components/dashboard/AttGoraSection.tsx +++ b/components/dashboard/AttGoraSection.tsx @@ -21,7 +21,7 @@ import { Inbox, Landmark, Loader2, - Receipt, + ReceiptText, ShieldCheck, Stamp, } from 'lucide-react' @@ -341,7 +341,7 @@ export default function AttGoraSection({ {counts.overdue_invoice > 0 && ( diff --git a/components/dashboard/DashboardNav.tsx b/components/dashboard/DashboardNav.tsx index 1fb00f12..d162b83e 100644 --- a/components/dashboard/DashboardNav.tsx +++ b/components/dashboard/DashboardNav.tsx @@ -9,7 +9,7 @@ import { Button } from '@/components/ui/button' import { LayoutDashboard, Home, - Receipt, + ReceiptText, Users, ArrowLeftRight, BookOpen, @@ -135,7 +135,7 @@ const navItems: NavItem[] = [ { href: '/transactions', labelKey: 'transactions', icon: ArrowLeftRight, group: 'top' }, { href: '/pending', labelKey: 'review', icon: ClipboardCheck, group: 'top' }, // Försäljning dropdown - { href: '/invoices', labelKey: 'invoices', icon: Receipt, group: 'försäljning' }, + { href: '/invoices', labelKey: 'invoices', icon: ReceiptText, group: 'försäljning' }, { href: '/customers', labelKey: 'customers', icon: Users, group: 'försäljning' }, { href: '/articles', labelKey: 'articles', icon: Tag, group: 'försäljning' }, // Inköp dropdown diff --git a/components/reports/ReportExportMenu.tsx b/components/reports/ReportExportMenu.tsx index 80dd514c..76a954c0 100644 --- a/components/reports/ReportExportMenu.tsx +++ b/components/reports/ReportExportMenu.tsx @@ -26,9 +26,11 @@ export interface ReportExportItem { export function ReportExportMenu({ items, children, + size = 'sm', }: { items?: ReportExportItem[] children?: React.ReactNode + size?: 'default' | 'sm' }) { const t = useTranslations('reports') const hasItems = !!items && items.length > 0 @@ -39,7 +41,7 @@ export function ReportExportMenu({ {hasItems && ( - diff --git a/components/transactions/BankSyncSinceLastVisit.tsx b/components/transactions/BankSyncSinceLastVisit.tsx index 17c8b9d4..30be547c 100644 --- a/components/transactions/BankSyncSinceLastVisit.tsx +++ b/components/transactions/BankSyncSinceLastVisit.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { useTranslations } from 'next-intl' -import { Sparkles, X } from 'lucide-react' +import { X } from 'lucide-react' import { createClient } from '@/lib/supabase/client' import { useCompany } from '@/contexts/CompanyContext' @@ -73,7 +73,6 @@ export default function BankSyncSinceLastVisit() { return (
- {count === 1 ? t('bank_sync_new_since_last_visit_one') diff --git a/components/ui/empty-state.tsx b/components/ui/empty-state.tsx index 6cfe5109..8e7fe03c 100644 --- a/components/ui/empty-state.tsx +++ b/components/ui/empty-state.tsx @@ -6,7 +6,7 @@ import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' import { - Receipt, + ReceiptText, Users, ArrowLeftRight, Camera, @@ -103,7 +103,7 @@ export function EmptyInvoices() { const t = useTranslations('empty') return (