diff --git a/src/components/admin/ForecastChart.tsx b/src/components/admin/ForecastChart.tsx
new file mode 100644
index 0000000..75683ec
--- /dev/null
+++ b/src/components/admin/ForecastChart.tsx
@@ -0,0 +1,61 @@
+import type { ForecastMonth } from "@/lib/forecast-queries";
+
+function fmtEur(n: number) {
+ return n.toLocaleString("it-IT", {
+ style: "currency",
+ currency: "EUR",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ });
+}
+
+export function ForecastChart({ data }: { data: ForecastMonth[] }) {
+ const max = Math.max(...data.map((d) => d.total), 1);
+ // index 0 = mese corrente, index 1 = mese prossimo (evidenziato)
+ const nextMonth = data[1];
+
+ return (
+
+ {nextMonth && (
+
+
+
+ Previsto il mese prossimo
+
+
{nextMonth.label}
+
+
+ {fmtEur(nextMonth.total)}
+
+
+ )}
+
+
+ {data.map((m, i) => {
+ const pct = Math.round((m.total / max) * 100);
+ const isNext = i === 1;
+ return (
+
+
+
0 ? "4px" : "0" }}
+ title={`${m.label}: ${m.total.toLocaleString("it-IT", { style: "currency", currency: "EUR", minimumFractionDigits: 2 })}`}
+ />
+
+
+ {m.label.split(" ")[0]}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/admin/dashboard/FollowUpWidget.tsx b/src/components/admin/dashboard/FollowUpWidget.tsx
index 24722f5..4b8b9d7 100644
--- a/src/components/admin/dashboard/FollowUpWidget.tsx
+++ b/src/components/admin/dashboard/FollowUpWidget.tsx
@@ -1,46 +1,96 @@
import { getLeadsNeedingFollowUp } from "@/lib/lead-service";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
-import { AlertCircle } from "lucide-react";
import Link from "next/link";
+const MAX_ROWS = 4;
+
+function initials(name: string): string {
+ const parts = name.trim().split(/\s+/).filter(Boolean);
+ if (parts.length === 0) return "?";
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
+}
+
+function relativeDays(date: Date | string | null | undefined): string | null {
+ if (!date) return "Mai contattato";
+ const d = date instanceof Date ? date : new Date(date);
+ const diffMs = Date.now() - d.getTime();
+ const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+ if (days <= 0) return "Contattato oggi";
+ if (days === 1) return "Contattato ieri";
+ return `Contattato ${days} giorni fa`;
+}
+
export async function FollowUpWidget() {
- const leadsNeedingFollowUp = await getLeadsNeedingFollowUp(7);
+ const leads = await getLeadsNeedingFollowUp(7);
+
+ if (leads.length === 0) {
+ return (
+
+
+
Follow-up
+
+
Tutti i lead sono aggiornati β
+
+ );
+ }
+
+ const visible = leads.slice(0, MAX_ROWS);
return (
-
-
-
-
- Richiedi Follow-up
-
-
-
- {leadsNeedingFollowUp.length > 0 ? (
- <>
-
- {leadsNeedingFollowUp.length} lead da contattare
-
-
- {leadsNeedingFollowUp.slice(0, 3).map((lead) => (
- -
- {lead.name}
-
-
- ))}
-
- {leadsNeedingFollowUp.length > 3 && (
-
- )}
- >
- ) : (
- Tutti i lead sono aggiornati!
- )}
-
-
+
+ {/* Header */}
+
+
+
Follow-up
+
+ {leads.length}
+
+
+
+ Vedi tutti β
+
+
+
+ {/* Lead rows */}
+
+ {visible.map((lead) => (
+ -
+
+
+ {initials(lead.name)}
+
+
+
{lead.name}
+
+ {lead.next_action
+ ? lead.next_action
+ : [lead.company, relativeDays(lead.last_contact_date)]
+ .filter(Boolean)
+ .join(" Β· ")}
+
+
+
+ Contatta β
+
+
+
+ ))}
+
+
+ {leads.length > MAX_ROWS && (
+
+ Vedi tutti i {leads.length} lead
+
+ )}
+
);
}
diff --git a/src/components/admin/tabs/PaymentsTab.tsx b/src/components/admin/tabs/PaymentsTab.tsx
index a9fb0f2..e5d7bf0 100644
--- a/src/components/admin/tabs/PaymentsTab.tsx
+++ b/src/components/admin/tabs/PaymentsTab.tsx
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import {
updatePaymentStatus,
updateAcceptedTotal,
+ setPaymentPaidAt,
} from "@/app/admin/clients/[id]/actions";
import { setPaymentPlan } from "@/app/admin/projects/project-actions";
import { Button } from "@/components/ui/button";
@@ -27,6 +28,16 @@ const statusLabels: Record
= {
saldato: "Saldato",
};
+// paid_at (Date | string | null, serializzato sul confine RSC) β "YYYY-MM" per
+function toMonthValue(paidAt: Date | string | null | undefined): string {
+ if (!paidAt) {
+ const now = new Date();
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
+ }
+ const d = paidAt instanceof Date ? paidAt : new Date(paidAt);
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
+}
+
type PlanMode = "single" | "two" | "three";
const planOptions: { mode: PlanMode; label: string; description: string }[] = [
@@ -90,6 +101,17 @@ export function PaymentsTab({
}
}
+ async function handlePaidMonthUpdate(paymentId: string, monthStr: string) {
+ if (!monthStr) return;
+ setStatusLoading(paymentId);
+ try {
+ await setPaymentPaidAt(paymentId, clientId, monthStr);
+ router.refresh();
+ } finally {
+ setStatusLoading(null);
+ }
+ }
+
return (
{/* Totale preventivo */}
@@ -240,6 +262,21 @@ export function PaymentsTab({
...
)}
+ {p.status === "saldato" && (
+
+
+ handlePaidMonthUpdate(p.id, e.target.value)}
+ className="text-sm border border-gray-200 rounded px-2 py-1 bg-white"
+ />
+
+ )}
);
})}
diff --git a/src/lib/admin-queries.ts b/src/lib/admin-queries.ts
index 9a92edd..7e78a82 100644
--- a/src/lib/admin-queries.ts
+++ b/src/lib/admin-queries.ts
@@ -836,6 +836,9 @@ export type ClientActiveOfferSummary = {
project_id: string;
project_name: string;
public_name: string; // offer_micros.public_name β NEVER internal_name
+ tier_letter: string | null; // A | B | C
+ category: string | null; // Entry | Signature | Retainer (offer_macros.category)
+ offer_type: string; // una_tantum | retainer (fallback for category)
accepted_total: string | null;
};
@@ -856,10 +859,14 @@ export async function getClientActiveOffers(clientId: string): Promise