diff --git a/.claude/skills/loop-design-scan/SKILL.md b/.claude/skills/loop-design-scan/SKILL.md new file mode 100644 index 00000000..0e4d8dab --- /dev/null +++ b/.claude/skills/loop-design-scan/SKILL.md @@ -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 (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 `. + +## 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 ``). +- **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] : +Labels: loop:auto, loop:design, enhancement +Body: what's wrong (with file:line) · which design rule it breaks · concrete before→after · screenshot ref + +``` +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.) diff --git a/.claude/skills/loop-issue-triage/SKILL.md b/.claude/skills/loop-issue-triage/SKILL.md new file mode 100644 index 00000000..294c75ab --- /dev/null +++ b/.claude/skills/loop-issue-triage/SKILL.md @@ -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 " 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""`. +- 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-`, minimal fix, run the **[`loop-verify`](../loop-verify/SKILL.md)** gate. +- Open PR with body `Closes #` + 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. diff --git a/.claude/skills/loop-pr-ci-triage/SKILL.md b/.claude/skills/loop-pr-ci-triage/SKILL.md new file mode 100644 index 00000000..9625a0f1 --- /dev/null +++ b/.claude/skills/loop-pr-ci-triage/SKILL.md @@ -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 `; open the failing run's log + (`gh run view --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 --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 ` (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. diff --git a/.claude/skills/loop-vercel-errors/SKILL.md b/.claude/skills/loop-vercel-errors/SKILL.md new file mode 100644 index 00000000..32fcbe8f --- /dev/null +++ b/.claude/skills/loop-vercel-errors/SKILL.md @@ -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 --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 " in:body state:all" --json number,state +``` +- Match open → add a comment ("still occurring, N times since "). 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] () +Labels: loop:auto, loop:vercel, bug +Body: + **Signature:** … **Count / window:** … **First/last seen:** … + **Route/file:** app/…:line **Sample stack:** (fenced) + **Likely cause:** <1–2 lines of hypothesis from the code> + +``` +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-` 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 #`. + +## 6. Report +List: new issues filed, existing issues updated, fix PRs opened, sources used, anything skipped. diff --git a/.claude/skills/loop-verify/SKILL.md b/.claude/skills/loop-verify/SKILL.md new file mode 100644 index 00000000..581155cd --- /dev/null +++ b/.claude/skills/loop-verify/SKILL.md @@ -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 # 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). diff --git a/.claude/workflows/design-scan.js b/.claude/workflows/design-scan.js new file mode 100644 index 00000000..4923242f --- /dev/null +++ b/.claude/workflows/design-scan.js @@ -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 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.', +} diff --git a/app/(dashboard)/articles/[id]/page.tsx b/app/(dashboard)/articles/[id]/page.tsx index 9d56907e..328280a3 100644 --- a/app/(dashboard)/articles/[id]/page.tsx +++ b/app/(dashboard)/articles/[id]/page.tsx @@ -274,7 +274,7 @@ export default function ArticleDetailPage({ {article.type === 'tjanst' && article.housework_type && (
{t('label_housework')} - {article.housework_type} + {article.housework_type}
)} diff --git a/app/(dashboard)/articles/page.tsx b/app/(dashboard)/articles/page.tsx index 8c9652fb..4ef3674b 100644 --- a/app/(dashboard)/articles/page.tsx +++ b/app/(dashboard)/articles/page.tsx @@ -375,10 +375,8 @@ function ArticlesPageInner() { {article.name} - - - {t(ARTICLE_TYPE_LABEL_KEYS[article.type])} - + + {t(ARTICLE_TYPE_LABEL_KEYS[article.type])} {article.unit} @@ -410,15 +408,9 @@ function ArticlesPageInner() { {article.name} -
- - {t(ARTICLE_TYPE_LABEL_KEYS[article.type])} - - {article.article_number && ( - - {article.article_number} - - )} +
+ {t(ARTICLE_TYPE_LABEL_KEYS[article.type])} + {article.article_number ? ` · #${article.article_number}` : ''}
diff --git a/app/(dashboard)/bookkeeping/page.tsx b/app/(dashboard)/bookkeeping/page.tsx index edaf0117..0dda8bf8 100644 --- a/app/(dashboard)/bookkeeping/page.tsx +++ b/app/(dashboard)/bookkeeping/page.tsx @@ -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('journal') const [showNewEntry, setShowNewEntry] = useState(false) const [copyPrefill, setCopyPrefill] = useState(null) const [isLoadingCopy, setIsLoadingCopy] = useState(false) @@ -146,20 +141,7 @@ export default function BookkeepingPage() { } /> - setActiveTab(v as TabValue)}> - - {t('tab_journal')} - {t('tab_accounts')} - - - - - - - - - - + + + + + ) +} diff --git a/app/(dashboard)/customers/[id]/page.tsx b/app/(dashboard)/customers/[id]/page.tsx index a6fb60db..6496406f 100644 --- a/app/(dashboard)/customers/[id]/page.tsx +++ b/app/(dashboard)/customers/[id]/page.tsx @@ -204,7 +204,7 @@ export default function CustomerDetailPage({

{customer.name}

- {t(CUSTOMER_TYPE_KEY[customer.customer_type])} +

{t(CUSTOMER_TYPE_KEY[customer.customer_type])}

@@ -339,7 +339,7 @@ export default function CustomerDetailPage({ {t('section_invoices')} {customer.invoices?.length > 0 && ( - {customer.invoices.length} + ({customer.invoices.length}) )} diff --git a/app/(dashboard)/customers/page.tsx b/app/(dashboard)/customers/page.tsx index 1f343695..de4f1a48 100644 --- a/app/(dashboard)/customers/page.tsx +++ b/app/(dashboard)/customers/page.tsx @@ -367,10 +367,8 @@ function CustomersPageInner() { {customer.name}
- - - {t(CUSTOMER_TYPE_LABEL_KEYS[customer.customer_type])} - + + {t(CUSTOMER_TYPE_LABEL_KEYS[customer.customer_type])}
@@ -413,9 +411,9 @@ function CustomersPageInner() { {customer.name} - +

{t(CUSTOMER_TYPE_LABEL_KEYS[customer.customer_type])} - +

diff --git a/app/(dashboard)/help/page.tsx b/app/(dashboard)/help/page.tsx index d496ffd1..cae60f63 100644 --- a/app/(dashboard)/help/page.tsx +++ b/app/(dashboard)/help/page.tsx @@ -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

{term.term}

{term.simpleTerm && ( - + {term.simpleTerm} - + )}
{!isExpanded && ( @@ -268,11 +267,9 @@ function TermCard({ term, isExpanded, onToggle }: { term: GlossaryTerm; isExpand {term.relatedTerms && term.relatedTerms.length > 0 && (
{t('related_label')} - {term.relatedTerms.map((related) => ( - - {related} - - ))} + + {term.relatedTerms.join(', ')} +
)} diff --git a/app/(dashboard)/invoices/[id]/page.tsx b/app/(dashboard)/invoices/[id]/page.tsx index a66a49b7..98da72c7 100644 --- a/app/(dashboard)/invoices/[id]/page.tsx +++ b/app/(dashboard)/invoices/[id]/page.tsx @@ -901,27 +901,19 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st {invoice.your_reference && ( -
+
{t('your_reference_label')} -
- {invoice.your_reference.split(',').map((ref, i) => ( - - {ref.trim()} - - ))} -
+ + {invoice.your_reference.split(',').map((ref) => ref.trim()).join(', ')} +
)} {invoice.our_reference && ( -
+
{t('our_reference_label')} -
- {invoice.our_reference.split(',').map((ref, i) => ( - - {ref.trim()} - - ))} -
+ + {invoice.our_reference.split(',').map((ref) => ref.trim()).join(', ')} +
)} {invoice.journal_entry_id && ( diff --git a/app/(dashboard)/skattekonto/page.tsx b/app/(dashboard)/skattekonto/page.tsx index d735e38f..024853e3 100644 --- a/app/(dashboard)/skattekonto/page.tsx +++ b/app/(dashboard)/skattekonto/page.tsx @@ -539,9 +539,7 @@ function TransactionTable({ Bokförd ) : row.match_suggestion ? ( - - Möjlig dublett - + Möjlig dublett ) : ( Ej bokförd )} diff --git a/app/(dashboard)/transactions/page.tsx b/app/(dashboard)/transactions/page.tsx index 33877330..b0cd3d0b 100644 --- a/app/(dashboard)/transactions/page.tsx +++ b/app/(dashboard)/transactions/page.tsx @@ -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 diff --git a/components/bokslut/DigitalInlamning.tsx b/components/bokslut/DigitalInlamning.tsx index 9939981e..61ab5bdb 100644 --- a/components/bokslut/DigitalInlamning.tsx +++ b/components/bokslut/DigitalInlamning.tsx @@ -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 (
-
- {badge.label} - {submission.environment !== 'prod' && ( - {submission.environment} - )} -
+ {badge.label}

+ {envLabel ? `${envLabel} · ` : ''} {formatDate(submission.created_at)} {submission.idnummer ? ` · id ${submission.idnummer}` : ''} {submission.undertecknare_namn ? ` · ${submission.undertecknare_namn}` : ''} diff --git a/components/bookkeeping/BookingTemplatePicker.tsx b/components/bookkeeping/BookingTemplatePicker.tsx index ee25f457..2e32c069 100644 --- a/components/bookkeeping/BookingTemplatePicker.tsx +++ b/components/bookkeeping/BookingTemplatePicker.tsx @@ -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 >

-
- {t.name} - - +
{t.name}
+
+ + {SCOPE_LABELS[scope]} - - {t.entity_type !== 'all' && ( - - {t.entity_type === 'enskild_firma' ? 'EF' : 'AB'} - - )} + {t.entity_type !== 'all' && + ` · ${t.entity_type === 'enskild_firma' ? 'EF' : 'AB'}`} +
{t.description && (

diff --git a/components/bookkeeping/ChartOfAccounts.tsx b/components/bookkeeping/ChartOfAccounts.tsx index cadbff8b..3ab7003a 100644 --- a/components/bookkeeping/ChartOfAccounts.tsx +++ b/components/bookkeeping/ChartOfAccounts.tsx @@ -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() { Klass {cls}: {CLASS_LABELS[classNum] || ''} - {classAccounts.length} konton + {classAccounts.length} konton

@@ -179,18 +178,16 @@ export default function ChartOfAccounts() { )} - - - {account.account_type === 'asset' - ? 'Tillgång' - : account.account_type === 'liability' - ? 'Skuld' - : account.account_type === 'equity' - ? 'EK' - : account.account_type === 'revenue' - ? 'Intäkt' - : 'Kostnad'} - + + {account.account_type === 'asset' + ? 'Tillgång' + : account.account_type === 'liability' + ? 'Skuld' + : account.account_type === 'equity' + ? 'EK' + : account.account_type === 'revenue' + ? 'Intäkt' + : 'Kostnad'} {account.normal_balance === 'debit' ? 'Debet' : 'Kredit'} diff --git a/components/bookkeeping/ChartOfAccountsManager.tsx b/components/bookkeeping/ChartOfAccountsManager.tsx index deb7cdfb..37739c2e 100644 --- a/components/bookkeeping/ChartOfAccountsManager.tsx +++ b/components/bookkeeping/ChartOfAccountsManager.tsx @@ -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() { {t('tab_my_accounts')} - - {accounts.length} - + + ({accounts.length}) + @@ -349,9 +348,9 @@ export default function ChartOfAccountsManager() { {t('class_heading', { cls, label: classLabel(classNum) })} - - {activeCount}/{classAccounts.length} - + + {t('active_count_label', { active: activeCount, total: classAccounts.length })} +
@@ -383,9 +382,9 @@ export default function ChartOfAccountsManager() { {account.account_name} {account.is_system_account && ( - + {t('system_badge')} - + )} @@ -395,9 +394,9 @@ export default function ChartOfAccountsManager() { - + {typeLabel(account.account_type)} - + {t('class_heading', { cls, label: classLabel(classNum) })} - + {t('active_count_label', { active: activatedCount, total: classAccounts.length })} - +
@@ -524,9 +523,9 @@ export default function ChartOfAccountsManager() { - + {typeLabel(account.account_type)} - + {account.is_activated ? ( diff --git a/components/bookkeeping/CorrectionEntryDialog.tsx b/components/bookkeeping/CorrectionEntryDialog.tsx index 1a4099aa..cc341864 100644 --- a/components/bookkeeping/CorrectionEntryDialog.tsx +++ b/components/bookkeeping/CorrectionEntryDialog.tsx @@ -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 */}
+ Original {formatVoucher(entry)} {formatDate(entry.entry_date)} - Original

{entry.description}

diff --git a/components/bookkeeping/InboxDocumentPicker.tsx b/components/bookkeeping/InboxDocumentPicker.tsx index 7a9dd647..d672bb16 100644 --- a/components/bookkeeping/InboxDocumentPicker.tsx +++ b/components/bookkeeping/InboxDocumentPicker.tsx @@ -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 {it.supplier_name ?? it.file_name} - - {sourceLabel} -
{it.invoice_date && {formatDate(it.invoice_date)}} {it.supplier_name && ( {it.file_name} )} + {sourceLabel}
{it.amount != null && ( diff --git a/components/bookkeeping/year-end/EfDeclarationSection.tsx b/components/bookkeeping/year-end/EfDeclarationSection.tsx index 38dac11e..7704c9e3 100644 --- a/components/bookkeeping/year-end/EfDeclarationSection.tsx +++ b/components/bookkeeping/year-end/EfDeclarationSection.tsx @@ -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({
{item.label}

{item.description}

- - {item.ne_ruta} - +

NE-ruta {item.ne_ruta}

{formatCurrency(item.amount)} diff --git a/components/common/CommandPalette.tsx b/components/common/CommandPalette.tsx index c135834f..67ea7f25 100644 --- a/components/common/CommandPalette.tsx +++ b/components/common/CommandPalette.tsx @@ -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' }, diff --git a/components/dashboard/DashboardNav.tsx b/components/dashboard/DashboardNav.tsx index d162b83e..2d5f1781 100644 --- a/components/dashboard/DashboardNav.tsx +++ b/components/dashboard/DashboardNav.tsx @@ -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' }, diff --git a/components/deadlines/TaxTodoWidget.tsx b/components/deadlines/TaxTodoWidget.tsx index 4e8a71ea..14680686 100644 --- a/components/deadlines/TaxTodoWidget.tsx +++ b/components/deadlines/TaxTodoWidget.tsx @@ -158,9 +158,9 @@ export function TaxTodoWidget({ deadlines, onStatusChange }: TaxTodoWidgetProps) {formatDate(deadline.due_date)} {deadline.tax_deadline_type && ( - - {TAX_DEADLINE_TYPE_LABELS[deadline.tax_deadline_type]} - + + · {TAX_DEADLINE_TYPE_LABELS[deadline.tax_deadline_type]} + )}

diff --git a/components/deadlines/UpcomingDeadlinesWidget.tsx b/components/deadlines/UpcomingDeadlinesWidget.tsx index 369c2ea3..3b2c0b93 100644 --- a/components/deadlines/UpcomingDeadlinesWidget.tsx +++ b/components/deadlines/UpcomingDeadlinesWidget.tsx @@ -142,17 +142,16 @@ export function UpcomingDeadlinesWidget({ deadlines, maxItems = 5, onStatusChang )}

{deadline.title}

-
-

- {formatDate(deadline.due_date)} - {deadline.due_time && ` ${t('time_prefix')} ${deadline.due_time.slice(0, 5)}`} -

+

+ {formatDate(deadline.due_date)} + {deadline.due_time && ` ${t('time_prefix')} ${deadline.due_time.slice(0, 5)}`} {deadline.tax_deadline_type && ( - - {t('tax_badge')} - + <> + {' '} + · {t('tax_badge')} + )} -

+

diff --git a/components/extensions/CategoryBadge.tsx b/components/extensions/CategoryBadge.tsx index cf128e1c..543c78dc 100644 --- a/components/extensions/CategoryBadge.tsx +++ b/components/extensions/CategoryBadge.tsx @@ -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 = { - 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 = { + 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 ( - - {t(config.labelKey)} + + {t(CATEGORY_LABEL_KEY[category])} ) } diff --git a/components/extensions/general/ArcimMigrationWorkspace.tsx b/components/extensions/general/ArcimMigrationWorkspace.tsx index 7fc4e5bc..5ff8ad5a 100644 --- a/components/extensions/general/ArcimMigrationWorkspace.tsx +++ b/components/extensions/general/ArcimMigrationWorkspace.tsx @@ -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({

{providerInfo?.name ?? consent.provider}

- + Ansluten - +
{consent.companyName && ( @@ -411,19 +412,13 @@ function ProviderStep({

{provider.name}

{comingSoon && ( - - Kommer snart - + Kommer snart )} {alreadyConnected && ( - - Ansluten - + Ansluten )} {needsSieFirst && !comingSoon && !alreadyConnected && ( - - SIE krävs först - + SIE krävs först )}

diff --git a/components/import/BankFileConfirmStep.tsx b/components/import/BankFileConfirmStep.tsx index 221a5f36..622f25ac 100644 --- a/components/import/BankFileConfirmStep.tsx +++ b/components/import/BankFileConfirmStep.tsx @@ -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 && ( -

- - - {refsCount} med OCR/referens - +
+ + {refsCount} med OCR/referens
)} diff --git a/components/import/BankFilePreviewStep.tsx b/components/import/BankFilePreviewStep.tsx index b1875df6..39824a19 100644 --- a/components/import/BankFilePreviewStep.tsx +++ b/components/import/BankFilePreviewStep.tsx @@ -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({ )} {transactions.some((t) => t.reference) && ( - - {tx.reference ? ( - - {tx.reference} - - ) : ( - '–' - )} + + {tx.reference ? tx.reference : '–'} )} diff --git a/components/import/ImportReviewStep.tsx b/components/import/ImportReviewStep.tsx index 831778ea..b971e48f 100644 --- a/components/import/ImportReviewStep.tsx +++ b/components/import/ImportReviewStep.tsx @@ -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({

diff --git a/components/invoices/InvoiceEditor.tsx b/components/invoices/InvoiceEditor.tsx index 0686e130..b51e7084 100644 --- a/components/invoices/InvoiceEditor.tsx +++ b/components/invoices/InvoiceEditor.tsx @@ -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 && (

- - {watchItems[index]?.deduction_type === 'rot' ? 'ROT(30)' : 'RUT(50)'} - + + {watchItems[index]?.deduction_type === 'rot' ? 'ROT 30%' : 'RUT 50%'} + {yourReference && ( -
- {t('your_reference')} -
- {yourReference.split(',').map((ref, i) => ( - - {ref.trim()} - - ))} -
-
+

+ {t('your_reference')} {yourReference} +

)} {ourReference && ( -
- {t('our_reference')} -
- {ourReference.split(',').map((ref, i) => ( - - {ref.trim()} - - ))} -
-
+

+ {t('our_reference')} {ourReference} +

)} {notes &&

{t('notes_prefix', { notes })}

}
diff --git a/components/onboarding/Step3ExtensionSuggestions.tsx b/components/onboarding/Step3ExtensionSuggestions.tsx index 16771a4d..a02be461 100644 --- a/components/onboarding/Step3ExtensionSuggestions.tsx +++ b/components/onboarding/Step3ExtensionSuggestions.tsx @@ -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 = { + 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,

{ext.name}

- + + {tExt(CATEGORY_LABEL_KEY[ext.category])} +

{ext.description}

diff --git a/components/reports/BankReconciliationView.tsx b/components/reports/BankReconciliationView.tsx index 5bc365d9..c498bc22 100644 --- a/components/reports/BankReconciliationView.tsx +++ b/components/reports/BankReconciliationView.tsx @@ -771,9 +771,9 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili {formatDate(m.entry_date)} - + {METHOD_LABELS[m.method] || m.method} - + ))} @@ -840,9 +840,7 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili
{formatDate(tx.date)} · - - {tx.currency} - + {tx.currency} {tx.reference && ( <> · @@ -1066,7 +1064,7 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili {formatDate(tx.date)} {tx.description} - {tx.currency} + {tx.currency} {formatCurrency(tx.amount)} @@ -1130,9 +1128,9 @@ export function BankReconciliationView({ periodId, periodBounds }: BankReconcili {tx.reconciliation_method && ( - + {METHOD_LABELS[tx.reconciliation_method] || tx.reconciliation_method} - + )} diff --git a/components/reports/INK2DeclarationView.tsx b/components/reports/INK2DeclarationView.tsx index 30337cbd..44d2d587 100644 --- a/components/reports/INK2DeclarationView.tsx +++ b/components/reports/INK2DeclarationView.tsx @@ -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 */} -
- - {data.companyInfo.companyName} - - - {data.fiscalYear.name} - -
- {data.companyInfo.orgNumber && ( -

- Org.nr: {data.companyInfo.orgNumber} -

- )} + + {data.companyInfo.companyName} + +

+ {data.fiscalYear.name} + {data.companyInfo.orgNumber && ` · Org.nr: ${data.companyInfo.orgNumber}`} +

diff --git a/components/reports/NEDeclarationView.tsx b/components/reports/NEDeclarationView.tsx index 1c03f894..7d4c3c16 100644 --- a/components/reports/NEDeclarationView.tsx +++ b/components/reports/NEDeclarationView.tsx @@ -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 */} -
- - {data.companyInfo.companyName} - - - {data.fiscalYear.name} - -
+ + {data.companyInfo.companyName} + +

+ {data.fiscalYear.name} +

{data.companyInfo.orgNumber && (

Org.nr: {data.companyInfo.orgNumber} diff --git a/components/reports/ReportLibrary.tsx b/components/reports/ReportLibrary.tsx index 71c42d1b..fb3b63ae 100644 --- a/components/reports/ReportLibrary.tsx +++ b/components/reports/ReportLibrary.tsx @@ -63,7 +63,9 @@ export function ReportLibrary({ } function EntityBadge({ item }: { item: ReportDescriptor }) { - if (item.entityType === 'enskild_firma') return EF - if (item.entityType === 'aktiebolag') return AB + if (item.entityType === 'enskild_firma') + return EF + if (item.entityType === 'aktiebolag') + return AB return null } diff --git a/components/reports/SkatteverketPanel.tsx b/components/reports/SkatteverketPanel.tsx index bfc537fc..da503a75 100644 --- a/components/reports/SkatteverketPanel.tsx +++ b/components/reports/SkatteverketPanel.tsx @@ -554,7 +554,7 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk Skicka till Skatteverket

- + Ansluten diff --git a/components/settings/AgentMemoryPanel.tsx b/components/settings/AgentMemoryPanel.tsx index 299df48d..2b43ddeb 100644 --- a/components/settings/AgentMemoryPanel.tsx +++ b/components/settings/AgentMemoryPanel.tsx @@ -298,8 +298,9 @@ export function AgentMemoryPanel() {
- {KIND_LABEL[row.kind]} - {SOURCE_LABEL[row.source]} + + {KIND_LABEL[row.kind]} · {SOURCE_LABEL[row.source]} + {dimmed && Dold}
@@ -388,16 +389,3 @@ export function AgentMemoryPanel() { ) } - -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' - } -} diff --git a/components/settings/ApiKeysPanel.tsx b/components/settings/ApiKeysPanel.tsx index 1605994e..90bc3cf8 100644 --- a/components/settings/ApiKeysPanel.tsx +++ b/components/settings/ApiKeysPanel.tsx @@ -479,7 +479,7 @@ export function ApiKeysPanel() {

Claude.ai

- {t('recommended_badge')} + {t('recommended_badge')}

{t.rich('claude_ai_instructions', { diff --git a/components/settings/BillingActions.tsx b/components/settings/BillingActions.tsx index 9d159c6d..4b6b9579 100644 --- a/components/settings/BillingActions.tsx +++ b/components/settings/BillingActions.tsx @@ -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 - Spara 2 mån + Spara 2 mån , )}

diff --git a/components/settings/BookingTemplatesPanel.tsx b/components/settings/BookingTemplatesPanel.tsx index e78ae6e5..dad5f1d4 100644 --- a/components/settings/BookingTemplatesPanel.tsx +++ b/components/settings/BookingTemplatesPanel.tsx @@ -314,7 +314,7 @@ function TemplateSection({

{title}

- {templates.length} + {templates.length}
{templates.map((tt) => { @@ -335,14 +335,10 @@ function TemplateSection({
{tt.name}
- + {TEMPLATE_CATEGORY_LABELS[tt.category]} - - {tt.entity_type !== 'all' && ( - - {entityLabels[tt.entity_type]} - - )} + {tt.entity_type !== 'all' && ` · ${entityLabels[tt.entity_type]}`} + {!isConvertible && ( {t('unconvertible_badge')} diff --git a/components/settings/CalendarFeedSettings.tsx b/components/settings/CalendarFeedSettings.tsx index 744e2aef..c894adb1 100644 --- a/components/settings/CalendarFeedSettings.tsx +++ b/components/settings/CalendarFeedSettings.tsx @@ -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', })} - + · + {t('times_count', { count: feed.access_count })} - +
)} diff --git a/components/settings/CompanyMembersSection.tsx b/components/settings/CompanyMembersSection.tsx index 3d313e5c..d6c0b167 100644 --- a/components/settings/CompanyMembersSection.tsx +++ b/components/settings/CompanyMembersSection.tsx @@ -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} {member.source === 'team' && ( - - {t('members_team_badge')} - + · {t('members_team_badge')} )}
@@ -295,9 +292,9 @@ export function CompanyMembersSection() {
- + {roleLabels[inv.role] || inv.role} - + {canInvite && (
@@ -172,9 +170,9 @@ export function CounterpartyTemplatesPanel() {
{tt.line_pattern!.map((lp, i) => (
- + {lp.side === 'debit' ? t('debit_label') : t('credit_label')} - + {formatAccountWithName(lp.account)} {lp.type === 'vat' && lp.vat_rate && ( {t('vat_paren', { rate: Math.round(lp.vat_rate * 100) })} @@ -188,11 +186,11 @@ export function CounterpartyTemplatesPanel() { ) : (
- {t('debit_label')} + {t('debit_label')} {formatAccountWithName(tt.debit_account)}
- {t('credit_label')} + {t('credit_label')} {formatAccountWithName(tt.credit_account)}
diff --git a/components/settings/SkatteverketConnectPanel.tsx b/components/settings/SkatteverketConnectPanel.tsx index efa80014..a3b62722 100644 --- a/components/settings/SkatteverketConnectPanel.tsx +++ b/components/settings/SkatteverketConnectPanel.tsx @@ -254,7 +254,7 @@ function EnvironmentBadge({ environment, disabled }: { environment?: Environment } if (environment === 'test') { return ( - + {t('env_test')} @@ -262,7 +262,7 @@ function EnvironmentBadge({ environment, disabled }: { environment?: Environment } if (environment === 'prod') { return ( - + {t('env_prod')} ) diff --git a/components/settings/TaxSettingsForm.tsx b/components/settings/TaxSettingsForm.tsx index ace631cb..098997fc 100644 --- a/components/settings/TaxSettingsForm.tsx +++ b/components/settings/TaxSettingsForm.tsx @@ -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')}
- + {settings.entity_type === 'aktiebolag' ? t('entity_aktiebolag') : t('entity_enskild_firma')} - +

{t('entity_form_help')}

diff --git a/components/transactions/BankSyncSinceLastVisit.tsx b/components/transactions/BankSyncSinceLastVisit.tsx index 30be547c..89c949ee 100644 --- a/components/transactions/BankSyncSinceLastVisit.tsx +++ b/components/transactions/BankSyncSinceLastVisit.tsx @@ -72,7 +72,7 @@ export default function BankSyncSinceLastVisit() { } return ( -
+
{count === 1 ? t('bank_sync_new_since_last_visit_one') diff --git a/components/transactions/BulkBookDialog.tsx b/components/transactions/BulkBookDialog.tsx index 2e03c02e..6264f82f 100644 --- a/components/transactions/BulkBookDialog.tsx +++ b/components/transactions/BulkBookDialog.tsx @@ -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({ {tpl.name} {tpl.is_system && ( - + {t('system_badge')} - + )}
{tpl.description && ( diff --git a/components/transactions/QuickReviewDialog.tsx b/components/transactions/QuickReviewDialog.tsx index d7809840..63ff3cd2 100644 --- a/components/transactions/QuickReviewDialog.tsx +++ b/components/transactions/QuickReviewDialog.tsx @@ -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')}
- + {template ? template.name_sv : categoryLabel} - + {onChangeTemplate && !isCounterpartyTemplate && (
@@ -169,18 +172,14 @@ function TemplateCard({ template, selected, onClick, compact }: TemplateCardProp {vatLabelKey && ( {t(vatLabelKey)} )} {template.requires_vat_registration_data && ( - + {t('requires_vat_reg')} diff --git a/components/transactions/TransactionInboxCard.tsx b/components/transactions/TransactionInboxCard.tsx index e74872c2..524da432 100644 --- a/components/transactions/TransactionInboxCard.tsx +++ b/components/transactions/TransactionInboxCard.tsx @@ -473,13 +473,12 @@ export default function TransactionInboxCard({ {transaction.title_edited_at && ( <> - {t('edited_badge')} - + )} {skvCounterpartDate && ( diff --git a/messages/en.json b/messages/en.json index 853cc965..0f1b8ab7 100644 --- a/messages/en.json +++ b/messages/en.json @@ -80,6 +80,7 @@ "review": "Review", "transactions": "Transactions", "bookkeeping": "Bookkeeping", + "chart_of_accounts": "Chart of accounts", "assets": "Fixed assets", "reports": "Reports", "budgets": "Budgets", diff --git a/messages/sv.json b/messages/sv.json index ddc46cf4..b1dcc4be 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -80,6 +80,7 @@ "review": "Granskning", "transactions": "Transaktioner", "bookkeeping": "Bokföring", + "chart_of_accounts": "Kontoplan", "assets": "Anläggningstillgångar", "reports": "Rapporter", "budgets": "Budgetar",