feat(06-02): replace redirect with real dashboard RSC at /admin
- 4 KPI cards: clienti attivi, revenue totale, progetti in corso, pagamenti in sospeso - Activity feed: last 10 events with icon, label, detail, timestamp - Page is RSC (async server component, no use client) - revalidate = 0 for fresh data on each request
This commit is contained in:
+116
-4
@@ -1,6 +1,118 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { getDashboardStats } from "@/lib/dashboard-queries";
|
||||||
|
import { Users, FolderOpen, Euro, Clock } from "lucide-react";
|
||||||
|
|
||||||
// Dashboard page — content added in Phase 6 Plan 02
|
export const revalidate = 0;
|
||||||
export default function AdminRoot() {
|
|
||||||
redirect("/admin/clients");
|
function KpiCard({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
sub,
|
||||||
|
icon: Icon,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
sub?: string;
|
||||||
|
icon: React.ElementType;
|
||||||
|
color: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 p-5 flex items-start gap-4">
|
||||||
|
<div className={`p-2 rounded-lg ${color}`}>
|
||||||
|
<Icon size={20} strokeWidth={1.8} className="text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 font-medium uppercase tracking-wide">{label}</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900 mt-0.5">{value}</p>
|
||||||
|
{sub && <p className="text-xs text-gray-400 mt-0.5">{sub}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTIVITY_ICONS: Record<string, string> = {
|
||||||
|
nuovo_cliente: "👤",
|
||||||
|
nuovo_progetto: "📁",
|
||||||
|
deliverable_approvato: "✅",
|
||||||
|
timer_stoppato: "⏱",
|
||||||
|
};
|
||||||
|
|
||||||
|
function fmt(ts: Date) {
|
||||||
|
return new Intl.DateTimeFormat("it-IT", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}).format(new Date(ts));
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtEur(val: string) {
|
||||||
|
return new Intl.NumberFormat("it-IT", { style: "currency", currency: "EUR" }).format(
|
||||||
|
parseFloat(val) || 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function AdminDashboard() {
|
||||||
|
const { kpi, activity } = await getDashboardStats();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-5xl">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
|
||||||
|
|
||||||
|
{/* KPI cards */}
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||||
|
<KpiCard
|
||||||
|
label="Clienti attivi"
|
||||||
|
value={kpi.clientiAttivi}
|
||||||
|
icon={Users}
|
||||||
|
color="bg-[#1A463C]"
|
||||||
|
/>
|
||||||
|
<KpiCard
|
||||||
|
label="Revenue totale"
|
||||||
|
value={fmtEur(kpi.revenueTotale)}
|
||||||
|
sub="progetti non archiviati"
|
||||||
|
icon={Euro}
|
||||||
|
color="bg-emerald-600"
|
||||||
|
/>
|
||||||
|
<KpiCard
|
||||||
|
label="Progetti in corso"
|
||||||
|
value={kpi.progettiInCorso}
|
||||||
|
icon={FolderOpen}
|
||||||
|
color="bg-blue-600"
|
||||||
|
/>
|
||||||
|
<KpiCard
|
||||||
|
label="Pagamenti in sospeso"
|
||||||
|
value={fmtEur(kpi.pagamentiInSospeso)}
|
||||||
|
sub="da_saldare + inviata"
|
||||||
|
icon={Clock}
|
||||||
|
color="bg-amber-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Activity feed */}
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200">
|
||||||
|
<div className="px-5 py-4 border-b border-gray-100">
|
||||||
|
<h2 className="text-sm font-semibold text-gray-700">Attività recente</h2>
|
||||||
|
</div>
|
||||||
|
{activity.length === 0 ? (
|
||||||
|
<p className="px-5 py-8 text-sm text-gray-400 text-center">Nessuna attività recente</p>
|
||||||
|
) : (
|
||||||
|
<ul className="divide-y divide-gray-50">
|
||||||
|
{activity.map((item, i) => (
|
||||||
|
<li key={i} className="px-5 py-3 flex items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-base">{ACTIVITY_ICONS[item.type]}</span>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-800">{item.label}</p>
|
||||||
|
<p className="text-xs text-gray-500">{item.detail}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-400 shrink-0">{fmt(item.timestamp)}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user