* 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>
106 lines
4.1 KiB
TypeScript
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>
|
|
)
|
|
}
|