Bring every record detail page onto the register-detail document grammar from #1624 (DetailSection/DefRow, one status element per the list pages' chips-mark-exceptions rule, one primary next step plus Förhandsgranska visible and everything else behind a ⋯ overflow menu, line tables on the dry-table idiom with the headline total in the serif): - invoices/[id] (11 cards, 13-button toolbar): Kund | Detaljer rows, Fakturarader table + totals, Anteckningar, Betalning, Påminnelser, Utskickshistorik (InvoiceDeliveryHistory flattened); title carries the doc type, related documents become link rows - supplier-invoices/[id], bookkeeping/[id] (serif title instead of font-mono, JournalEntryAttachments variant="section", CorrectionChain flattened), invoices/[id]/credit, assets/[id]/dispose (form as Fönster rows), salary employees/[id] (edit form behind Redigera in a dialog, Ingående saldon collapsed), salary runs/[id] + run panels (Betalfil, Skattebetalning, AGI, förmåner, override) and the payslip page - DetailSection gains an optional help slot (convention 7) Styling/structure only: no API, fetch, validation, state, dialog or permission change; every action stays reachable. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
65 lines
2.0 KiB
TypeScript
65 lines
2.0 KiB
TypeScript
'use client'
|
|
|
|
import { useTranslations } from 'next-intl'
|
|
import { formatCurrency } from '@/lib/utils'
|
|
import type { SalaryRunEmployee } from '@/types'
|
|
import type { RunDetail } from './types'
|
|
|
|
interface RunKpiCardsProps {
|
|
run: RunDetail
|
|
employees: SalaryRunEmployee[]
|
|
}
|
|
|
|
/**
|
|
* Recomputed from per-employee rows so manual overrides (avancerat läge) are
|
|
* reflected immediately, without relying on run.total_* columns which are
|
|
* frozen at calculate-time.
|
|
*/
|
|
export function RunKpiCards({ run, employees }: RunKpiCardsProps) {
|
|
const t = useTranslations('salary_run')
|
|
|
|
const effTax = employees.reduce((s, e) => s + (e.tax_withheld_override ?? e.tax_withheld), 0)
|
|
const effAvgifter = employees.reduce(
|
|
(s, e) => s + (e.avgifter_amount_override ?? e.avgifter_amount),
|
|
0,
|
|
)
|
|
const effNet = employees.reduce(
|
|
(s, e) => s + (e.net_salary + (e.tax_withheld - (e.tax_withheld_override ?? e.tax_withheld))),
|
|
0,
|
|
)
|
|
const effEmployerCost = employees.reduce(
|
|
(s, e) =>
|
|
s +
|
|
e.gross_salary +
|
|
(e.avgifter_amount_override ?? e.avgifter_amount) +
|
|
e.vacation_accrual +
|
|
e.vacation_accrual_avgifter,
|
|
0,
|
|
)
|
|
|
|
const cards = [
|
|
{ label: t('kpi_gross'), value: run.total_gross },
|
|
{ label: t('kpi_tax'), value: effTax },
|
|
{ label: t('kpi_net'), value: effNet, accent: true },
|
|
{ label: t('kpi_avgifter'), value: effAvgifter },
|
|
{ label: t('kpi_employer_cost'), value: effEmployerCost },
|
|
]
|
|
|
|
// Flat stat row: label/number pairs on whitespace, no boxes or rules.
|
|
// Metrics wrap on narrow viewports.
|
|
return (
|
|
<div className="flex flex-wrap gap-x-10 gap-y-4">
|
|
{cards.map(({ label, value, accent }) => (
|
|
<div key={label} className="min-w-0">
|
|
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">{label}</p>
|
|
<p
|
|
className={`mt-1 font-display text-xl tabular-nums leading-none ${accent ? 'text-success' : ''}`}
|
|
>
|
|
{formatCurrency(value)}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|