Files
clienthub/src/app/admin/page.tsx
T
simone 1115bb2265 feat(dashboard): l'inbox sale in cima e dice da quanto aspettano
Il widget dei messaggi esisteva gia', ma era il terzo riquadro della colonna
stretta: sotto la piega, cioe' invisibile. Un messaggio non letto e' la cosa
piu' urgente della giornata e ora sta in testa alla dashboard, a piena
larghezza, due colonne.

Due dati erano gia' in ConversationSummary e non venivano mostrati: da quanto
aspetta il messaggio e su cosa e' stato scritto (fase, task, deliverable o
generale). Sono esattamente i due che dicono con che fretta rispondere. Il
tempo relativo si ferma alla settimana e poi passa alla data: oltre, "23 giorni
fa" non aiuta piu' nessuno.

La fascia sparisce del tutto quando non c'e' niente da leggere, invece di
lasciare a video un riquadro vuoto che si impara a saltare — e con lui si
imparerebbe a saltare anche quello pieno.

relativeTime va in src/lib/dates.ts perche' serve anche altrove. FollowUpWidget
ha ancora la sua copia locale con il prefisso "Contattato": la si unifica
quando la si tocca, non oggi.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:42:52 +02:00

91 lines
3.2 KiB
TypeScript

import { Suspense } from "react";
import { getDashboardStats } from "@/lib/dashboard-queries";
import { FollowUpWidget } from "@/components/admin/dashboard/FollowUpWidget";
import { InboxBand } from "@/components/admin/dashboard/InboxBand";
import {
getAnalyticsByYear,
getTotalTrackedHours,
getClientProfitability,
} from "@/lib/analytics-queries";
import { getRevenueForecast12Months, getOffersSoldBreakdown } from "@/lib/forecast-queries";
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;
export default async function AdminDashboard({
searchParams,
}: {
searchParams: Promise<{ year?: string }>;
}) {
const { year: yearParam } = await searchParams;
const year = parseInt(yearParam ?? "") || new Date().getFullYear();
const { kpi } = await getDashboardStats();
const [data, totalHours, profitability, forecast, offersSold] = await Promise.all([
getAnalyticsByYear(year),
getTotalTrackedHours(year),
getClientProfitability(year),
getRevenueForecast12Months(),
getOffersSoldBreakdown(),
]);
const collectedPct =
data.contracted > 0 ? Math.round((data.collected / data.contracted) * 100) : 0;
return (
<div className="flex flex-col gap-8">
{/* Messaggi in attesa — in cima perché è la cosa più urgente della
giornata. Non renderizza nulla quando non c'è niente da leggere. */}
<Suspense fallback={null}>
<InboxBand />
</Suspense>
{/* KPI strip */}
<section className="grid grid-cols-2 lg:grid-cols-4 gap-6">
<MetricCard
label="Clienti Attivi"
value={String(kpi.clientiAttivi)}
delta={kpi.clientiNuoviMese > 0 ? `+${kpi.clientiNuoviMese} questo mese` : undefined}
sub="Totale corrente"
/>
<MetricCard
label="Contrattualizzato"
value={fmtEur0(data.contracted)}
sub={`Totale anno ${year}`}
/>
<MetricCard
label="Incassato Reale"
value={fmtEur0(data.collected)}
valueAccent={data.collected > 0}
sub={`${collectedPct}% del totale`}
/>
<MetricCard label="Ore Tracciate" value={`${totalHours}h`} sub="Su progetti attivi" />
</section>
{/* Griglia contenuti: 2/3 + 1/3 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Colonna 2/3 */}
<div className="lg:col-span-2 flex flex-col gap-8">
<ForecastChart data={forecast} headerAction={<YearSelector currentYear={year} />} />
<ClientProfitability data={profitability} />
</div>
{/* Colonna 1/3 */}
<div className="lg:col-span-1 flex flex-col gap-8">
<Suspense
fallback={<div className="animate-pulse h-40 bg-muted rounded-xl" />}
>
<FollowUpWidget />
</Suspense>
<OffersSoldChart data={offersSold} />
</div>
</div>
</div>
);
}