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:
+1
-45
@@ -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,
|
||||
}: {
|
||||
|
||||
Reference in New Issue
Block a user