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:
co-authored by
Claude Opus 4.8
parent
db843a7a5b
commit
d63d2aecf0
@@ -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>
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
|
||||
@@ -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' },
|
||||
}
|
||||
|
||||
|
||||
@@ -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' },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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' },
|
||||
|
||||
@@ -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 && (
|
||||
<WorklistRow
|
||||
href="/invoices?status=unpaid"
|
||||
icon={Receipt}
|
||||
icon={ReceiptText}
|
||||
label={t('row_overdue_invoices')}
|
||||
count={counts.overdue_invoice}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline" size={size}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{t('export')}
|
||||
</Button>
|
||||
|
||||
@@ -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 (
|
||||
<div className="inline-flex items-center gap-2 rounded-md border border-success/30 bg-success/5 px-2.5 py-1 text-xs text-success">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
<span>
|
||||
{count === 1
|
||||
? t('bank_sync_new_since_last_visit_one')
|
||||
|
||||
@@ -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 (
|
||||
<EmptyState
|
||||
icon={Receipt}
|
||||
icon={ReceiptText}
|
||||
title={t('preset_invoices_title')}
|
||||
description={t('preset_invoices_description')}
|
||||
actionLabel={t('preset_invoices_action')}
|
||||
|
||||
@@ -129,6 +129,17 @@ export const REPORT_CATALOG: ReportDescriptor[] = [
|
||||
},
|
||||
|
||||
// --- Bokslut (year-end) ---
|
||||
{
|
||||
// The year-end closing wizard (dispositions, accruals, execute). Owns its
|
||||
// route under /bookkeeping; surfaced here so the closing flow is reachable
|
||||
// from Rapporter rather than only via the Bokföring header.
|
||||
slug: 'year-end-closing',
|
||||
labelKey: 'name_year_end_closing',
|
||||
descKey: 'desc_year_end_closing',
|
||||
category: 'year_end',
|
||||
params: 'fiscal',
|
||||
route: '/bookkeeping/year-end',
|
||||
},
|
||||
{
|
||||
slug: 'income-statement',
|
||||
labelKey: 'name_income_statement',
|
||||
|
||||
+7
-3
@@ -15,12 +15,16 @@ export function cn(...inputs: ClassValue[]) {
|
||||
*/
|
||||
const INVALID_DATE_PLACEHOLDER = '—'
|
||||
|
||||
export function formatCurrency(amount: number, currency: string = 'SEK'): string {
|
||||
export function formatCurrency(
|
||||
amount: number,
|
||||
currency: string = 'SEK',
|
||||
options?: { minimumFractionDigits?: number; maximumFractionDigits?: number },
|
||||
): string {
|
||||
return new Intl.NumberFormat('sv-SE', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
minimumFractionDigits: options?.minimumFractionDigits ?? 0,
|
||||
maximumFractionDigits: options?.maximumFractionDigits ?? 2,
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -423,7 +423,6 @@
|
||||
},
|
||||
"suppliers": {
|
||||
"title": "Suppliers",
|
||||
"subtitle": "Manage your suppliers and their payment details",
|
||||
"new_supplier": "New supplier",
|
||||
"add_supplier": "Add supplier",
|
||||
"viewer_disabled_tooltip": "You only have viewer access in this company",
|
||||
@@ -4141,6 +4140,7 @@
|
||||
"desc_balance_sheet": "Financial position at the end of the period",
|
||||
"desc_kassaflodesanalys": "Change in liquidity during the year",
|
||||
"desc_arsredovisning": "Directors' report, notes and signatures",
|
||||
"desc_year_end_closing": "Close the fiscal year — dispositions, accruals and closing entries",
|
||||
"desc_vat_declaration": "Basis for the VAT return (boxes)",
|
||||
"desc_periodisk_sammanstallning": "EU sales of goods and services",
|
||||
"desc_ne_declaration": "NE appendix for sole traders",
|
||||
@@ -4164,6 +4164,7 @@
|
||||
"name_balance_sheet": "Balance sheet",
|
||||
"name_kassaflodesanalys": "Cash flow statement",
|
||||
"name_arsredovisning": "Annual report",
|
||||
"name_year_end_closing": "Year-end closing",
|
||||
"name_vat_declaration": "VAT declaration",
|
||||
"name_periodisk_sammanstallning": "EU sales report",
|
||||
"name_ne_declaration": "NE-bilaga",
|
||||
|
||||
+2
-1
@@ -423,7 +423,6 @@
|
||||
},
|
||||
"suppliers": {
|
||||
"title": "Leverantörer",
|
||||
"subtitle": "Hantera dina leverantörer och deras betalningsuppgifter",
|
||||
"new_supplier": "Ny leverantör",
|
||||
"add_supplier": "Lägg till leverantör",
|
||||
"viewer_disabled_tooltip": "Du har endast läsbehörighet i detta företag",
|
||||
@@ -4141,6 +4140,7 @@
|
||||
"desc_balance_sheet": "Ekonomisk ställning vid periodens slut",
|
||||
"desc_kassaflodesanalys": "Likviditetens förändring under året",
|
||||
"desc_arsredovisning": "Förvaltningsberättelse, noter och underskrifter",
|
||||
"desc_year_end_closing": "Stäng räkenskapsåret – dispositioner, periodiseringar och bokslutstransaktioner",
|
||||
"desc_vat_declaration": "Underlag till momsdeklarationen (rutor)",
|
||||
"desc_periodisk_sammanstallning": "EU-försäljning av varor och tjänster",
|
||||
"desc_ne_declaration": "NE-bilaga för enskild firma",
|
||||
@@ -4164,6 +4164,7 @@
|
||||
"name_balance_sheet": "Balansräkning",
|
||||
"name_kassaflodesanalys": "Kassaflödesanalys",
|
||||
"name_arsredovisning": "Årsredovisning",
|
||||
"name_year_end_closing": "Årsbokslut",
|
||||
"name_vat_declaration": "Momsdeklaration",
|
||||
"name_periodisk_sammanstallning": "Periodisk sammanställning",
|
||||
"name_ne_declaration": "NE-bilaga",
|
||||
|
||||
Reference in New Issue
Block a user