fix(offer-editor): fix category bug, add ripristina, always-saveable

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 18:25:50 +02:00
parent 56f9fd1d07
commit 4f7e0033c4
2 changed files with 65 additions and 24 deletions
@@ -92,6 +92,7 @@ export function OfferEditorClient({
const [obiettivoOptions, setObiettivoOptions] = useState<string[]>(fieldOptions.obiettivo);
const [initialArchived] = useState<boolean>(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<K extends keyof MacroFields>(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 && <p className="text-sm text-red-600">{saveError}</p>}
{savedDraft && !saveError && (
<p className="text-sm text-[#1A463C]">Bozza salvata.</p>
)}
<div className="flex items-center gap-2">
<button
type="button"
onClick={handleSave}
disabled={!canSave || isPending}
title={!canSave ? "Seleziona almeno un servizio" : undefined}
className="bg-[#1A463C] text-white text-sm font-semibold px-6 py-4 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-[#163a31] transition-colors duration-150"
>
Salva Offerta
</button>
{isDraft ? (
<button
type="button"
onClick={handleSave}
disabled={isPending}
className="border border-[#e5e7eb] text-[#1a1a1a] text-sm font-semibold px-6 py-4 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-[#f9f9f9] transition-colors duration-150"
>
Salva Bozza
</button>
) : (
<button
type="button"
onClick={handleSave}
disabled={isPending}
className="bg-[#1A463C] text-white text-sm font-semibold px-6 py-4 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-[#163a31] transition-colors duration-150"
>
Salva Offerta
</button>
)}
<Link
href="/admin/offers"
className="border border-[#e5e7eb] text-[#1a1a1a] text-sm font-semibold px-6 py-4 rounded hover:bg-[#f9f9f9] transition-colors duration-150"
@@ -494,6 +531,16 @@ export function OfferEditorClient({
Archivia
</button>
)}
{initialArchived && (
<button
type="button"
onClick={handleRestore}
disabled={isPending}
className="text-[#1A463C] text-sm font-semibold px-6 py-4 rounded hover:bg-[#f0f7f4] transition-colors duration-150 disabled:opacity-50"
>
Ripristina
</button>
)}
</div>
</div>
);