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>
59 lines
2.1 KiB
TypeScript
59 lines
2.1 KiB
TypeScript
'use client'
|
|
|
|
import { useTranslations } from 'next-intl'
|
|
import { DetailSection } from '@/components/ui/detail-section'
|
|
import { TaxTableStatus } from '@/components/salary/TaxTableStatus'
|
|
import { formatCurrency } from '@/lib/utils'
|
|
import type { SalaryRunEmployee } from '@/types'
|
|
|
|
type SreWithEmployee = SalaryRunEmployee & {
|
|
employee?: { first_name: string; last_name: string }
|
|
}
|
|
|
|
interface RunCalculationDetailsProps {
|
|
periodYear: number
|
|
employees: SalaryRunEmployee[]
|
|
}
|
|
|
|
export function RunCalculationDetails({ periodYear, employees }: RunCalculationDetailsProps) {
|
|
const t = useTranslations('salary_run')
|
|
const withBreakdown = employees.filter(e => e.calculation_breakdown)
|
|
if (withBreakdown.length === 0) return null
|
|
|
|
return (
|
|
<DetailSection kicker={t('calculation_details_title')}>
|
|
<div className="space-y-6">
|
|
<TaxTableStatus year={periodYear} compact />
|
|
{withBreakdown.map(sre => {
|
|
const breakdown = sre.calculation_breakdown as {
|
|
steps?: Array<{ label: string; formula: string; output: number | null }>
|
|
}
|
|
const employee = (sre as SreWithEmployee).employee
|
|
return (
|
|
<div key={sre.id}>
|
|
<h4 className="text-sm font-medium">
|
|
{employee
|
|
? `${employee.first_name} ${employee.last_name}`
|
|
: sre.employee_id.slice(0, 8)}
|
|
</h4>
|
|
{/* Flat calculation steps: one hairline per step, no box. */}
|
|
<div className="mt-1 divide-y divide-border text-xs">
|
|
{(breakdown?.steps || []).map((step, i) => (
|
|
<div key={i} className="flex justify-between gap-4 py-1">
|
|
<span className="min-w-0 text-muted-foreground">
|
|
{step.label}: <span className="font-mono">{step.formula}</span>
|
|
</span>
|
|
{step.output !== null && (
|
|
<span className="shrink-0 tabular-nums">{formatCurrency(step.output)}</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</DetailSection>
|
|
)
|
|
}
|