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:
Jakob Wennberg
2026-07-01 14:29:23 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent d63d2aecf0
commit 2da9c71eb3
57 changed files with 613 additions and 315 deletions
+1 -1
View File
@@ -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>
+5 -13
View File
@@ -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">
+1 -19
View File
@@ -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>
)
}
+2 -2
View File
@@ -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>
+4 -6
View File
@@ -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>
+5 -8
View File
@@ -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>
)}
+8 -16
View File
@@ -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 && (
+1 -3
View File
@@ -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>
)}
+2 -2
View File
@@ -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