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
+50
View File
@@ -0,0 +1,50 @@
---
name: loop-design-scan
description: Local loop that scans a given area of the Accounted UI against the locked design system (.claude/rules/design.md) for UX friction, visual inconsistency, missing states, motion gaps, and accessibility issues — rendering pages in Chrome — then files GitHub issues for approved findings. Run locally (needs npm run dev + Chrome). Usage: /loop-design-scan <area> (e.g. bookkeeping, invoices, settings).
---
# loop-design-scan
**Goal:** for one area of the app, surface concrete, design-system-grounded UI/UX improvements and file
them as GitHub issues (deduped). This is the GitHub-Issues sibling of the `scout-design` skill (which
targets Linear). Read `dev_docs/loops.md` first. **Runs locally** — it needs to render the UI.
## Why local
A headless cloud session can't see the UI. This loop starts `npm run dev` and drives Chrome to render
and screenshot real pages, then judges them against the design rules. Run on-demand or `/loop 6h /loop-design-scan <area>`.
## 1. Set up
- `npm run dev` (starts on :3000). Load the **design contract**: `.claude/rules/design.md`
(editorial monochrome, locked tokens, primitives, spacing scale, a11y bar).
- Map the area to its route(s), e.g. `bookkeeping``app/(dashboard)/bookkeeping/*`.
## 2. Render & inspect each page (Chrome MCP)
For each page in the area: navigate, screenshot, and check the **console for zero new errors/warnings**.
Exercise key states where possible: loading, empty, populated, error.
## 3. Evaluate against the design system (what "good" means here)
Flag concrete deviations, each tied to `file:line` and a rule:
- **Tokens/spacing**: forbidden spacing (`p-5`, `2.5`, hardcoded px), non-token colors for status
(`bg-blue-100`, `bg-emerald-500/10` instead of `<Badge variant>`).
- **Primitives not used**: hand-rolled empty state / skeleton / page header / table instead of the
components in `.claude/rules/design.md` (`EmptyState`, `Skeleton`, `PageHeader`, `Table`).
- **Missing states**: no loading skeleton, no empty state, no error handling on a data view.
- **Typography**: `font-medium` on Hedvig display text; missing `tabular-nums` on financial numbers.
- **Motion**: press-scale / hover-lift / spring overshoot (all forbidden); missing `transition-colors`.
- **A11y**: contrast < AA, icon-only button without `aria-label`, no visible focus ring, touch target < 40px.
- **UX friction**: unclear labels, redundant `PageHeader.description`, slow paths for the 90-second session.
## 4. Adversarial verification (kill false positives)
Prefer the **`.claude/workflows/design-scan.js`** workflow: it fans out one agent per page, then runs a
skeptic agent per finding ("is this a real regression against the locked design system, or a nitpick?").
Only findings that survive get filed. Cap **6 findings/run**; log the rest.
## 5. File issues (deduped — see loops.md)
```
Title: [design] <area>: <short problem>
Labels: loop:auto, loop:design, enhancement
Body: what's wrong (with file:line) · which design rule it breaks · concrete before→after · screenshot ref
<!-- loop-fingerprint: <area>:<file>:<rule> -->
```
Dedupe against existing `loop:design` issues before filing. Do not auto-fix UI here — design changes
want human taste; file the ticket. (If asked to fix, use `/frontend-design` on an approved ticket.)
+42
View File
@@ -0,0 +1,42 @@
---
name: loop-issue-triage
description: Proactive loop that keeps GitHub Issues in erp-mafia/accounted tidy (label, dedupe, close stale/already-fixed, reconcile with merged PRs) and auto-implements small, well-scoped fixes as PRs. Use on a schedule (cloud routine) or on-demand via /loop-issue-triage. Follows dev_docs/loops.md (propose-don't-merge).
---
# loop-issue-triage
**Goal:** the open-issue list is accurately labeled and free of stale/duplicate/already-fixed items,
and a couple of small fixes ship as PRs each run. **Never merge.** Read `dev_docs/loops.md` first.
## Preflight
`gh auth status`, repo `erp-mafia/accounted`. If it fails, stop and report "environment not provisioned".
## 1. Triage every open issue
```bash
gh issue list --state open --limit 100 --json number,title,labels,createdAt,updatedAt,comments
```
For each issue lacking a type label:
- **Classify** → apply one of `bug` / `enhancement` / `question` / `documentation`.
- **Dedupe** → search for near-duplicates (`gh issue list --search "<keywords> in:title,body state:all"`).
If a duplicate, label `duplicate`, comment linking the canonical issue, and close the newer one.
- **Stale** → no activity > 30 days AND not actionable: comment asking if still relevant; don't close
unilaterally unless clearly obsolete.
## 2. Reconcile with completed work (the "are tickets up to date?" job)
- For each open issue, check whether a **merged** PR or recent commit already resolved it:
`gh pr list --state merged --search "<#num or keywords>"`, `git log --oneline -S"<keyword>"`.
- If resolved: comment with the PR/commit link, verify the fix exists in `main`, then close.
- Conversely, if a merged PR said `Closes #N` but #N is still open, close it with a pointer.
## 3. Auto-fix small issues (propose-don't-merge) — cap 2 PRs/run
Pick issues that are **small, localized, and low-risk** (a clear bug in one file, a copy/label fix, a
missing empty/loading state, a validation gap). **Never** auto-fix anything touching the bookkeeping
engine, money math, migrations/triggers, auth, or compliance logic — those get `loop:needs-human`.
- Branch `loop/issue-<n>`, minimal fix, run the **[`loop-verify`](../loop-verify/SKILL.md)** gate.
- Open PR with body `Closes #<n>` + a one-line summary. Label `loop:auto`, `loop:triage`.
- If the fix turns out larger/riskier than expected mid-way, abandon the branch and instead add a
scoping comment on the issue + `loop:needs-human`.
## 4. Anti-thrash & report
Don't re-triage issues already labeled this run. Summarize: labeled N, deduped N, closed N (with why),
fix PRs opened, escalated N. That summary is the completion notification.
+56
View File
@@ -0,0 +1,56 @@
---
name: loop-pr-ci-triage
description: Proactive loop that watches open PRs in erp-mafia/accounted, fixes failing CI, and addresses actionable review-bot / reviewer comments — on the PR branch, never merging. Use on a schedule (cloud routine) or on-demand via /loop-pr-ci-triage. Follows dev_docs/loops.md (propose-don't-merge, dedupe, loop-verify gate).
---
# loop-pr-ci-triage
**Goal:** every open, non-draft PR authored by our team or dependabot is either green + review-addressed,
or explicitly escalated with `loop:needs-human`. **Never merge.** Read `dev_docs/loops.md` first.
## Preflight
- `gh auth status` and confirm repo `erp-mafia/accounted`. If either fails, stop and report "environment not provisioned".
- Respect [foreign WIP](../../../CLAUDE.md): never touch a branch with another human's uncommitted/active work; never force-push over someone's commits.
## 1. Enumerate & prioritize
```bash
gh pr list --state open --json number,title,isDraft,headRefName,author,reviewDecision,statusCheckRollup,updatedAt
```
Skip: drafts; PRs by `contributor:flagged`/`pr:flagged` authors; PRs already labeled `loop:needs-human`.
Prioritize (cap **5 PRs/run**, log the rest):
1. Failing CI that's a mechanical fix (lint, type error, snapshot, a flaky/needs-rerun check).
2. Dependabot bumps failing `core-only` / `pg-real` (there is a live backlog — #730, #639, #638, #637).
3. Unresolved actionable review comments (from "The PR Agent", Superagent, or a human).
## 2. Diagnose each PR
- Failing checks: read the failing job. `gh pr checks <n>`; open the failing run's log
(`gh run view <run-id> --log-failed`). Identify the *specific* failing command from `core-build.yml` /
`test-pg-real.yml` (e.g. `check:lint`, `npm test`, `test:pg`, the extension-import grep).
- Review comments: `gh pr view <n> --comments`. Separate *actionable* (a concrete bug/change) from
informational. Follow the [bot-triage rule](../../../CLAUDE.md): honest triage beats chasing every finding.
## 3. Fix (propose-don't-merge)
- `gh pr checkout <n>` (or for dependabot, check out its branch). Reproduce the failure locally by
running the exact failing command. Make the **minimal** fix.
- Run the **[`loop-verify`](../loop-verify/SKILL.md)** gate. If it can't pass safely → step 5.
- `git push` to the PR's own branch. Leave a concise comment on the PR: what was failing, what you
changed, verify output. Do **not** `gh pr merge`.
## 4. Dependabot specifics
If a bump breaks the build/tests, the fix is usually a small code adaptation to the new API or a
peer-dep pin. If the bump is a **major** version with wide breakage, don't force it — comment with the
breaking changes found and label `loop:needs-human` instead of a risky rewrite.
## 5. Escalate / anti-thrash
If you already attempted the same fix on this PR (same failure signature) and it failed again, or the
fix is unsafe/large: **stop**, add label `loop:needs-human`, comment what you tried and why you stopped.
Never retry the same failing push in a cycle.
## 6. Report
Summarize per PR: fixed & pushed / commented / escalated / skipped (with reason). This summary is the
routine's completion notification.
## Batch mode (optional)
For many PRs at once, drive this with a dynamic **workflow**: one agent per PR in its own worktree
(`isolation: 'worktree'`) so parallel fixes don't collide, then an adversarial reviewer per fix before
it's pushed. Keep the 5-PR cap unless told otherwise.
@@ -0,0 +1,56 @@
---
name: loop-vercel-errors
description: Proactive loop that pulls recent production runtime errors from Vercel (and Sentry if configured), groups + dedupes them, files well-formed GitHub issues, and opens a fix PR only for clearly trivial/safe cases. Use on a schedule (cloud routine) or on-demand via /loop-vercel-errors. Follows dev_docs/loops.md.
---
# loop-vercel-errors
**Goal:** every distinct, real production runtime error is tracked as a GitHub issue (deduped), and the
obviously-trivial ones have a proposed fix PR. **Never merge.** Read `dev_docs/loops.md` first.
Project: Vercel `erp-base` (`prj_zOvCFaOMXS166cUY5VYEGHKke00X`, team `team_WPj3QZgcSVRWZKcHJQB3wfv8`).
## 1. Fetch errors (try sources in order; report which worked)
1. **Vercel MCP** (`mcp__plugin_vercel_vercel__*`) — authenticate if needed, then pull recent runtime
logs / observability for the production deployment. Prefer this.
2. **Vercel CLI** fallback: `vercel logs <prod-deployment-url> --json` (tails a window) or
`vercel inspect`. The CLI here is old (v48) — if it errors, note it and move on.
3. **Sentry** (the app ships `@sentry/*`; `SENTRY_DSN` may be set) — if a Sentry token is available,
its issue stream is richer than Vercel logs. Use it if present.
If **no** source is reachable in this environment, stop and report that clearly (do not invent errors).
## 2. Group into distinct errors
Cluster raw log lines by **signature**: normalized message + top of stack + route. Drop noise
(expected 4xx, aborted requests, health checks). For each cluster capture: signature, first/last seen,
count, sample stack, affected route/file.
## 3. Dedupe (mandatory — see loops.md)
Fingerprint = hash of the normalized signature.
```bash
gh issue list --search "<fingerprint> in:body state:all" --json number,state
```
- Match open → add a comment ("still occurring, N times since <date>"). Do not file a duplicate.
- Match closed but recurring → reopen with a note.
- No match → file new (below). **Cap 8 new issues/run**; log the rest.
## 4. File the issue
```
Title: [prod error] <short normalized message> (<route>)
Labels: loop:auto, loop:vercel, bug
Body:
**Signature:** … **Count / window:** … **First/last seen:** …
**Route/file:** app/…:line **Sample stack:** (fenced)
**Likely cause:** <12 lines of hypothesis from the code>
<!-- loop-fingerprint: <hash> -->
```
Point at the suspected `file:line` by reading the code the stack references.
## 5. Trivial fix only (propose-don't-merge) — cap 2 PRs/run
Open a `loop/vercel-<hash>` fix PR **only** when the cause is unambiguous and low-risk (e.g. a missing
null guard, an unhandled `undefined`, a bad `.env` read with an obvious default, a narrow type fix).
Anything touching bookkeeping/money/migrations/auth → issue only, label `loop:needs-human`.
Every fix must pass the **[`loop-verify`](../loop-verify/SKILL.md)** gate. PR body: `Closes #<issue>`.
## 6. Report
List: new issues filed, existing issues updated, fix PRs opened, sources used, anything skipped.
+68
View File
@@ -0,0 +1,68 @@
---
name: loop-verify
description: The self-verification gate every automated code change must pass before a loop opens a PR. Encodes Accounted's real CI commands (check:lint, vitest, pg-real, check:guards, no-extension-imports) plus the accounting guard rails and i18n sync. Invoke from any loop-* skill after making a fix and before pushing.
---
# loop-verify — verification gate for automated changes
Never report a change as done, and never open a PR, based on "the edit applied" alone. Verify it
the way our CI would. If any step fails, fix and rerun from the top — do not push partially
verified work.
## 0. Guard rails first (hard stops — no fix is worth breaking these)
Read [Accounting Guard Rails](../../../CLAUDE.md#accounting-guard-rails). A change is **rejected outright** if it:
- edits or deletes a `posted` journal entry (use `reverseEntry`/`correctEntry` — storno, never edit),
- makes an entry that doesn't balance, or sets a voucher number manually,
- writes to a locked/closed period, or deletes a retained document,
- uses `toFixed()` for money (must be `Math.round(x * 100) / 100`), or treats account numbers as numbers (they are strings: `'1930'`).
- touches a trigger/RPC/RLS/DEFERRABLE object without a `*.pg.test.ts` (see step 3).
## 1. Scope the blast radius
List changed files (`git diff --name-only`). Decide which checks below are required:
- always: lint + guards on touched code.
- touched `lib/**` or `app/api/**` → run the matching `__tests__` with vitest.
- touched `supabase/migrations/**` (trigger/RPC/RLS/DEFERRABLE) → **pg-real is mandatory**.
- touched types / config / a shared primitive → run the build.
- touched a user-facing string → i18n sync (step 5).
## 2. Lint + guards (fast, always)
```bash
npm run check:lint # fails on NEW eslint errors (legacy errors are grandfathered)
npm run check:guards # scripts/checks/no-new-antipatterns.mjs
```
Core must not import extensions (CI enforces this):
```bash
grep -r "from '@/extensions/" lib/ app/api/ components/ --include="*.ts" --include="*.tsx" \
| grep -v "app/api/extensions/" | grep -v "components/extensions/" \
| grep -v "lib/extensions/_generated/" | grep -v "lib/extensions/loader.ts"
# any output = FAIL
```
## 3. Tests (targeted)
Run the tests covering the touched area, not the whole suite (cost):
```bash
npx vitest run --project unit <dir> # e.g. lib/bookkeeping app/api/invoices
```
If a trigger / RPC / RLS / DEFERRABLE constraint or any `supabase/migrations/**` object changed,
a `*.pg.test.ts` **must** exist/extend and pass:
```bash
npm run test:pg # vitest run --project pg-real (real Postgres)
```
No pg-real coverage for a DB-behavior change ⇒ do not open the PR; add the test first.
## 4. Build (only when needed)
If types/config/generated inputs changed, or lint/tests can't prove the change compiles:
```bash
npm run build
```
If you edited an atom `SKILL.md` run `npm run skills:check`; taxonomy inputs → `npm run taxonomy:check`.
## 5. i18n sync
For any new/changed user-facing string, update **both** `messages/en.json` and `messages/sv.json`
(mirror the same keys). See [i18n rule](../../rules/i18n.md) for "stays Swedish" surfaces
(email/invoices/reports/salary) — those stay Swedish regardless of locale.
## 6. Verdict
- All required steps green → safe to push the `loop/*` branch and open the PR.
- Anything red you can't safely fix in-scope → **stop**, label the PR/issue `loop:needs-human`,
and comment exactly which step failed and the output. Never merge (`gh pr merge` is forbidden).
+112
View File
@@ -0,0 +1,112 @@
export const meta = {
name: 'design-scan',
description: 'Scan an app area against the Accounted design system; adversarially verify findings; report the survivors for the loop-design-scan skill to file as GitHub issues.',
phases: [
{ title: 'Enumerate' },
{ title: 'Scan' },
{ title: 'Verify' },
{ title: 'Synthesize' },
],
}
// Usage: Workflow({ name: 'design-scan', args: { area: 'bookkeeping' } })
// Pattern: fan-out (one agent per page) -> adversarial verify (skeptic per finding) -> synthesize.
// This workflow only PRODUCES verified findings. The loop-design-scan skill dedupes + files the
// GitHub issues (workflow agents shouldn't take high-privilege outward actions).
const area = (args && args.area) || 'dashboard'
const DESIGN_CONTRACT = `Judge strictly against .claude/rules/design.md (the LOCKED Accounted design system):
editorial monochrome; spacing tokens only (1,2,3,4,6,8,10,12 — p-5/2.5/hardcoded px are violations);
status colors via <Badge variant> only (no bg-blue-100 / bg-emerald-500/10 as chrome); use the
primitives PageHeader/Table/EmptyState/Skeleton/InfoTooltip (hand-rolled equivalents are violations);
Hedvig serif display must NOT be font-medium; financial numbers need tabular-nums; motion is
transition-colors only (no press-scale/hover-lift/spring); a11y WCAG AA, aria-label on icon buttons,
visible focus, touch targets >=40px. A "finding" must cite file:line and the exact rule it breaks.`
const FINDINGS_SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['findings'],
properties: {
findings: {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
required: ['title', 'file', 'rule', 'problem', 'fix', 'severity'],
properties: {
title: { type: 'string' },
file: { type: 'string', description: 'path:line' },
rule: { type: 'string', description: 'which design.md rule it breaks' },
problem: { type: 'string' },
fix: { type: 'string', description: 'concrete before -> after' },
severity: { type: 'string', enum: ['high', 'medium', 'low'] },
},
},
},
},
}
const VERDICT_SCHEMA = {
type: 'object',
additionalProperties: false,
required: ['isReal', 'reason'],
properties: {
isReal: { type: 'boolean', description: 'true only if a real regression against the LOCKED system, not a nitpick or a matter of taste' },
reason: { type: 'string' },
},
}
// 1. Enumerate the pages/components in the area.
phase('Enumerate')
const PAGE_LIST_SCHEMA = {
type: 'object', additionalProperties: false, required: ['pages'],
properties: { pages: { type: 'array', items: { type: 'string' }, description: 'route or component file paths in this area' } },
}
const enumerated = await agent(
`List the distinct page/route files and their primary child components for the "${area}" area of this Next.js app ` +
`(look under app/(dashboard)/${area} and components/${area} and any obviously related components). ` +
`Return concrete file paths a reviewer should open. Keep it to the ~8 most important.`,
{ label: `enumerate:${area}`, phase: 'Enumerate', schema: PAGE_LIST_SCHEMA }
)
const pages = (enumerated?.pages || []).slice(0, 8)
log(`Scanning ${pages.length} pages/components in "${area}"`)
// 2+3. Pipeline: scan each page, then adversarially verify each finding as soon as that page's scan lands.
const perPage = await pipeline(
pages,
(file) => agent(
`Review this file for design-system compliance and UX quality: ${file}\n\n${DESIGN_CONTRACT}\n\n` +
`Read the file (and imports it renders). Report only concrete, defensible findings with file:line.`,
{ label: `scan:${file}`, phase: 'Scan', schema: FINDINGS_SCHEMA }
),
(scan, file) => parallel((scan?.findings || []).map((f) => () =>
agent(
`Adversarially verify this design finding. Default to isReal=false unless it is clearly a real ` +
`regression against the LOCKED design system (not taste, not a pre-existing site-wide pattern).\n\n` +
`${DESIGN_CONTRACT}\n\nFinding: ${JSON.stringify(f)}\nFile: ${file}`,
{ label: `verify:${f.file}`, phase: 'Verify', schema: VERDICT_SCHEMA }
).then((v) => ({ ...f, verdict: v }))
))
)
// 4. Synthesize: keep only verified findings, rank by severity, cap at 6.
phase('Synthesize')
const sevRank = { high: 0, medium: 1, low: 2 }
const confirmed = perPage
.flat()
.filter(Boolean)
.filter((f) => f.verdict?.isReal)
.sort((a, b) => (sevRank[a.severity] ?? 3) - (sevRank[b.severity] ?? 3))
const kept = confirmed.slice(0, 6)
const dropped = confirmed.length - kept.length
if (dropped > 0) log(`Capped: reporting ${kept.length} findings, dropped ${dropped} lower-severity ones (raise the cap to see them).`)
return {
area,
pagesScanned: pages.length,
findings: kept,
note: 'Hand these to the loop-design-scan skill to dedupe (loop-fingerprint) and file as GitHub issues labeled loop:auto, loop:design.',
}
+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
+11 -6
View File
@@ -641,19 +641,24 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
label: submission.status,
variant: 'outline' as const,
}
const envLabel =
submission.environment === 'prod'
? ''
: submission.environment === 'test'
? 'Testmiljö'
: submission.environment === 'acceptans'
? 'Acceptansmiljö'
: submission.environment.charAt(0).toUpperCase() +
submission.environment.slice(1)
return (
<div
key={submission.id}
className="flex items-start justify-between gap-4 border-b border-border last:border-b-0 pb-3 last:pb-0"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<Badge variant={badge.variant}>{badge.label}</Badge>
{submission.environment !== 'prod' && (
<Badge variant="outline">{submission.environment}</Badge>
)}
</div>
<Badge variant={badge.variant}>{badge.label}</Badge>
<p className="text-xs text-muted-foreground tabular-nums">
{envLabel ? `${envLabel} · ` : ''}
{formatDate(submission.created_at)}
{submission.idnummer ? ` · id ${submission.idnummer}` : ''}
{submission.undertecknare_namn ? ` · ${submission.undertecknare_namn}` : ''}
@@ -3,7 +3,6 @@
import { useState, useEffect, useMemo, useCallback } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { useToast } from '@/components/ui/use-toast'
import {
Dialog,
@@ -193,17 +192,14 @@ export default function BookingTemplatePicker({ onApply, entityType, defaultAmou
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{t.name}</span>
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 shrink-0">
<ScopeIcon className="h-3 w-3 mr-0.5" />
<div className="font-medium text-sm">{t.name}</div>
<div className="flex items-center gap-1 text-[11px] text-muted-foreground mt-0.5">
<ScopeIcon className="h-3 w-3 shrink-0" />
<span>
{SCOPE_LABELS[scope]}
</Badge>
{t.entity_type !== 'all' && (
<Badge variant="outline" className="text-[10px] px-1.5 py-0 shrink-0">
{t.entity_type === 'enskild_firma' ? 'EF' : 'AB'}
</Badge>
)}
{t.entity_type !== 'all' &&
` · ${t.entity_type === 'enskild_firma' ? 'EF' : 'AB'}`}
</span>
</div>
{t.description && (
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
+11 -14
View File
@@ -3,7 +3,6 @@
import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { ChevronDown, ChevronRight, Search } from 'lucide-react'
@@ -127,7 +126,7 @@ export default function ChartOfAccounts() {
<span className="font-semibold">
Klass {cls}: {CLASS_LABELS[classNum] || ''}
</span>
<Badge variant="secondary">{classAccounts.length} konton</Badge>
<span className="text-sm text-muted-foreground">{classAccounts.length} konton</span>
</div>
</button>
@@ -179,18 +178,16 @@ export default function ChartOfAccounts() {
</button>
)}
</td>
<td className="py-2 text-center">
<Badge variant="outline" className="text-xs">
{account.account_type === 'asset'
? 'Tillgång'
: account.account_type === 'liability'
? 'Skuld'
: account.account_type === 'equity'
? 'EK'
: account.account_type === 'revenue'
? 'Intäkt'
: 'Kostnad'}
</Badge>
<td className="py-2 text-center text-xs text-muted-foreground">
{account.account_type === 'asset'
? 'Tillgång'
: account.account_type === 'liability'
? 'Skuld'
: account.account_type === 'equity'
? 'EK'
: account.account_type === 'revenue'
? 'Intäkt'
: 'Kostnad'}
</td>
<td className="py-2 text-center text-xs text-muted-foreground">
{account.normal_balance === 'debit' ? 'Debet' : 'Kredit'}
@@ -3,7 +3,6 @@
import { useState, useEffect, useCallback, useMemo } from 'react'
import { useTranslations } from 'next-intl'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
@@ -283,9 +282,9 @@ export default function ChartOfAccountsManager() {
<TabsList>
<TabsTrigger value="my-accounts">
{t('tab_my_accounts')}
<Badge variant="secondary" className="ml-1.5 text-xs">
{accounts.length}
</Badge>
<span className="ml-1.5 text-xs text-muted-foreground tabular-nums">
({accounts.length})
</span>
</TabsTrigger>
<TabsTrigger value="bas-catalog">
<BookOpen className="mr-1.5 h-3.5 w-3.5" />
@@ -349,9 +348,9 @@ export default function ChartOfAccountsManager() {
<span className="font-semibold text-left">
{t('class_heading', { cls, label: classLabel(classNum) })}
</span>
<Badge variant="secondary" className="text-xs">
{activeCount}/{classAccounts.length}
</Badge>
<span className="text-xs text-muted-foreground tabular-nums">
{t('active_count_label', { active: activeCount, total: classAccounts.length })}
</span>
</div>
</button>
@@ -383,9 +382,9 @@ export default function ChartOfAccountsManager() {
<span className="flex items-center gap-1.5">
{account.account_name}
{account.is_system_account && (
<Badge variant="outline" className="text-[10px] px-1 py-0">
<span className="text-[10px] uppercase tracking-wider text-muted-foreground">
{t('system_badge')}
</Badge>
</span>
)}
</span>
</td>
@@ -395,9 +394,9 @@ export default function ChartOfAccountsManager() {
</span>
</td>
<td className="py-2 text-center">
<Badge variant="outline" className="text-xs">
<span className="text-xs text-muted-foreground">
{typeLabel(account.account_type)}
</Badge>
</span>
</td>
<td className="py-2 text-center">
<Switch
@@ -479,9 +478,9 @@ export default function ChartOfAccountsManager() {
<span className="font-semibold text-left">
{t('class_heading', { cls, label: classLabel(classNum) })}
</span>
<Badge variant="secondary" className="text-xs">
<span className="text-xs text-muted-foreground tabular-nums">
{t('active_count_label', { active: activatedCount, total: classAccounts.length })}
</Badge>
</span>
</div>
</button>
@@ -524,9 +523,9 @@ export default function ChartOfAccountsManager() {
</span>
</td>
<td className="py-2 text-center">
<Badge variant="outline" className="text-xs">
<span className="text-xs text-muted-foreground">
{typeLabel(account.account_type)}
</Badge>
</span>
</td>
<td className="py-2 text-right">
{account.is_activated ? (
@@ -11,7 +11,6 @@ import {
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import AccountCombobox from '@/components/bookkeeping/AccountCombobox'
import CorrectionPreview from '@/components/bookkeeping/CorrectionPreview'
import { useToast } from '@/components/ui/use-toast'
@@ -166,9 +165,9 @@ export default function CorrectionEntryDialog({ entry, open, onOpenChange, onCor
{/* Original entry metadata — lines live inside CorrectionPreview below */}
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground flex-wrap">
<span className="text-muted-foreground">Original</span>
<span className="font-mono">{formatVoucher(entry)}</span>
<span className="tabular-nums">{formatDate(entry.entry_date)}</span>
<Badge variant="outline" className="text-xs">Original</Badge>
</div>
<p className="text-sm">{entry.description}</p>
</div>
@@ -10,7 +10,6 @@ import {
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { Input } from '@/components/ui/input'
@@ -228,15 +227,13 @@ export default function InboxDocumentPicker({ open, onClose, journalEntryId, onL
<span className="text-sm font-medium truncate">
{it.supplier_name ?? it.file_name}
</span>
<Badge variant="outline" className="shrink-0 text-[10px]">
{sourceLabel}
</Badge>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground tabular-nums">
{it.invoice_date && <span>{formatDate(it.invoice_date)}</span>}
{it.supplier_name && (
<span className="truncate font-normal">{it.file_name}</span>
)}
<span className="shrink-0">{sourceLabel}</span>
</div>
</div>
{it.amount != null && (
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Skeleton } from '@/components/ui/skeleton'
@@ -327,9 +326,7 @@ export function EfDeclarationSection({
<div className="flex-1">
<CardTitle className="text-base">{item.label}</CardTitle>
<p className="text-sm text-muted-foreground mt-1">{item.description}</p>
<Badge variant="outline" className="mt-2">
{item.ne_ruta}
</Badge>
<p className="text-xs text-muted-foreground mt-1">NE-ruta {item.ne_ruta}</p>
</div>
<p className="font-display text-2xl tabular-nums shrink-0">
{formatCurrency(item.amount)}
+2
View File
@@ -10,6 +10,7 @@ import {
Wallet,
Building2,
BookOpen,
ListTree,
BarChart3,
Upload,
Package,
@@ -47,6 +48,7 @@ const PAGE_ENTRIES: Entry[] = [
{ id: 'leverantörer', label: 'Leverantörer', icon: Building2, href: '/suppliers' },
{ id: 'leverantörsfakturor', label: 'Leverantörsfakturor', icon: Wallet, href: '/supplier-invoices' },
{ id: 'bokföring', label: 'Bokföring', icon: BookOpen, href: '/bookkeeping', keywords: 'verifikat journal ledger' },
{ id: 'kontoplan', label: 'Kontoplan', icon: ListTree, href: '/chart-of-accounts', keywords: 'kontoplan konton bas chart of accounts konto' },
{ id: 'anläggningstillgångar', label: 'Anläggningstillgångar', icon: Package, href: '/assets', keywords: 'tillgångar assets' },
{ id: 'rapporter', label: 'Rapporter', icon: BarChart3, href: '/reports' },
{ id: 'rapport-resultatrapport', label: 'Visa rapport: Resultatrapport', icon: BarChart3, href: '/reports/resultatrapport', keywords: 'rapport resultat intäkter kostnader' },
+3
View File
@@ -13,6 +13,7 @@ import {
Users,
ArrowLeftRight,
BookOpen,
ListTree,
BarChart3,
Settings,
LogOut,
@@ -92,6 +93,7 @@ type NavLabelKey =
| 'review'
| 'transactions'
| 'bookkeeping'
| 'chart_of_accounts'
| 'assets'
| 'reports'
| 'import'
@@ -144,6 +146,7 @@ const navItems: NavItem[] = [
// Redovisning dropdown
{ href: '/kpi', labelKey: 'kpi', icon: TrendingUp, group: 'redovisning' },
{ href: '/bookkeeping', labelKey: 'bookkeeping', icon: BookOpen, group: 'redovisning' },
{ href: '/chart-of-accounts', labelKey: 'chart_of_accounts', icon: ListTree, group: 'redovisning' },
{ href: '/assets', labelKey: 'assets', icon: Package, group: 'redovisning' },
{ href: '/reports', labelKey: 'reports', icon: BarChart3, group: 'redovisning' },
{ href: '/import', labelKey: 'import', icon: Upload, group: 'redovisning' },
+3 -3
View File
@@ -158,9 +158,9 @@ export function TaxTodoWidget({ deadlines, onStatusChange }: TaxTodoWidgetProps)
{formatDate(deadline.due_date)}
</span>
{deadline.tax_deadline_type && (
<Badge variant="outline" className="text-xs px-1.5 py-0 h-5">
{TAX_DEADLINE_TYPE_LABELS[deadline.tax_deadline_type]}
</Badge>
<span className="text-xs text-muted-foreground">
· {TAX_DEADLINE_TYPE_LABELS[deadline.tax_deadline_type]}
</span>
)}
</div>
</div>
@@ -142,17 +142,16 @@ export function UpcomingDeadlinesWidget({ deadlines, maxItems = 5, onStatusChang
)}
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{deadline.title}</p>
<div className="flex items-center gap-2">
<p className="text-xs text-muted-foreground">
{formatDate(deadline.due_date)}
{deadline.due_time && ` ${t('time_prefix')} ${deadline.due_time.slice(0, 5)}`}
</p>
<p className="text-xs text-muted-foreground">
{formatDate(deadline.due_date)}
{deadline.due_time && ` ${t('time_prefix')} ${deadline.due_time.slice(0, 5)}`}
{deadline.tax_deadline_type && (
<Badge variant="outline" className="text-xs px-1 py-0">
{t('tax_badge')}
</Badge>
<>
{' '}
<span className="text-muted-foreground/30">·</span> {t('tax_badge')}
</>
)}
</div>
</p>
</div>
</div>
+7 -9
View File
@@ -2,22 +2,20 @@
import { useTranslations } from 'next-intl'
import { Badge } from '@/components/ui/badge'
import { cn } from '@/lib/utils'
import type { ExtensionCategory } from '@/lib/extensions/types'
const CATEGORY_CONFIG: Record<ExtensionCategory, { labelKey: string; className: string }> = {
accounting: { labelKey: 'category_accounting', className: 'bg-destructive/10 text-destructive border-destructive/30' },
reports: { labelKey: 'category_reports', className: 'bg-primary/10 text-primary border-primary/30' },
import: { labelKey: 'category_import', className: 'bg-success/10 text-success border-success/30' },
operations: { labelKey: 'category_operations', className: 'bg-muted text-muted-foreground border-border' },
const CATEGORY_LABEL_KEY: Record<ExtensionCategory, string> = {
accounting: 'category_accounting',
reports: 'category_reports',
import: 'category_import',
operations: 'category_operations',
}
export default function CategoryBadge({ category }: { category: ExtensionCategory }) {
const t = useTranslations('extensions')
const config = CATEGORY_CONFIG[category]
return (
<Badge variant="outline" className={cn('text-[10px] font-medium', config.className)}>
{t(config.labelKey)}
<Badge variant="outline" className="text-[10px] font-medium">
{t(CATEGORY_LABEL_KEY[category])}
</Badge>
)
}
@@ -2,6 +2,7 @@
import { useState, useCallback, useEffect } from 'react'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Button, buttonVariants } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -304,10 +305,10 @@ function ProviderStep({
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="font-medium">{providerInfo?.name ?? consent.provider}</p>
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/10 px-2 py-0.5 text-[10px] font-medium text-emerald-600 dark:text-emerald-400">
<Badge variant="success" className="gap-1">
<CheckCircle className="h-3 w-3" />
Ansluten
</span>
</Badge>
</div>
<div className="mt-0.5 space-y-0.5">
{consent.companyName && (
@@ -411,19 +412,13 @@ function ProviderStep({
<div className="flex items-center gap-2">
<p className="font-medium">{provider.name}</p>
{comingSoon && (
<span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
Kommer snart
</span>
<Badge variant="secondary">Kommer snart</Badge>
)}
{alreadyConnected && (
<span className="inline-flex items-center rounded-full bg-emerald-500/10 px-2 py-0.5 text-[10px] font-medium text-emerald-600 dark:text-emerald-400">
Ansluten
</span>
<Badge variant="success">Ansluten</Badge>
)}
{needsSieFirst && !comingSoon && !alreadyConnected && (
<span className="inline-flex items-center rounded-full bg-amber-500/10 px-2 py-0.5 text-[10px] font-medium text-amber-600 dark:text-amber-500">
SIE krävs först
</span>
<Badge variant="warning">SIE krävs först</Badge>
)}
</div>
<p className="text-xs text-muted-foreground">
+3 -6
View File
@@ -3,7 +3,6 @@
import { useState, useEffect } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import {
@@ -169,11 +168,9 @@ export default function BankFileConfirmStep({
{/* Additional info */}
{refsCount > 0 && (
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="text-primary border-primary/30">
<Link2 className="mr-1 h-3 w-3" />
{refsCount} med OCR/referens
</Badge>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Link2 className="h-3 w-3" />
{refsCount} med OCR/referens
</div>
)}
</CardContent>
+2 -9
View File
@@ -2,7 +2,6 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Table,
TableBody,
@@ -160,14 +159,8 @@ export default function BankFilePreviewStep({
</TableCell>
)}
{transactions.some((t) => t.reference) && (
<TableCell className="text-sm">
{tx.reference ? (
<Badge variant="outline" className="font-mono text-xs">
{tx.reference}
</Badge>
) : (
''
)}
<TableCell className="font-mono text-sm text-muted-foreground">
{tx.reference ? tx.reference : ''}
</TableCell>
)}
</TableRow>
+3 -4
View File
@@ -4,7 +4,6 @@ import { useState, useEffect, useRef } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import { Badge } from '@/components/ui/badge'
import { Label } from '@/components/ui/label'
import {
Select,
@@ -373,9 +372,9 @@ export default function ImportReviewStep({
<Label htmlFor="mark-no-doc-required" className="font-medium flex items-center gap-2">
Markera som &quot;Inget underlag krävs&quot;
{isHistoricalImport && (
<Badge variant="secondary" className="text-[10px] font-normal">
Rekommenderas vid migrering
</Badge>
<span className="text-xs font-normal text-muted-foreground">
· Rekommenderas vid migrering
</span>
)}
</Label>
<p className="text-sm text-muted-foreground">
+3 -4
View File
@@ -25,7 +25,6 @@ import { formatCurrency } from '@/lib/utils'
import { getVatRules, getAvailableVatRates } from '@/lib/invoices/vat-rules'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
import { Loader2, Plus, Trash2, ArrowLeft, Send, Eye, Landmark, Lock, AlertTriangle, MoreVertical, CalendarClock } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import {
DropdownMenu,
DropdownMenuTrigger,
@@ -1526,9 +1525,9 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
{isInvoiceDoc && watchItems[index]?.deduction_type && (
<div className="md:col-span-12 mt-2 md:mt-3">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary" className="tabular-nums">
{watchItems[index]?.deduction_type === 'rot' ? 'ROT(30)' : 'RUT(50)'}
</Badge>
<span className="text-xs font-medium tabular-nums text-muted-foreground">
{watchItems[index]?.deduction_type === 'rot' ? 'ROT 30%' : 'RUT 50%'}
</span>
<Controller
name={`items.${index}.work_type`}
control={control}
+6 -20
View File
@@ -236,28 +236,14 @@ export function InvoiceReviewContent({
{(yourReference || ourReference || notes) && (
<div className="border-t pt-3 space-y-2 text-sm text-muted-foreground">
{yourReference && (
<div>
<span>{t('your_reference')}</span>
<div className="flex flex-wrap gap-1 mt-1">
{yourReference.split(',').map((ref, i) => (
<Badge key={i} variant="secondary" className="text-xs font-normal">
{ref.trim()}
</Badge>
))}
</div>
</div>
<p>
<span>{t('your_reference')}</span> {yourReference}
</p>
)}
{ourReference && (
<div>
<span>{t('our_reference')}</span>
<div className="flex flex-wrap gap-1 mt-1">
{ourReference.split(',').map((ref, i) => (
<Badge key={i} variant="secondary" className="text-xs font-normal">
{ref.trim()}
</Badge>
))}
</div>
</div>
<p>
<span>{t('our_reference')}</span> {ourReference}
</p>
)}
{notes && <p>{t('notes_prefix', { notes })}</p>}
</div>
@@ -6,9 +6,16 @@ import { Switch } from '@/components/ui/switch'
import { cn } from '@/lib/utils'
import { getSector } from '@/lib/extensions/sectors'
import { resolveIcon } from '@/lib/extensions/icon-resolver'
import CategoryBadge from '@/components/extensions/CategoryBadge'
import { useTranslations } from 'next-intl'
import { ArrowRight, Loader2 } from 'lucide-react'
import type { SectorSlug, ExtensionDefinition } from '@/lib/extensions/types'
import type { SectorSlug, ExtensionDefinition, ExtensionCategory } from '@/lib/extensions/types'
const CATEGORY_LABEL_KEY: Record<ExtensionCategory, string> = {
accounting: 'category_accounting',
reports: 'category_reports',
import: 'category_import',
operations: 'category_operations',
}
interface Step3Props {
sectorSlug: string | null
@@ -18,6 +25,7 @@ interface Step3Props {
}
export default function Step3ExtensionSuggestions({ sectorSlug, onNext, onBack, isSaving }: Step3Props) {
const tExt = useTranslations('extensions')
const generalSector = getSector('general')
const selectedSector = sectorSlug ? getSector(sectorSlug as SectorSlug) : null
@@ -90,7 +98,9 @@ export default function Step3ExtensionSuggestions({ sectorSlug, onNext, onBack,
<div className="min-w-0">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">{ext.name}</p>
<CategoryBadge category={ext.category} />
<span className="text-[10px] text-muted-foreground">
{tExt(CATEGORY_LABEL_KEY[ext.category])}
</span>
</div>
<p className="text-xs text-muted-foreground line-clamp-1">{ext.description}</p>
</div>
@@ -771,9 +771,9 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili
</td>
<td className="py-2 tabular-nums">{formatDate(m.entry_date)}</td>
<td className="py-2">
<Badge variant="outline" className="text-xs">
<span className="text-xs text-muted-foreground">
{METHOD_LABELS[m.method] || m.method}
</Badge>
</span>
</td>
</tr>
))}
@@ -840,9 +840,7 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground tabular-nums">
<span>{formatDate(tx.date)}</span>
<span aria-hidden>·</span>
<Badge variant="outline" className="text-[10px] uppercase tracking-wider">
{tx.currency}
</Badge>
<span>{tx.currency}</span>
{tx.reference && (
<>
<span aria-hidden>·</span>
@@ -1066,7 +1064,7 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili
<td className="py-2 tabular-nums">{formatDate(tx.date)}</td>
<td className="py-2 truncate max-w-[300px]">{tx.description}</td>
<td className="py-2 text-xs">
<Badge variant="outline" className="text-xs">{tx.currency}</Badge>
<span className="tabular-nums">{tx.currency}</span>
</td>
<td className="py-2 text-right tabular-nums">
{formatCurrency(tx.amount)}
@@ -1130,9 +1128,9 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili
</td>
<td className="py-2">
{tx.reconciliation_method && (
<Badge variant="outline" className="text-xs">
<span className="text-xs text-muted-foreground">
{METHOD_LABELS[tx.reconciliation_method] || tx.reconciliation_method}
</Badge>
</span>
)}
</td>
<td className="py-2">
+7 -14
View File
@@ -3,7 +3,6 @@
import { useState } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Download, AlertCircle, Info } from 'lucide-react'
import { AccountNumber } from '@/components/ui/account-number'
import { formatCurrency } from '@/lib/utils'
@@ -133,19 +132,13 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
{/* Company info */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>
{data.companyInfo.companyName}
</CardTitle>
<Badge variant="secondary">
{data.fiscalYear.name}
</Badge>
</div>
{data.companyInfo.orgNumber && (
<p className="text-sm text-muted-foreground">
Org.nr: {data.companyInfo.orgNumber}
</p>
)}
<CardTitle>
{data.companyInfo.companyName}
</CardTitle>
<p className="text-sm text-muted-foreground">
{data.fiscalYear.name}
{data.companyInfo.orgNumber && ` · Org.nr: ${data.companyInfo.orgNumber}`}
</p>
</CardHeader>
</Card>
+6 -9
View File
@@ -3,7 +3,6 @@
import { useState } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Download, AlertCircle } from 'lucide-react'
import { AccountNumber } from '@/components/ui/account-number'
import type { NEDeclaration } from '@/lib/reports/ne-bilaga/types'
@@ -128,14 +127,12 @@ export function NEDeclarationView({ periodId }: { periodId: string }) {
{/* Company info */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>
{data.companyInfo.companyName}
</CardTitle>
<Badge variant="secondary">
{data.fiscalYear.name}
</Badge>
</div>
<CardTitle>
{data.companyInfo.companyName}
</CardTitle>
<p className="text-sm text-muted-foreground">
{data.fiscalYear.name}
</p>
{data.companyInfo.orgNumber && (
<p className="text-sm text-muted-foreground">
Org.nr: {data.companyInfo.orgNumber}
+4 -2
View File
@@ -63,7 +63,9 @@ export function ReportLibrary({
}
function EntityBadge({ item }: { item: ReportDescriptor }) {
if (item.entityType === 'enskild_firma') return <Badge variant="outline">EF</Badge>
if (item.entityType === 'aktiebolag') return <Badge variant="outline">AB</Badge>
if (item.entityType === 'enskild_firma')
return <span className="text-xs text-muted-foreground">EF</span>
if (item.entityType === 'aktiebolag')
return <span className="text-xs text-muted-foreground">AB</span>
return null
}
+1 -1
View File
@@ -554,7 +554,7 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
Skicka till Skatteverket
</CardTitle>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-success border-success/30 gap-1">
<Badge variant="success" className="gap-1">
<CheckCircle2 className="h-3 w-3" />
Ansluten
</Badge>
+3 -15
View File
@@ -298,8 +298,9 @@ export function AgentMemoryPanel() {
<div className="flex-1 min-w-0 space-y-2">
<div className="flex flex-wrap items-center gap-2">
<Badge variant={kindBadge(row.kind)}>{KIND_LABEL[row.kind]}</Badge>
<Badge variant="outline">{SOURCE_LABEL[row.source]}</Badge>
<span className="text-[11px] text-muted-foreground">
{KIND_LABEL[row.kind]} · {SOURCE_LABEL[row.source]}
</span>
{dimmed && <Badge variant="secondary">Dold</Badge>}
</div>
@@ -388,16 +389,3 @@ export function AgentMemoryPanel() {
</Card>
)
}
function kindBadge(kind: Kind): 'default' | 'secondary' | 'outline' | 'success' | 'warning' {
switch (kind) {
case 'fact':
return 'default'
case 'preference':
return 'secondary'
case 'pattern':
return 'outline'
case 'correction':
return 'warning'
}
}
+1 -1
View File
@@ -479,7 +479,7 @@ export function ApiKeysPanel() {
<div>
<div className="flex items-center gap-2 mb-2">
<p className="text-sm font-medium">Claude.ai</p>
<Badge variant="secondary" className="text-[10px] font-normal px-1.5 py-0">{t('recommended_badge')}</Badge>
<span className="text-xs text-muted-foreground">{t('recommended_badge')}</span>
</div>
<p className="text-xs text-muted-foreground mb-2">
{t.rich('claude_ai_instructions', {
+1 -2
View File
@@ -2,7 +2,6 @@
import { useState, type ReactNode } from 'react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { useToast } from '@/components/ui/use-toast'
import type { BillingPlan } from '@/lib/stripe/client'
@@ -96,7 +95,7 @@ export function BillingActions({ isPaying, configured }: { isPaying: boolean; co
'yearly',
<>
Årsvis
<Badge variant="success" className="ml-2">Spara 2 mån</Badge>
<span className="ml-2 text-xs text-muted-foreground">Spara 2 mån</span>
</>,
)}
</div>
@@ -314,7 +314,7 @@ function TemplateSection({
<div className="flex items-center gap-2 mb-2">
<Icon className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">{title}</h3>
<Badge variant="secondary" className="text-xs">{templates.length}</Badge>
<span className="text-xs text-muted-foreground tabular-nums">{templates.length}</span>
</div>
<div className="space-y-1">
{templates.map((tt) => {
@@ -335,14 +335,10 @@ function TemplateSection({
<div className="flex-1 min-w-0">
<span className="text-sm font-medium">{tt.name}</span>
<div className="flex items-center gap-1.5 mt-0.5 flex-wrap">
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
<span className="text-xs text-muted-foreground">
{TEMPLATE_CATEGORY_LABELS[tt.category]}
</Badge>
{tt.entity_type !== 'all' && (
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
{entityLabels[tt.entity_type]}
</Badge>
)}
{tt.entity_type !== 'all' && ` · ${entityLabels[tt.entity_type]}`}
</span>
{!isConvertible && (
<Badge variant="warning" className="text-[10px] px-1.5 py-0">
{t('unconvertible_badge')}
+3 -3
View File
@@ -8,7 +8,6 @@ import { Switch } from '@/components/ui/switch'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { useToast } from '@/components/ui/use-toast'
import { Badge } from '@/components/ui/badge'
import { Calendar, Copy, RefreshCw, Loader2, ExternalLink, Check } from 'lucide-react'
import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog'
import type { CalendarFeed } from '@/types'
@@ -261,9 +260,10 @@ export function CalendarFeedSettings() {
minute: '2-digit',
})}
</span>
<Badge variant="secondary" className="text-xs">
<span className="text-muted-foreground/30">·</span>
<span className="tabular-nums">
{t('times_count', { count: feed.access_count })}
</Badge>
</span>
</div>
)}
@@ -6,7 +6,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { useToast } from '@/components/ui/use-toast'
import { useCompany } from '@/contexts/CompanyContext'
@@ -242,9 +241,7 @@ export function CompanyMembersSection() {
{roleLabels[member.role] || member.role}
</span>
{member.source === 'team' && (
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
{t('members_team_badge')}
</Badge>
<span className="text-xs text-muted-foreground">· {t('members_team_badge')}</span>
)}
</div>
</div>
@@ -295,9 +292,9 @@ export function CompanyMembersSection() {
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="secondary" className="text-xs">
<span className="text-xs text-muted-foreground">
{roleLabels[inv.role] || inv.role}
</Badge>
</span>
{canInvite && (
<Button
variant="ghost"
@@ -4,7 +4,6 @@ import { useTranslations } from 'next-intl'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { useToast } from '@/components/ui/use-toast'
import { Loader2, Trash2, Users, ChevronDown } from 'lucide-react'
import { formatAccountWithName } from '@/lib/bookkeeping/client-account-names'
@@ -130,9 +129,6 @@ export function CounterpartyTemplatesPanel() {
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="text-sm font-medium truncate">{formatCounterpartyName(tt.counterparty_name)}</p>
<Badge variant="outline" className="text-[10px] shrink-0">
{SOURCE_LABELS[tt.source] || tt.source}
</Badge>
</div>
<div className="flex items-center gap-1.5 mt-0.5 text-xs text-muted-foreground">
{isMultiLine ? (
@@ -157,6 +153,8 @@ export function CounterpartyTemplatesPanel() {
<span className={`tabular-nums ${confidenceColor(Number(tt.confidence))}`}>
{Math.round(Number(tt.confidence) * 100)}%
</span>
<span className="text-muted-foreground/30">·</span>
<span>{SOURCE_LABELS[tt.source] || tt.source}</span>
</div>
</div>
<ChevronDown className={`h-4 w-4 text-muted-foreground shrink-0 transition-transform ${isExpanded ? 'rotate-180' : ''}`} />
@@ -172,9 +170,9 @@ export function CounterpartyTemplatesPanel() {
<div className="space-y-1">
{tt.line_pattern!.map((lp, i) => (
<div key={i} className="flex items-center gap-2 text-xs">
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 w-14 justify-center">
<span className="w-14 shrink-0 text-[10px] uppercase tracking-wider text-muted-foreground">
{lp.side === 'debit' ? t('debit_label') : t('credit_label')}
</Badge>
</span>
<span className="font-mono">{formatAccountWithName(lp.account)}</span>
{lp.type === 'vat' && lp.vat_rate && (
<span className="text-muted-foreground">{t('vat_paren', { rate: Math.round(lp.vat_rate * 100) })}</span>
@@ -188,11 +186,11 @@ export function CounterpartyTemplatesPanel() {
) : (
<div className="space-y-1">
<div className="flex items-center gap-2 text-xs">
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 w-14 justify-center">{t('debit_label')}</Badge>
<span className="w-14 shrink-0 text-[10px] uppercase tracking-wider text-muted-foreground">{t('debit_label')}</span>
<span className="font-mono">{formatAccountWithName(tt.debit_account)}</span>
</div>
<div className="flex items-center gap-2 text-xs">
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 w-14 justify-center">{t('credit_label')}</Badge>
<span className="w-14 shrink-0 text-[10px] uppercase tracking-wider text-muted-foreground">{t('credit_label')}</span>
<span className="font-mono">{formatAccountWithName(tt.credit_account)}</span>
</div>
</div>
@@ -254,7 +254,7 @@ function EnvironmentBadge({ environment, disabled }: { environment?: Environment
}
if (environment === 'test') {
return (
<Badge variant="outline" className="border-amber-400 text-amber-700 dark:border-amber-600 dark:text-amber-400">
<Badge variant="warning">
<FlaskConical className="mr-1 h-3 w-3" />
{t('env_test')}
</Badge>
@@ -262,7 +262,7 @@ function EnvironmentBadge({ environment, disabled }: { environment?: Environment
}
if (environment === 'prod') {
return (
<Badge variant="outline" className="border-emerald-400 text-emerald-700 dark:border-emerald-600 dark:text-emerald-400">
<Badge variant="success">
{t('env_prod')}
</Badge>
)
+2 -3
View File
@@ -6,7 +6,6 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Checkbox } from '@/components/ui/checkbox'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Badge } from '@/components/ui/badge'
import type { CompanySettings } from '@/types'
interface TaxSettingsFormProps {
@@ -35,9 +34,9 @@ export function TaxSettingsForm({ settings }: TaxSettingsFormProps) {
{t('entity_form_heading')}
</h2>
<div className="flex items-center gap-3">
<Badge variant="secondary" className="text-sm">
<span className="text-sm font-medium">
{settings.entity_type === 'aktiebolag' ? t('entity_aktiebolag') : t('entity_enskild_firma')}
</Badge>
</span>
<p className="text-xs text-muted-foreground">
{t('entity_form_help')}
</p>
@@ -72,7 +72,7 @@ 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">
<div className="inline-flex items-center gap-2 rounded-md border border-border bg-secondary px-2.5 py-1 text-xs text-foreground">
<span>
{count === 1
? t('bank_sync_new_since_last_visit_one')
+2 -3
View File
@@ -17,7 +17,6 @@ import {
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useToast } from '@/components/ui/use-toast'
@@ -463,9 +462,9 @@ export default function BulkBookDialog({
<FileText className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
<span className="text-sm font-medium truncate">{tpl.name}</span>
{tpl.is_system && (
<Badge variant="outline" className="text-[10px]">
<span className="flex-shrink-0 text-[10px] text-muted-foreground">
{t('system_badge')}
</Badge>
</span>
)}
</div>
{tpl.description && (
@@ -3,7 +3,6 @@
import { useState, useEffect, useCallback } from 'react'
import { useTranslations } from 'next-intl'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import { useToast } from '@/components/ui/use-toast'
import { formatCurrency, formatDate } from '@/lib/utils'
@@ -316,9 +315,9 @@ export default function QuickReviewDialog({
{isCounterpartyTemplate ? t('label_counterparty_template') : template ? t('label_template') : t('label_category')}
</label>
<div className="mt-1 flex items-center gap-2">
<Badge variant="outline" className="text-sm py-1 px-3">
<span className="text-sm font-medium text-foreground">
{template ? template.name_sv : categoryLabel}
</Badge>
</span>
{onChangeTemplate && !isCounterpartyTemplate && (
<button
type="button"
@@ -151,10 +151,10 @@ export default function SkattekontoInboxCard({
<DataListMeta>
<span className="tabular-nums">{formatDate(row.transaktionsdatum)}</span>
<DataListMetaSeparator />
<Badge variant="outline" className="h-4 gap-1 px-1.5 py-0 text-[10px]">
<span className="inline-flex items-center gap-1 text-muted-foreground">
<Landmark className="h-3 w-3" />
{t('skv_badge')}
</Badge>
</span>
{matchSuggestion && (
<>
<DataListMetaSeparator />
+15 -16
View File
@@ -5,7 +5,7 @@ import { useTranslations } from 'next-intl'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Search, ChevronDown, ChevronUp, AlertTriangle, Info, Building2 } from 'lucide-react'
import { Search, ChevronDown, ChevronUp, AlertTriangle, Info, Building2, PenLine } from 'lucide-react'
import {
getCommonTemplates,
getAdvancedTemplates,
@@ -60,6 +60,12 @@ function getVatLabelKey(template: BookingTemplate): string | null {
}
}
// Reverse charge (omvänd moms) carries the ochre emphasis via the sanctioned
// `warning` variant; every other VAT treatment uses the neutral `secondary`.
function getVatBadgeVariant(vatTreatment: string | null | undefined): 'secondary' | 'warning' {
return vatTreatment === 'reverse_charge' ? 'warning' : 'secondary'
}
function groupTemplates(templates: BookingTemplate[]): Map<TemplateGroup, BookingTemplate[]> {
const grouped = new Map<TemplateGroup, BookingTemplate[]>()
for (const t of templates) {
@@ -117,20 +123,17 @@ function LibraryTemplateCard({ raw, converted, selected, onClick }: LibraryTempl
)}
{vatLabelKey && (
<Badge
variant="secondary"
className={`text-[10px] px-1.5 py-0 ${
converted?.vat_treatment === 'reverse_charge'
? 'bg-warning/10 text-warning-foreground'
: ''
}`}
variant={getVatBadgeVariant(converted?.vat_treatment)}
className="text-[10px] px-1.5 py-0"
>
{t(vatLabelKey)}
</Badge>
)}
{!converted && (
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
<span className="inline-flex items-center gap-1 text-[10px] text-muted-foreground">
<PenLine className="h-3 w-3" />
{t('opens_editor_badge')}
</Badge>
</span>
)}
</div>
</div>
@@ -169,18 +172,14 @@ function TemplateCard({ template, selected, onClick, compact }: TemplateCardProp
</span>
{vatLabelKey && (
<Badge
variant="secondary"
className={`text-[10px] px-1.5 py-0 ${
template.vat_treatment === 'reverse_charge'
? 'bg-warning/10 text-warning-foreground'
: ''
}`}
variant={getVatBadgeVariant(template.vat_treatment)}
className="text-[10px] px-1.5 py-0"
>
{t(vatLabelKey)}
</Badge>
)}
{template.requires_vat_registration_data && (
<Badge variant="outline" className="text-[10px] px-1.5 py-0 border-warning/30 text-warning-foreground gap-0.5">
<Badge variant="warning" className="text-[10px] px-1.5 py-0 gap-0.5">
<AlertTriangle className="h-2.5 w-2.5" />
{t('requires_vat_reg')}
</Badge>
@@ -473,13 +473,12 @@ export default function TransactionInboxCard({
{transaction.title_edited_at && (
<>
<DataListMetaSeparator />
<Badge
variant="secondary"
className="h-4 px-1.5 py-0 text-[10px]"
<span
className="text-muted-foreground"
title={originalName ? t('original_name_tooltip', { name: originalName }) : undefined}
>
{t('edited_badge')}
</Badge>
</span>
</>
)}
{skvCounterpartDate && (
+1
View File
@@ -80,6 +80,7 @@
"review": "Review",
"transactions": "Transactions",
"bookkeeping": "Bookkeeping",
"chart_of_accounts": "Chart of accounts",
"assets": "Fixed assets",
"reports": "Reports",
"budgets": "Budgets",
+1
View File
@@ -80,6 +80,7 @@
"review": "Granskning",
"transactions": "Transaktioner",
"bookkeeping": "Bokföring",
"chart_of_accounts": "Kontoplan",
"assets": "Anläggningstillgångar",
"reports": "Rapporter",
"budgets": "Budgetar",