feat: overhaul all 12 sector extensions with full CRUD, validation, and tests
Add shared components (ConfirmDeleteDialog, EditEntryDialog, validation utils), enhance all 12 extension workspaces with edit/delete dialogs, input validation, period comparisons, and new analytics features. Fix critical bugs in ProjectBilling margin calculation and EarningsPerLiter revenue allocation. Add pure calculation modules with 183 new tests across all extensions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface MonthlyRow {
|
||||
month: string
|
||||
value: number
|
||||
label?: string
|
||||
}
|
||||
|
||||
interface MonthlyTrendTableProps {
|
||||
rows: MonthlyRow[]
|
||||
valueLabel?: string
|
||||
valueSuffix?: string
|
||||
formatValue?: (v: number) => string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function MonthlyTrendTable({
|
||||
rows,
|
||||
valueLabel = 'Belopp',
|
||||
valueSuffix = 'kr',
|
||||
formatValue,
|
||||
className,
|
||||
}: MonthlyTrendTableProps) {
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
<div className={cn('rounded-xl border p-6 text-center text-sm text-muted-foreground', className)}>
|
||||
Ingen data att visa
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const maxValue = Math.max(...rows.map(r => Math.abs(r.value)), 1)
|
||||
const fmt = formatValue ?? ((v: number) => v.toLocaleString('sv-SE'))
|
||||
|
||||
return (
|
||||
<div className={cn('rounded-xl border', className)}>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Period</TableHead>
|
||||
<TableHead className="text-right">{valueLabel}</TableHead>
|
||||
<TableHead className="w-[40%]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row) => {
|
||||
const barWidth = Math.round((Math.abs(row.value) / maxValue) * 100)
|
||||
return (
|
||||
<TableRow key={row.month}>
|
||||
<TableCell className="font-medium">{row.label ?? row.month}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{fmt(row.value)} {valueSuffix}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="h-4 w-full rounded-sm bg-muted overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-sm bg-primary/60 transition-all"
|
||||
style={{ width: `${barWidth}%` }}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user