From 4f7e0033c4d8376a2884d3e3cc1a8911871dee71 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Thu, 18 Jun 2026 18:25:50 +0200 Subject: [PATCH] fix(offer-editor): fix category bug, add ripristina, always-saveable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bug: remove server-side category pre-filter from getOfferEditorData; services now loaded unfiltered, client-side filter handles display → changing category no longer wipes the service list - Fix tierTotals to use full service map (not filtered subset) so totals stay correct when category is edited mid-session - Add Ripristina button for archived offers (calls toggleOfferArchived false) - Remove canSave gate: always allow saving; button is "Salva Bozza" (secondary style, stays on page) when no services assigned, "Salva Offerta" (primary, redirects to list) when at least one tier has services - Show "Bozza salvata." inline feedback after draft save Co-Authored-By: Claude Sonnet 4.6 --- .../admin/offers/OfferEditorClient.tsx | 73 +++++++++++++++---- src/lib/offer-queries.ts | 16 ++-- 2 files changed, 65 insertions(+), 24 deletions(-) diff --git a/src/components/admin/offers/OfferEditorClient.tsx b/src/components/admin/offers/OfferEditorClient.tsx index f3ec2e9..c9abfe1 100644 --- a/src/components/admin/offers/OfferEditorClient.tsx +++ b/src/components/admin/offers/OfferEditorClient.tsx @@ -92,6 +92,7 @@ export function OfferEditorClient({ const [obiettivoOptions, setObiettivoOptions] = useState(fieldOptions.obiettivo); const [initialArchived] = useState(data.macro.is_archived); + const [savedDraft, setSavedDraft] = useState(false); const filteredServices = useMemo(() => { return macro.category @@ -99,16 +100,22 @@ export function OfferEditorClient({ : data.availableServices; }, [macro.category, data.availableServices]); + // 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 = filteredServices.find((s) => s.id === id); + const service = serviceById.get(id); return sum + Number(service?.unit_price ?? 0); }, 0) ); - }, [tiers, filteredServices]); + }, [tiers, serviceById]); - const canSave = tiers.some((t) => t.assignedServiceIds.length > 0); + const isDraft = !tiers.some((t) => t.assignedServiceIds.length > 0); function updateMacro(key: K, value: MacroFields[K]) { setMacro((prev) => ({ ...prev, [key]: value })); @@ -164,7 +171,12 @@ export function OfferEditorClient({ startTransition(async () => { try { await saveOfferEditor(data.macro.id, payload); - router.push("/admin/offers"); + if (isDraft) { + setSavedDraft(true); + router.refresh(); + } else { + router.push("/admin/offers"); + } } catch { setSaveError("Errore nel salvataggio. Verifica i campi."); } @@ -183,6 +195,18 @@ export function OfferEditorClient({ }); } + 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); @@ -468,16 +492,29 @@ export function OfferEditorClient({ {/* Action buttons */} {saveError &&

{saveError}

} + {savedDraft && !saveError && ( +

Bozza salvata.

+ )}
- + {isDraft ? ( + + ) : ( + + )} )} + {initialArchived && ( + + )}
); diff --git a/src/lib/offer-queries.ts b/src/lib/offer-queries.ts index 65f4dfd..e049393 100644 --- a/src/lib/offer-queries.ts +++ b/src/lib/offer-queries.ts @@ -206,23 +206,17 @@ export async function getOfferEditorData(macroId: string): Promise