feat(progetti): riepilogo soldi e avanzamento in testa al progetto

Per sapere a che punto era un progetto bisognava aprire tre tab e sommare a
mente. Ora la risposta e' sopra i tab: contrattualizzato, incassato, da
incassare, redditivita' oraria, e una barra con i task fatti sul totale.

Nessuna query nuova: sono tutti dati che getProjectFullDetail restituisce gia'
per i tab sottostanti. E' aritmetica su quello che c'e'.

Una scelta: l'incassato si legge dai `payments`, non da `accepted_total`. Il
contratto dice quanto vale il progetto, le rate dicono quanto e' entrato
davvero, e quando i due non tornano e' un'informazione — non un errore di
calcolo da nascondere pareggiando i conti.

MetricCard esce da admin/page.tsx e diventa un componente condiviso: due copie
della stessa card avrebbero iniziato a divergere alla prima modifica.

Numeri verificati contro il DB di produzione, progetto per progetto. Rossi Inc:
7.000 EUR su 30h tracciate = 233 EUR/h sopra il target di 100, 3 task su 28,
1 fase su 4. Teckell, che ha ore ma nessun contratto, mostra "—" e non uno
zero travestito da dato.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 22:40:48 +02:00
parent a9358da96f
commit d6e95ef66a
4 changed files with 185 additions and 45 deletions
+1 -45
View File
@@ -12,54 +12,10 @@ import { YearSelector } from "@/components/admin/YearSelector";
import { ForecastChart } from "@/components/admin/ForecastChart";
import { OffersSoldChart } from "@/components/admin/OffersSoldChart";
import { ClientProfitability } from "@/components/admin/dashboard/ClientProfitability";
import { MetricCard, fmtEur0 } from "@/components/admin/MetricCard";
export const revalidate = 0;
function MetricCard({
label,
value,
valueAccent,
delta,
sub,
}: {
label: string;
value: string;
valueAccent?: boolean;
delta?: string;
sub?: string;
}) {
return (
<div className="bg-card p-5 rounded-xl border border-border-light shadow-card">
<span className="text-[10px] uppercase font-bold text-muted-foreground tracking-wider">
{label}
</span>
<div className="flex items-baseline gap-2 mt-1">
<span
className={`text-2xl font-bold font-mono ${
valueAccent ? "text-emerald-700 dark:text-emerald-400" : "text-foreground"
}`}
>
{value}
</span>
{delta ? (
<span className="text-xs font-medium text-emerald-600 dark:text-emerald-400">{delta}</span>
) : sub ? (
<span className="text-xs text-muted-foreground">{sub}</span>
) : null}
</div>
</div>
);
}
function fmtEur0(n: number) {
return n.toLocaleString("it-IT", {
style: "currency",
currency: "EUR",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
}
export default async function AdminDashboard({
searchParams,
}: {