b800dcd403
* style(ui): system-wide UX/UI polish pass — design-system conformance + copy cleanup Multi-agent scan of all 404 UI files against the locked design system, then 141 verified surgical fixes across 109 files (net -32 lines): - Remove forbidden elevation/motion: shadow-* and rounded-xl on cards, active:scale bounce, hover:shadow on list items, transition-all -> transition-colors. - Drop font-medium from single-weight Hedvig display headings/numerals. - Replace raw rainbow Tailwind status colors with Badge variants / brand tokens / neutral surfaces (achromatic chrome, semantic colors stay data-only). - Route raw dates through formatDate(), hand-rolled currency through formatCurrency(), add tabular-nums to financial figures; text-gray-* -> text-foreground tokens. - Swap hand-rolled skeletons for the Skeleton primitive; off-scale spacing -> token scale. - Fix copy: mislabeled "Leverantörsfakturor" -> "Utgifter" on bank-import outflow total, collapse no-op identical-branch ternaries, broken Swedish diacritics (mojibake), correct mismatch-password toast, correct supplier currency-field label. - Remove PII-leaking debug console.log on register, stray console.logs. Verified: tsc clean on all changed files, eslint clean, production build passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): sanitize residual error logs in register flow Follow-up to PR review (compliance swarm V16 / GDPR Art.5(1)(f)): the remaining console.error calls in the register flow passed raw error objects, which Supabase may populate with PII (email) in nested fields. Log only sanitized message strings instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
3.7 KiB
TypeScript
112 lines
3.7 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo } from 'react'
|
|
import { useTranslations } from 'next-intl'
|
|
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { formatCurrency } from '@/lib/utils'
|
|
|
|
interface KPIExpenseMixChartProps {
|
|
composition: {
|
|
class4: number
|
|
class5: number
|
|
class6: number
|
|
class7: number
|
|
}
|
|
}
|
|
|
|
const SEGMENT_COLORS = [
|
|
'hsl(var(--chart-1))',
|
|
'hsl(var(--chart-3))',
|
|
'hsl(var(--chart-2))',
|
|
'hsl(var(--chart-4))',
|
|
]
|
|
|
|
export function KPIExpenseMixChart({ composition }: KPIExpenseMixChartProps) {
|
|
const t = useTranslations('kpi')
|
|
const { class4, class5, class6, class7 } = composition
|
|
const chartData = useMemo(
|
|
() =>
|
|
[
|
|
{ name: t('expense_mix_class4'), value: class4 },
|
|
{ name: t('expense_mix_class5'), value: class5 },
|
|
{ name: t('expense_mix_class6'), value: class6 },
|
|
{ name: t('expense_mix_class7'), value: class7 },
|
|
].filter((s) => s.value > 0),
|
|
[class4, class5, class6, class7, t]
|
|
)
|
|
|
|
const total = class4 + class5 + class6 + class7
|
|
const totalCompact =
|
|
new Intl.NumberFormat('sv-SE', {
|
|
notation: 'compact',
|
|
maximumFractionDigits: 1,
|
|
}).format(total) + ' kr'
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-base">{t('expense_mix_title')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{chartData.length === 0 ? (
|
|
<div className="flex h-[240px] items-center justify-center text-sm text-muted-foreground">
|
|
{t('expense_mix_empty')}
|
|
</div>
|
|
) : (
|
|
<div className="relative flex flex-col items-center">
|
|
<ResponsiveContainer width="100%" height={180}>
|
|
<PieChart>
|
|
<Pie
|
|
data={chartData}
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={58}
|
|
outerRadius={84}
|
|
paddingAngle={2}
|
|
dataKey="value"
|
|
>
|
|
{chartData.map((_, index) => (
|
|
<Cell key={index} fill={SEGMENT_COLORS[index % SEGMENT_COLORS.length]} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip
|
|
formatter={(value) => [formatCurrency(Number(value)), '']}
|
|
contentStyle={{
|
|
fontSize: '12px',
|
|
borderRadius: '8px',
|
|
border: '1px solid hsl(var(--border))',
|
|
backgroundColor: 'hsl(var(--card))',
|
|
}}
|
|
/>
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
<div className="pointer-events-none absolute left-0 right-0 top-0 h-[180px] flex flex-col items-center justify-center">
|
|
<span className="text-[10px] uppercase tracking-wider text-muted-foreground">
|
|
{t('expense_mix_total')}
|
|
</span>
|
|
<span
|
|
className="font-display text-lg tabular-nums"
|
|
title={formatCurrency(total)}
|
|
>
|
|
{totalCompact}
|
|
</span>
|
|
</div>
|
|
<div className="mt-2 flex flex-wrap justify-center gap-x-4 gap-y-2 text-[11px] text-muted-foreground">
|
|
{chartData.map((seg, i) => (
|
|
<div key={seg.name} className="flex items-center gap-2">
|
|
<span
|
|
className="h-2 w-2 rounded-[2px]"
|
|
style={{ backgroundColor: SEGMENT_COLORS[i % SEGMENT_COLORS.length] }}
|
|
/>
|
|
<span>{seg.name}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|