"use client"; import { useState, useMemo, useTransition, useEffect } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { saveOfferEditor, toggleOfferArchived, renameOfferOption, type SaveOfferEditorPayload, } from "@/app/admin/offers/actions"; import { OptionSelect } from "@/components/ui/option-select"; import { OptionMultiSelect } from "@/components/ui/option-multi-select"; import { EditableCell } from "@/components/ui/editable-cell"; import type { OfferEditorData, OfferFieldOptions, OfferTierData } from "@/lib/offer-queries"; type TierLetter = "A" | "B" | "C"; const TIER_LETTERS: TierLetter[] = ["A", "B", "C"]; function emptyTier(letter: TierLetter): OfferTierData { return { id: "", tier_letter: letter, internal_name: "", public_name: "", duration_months: 1, public_price: null, assignedServiceIds: [], servicesTotal: "0", }; } function padTiers(tiers: OfferTierData[]): OfferTierData[] { return TIER_LETTERS.map( (letter) => tiers.find((t) => t.tier_letter === letter) ?? emptyTier(letter) ); } function formatEuro(value: number): string { return `€${value.toLocaleString("it-IT", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } function formatUnitPrice(raw: string): string { const num = parseFloat(raw); return `€${(isNaN(num) ? 0 : num).toLocaleString("it-IT", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } type OfferType = "una_tantum" | "retainer"; type MacroFields = { internal_name: string; public_name: string; description: string; category: string | null; ticket: string | null; offer_type: OfferType; cliente_ideale: string; risultato: string; tempo: string; pain: string; metodo: string; }; export function OfferEditorClient({ data, fieldOptions, }: { data: OfferEditorData; fieldOptions: OfferFieldOptions; }) { const router = useRouter(); const [isPending, startTransition] = useTransition(); const [saveError, setSaveError] = useState(null); const [macro, setMacro] = useState({ internal_name: data.macro.internal_name, public_name: data.macro.public_name, description: data.macro.description ?? "", category: data.macro.category, ticket: data.macro.ticket, offer_type: (data.macro.offer_type === "retainer" ? "retainer" : "una_tantum") as OfferType, cliente_ideale: data.macro.cliente_ideale ?? "", risultato: data.macro.risultato ?? "", tempo: data.macro.tempo ?? "", pain: data.macro.pain ?? "", metodo: data.macro.metodo ?? "", }); const [tiers, setTiers] = useState(() => padTiers(data.tiers)); const [tipoTags, setTipoTags] = useState(data.tipoTags); const [obiettivoTags, setObiettivoTags] = useState(data.obiettivoTags); // After first draft save router.refresh() assigns real DB IDs to new tiers. // Re-sync tiers state when those IDs arrive so subsequent saves UPDATE instead of INSERT. const tierIdKey = data.tiers.map((t) => t.id).sort().join(","); useEffect(() => { setTiers(padTiers(data.tiers)); }, [tierIdKey]); // eslint-disable-line react-hooks/exhaustive-deps const [categoriaOptions, setCategoriaOptions] = useState(fieldOptions.categoria); const [ticketOptions, setTicketOptions] = useState(fieldOptions.ticket); const [tipoOptions, setTipoOptions] = useState(fieldOptions.tipo); const [obiettivoOptions, setObiettivoOptions] = useState(fieldOptions.obiettivo); const [initialArchived] = useState(data.macro.is_archived); const [savedDraft, setSavedDraft] = useState(false); const [offerFilter, setOfferFilter] = useState(null); const offerFilterOptions = useMemo(() => { const all = new Set(); for (const s of data.availableServices) s.offerTags.forEach((t) => all.add(t)); return Array.from(all).sort(); }, [data.availableServices]); const filteredServices = useMemo( () => offerFilter ? data.availableServices.filter((s) => s.offerTags.includes(offerFilter)) : data.availableServices, [data.availableServices, offerFilter] ); // Totals use the full catalog (not filtered) so changing category doesn't zero them. const serviceById = useMemo( () => new Map(data.availableServices.map((s) => [s.id, s])), [data.availableServices] ); const tierTotals = useMemo(() => { return tiers.map((tier) => tier.assignedServiceIds.reduce((sum, id) => { const service = serviceById.get(id); return sum + Number(service?.unit_price ?? 0); }, 0) ); }, [tiers, serviceById]); const isDraft = !tiers.some((t) => t.assignedServiceIds.length > 0); function updateMacro(key: K, value: MacroFields[K]) { setMacro((prev) => ({ ...prev, [key]: value })); } function toggleService(tierIdx: number, serviceId: string) { setTiers((prev) => prev.map((tier, idx) => { if (idx !== tierIdx) return tier; const has = tier.assignedServiceIds.includes(serviceId); return { ...tier, assignedServiceIds: has ? tier.assignedServiceIds.filter((id) => id !== serviceId) : [...tier.assignedServiceIds, serviceId], }; }) ); } function updateTierPublicPrice(tierIdx: number, value: string) { setTiers((prev) => prev.map((tier, idx) => (idx === tierIdx ? { ...tier, public_price: value } : tier)) ); } function handleSave() { setSaveError(null); const payload: SaveOfferEditorPayload = { internal_name: macro.internal_name, public_name: macro.public_name || macro.internal_name, description: macro.description, category: macro.category ?? undefined, ticket: macro.ticket ?? undefined, offer_type: macro.offer_type, cliente_ideale: macro.cliente_ideale, risultato: macro.risultato, tempo: macro.tempo, pain: macro.pain, metodo: macro.metodo, tiers: tiers.map((t) => ({ id: t.id || undefined, tier_letter: (t.tier_letter ?? "A") as "A" | "B" | "C", internal_name: t.internal_name || `Tier ${t.tier_letter}`, public_name: t.public_name || t.tier_letter || "Tier", duration_months: t.duration_months || 1, public_price: t.public_price ? Number(t.public_price) : null, assignedServiceIds: t.assignedServiceIds, })), tipoTags, obiettivoTags, }; startTransition(async () => { try { await saveOfferEditor(data.macro.id, payload); if (isDraft) { setSavedDraft(true); router.refresh(); } else { router.push("/admin/offers"); } } catch { setSaveError("Errore nel salvataggio. Verifica i campi."); } }); } function handleArchive() { if (!window.confirm("Sei sicuro? L'offerta non sarà visibile nella lista.")) return; startTransition(async () => { try { await toggleOfferArchived(data.macro.id, true); router.push("/admin/offers"); } catch { setSaveError("Errore nel salvataggio. Verifica i campi."); } }); } function handleRestore() { if (!window.confirm("Ripristinare l'offerta? Tornerà visibile nella lista.")) return; startTransition(async () => { try { await toggleOfferArchived(data.macro.id, false); router.push("/admin/offers"); } catch { setSaveError("Errore nel salvataggio. Verifica i campi."); } }); } function handleRenameCategoria(oldValue: string, newValue: string) { setCategoriaOptions((prev) => prev.map((o) => (o === oldValue ? newValue : o))); if (macro.category === oldValue) updateMacro("category", newValue); startTransition(async () => { try { await renameOfferOption("categoria", oldValue, newValue); } catch { setSaveError("Errore nel salvataggio. Verifica i campi."); } }); } function handleRenameTicket(oldValue: string, newValue: string) { setTicketOptions((prev) => prev.map((o) => (o === oldValue ? newValue : o))); if (macro.ticket === oldValue) updateMacro("ticket", newValue); startTransition(async () => { try { await renameOfferOption("ticket", oldValue, newValue); } catch { setSaveError("Errore nel salvataggio. Verifica i campi."); } }); } function handleRenameTipo(oldValue: string, newValue: string) { setTipoOptions((prev) => prev.map((o) => (o === oldValue ? newValue : o))); setTipoTags((prev) => prev.map((t) => (t === oldValue ? newValue : t))); startTransition(async () => { try { await renameOfferOption("tipo", oldValue, newValue); } catch { setSaveError("Errore nel salvataggio. Verifica i campi."); } }); } function handleRenameObiettivo(oldValue: string, newValue: string) { setObiettivoOptions((prev) => prev.map((o) => (o === oldValue ? newValue : o))); setObiettivoTags((prev) => prev.map((t) => (t === oldValue ? newValue : t))); startTransition(async () => { try { await renameOfferOption("obiettivo", oldValue, newValue); } catch { setSaveError("Errore nel salvataggio. Verifica i campi."); } }); } return (
{/* Header & navigation */}
← Indietro

Modifica Offerta

updateMacro("internal_name", v)} />

{/* Modalità toggle — directly under title */}
Modalità
{([ { value: "una_tantum", label: "Una tantum" }, { value: "retainer", label: "Retainer" }, ] as const).map((opt) => ( ))}
{/* Descrizione — nota interna, non visibile al cliente */}
Descrizione