diff --git a/.claude/rules/design.md b/.claude/rules/design.md
index 2cd4676c..ce17ea3c 100644
--- a/.claude/rules/design.md
+++ b/.claude/rules/design.md
@@ -85,7 +85,8 @@ Compact metric cards (e.g. dashboard tiles, salary KPI row) use `p-4`. Detail ca
| Need | Component | Notes |
|---|---|---|
| Page title + action | `components/ui/page-header.tsx` `PageHeader` | Use this, not bespoke `
` + `
` blocks. Drop the `description` prop when it just paraphrases the title. |
-| Data table | `components/ui/table.tsx` `Table / TableHeader / TableHead / TableRow / TableCell` | Header style is baked in: `text-[11px] font-medium uppercase tracking-wider text-muted-foreground`. Wrap in `` when the table is a card's primary content. Add `tabular-nums` to numeric cells. |
+| Data table, **page-level list** | `components/ui/dry-table.tsx` `TH_CLASS` / `TD_CLASS` on a plain `
`, rows `hover:bg-secondary/35` | The concept list table: borderless, straight on the panel, 13px rows, hairline heads. This is what every migrated list page uses. Add `tabular-nums` to numeric cells. Hover-revealed row controls use `HOVER_REVEAL_CLASS` from the same file, never a hand-rolled `opacity-0 group-hover:opacity-100` (coarse pointers never hover, so the control would be unreachable on touch). |
+| Data table, **dialog or report view** | `components/ui/table.tsx` `Table / TableHeader / TableHead / TableRow / TableCell` | Header style is baked in: `text-[11px] font-medium uppercase tracking-wider text-muted-foreground`. Wrap in `` when the table is a card's primary content. `TableCell` is `px-4 py-3` on `text-sm`, so a page-level list built from this primitive comes out ~15% taller with a different hover tint: use the dry-table row above instead. |
| Status indicator | `components/ui/badge.tsx` `` | Chips mark exceptions only: normal states (Aktiv, Bokförd, Betald-i-tid) render as muted text (`text-muted-foreground text-xs`); Badge is reserved for rows that deviate (Utkast, Förfallen, Ej bokförd). A table where every row carries the same chip is wrong. Variants: `default / secondary / success / warning / destructive / outline`. **Never** use raw Tailwind colors (`bg-blue-100`, `bg-emerald-500/10`, etc.) for status. Map status → variant via a small `Record` per feature. |
| No-data state | `components/ui/empty-state.tsx` `EmptyState` | Don't hand-roll `
…
`. Preset variants exist (`EmptyInvoices`, `EmptyCustomers`, `EmptyTransactions`, etc.). |
| Loading placeholder | `components/ui/skeleton.tsx` `` | Don't hand-roll `bg-muted rounded animate-pulse` divs. |
diff --git a/app/(dashboard)/bookkeeping/[id]/page.tsx b/app/(dashboard)/bookkeeping/[id]/page.tsx
index 09a5a536..70cb4f6c 100644
--- a/app/(dashboard)/bookkeeping/[id]/page.tsx
+++ b/app/(dashboard)/bookkeeping/[id]/page.tsx
@@ -17,7 +17,7 @@ import {
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu'
import { useCanWrite } from '@/lib/hooks/use-can-write'
-import { formatDate } from '@/lib/utils'
+import { formatCurrency, formatDate } from '@/lib/utils'
import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver'
import JournalEntryAttachments from '@/components/bookkeeping/JournalEntryAttachments'
import JournalEntryStatusBadge, { useSourceTypeLabels } from '@/components/bookkeeping/JournalEntryStatusBadge'
@@ -32,6 +32,7 @@ import CorrectionChain from '@/components/bookkeeping/CorrectionChain'
import RetagLineDialog, { type RetagLine } from '@/components/dimensions/RetagLineDialog'
import { useCompanySettings } from '@/components/settings/useSettings'
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'
+import { ConfirmDialog } from '@/components/ui/confirm-dialog'
import { Badge } from '@/components/ui/badge'
import { useToast } from '@/components/ui/use-toast'
import { getErrorMessage } from '@/lib/errors/get-error-message'
@@ -85,6 +86,10 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
const [isReversing, setIsReversing] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [isCommitting, setIsCommitting] = useState(false)
+ // Confirm-before-posting (convention 10). The list already gates this exact
+ // action; the detail page used to fire the commit straight from the button.
+ const [showCommitConfirm, setShowCommitConfirm] = useState(false)
+ const [commitVoucherPreview, setCommitVoucherPreview] = useState(null)
const [isLastInSeries, setIsLastInSeries] = useState(false)
const [attachmentCount, setAttachmentCount] = useState(0)
const [references, setReferences] = useState([])
@@ -183,6 +188,20 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
}
}, [id, toast, t])
+ // Open the confirm dialog and fetch the predicted voucher number. The
+ // prediction is indicative (numbers are assigned atomically at commit); the
+ // success toast always shows the real one. Mirrors JournalEntryList.
+ const openCommitConfirm = useCallback(() => {
+ setShowCommitConfirm(true)
+ setCommitVoucherPreview(null)
+ fetch('/api/bookkeeping/voucher-sequences/next')
+ .then((r) => r.json())
+ .then(({ data }) => {
+ if (data?.next != null) setCommitVoucherPreview(`${data.series}${data.next}`)
+ })
+ .catch(() => {})
+ }, [])
+
const handleCommit = useCallback(async () => {
setIsCommitting(true)
try {
@@ -443,7 +462,7 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i