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:
@@ -92,6 +92,7 @@ export function OfferEditorClient({
|
|||||||
const [obiettivoOptions, setObiettivoOptions] = useState<string[]>(fieldOptions.obiettivo);
|
const [obiettivoOptions, setObiettivoOptions] = useState<string[]>(fieldOptions.obiettivo);
|
||||||
|
|
||||||
const [initialArchived] = useState<boolean>(data.macro.is_archived);
|
const [initialArchived] = useState<boolean>(data.macro.is_archived);
|
||||||
|
const [savedDraft, setSavedDraft] = useState(false);
|
||||||
|
|
||||||
const filteredServices = useMemo(() => {
|
const filteredServices = useMemo(() => {
|
||||||
return macro.category
|
return macro.category
|
||||||
@@ -99,16 +100,22 @@ export function OfferEditorClient({
|
|||||||
: data.availableServices;
|
: data.availableServices;
|
||||||
}, [macro.category, 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(() => {
|
const tierTotals = useMemo(() => {
|
||||||
return tiers.map((tier) =>
|
return tiers.map((tier) =>
|
||||||
tier.assignedServiceIds.reduce((sum, id) => {
|
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);
|
return sum + Number(service?.unit_price ?? 0);
|
||||||
}, 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]) {
|
function updateMacro<K extends keyof MacroFields>(key: K, value: MacroFields[K]) {
|
||||||
setMacro((prev) => ({ ...prev, [key]: value }));
|
setMacro((prev) => ({ ...prev, [key]: value }));
|
||||||
@@ -164,7 +171,12 @@ export function OfferEditorClient({
|
|||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
try {
|
try {
|
||||||
await saveOfferEditor(data.macro.id, payload);
|
await saveOfferEditor(data.macro.id, payload);
|
||||||
|
if (isDraft) {
|
||||||
|
setSavedDraft(true);
|
||||||
|
router.refresh();
|
||||||
|
} else {
|
||||||
router.push("/admin/offers");
|
router.push("/admin/offers");
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setSaveError("Errore nel salvataggio. Verifica i campi.");
|
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) {
|
function handleRenameCategoria(oldValue: string, newValue: string) {
|
||||||
setCategoriaOptions((prev) => prev.map((o) => (o === oldValue ? newValue : o)));
|
setCategoriaOptions((prev) => prev.map((o) => (o === oldValue ? newValue : o)));
|
||||||
if (macro.category === oldValue) updateMacro("category", newValue);
|
if (macro.category === oldValue) updateMacro("category", newValue);
|
||||||
@@ -468,16 +492,29 @@ export function OfferEditorClient({
|
|||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
{saveError && <p className="text-sm text-red-600">{saveError}</p>}
|
{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">
|
<div className="flex items-center gap-2">
|
||||||
|
{isDraft ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={!canSave || isPending}
|
disabled={isPending}
|
||||||
title={!canSave ? "Seleziona almeno un servizio" : undefined}
|
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"
|
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
|
Salva Offerta
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
<Link
|
<Link
|
||||||
href="/admin/offers"
|
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"
|
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
|
Archivia
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -206,23 +206,17 @@ export async function getOfferEditorData(macroId: string): Promise<OfferEditorDa
|
|||||||
servicesTotal: totalsMap.get(tier.id) ?? "0",
|
servicesTotal: totalsMap.get(tier.id) ?? "0",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Category-filtered service catalog for the composition matrix (D-4
|
// Full service catalog (unfiltered by category) — category filtering happens
|
||||||
// pre-filter, best-effort). If macro.category is unset, return all active
|
// client-side so changing the category field doesn't wipe the service list.
|
||||||
// services unfiltered.
|
const availableServices = await db
|
||||||
const availableServicesQuery = db
|
|
||||||
.select({
|
.select({
|
||||||
id: services.id,
|
id: services.id,
|
||||||
name: services.name,
|
name: services.name,
|
||||||
unit_price: services.unit_price,
|
unit_price: services.unit_price,
|
||||||
category: services.category,
|
category: services.category,
|
||||||
})
|
})
|
||||||
.from(services);
|
.from(services)
|
||||||
|
.where(eq(services.active, true));
|
||||||
const availableServices = macro.category
|
|
||||||
? await availableServicesQuery.where(
|
|
||||||
and(eq(services.active, true), eq(services.category, macro.category))
|
|
||||||
)
|
|
||||||
: await availableServicesQuery.where(eq(services.active, true));
|
|
||||||
|
|
||||||
// Tipo/Obiettivo tags for this macro.
|
// Tipo/Obiettivo tags for this macro.
|
||||||
const tagRows = await db
|
const tagRows = await db
|
||||||
|
|||||||
Reference in New Issue
Block a user