Files
accounted/components/bookkeeping/CorrectionChain.tsx
T
314efe8b22 fix(design): stop synthesizing bold on Hedvig display headings (#1555)
* fix(design): stop synthesizing bold on Hedvig display headings

Hedvig Letters Serif ships weight 400 only, but the h1-h3 base rule
forced font-weight 500 and DialogTitle/SheetTitle stacked font-semibold
on top, so every display heading rendered browser-synthesized bold: the
smudged heavy look on dialog titles and page headings. Drop the base
rule to 400, remove the weight utilities from the title primitives, and
sweep the 41 files that hand-set font-medium/semibold/bold on serif
headings (a pattern design.md already forbids). Headings that opt into
font-sans keep their weight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(design): drop empty className left by the weight sweep

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 11:27:25 +02:00

106 lines
4.1 KiB
TypeScript

'use client'
import Link from 'next/link'
import { useTranslations } from 'next-intl'
import { Badge } from '@/components/ui/badge'
import { Info } from 'lucide-react'
import JournalEntryStatusBadge from '@/components/bookkeeping/JournalEntryStatusBadge'
import { formatDate, formatCurrency } from '@/lib/utils'
import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver'
import type { JournalEntry, JournalEntryLine } from '@/types'
interface Props {
currentEntryId: string
chain: JournalEntry[]
}
function useGetRole() {
const t = useTranslations('journal_correction')
return (entry: JournalEntry): { label: string; color: string } => {
if (entry.source_type === 'storno') {
return { label: t('role_storno'), color: 'bg-destructive' }
}
if (entry.source_type === 'correction') {
return { label: t('role_correction'), color: 'bg-primary' }
}
return { label: t('role_original'), color: 'bg-muted-foreground' }
}
}
function getTotal(entry: JournalEntry): number {
const lines = (entry.lines || []) as JournalEntryLine[]
return lines.reduce((sum, l) => sum + (Number(l.debit_amount) || 0), 0)
}
export default function CorrectionChain({ currentEntryId, chain }: Props) {
const t = useTranslations('journal_correction')
const getRole = useGetRole()
if (chain.length === 0) return null
// Combine current entry isn't in chain: chain is "other" entries
// Sort chronologically
const sorted = [...chain].sort(
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
)
return (
<div className="space-y-3">
<h3 className="text-sm">{t('title')}</h3>
<div className="rounded-lg bg-muted/50 border p-3 flex gap-2 text-sm text-muted-foreground">
<Info className="h-4 w-4 shrink-0 mt-0.5" />
<p>{t('info')}</p>
</div>
<div className="relative space-y-0">
{/* Vertical line connecting nodes */}
<div className="absolute left-[7px] top-3 bottom-3 w-px bg-border" />
{sorted.map((entry) => {
const role = getRole(entry)
const total = getTotal(entry)
const isCurrent = entry.id === currentEntryId
// A cancelled entry is residue from an aborted correction attempt:
// it was voided before taking effect and its lines were removed, so
// it always sums to 0,00. Without the status badge it renders
// exactly like a live storno: dim it and say what it is.
const isCancelled = entry.status === 'cancelled'
return (
<Link
key={entry.id}
href={`/bookkeeping/${entry.id}`}
className="block"
>
<div className={`relative pl-7 py-2 rounded-md transition-colors hover:bg-muted/50 ${isCurrent ? 'bg-muted/30' : ''} ${isCancelled ? 'opacity-60' : ''}`}>
{/* Timeline dot */}
<div className={`absolute left-0.5 top-[18px] h-3 w-3 rounded-full border-2 border-background ${isCancelled ? 'bg-muted-foreground' : role.color}`} />
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs font-medium text-muted-foreground">{role.label}</span>
<span className="font-mono text-sm">
{formatVoucher(entry)}
</span>
<span className="text-sm text-muted-foreground tabular-nums">{formatDate(entry.entry_date)}</span>
<JournalEntryStatusBadge entry={entry} showStatus={isCancelled} />
{isCurrent && (
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
{t('current')}
</Badge>
)}
<span className="ml-auto text-sm tabular-nums text-muted-foreground">
{formatCurrency(total)}
</span>
</div>
{entry.description && (
<p className="text-xs text-muted-foreground truncate mt-0.5">{entry.description}</p>
)}
</div>
</Link>
)
})}
</div>
</div>
)
}