UI badge cleanup + /chart-of-accounts route + loop skills (#850)
Bundles three separable concerns: - style(ui): badge audit + cleanup across 45 files — real-status chips use Badge variants (raw Tailwind colors dropped), non-status count/type/label chips demoted to muted text, clustered badges consolidated; 20 unused imports removed. - feat(bookkeeping): Kontoplan moved to a dedicated /chart-of-accounts route (nav + command palette wired); /bookkeeping shows the journal list only. - chore(skills): loop-* automation skills + design-scan workflow under .claude/. fix(reports): restored the destructive count badge on blocking errors in the periodisk sammanställning (EC Sales List) — a genuine status cue the audit had wrongly flattened; flagged by the PR reviewer and Swedish compliance bot, now clean. All CI green; compliance bots report no findings. 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
d63d2aecf0
commit
2da9c71eb3
@@ -274,7 +274,7 @@ export default function ArticleDetailPage({
|
||||
{article.type === 'tjanst' && article.housework_type && (
|
||||
<div className="text-sm flex items-center justify-between">
|
||||
<span className="text-muted-foreground">{t('label_housework')}</span>
|
||||
<Badge variant="secondary">{article.housework_type}</Badge>
|
||||
<span>{article.housework_type}</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -375,10 +375,8 @@ function ArticlesPageInner() {
|
||||
{article.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">
|
||||
{t(ARTICLE_TYPE_LABEL_KEYS[article.type])}
|
||||
</Badge>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{t(ARTICLE_TYPE_LABEL_KEYS[article.type])}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{article.unit}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
@@ -410,15 +408,9 @@ function ArticlesPageInner() {
|
||||
<CardTitle className="text-base truncate group-hover:text-primary transition-colors">
|
||||
{article.name}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="secondary">
|
||||
{t(ARTICLE_TYPE_LABEL_KEYS[article.type])}
|
||||
</Badge>
|
||||
{article.article_number && (
|
||||
<span className="text-xs text-muted-foreground tabular-nums">
|
||||
{article.article_number}
|
||||
</span>
|
||||
)}
|
||||
<div className="mt-1 text-xs text-muted-foreground tabular-nums">
|
||||
{t(ARTICLE_TYPE_LABEL_KEYS[article.type])}
|
||||
{article.article_number ? ` · #${article.article_number}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-display text-lg tabular-nums shrink-0">
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import JournalEntryList from '@/components/bookkeeping/JournalEntryList'
|
||||
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 { Plus } from 'lucide-react'
|
||||
import { PageHeader } from '@/components/ui/page-header'
|
||||
@@ -20,8 +18,6 @@ interface NextVoucher {
|
||||
series: string
|
||||
}
|
||||
|
||||
type TabValue = 'journal' | 'accounts'
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
|
||||
export default function BookkeepingPage() {
|
||||
@@ -34,7 +30,6 @@ export default function BookkeepingPage() {
|
||||
}, [searchParams])
|
||||
|
||||
const [refreshKey, setRefreshKey] = useState(0)
|
||||
const [activeTab, setActiveTab] = useState<TabValue>('journal')
|
||||
const [showNewEntry, setShowNewEntry] = useState(false)
|
||||
const [copyPrefill, setCopyPrefill] = useState<CopyPrefill | null>(null)
|
||||
const [isLoadingCopy, setIsLoadingCopy] = useState(false)
|
||||
@@ -146,20 +141,7 @@ export default function BookkeepingPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabValue)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="journal">{t('tab_journal')}</TabsTrigger>
|
||||
<TabsTrigger value="accounts">{t('tab_accounts')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="journal" forceMount className="space-y-4">
|
||||
<JournalEntryList key={refreshKey} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="accounts" forceMount>
|
||||
<ChartOfAccountsManager />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<JournalEntryList key={refreshKey} />
|
||||
|
||||
<NewJournalEntryDialog
|
||||
open={showNewEntry}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import { PageHeader } from '@/components/ui/page-header'
|
||||
import ChartOfAccountsManager from '@/components/bookkeeping/ChartOfAccountsManager'
|
||||
|
||||
/**
|
||||
* Kontoplan (chart of accounts) — its own page in the Redovisning group, peer to
|
||||
* Bokföring. It is reference/configuration (activate/add/edit accounts, browse
|
||||
* the BAS catalog), not a daily task, so it lives beside the ledger rather than
|
||||
* as a co-equal tab inside it. Balances/lookup stay in Rapporter (Huvudbok).
|
||||
*/
|
||||
export default async function ChartOfAccountsPage() {
|
||||
const t = await getTranslations('nav')
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<PageHeader title={t('chart_of_accounts')} />
|
||||
<ChartOfAccountsManager />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -204,7 +204,7 @@ export default function CustomerDetailPage({
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{customer.name}</h1>
|
||||
<Badge variant="secondary">{t(CUSTOMER_TYPE_KEY[customer.customer_type])}</Badge>
|
||||
<p className="text-sm text-muted-foreground">{t(CUSTOMER_TYPE_KEY[customer.customer_type])}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -339,7 +339,7 @@ export default function CustomerDetailPage({
|
||||
<ReceiptText className="h-4 w-4" />
|
||||
{t('section_invoices')}
|
||||
{customer.invoices?.length > 0 && (
|
||||
<Badge variant="secondary">{customer.invoices.length}</Badge>
|
||||
<span className="text-sm text-muted-foreground tabular-nums">({customer.invoices.length})</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
@@ -367,10 +367,8 @@ function CustomersPageInner() {
|
||||
{customer.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">
|
||||
{t(CUSTOMER_TYPE_LABEL_KEYS[customer.customer_type])}
|
||||
</Badge>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{t(CUSTOMER_TYPE_LABEL_KEYS[customer.customer_type])}
|
||||
</TableCell>
|
||||
<TableCell className="tabular-nums text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -413,9 +411,9 @@ function CustomersPageInner() {
|
||||
<CardTitle className="text-base truncate group-hover:text-primary transition-colors">
|
||||
{customer.name}
|
||||
</CardTitle>
|
||||
<Badge variant="secondary" className="mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1 truncate">
|
||||
{t(CUSTOMER_TYPE_LABEL_KEYS[customer.customer_type])}
|
||||
</Badge>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useState, useMemo } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { HelpLink } from '@/components/ui/info-tooltip'
|
||||
import { PageHeader } from '@/components/ui/page-header'
|
||||
import {
|
||||
@@ -239,9 +238,9 @@ function TermCard({ term, isExpanded, onToggle }: { term: GlossaryTerm; isExpand
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="font-medium">{term.term}</h3>
|
||||
{term.simpleTerm && (
|
||||
<Badge variant="secondary" className="font-normal">
|
||||
<span className="text-sm text-muted-foreground font-normal">
|
||||
{term.simpleTerm}
|
||||
</Badge>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!isExpanded && (
|
||||
@@ -268,11 +267,9 @@ function TermCard({ term, isExpanded, onToggle }: { term: GlossaryTerm; isExpand
|
||||
{term.relatedTerms && term.relatedTerms.length > 0 && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground">{t('related_label')}</span>
|
||||
{term.relatedTerms.map((related) => (
|
||||
<Badge key={related} variant="outline" className="text-xs">
|
||||
{related}
|
||||
</Badge>
|
||||
))}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{term.relatedTerms.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -901,27 +901,19 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
</span>
|
||||
</div>
|
||||
{invoice.your_reference && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="text-muted-foreground">{t('your_reference_label')}</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{invoice.your_reference.split(',').map((ref, i) => (
|
||||
<Badge key={i} variant="secondary" className="text-xs font-normal">
|
||||
{ref.trim()}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-right">
|
||||
{invoice.your_reference.split(',').map((ref) => ref.trim()).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{invoice.our_reference && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="text-muted-foreground">{t('our_reference_label')}</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{invoice.our_reference.split(',').map((ref, i) => (
|
||||
<Badge key={i} variant="secondary" className="text-xs font-normal">
|
||||
{ref.trim()}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-right">
|
||||
{invoice.our_reference.split(',').map((ref) => ref.trim()).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{invoice.journal_entry_id && (
|
||||
|
||||
@@ -539,9 +539,7 @@ function TransactionTable({
|
||||
Bokförd
|
||||
</Badge>
|
||||
) : row.match_suggestion ? (
|
||||
<Badge variant="outline" className="border-warning text-warning">
|
||||
Möjlig dublett
|
||||
</Badge>
|
||||
<Badge variant="warning">Möjlig dublett</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">Ej bokförd</Badge>
|
||||
)}
|
||||
|
||||
@@ -797,7 +797,7 @@ export default function TransactionsPage() {
|
||||
if (Array.isArray(activateBody.unknown) && activateBody.unknown.length > 0) {
|
||||
toast({
|
||||
title: 'Kontot finns inte i BAS-planen',
|
||||
description: `Lägg till ${accountNumber} manuellt under Inställningar → Kontoplan.`,
|
||||
description: `Lägg till ${accountNumber} manuellt i Kontoplan.`,
|
||||
variant: 'destructive',
|
||||
})
|
||||
return
|
||||
@@ -857,7 +857,7 @@ export default function TransactionsPage() {
|
||||
if (Array.isArray(activateBody.unknown) && activateBody.unknown.length > 0) {
|
||||
toast({
|
||||
title: 'Kunde inte hitta alla konton',
|
||||
description: `Lägg till ${activateBody.unknown.join(', ')} manuellt under Inställningar → Kontoplan.`,
|
||||
description: `Lägg till ${activateBody.unknown.join(', ')} manuellt i Kontoplan.`,
|
||||
variant: 'destructive',
|
||||
})
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user