feat: Dashboard admin redesign to Quiet Luxury + client profitability widget
Replica il mock design-reference/pagina-dashboard: layout condensato a schermata singola (4 KPI + griglia 2/3-1/3), token semantici per dual-theme. - Nuova query getClientProfitability(year): valore orario reale per cliente (contrattualizzato / ore tracciate) con badge margine, target 100 €/h - Nuovo widget ClientProfitability "Analisi Oraria & Redditività Clienti" - Rewrite /admin: KPI strip + Cashflow + Redditività | Follow-up + Offerte Più Richieste. Rimossi chart mensile, card extra, barre ore/cliente - Tokenizzati ForecastChart, OffersSoldChart, FollowUpWidget, YearSelector (ora pill interattiva); rimosso MonthlyChart e prop availableYears inutile - DESIGN-SYSTEM.md: inventario + note dashboard (applied 2026-07-11) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import type { ClientProfitabilityRow } from "@/lib/analytics-queries";
|
||||
|
||||
function fmtRate(n: number) {
|
||||
return n.toLocaleString("it-IT", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
const MARGIN_LABEL: Record<ClientProfitabilityRow["margin"], string> = {
|
||||
ottimo: "Margine Ottimo",
|
||||
in_linea: "In Linea",
|
||||
sotto: "Sotto Target",
|
||||
};
|
||||
|
||||
export function ClientProfitability({ data }: { data: ClientProfitabilityRow[] }) {
|
||||
return (
|
||||
<div className="bg-card rounded-xl border border-border shadow-card p-6">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-4">
|
||||
Analisi Oraria & Redditività Clienti
|
||||
</h3>
|
||||
|
||||
{data.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground italic text-center py-4">
|
||||
Nessuna sessione tracciata per calcolare la redditività.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{data.map((row) => {
|
||||
const isOttimo = row.margin === "ottimo";
|
||||
return (
|
||||
<div
|
||||
key={row.clientId}
|
||||
className="flex items-center justify-between gap-4 p-3 rounded-lg border border-border bg-muted/30"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<h4 className="text-xs font-semibold text-foreground truncate">
|
||||
{row.clientName}
|
||||
</h4>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Tracciate:{" "}
|
||||
<span className="font-mono">{row.hours.toLocaleString("it-IT")}h</span> — Valore
|
||||
Orario Target:{" "}
|
||||
<span className="font-mono">{row.target}€/h</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<span
|
||||
className={`text-xs font-bold font-mono px-2 py-0.5 rounded border ${
|
||||
isOttimo
|
||||
? "text-primary bg-primary/10 border-primary/20"
|
||||
: "text-foreground bg-muted border-border"
|
||||
}`}
|
||||
>
|
||||
{fmtRate(row.realRate)} /h Reali
|
||||
</span>
|
||||
<p className="text-[9px] text-muted-foreground mt-1">{MARGIN_LABEL[row.margin]}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user