"use client"; import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { RotateCcw } from "lucide-react"; import { updatePaymentStatus, updateAcceptedTotal, setPaymentPaidAt, updatePaymentField, clearPaymentOverride, setPaymentDueDate, } from "@/app/admin/clients/[id]/actions"; import { setPaymentPlan } from "@/app/admin/projects/project-actions"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { EditableCell } from "@/components/ui/editable-cell"; import { SplitPaymentForm } from "@/components/admin/SplitPaymentForm"; import type { Payment } from "@/db/schema"; type Props = { payments: Payment[]; acceptedTotal: string; clientId: string; projectId?: string; // set only from project detail page — enables init & split offersAcceptedTotal?: number; // sum of accepted_total from active offers }; const statusLabels: Record = { da_saldare: "Da saldare", inviata: "Inviata", saldato: "Saldato", }; function formatEuro(value: number): string { return value.toLocaleString("it-IT", { minimumFractionDigits: 2 }); } // Date | string | null (serializzato sul confine RSC) → "YYYY-MM-DD" per . // Le date sono salvate a mezzogiorno UTC apposta, quindi i getter locali leggono // il giorno giusto in qualunque fuso senza scivolare di uno. function toDateValue(value: Date | string | null | undefined): string { if (!value) return ""; const d = value instanceof Date ? value : new Date(value); if (Number.isNaN(d.getTime())) return ""; return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } function todayValue(): string { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; } type PlanMode = "single" | "two" | "three"; const planOptions: { mode: PlanMode; label: string; description: string }[] = [ { mode: "single", label: "Pagamento unico", description: "100% in un'unica soluzione" }, { mode: "two", label: "2 step", description: "Acconto 50% + Saldo 50%" }, { mode: "three", label: "3 step", description: "50% + 25% + 25%" }, ]; export function PaymentsTab({ payments, acceptedTotal, clientId, projectId, offersAcceptedTotal = 0, }: Props) { const router = useRouter(); const [overrideValue, setOverrideValue] = useState(acceptedTotal); const [planLoading, setPlanLoading] = useState(null); const [statusLoading, setStatusLoading] = useState(null); const [, startTransition] = useTransition(); const [error, setError] = useState(null); // The input is seeded from a prop, so it has to follow the prop after a // router.refresh() — otherwise it keeps showing the pre-save value. Adjusted // during render rather than in an effect (React's documented pattern for // "resetting state when a prop changes"): no extra pass, no flash of stale value. const [prevAcceptedTotal, setPrevAcceptedTotal] = useState(acceptedTotal); if (prevAcceptedTotal !== acceptedTotal) { setPrevAcceptedTotal(acceptedTotal); setOverrideValue(acceptedTotal); } const currentTotal = parseFloat(acceptedTotal) || 0; const offersTotal = offersAcceptedTotal; const hasOffers = offersTotal > 0; const rowsTotal = payments.reduce((sum, p) => sum + (parseFloat(p.amount) || 0), 0); const drift = rowsTotal - currentTotal; const hasDrift = payments.length > 0 && Math.abs(drift) >= 0.01; // Shared runner: every mutation goes through here so a failure is visible // instead of vanishing into an unhandled rejection. function run(fn: () => Promise) { setError(null); startTransition(async () => { try { await fn(); router.refresh(); } catch (e) { setError(e instanceof Error ? e.message : "Errore nel salvataggio"); } }); } async function handleUseOffersTotal() { if (!projectId) return; setOverrideValue(offersTotal.toFixed(2)); const fd = new FormData(); fd.set("accepted_total", offersTotal.toFixed(2)); run(() => updateAcceptedTotal(projectId, fd)); } async function handleSaveTotal(e: React.FormEvent) { e.preventDefault(); const fd = new FormData(); fd.set("accepted_total", overrideValue); run(() => updateAcceptedTotal(clientId, fd)); } async function handleSetPlan(mode: PlanMode) { if (!projectId) return; // Picking a plan deletes every existing row — including status, paid_at and // due_date. Only worth interrupting when there is actually something to lose. // La scadenza conta quanto lo stato: una rata "da saldare" con una data // concordata è informazione che il cliente sta già leggendo nel portale, e // sparirebbe senza che nessuno se ne accorga. const tracked = payments.filter( (p) => p.status === "saldato" || p.status === "inviata" || p.due_date !== null ).length; if (tracked > 0) { const what = tracked === 1 ? "1 rata con dati inseriti" : `${tracked} rate con dati inseriti`; const ok = window.confirm( `Cambiare schema cancella tutte le rate di questo progetto.\n\nCi sono ${what} (stato, scadenza o data di incasso): andranno persi.\n\nProcedere?` ); if (!ok) return; } setPlanLoading(mode); setError(null); try { const total = parseFloat(overrideValue) || currentTotal; await setPaymentPlan(projectId, mode, total); router.refresh(); } catch (e) { setError(e instanceof Error ? e.message : "Errore nel salvataggio"); } finally { setPlanLoading(null); } } async function handleStatusUpdate(paymentId: string, status: string) { setStatusLoading(paymentId); try { await updatePaymentStatus(paymentId, clientId, status); router.refresh(); } finally { setStatusLoading(null); } } async function handlePaidDateUpdate(paymentId: string, dateStr: string) { if (!dateStr) return; setStatusLoading(paymentId); try { await setPaymentPaidAt(paymentId, clientId, dateStr); router.refresh(); } finally { setStatusLoading(null); } } // La scadenza si può anche togliere: la stringa vuota è un valore, non un no-op. async function handleDueDateUpdate(paymentId: string, dateStr: string) { setStatusLoading(paymentId); try { await setPaymentDueDate(paymentId, clientId, dateStr); router.refresh(); } finally { setStatusLoading(null); } } return (
{error && (

{error}

)} {/* Totale preventivo */}

Totale preventivo

{/* Totale ereditato dalle offerte */} {hasOffers && (
Ereditato dalle offerte attive:{" "} € {formatEuro(offersTotal)} {projectId && ( )}
)} {/* Override manuale */}
setOverrideValue(e.target.value)} className="max-w-xs" />
{/* Selettore schema rate */} {projectId && (

Schema di pagamento

{planOptions.map(({ mode, label, description }) => ( ))}

Selezionare uno schema cancella le rate esistenti e crea le nuove in base al totale corrente.

)} {/* Empty state — project has no payments yet */} {projectId && payments.length === 0 && (

Nessun pagamento configurato. Scegli uno schema sopra o crea le rate standard.

)} {/* Payment rows */} {payments.map((p) => { const amount = parseFloat(p.amount); const pct = p.percent !== null && p.percent !== undefined ? parseFloat(String(p.percent)) : null; return (
run(() => updatePaymentField(p.id, clientId, "label", v))} />
{pct !== null && !p.amount_locked && (

{pct}% del totale

)} {p.amount_locked && (

Importo scritto a mano

)}
`€ ${formatEuro(parseFloat(raw) || 0)}`} onSave={(v) => run(() => updatePaymentField(p.id, clientId, "amount", v))} />
{projectId && amount > 0 && ( )}
{statusLoading === p.id && ...}
{/* Scadenza: è questa che il cliente vede nel portale, col conto alla rovescia, ed è l'aggancio del futuro promemoria via email. Sempre modificabile, anche a rata saldata: resta lo storico. */}
handleDueDateUpdate(p.id, e.target.value)} className="text-sm border border-gray-200 rounded px-2 py-1 bg-white" /> {!p.due_date && ( non mostrata al cliente )}
{p.status === "saldato" && (
handlePaidDateUpdate(p.id, e.target.value)} className="text-sm border border-gray-200 rounded px-2 py-1 bg-white" />
)}
); })} {/* Scarto fra la somma delle rate e il totale del progetto. Si dice, non si sistema di nascosto: un importo scritto a mano è una scelta, non un errore. */} {hasDrift && (

{drift > 0 ? `Le rate superano il totale di € ${formatEuro(drift)}` : `Le rate coprono € ${formatEuro(-drift)} in meno del totale`}

Somma rate € {formatEuro(rowsTotal)} · Totale € {formatEuro(currentTotal)}

)}
); }