feat: UI slop cleanup, invoice icon/header polish + year-end in Rapporter, journal-list DataList refactor (#847)

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) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-06-30 20:42:56 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent db843a7a5b
commit d63d2aecf0
19 changed files with 569 additions and 559 deletions
+4 -6
View File
@@ -191,15 +191,13 @@ export default function ArticleDetailPage({
<div>
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{article.name}</h1>
<div className="flex items-center gap-2 mt-1">
<Badge variant="secondary">{t(ARTICLE_TYPE_KEY[article.type])}</Badge>
{article.article_number && (
<span className="text-sm text-muted-foreground tabular-nums">
{article.article_number}
</span>
)}
<Badge variant={article.active ? 'success' : 'secondary'}>
{article.active ? t('status_active') : t('status_inactive')}
</Badge>
<span className="text-sm text-muted-foreground tabular-nums">
{t(ARTICLE_TYPE_KEY[article.type])}
{article.article_number ? ` · #${article.article_number}` : ''}
</span>
</div>
</div>
</div>
+1
View File
@@ -240,6 +240,7 @@ function ArticlesPageInner() {
action={
<div className="flex items-center gap-2">
<ReportExportMenu
size="default"
items={[
{ format: 'xlsx', href: '/api/export/articles' },
{ format: 'csv', href: '/api/export/articles?format=csv' },
+16 -25
View File
@@ -1,7 +1,6 @@
'use client'
import { useState, useEffect, useMemo } from 'react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
@@ -11,7 +10,7 @@ import { type FormLine } from '@/components/bookkeeping/JournalEntryForm'
import NewJournalEntryDialog, { type CopyPrefill } from '@/components/bookkeeping/NewJournalEntryDialog'
import ChartOfAccountsManager from '@/components/bookkeeping/ChartOfAccountsManager'
import { useToast } from '@/components/ui/use-toast'
import { Lock, Plus } from 'lucide-react'
import { Plus } from 'lucide-react'
import { PageHeader } from '@/components/ui/page-header'
import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver'
import type { JournalEntry, JournalEntryLine } from '@/types'
@@ -129,29 +128,21 @@ export default function BookkeepingPage() {
<PageHeader
title={t('title')}
action={
<div className="flex gap-2 w-full sm:w-auto">
<Button
className="w-full sm:w-auto"
onClick={() => {
setCopyPrefill(null)
setShowNewEntry(true)
}}
>
<Plus className="mr-2 h-4 w-4" />
{t('tab_new_entry')}
{nextVoucher && (
<span className="ml-1 text-primary-foreground/70 tabular-nums">
({nextVoucher.series}{nextVoucher.next})
</span>
)}
</Button>
<Button variant="outline" asChild className="w-full sm:w-auto">
<Link href="/bookkeeping/year-end">
<Lock className="mr-2 h-4 w-4" />
{t('year_end')}
</Link>
</Button>
</div>
<Button
className="w-full sm:w-auto"
onClick={() => {
setCopyPrefill(null)
setShowNewEntry(true)
}}
>
<Plus className="mr-2 h-4 w-4" />
{t('tab_new_entry')}
{nextVoucher && (
<span className="ml-1 text-primary-foreground/70 tabular-nums">
({nextVoucher.series}{nextVoucher.next})
</span>
)}
</Button>
}
/>
+3 -3
View File
@@ -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({
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center gap-2 text-sm">
<Receipt className="h-4 w-4 text-muted-foreground" />
<ReceiptText className="h-4 w-4 text-muted-foreground" />
<span>{t('invoice_count', { count: customer.invoices?.length || 0 })}</span>
</div>
</CardContent>
@@ -336,7 +336,7 @@ export default function CustomerDetailPage({
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Receipt className="h-4 w-4" />
<ReceiptText className="h-4 w-4" />
{t('section_invoices')}
{customer.invoices?.length > 0 && (
<Badge variant="secondary">{customer.invoices.length}</Badge>
+1
View File
@@ -244,6 +244,7 @@ function CustomersPageInner() {
action={
<div className="flex items-center gap-2">
<ReportExportMenu
size="default"
items={[
{ format: 'xlsx', href: '/api/export/customers' },
{ format: 'csv', href: '/api/export/customers?format=csv' },
+3 -3
View File
@@ -24,7 +24,7 @@ import { formatCurrency, formatDate } from '@/lib/utils'
import { cn } from '@/lib/utils'
import { invoiceDisplayNumber } from '@/lib/invoices/display'
import { getDisplayTotal } from '@/lib/invoices/rounding'
import { Plus, Search, Receipt, Lock, Repeat } from 'lucide-react'
import { Plus, Search, ReceiptText, Lock, Repeat } from 'lucide-react'
import { EmptyInvoices } from '@/components/ui/empty-state'
import { useCompany } from '@/contexts/CompanyContext'
import { useCanWrite } from '@/lib/hooks/use-can-write'
@@ -256,7 +256,7 @@ export default function InvoicesPage() {
) : filteredInvoices.length === 0 ? (
searchTerm ? (
<DataListEmpty
icon={<Receipt className="h-6 w-6" />}
icon={<ReceiptText className="h-6 w-6" />}
title={t('no_search_results_title')}
description={t('no_search_results_description', { term: searchTerm })}
/>
@@ -264,7 +264,7 @@ export default function InvoicesPage() {
<EmptyInvoices />
) : (
<DataListEmpty
icon={<Receipt className="h-6 w-6" />}
icon={<ReceiptText className="h-6 w-6" />}
title={t('no_category_title')}
description={t('no_category_description')}
/>
+5 -5
View File
@@ -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<string, { labelKey: string; icon: typeof ArrowLeftRight; variant: 'default' | 'secondary' | 'outline' }> = {
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' },
}
+1 -3
View File
@@ -116,12 +116,10 @@ export default function SuppliersPage() {
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{t('title')}</h1>
<p className="text-muted-foreground">
{t('subtitle')}
</p>
</div>
<div className="flex items-center gap-2">
<ReportExportMenu
size="default"
items={[
{ format: 'xlsx', href: '/api/export/suppliers' },
{ format: 'csv', href: '/api/export/suppliers?format=csv' },