feat: chat globale revisioni + pagina statistiche admin

- Chat revisioni: rimuovi commenti inline da timeline/kanban, aggiungi
  ChatSection con feed cronologico + selector task opzionale (Invio per inviare)
  Bolle stile chat: Tu (destra, giallo) / iamcavalli (sinistra, verde)
  Tag task su ogni messaggio quando il messaggio non è generale
- API /api/client/comment: supporto entity_type "general" (entity_id = clientId)
- Pagina /admin/analytics: year selector ←→, 4 metric card (contrattualizzato,
  incassato, da incassare, clienti acquisiti), bar chart mensile incassato via CSS
- NavBar: link "Statistiche"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simone Cavalli
2026-05-16 12:52:25 +02:00
parent 457656a2a9
commit d322162c0a
3 changed files with 171 additions and 4 deletions
+97
View File
@@ -0,0 +1,97 @@
import { getAnalyticsByYear, getMonthlyCollected, getAvailableYears } from "@/lib/analytics-queries";
import { YearSelector, MonthlyChart } from "@/components/admin/YearSelector";
export const revalidate = 0;
function fmt(n: number) {
return n.toLocaleString("it-IT", { style: "currency", currency: "EUR", minimumFractionDigits: 2 });
}
interface MetricCardProps {
label: string;
value: string;
sub?: string;
accent?: boolean;
}
function MetricCard({ label, value, sub, accent }: MetricCardProps) {
return (
<div className={`rounded-xl border p-5 ${accent ? "bg-[#1A463C] border-[#1A463C] text-white" : "bg-white border-[#e5e7eb]"}`}>
<p className={`text-xs font-bold uppercase tracking-wider mb-2 ${accent ? "text-white/60" : "text-[#71717a]"}`}>
{label}
</p>
<p className={`text-2xl font-bold tracking-tight ${accent ? "text-white" : "text-[#1a1a1a]"}`}>
{value}
</p>
{sub && (
<p className={`text-xs mt-1 ${accent ? "text-white/60" : "text-[#71717a]"}`}>{sub}</p>
)}
</div>
);
}
export default async function AnalyticsPage({
searchParams,
}: {
searchParams: Promise<{ year?: string }>;
}) {
const { year: yearParam } = await searchParams;
const year = parseInt(yearParam ?? "") || new Date().getFullYear();
const [data, monthly, availableYears] = await Promise.all([
getAnalyticsByYear(year),
getMonthlyCollected(year),
getAvailableYears(),
]);
const collectedPct = data.contracted > 0
? Math.round((data.collected / data.contracted) * 100)
: 0;
return (
<div className="space-y-8">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-[#1a1a1a]">Statistiche</h1>
<p className="text-sm text-[#71717a] mt-0.5">Panoramica finanziaria per anno</p>
</div>
<YearSelector currentYear={year} availableYears={availableYears} />
</div>
{/* Metrics grid */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard
label="Contrattualizzato"
value={fmt(data.contracted)}
sub={`${data.clientsAcquired} client${data.clientsAcquired === 1 ? "e" : "i"} acquisit${data.clientsAcquired === 1 ? "o" : "i"}`}
accent
/>
<MetricCard
label="Incassato"
value={fmt(data.collected)}
sub={`${collectedPct}% del contrattualizzato`}
/>
<MetricCard
label="Da incassare"
value={fmt(data.pending)}
sub="Pagamenti in sospeso (tutti gli anni)"
/>
<MetricCard
label="Clienti acquisiti"
value={String(data.clientsAcquired)}
sub={`Anno ${year}`}
/>
</div>
{/* Monthly chart */}
<MonthlyChart data={monthly} year={year} />
{data.contracted === 0 && (
<p className="text-sm text-[#71717a] italic text-center py-4">
Nessun cliente registrato nel {year}.
</p>
)}
</div>
);
}