64030afef3
1. AdminSidebar: removed yellow "Genera preventivo" CTA (duplicated Preventivi tab nav item) 2. Delete phase/task: new deletePhase/deleteTask server actions with FK cascade and phase-status recompute; DeletePhaseTaskButton client component with window.confirm; trash icons in PhasesTab per phase and per task 3. Public dashboard: extend activeOffers query with offer_macros join (offer_name, offer_type); reorder sidebar 1°Offerte 2°Pagamenti 3°Documenti; OffersSection shows macro public_name as heading (never tier letter/internal_name); PaymentStatus gains totalLabel/overrideAmount/hideRows props — retainer=monthly label + no Acconto/Saldo rows 4. Offer editor: Descrizione textarea (nota interna) and Modalità toggle moved directly under title; Tags section now contains only Categoria/Ticket/Tipo/Obiettivo 5. Basecamp chat: API route supports entity_type="phase" with authorization scoping; comments scope in client-view broadened to include phaseIds and client_id (general); CommentsTab updated with phase/general entities; ChatProvider React context; ChatPanel fixed slide-in panel with FAB (bottom-right) and composer tag selector (Generale + phases); PhaseCard gets chat-bubble icon pre-tagging that phase; inline ChatSection block removed from dashboard main column Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
634 lines
23 KiB
TypeScript
634 lines
23 KiB
TypeScript
"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<string | null>(null);
|
|
|
|
const [macro, setMacro] = useState<MacroFields>({
|
|
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<OfferTierData[]>(() => padTiers(data.tiers));
|
|
const [tipoTags, setTipoTags] = useState<string[]>(data.tipoTags);
|
|
const [obiettivoTags, setObiettivoTags] = useState<string[]>(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<string[]>(fieldOptions.categoria);
|
|
const [ticketOptions, setTicketOptions] = useState<string[]>(fieldOptions.ticket);
|
|
const [tipoOptions, setTipoOptions] = useState<string[]>(fieldOptions.tipo);
|
|
const [obiettivoOptions, setObiettivoOptions] = useState<string[]>(fieldOptions.obiettivo);
|
|
|
|
const [initialArchived] = useState<boolean>(data.macro.is_archived);
|
|
const [savedDraft, setSavedDraft] = useState(false);
|
|
const [offerFilter, setOfferFilter] = useState<string | null>(null);
|
|
|
|
const offerFilterOptions = useMemo(() => {
|
|
const all = new Set<string>();
|
|
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<K extends keyof MacroFields>(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 (
|
|
<div className="max-w-4xl mx-auto py-8 px-4 space-y-8">
|
|
{/* Header & navigation */}
|
|
<div>
|
|
<Link href="/admin/offers" className="text-sm text-[#71717a] hover:text-[#1a1a1a]">
|
|
← Indietro
|
|
</Link>
|
|
<div className="mt-2 flex items-center justify-between">
|
|
<h2 className="text-xl font-semibold text-[#1a1a1a]">Modifica Offerta</h2>
|
|
</div>
|
|
<div className="mt-2">
|
|
<h3 className="text-base font-semibold text-[#1a1a1a]">
|
|
<EditableCell
|
|
value={macro.internal_name}
|
|
type="text"
|
|
required
|
|
onSave={(v) => updateMacro("internal_name", v)}
|
|
/>
|
|
</h3>
|
|
</div>
|
|
|
|
{/* Modalità toggle — directly under title */}
|
|
<div className="mt-3 flex items-center gap-3">
|
|
<span className="text-xs text-[#71717a] w-24 shrink-0">Modalità</span>
|
|
<div className="inline-flex rounded-lg border border-[#e5e7eb] bg-[#fafafa] p-0.5">
|
|
{([
|
|
{ value: "una_tantum", label: "Una tantum" },
|
|
{ value: "retainer", label: "Retainer" },
|
|
] as const).map((opt) => (
|
|
<button
|
|
key={opt.value}
|
|
type="button"
|
|
onClick={() => updateMacro("offer_type", opt.value)}
|
|
className={`px-3 py-1 text-sm rounded-md transition-colors ${
|
|
macro.offer_type === opt.value
|
|
? "bg-[#1A463C] text-white font-medium"
|
|
: "text-[#71717a] hover:text-[#1a1a1a]"
|
|
}`}
|
|
>
|
|
{opt.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Descrizione — nota interna, non visibile al cliente */}
|
|
<div className="mt-3 flex items-start gap-3">
|
|
<span className="text-xs text-[#71717a] w-24 shrink-0 pt-2">Descrizione</span>
|
|
<textarea
|
|
value={macro.description}
|
|
onChange={(e) => updateMacro("description", e.target.value)}
|
|
placeholder="Descrizione interna dell'offerta (non visibile al cliente)..."
|
|
rows={3}
|
|
className="flex-1 text-sm border border-[#e5e7eb] rounded-lg px-3 py-2 bg-white resize-none focus:outline-none focus:ring-2 focus:ring-[#1A463C]/30 text-[#1a1a1a] placeholder:text-[#aaaaaa]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tags block */}
|
|
<section className="space-y-4">
|
|
<h3 className="text-base font-semibold text-[#1a1a1a]">Tag</h3>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs text-[#71717a] w-24 shrink-0">Categoria</span>
|
|
<OptionSelect
|
|
value={macro.category}
|
|
options={categoriaOptions}
|
|
onChange={(v) => updateMacro("category", v)}
|
|
onRename={handleRenameCategoria}
|
|
placeholder="Seleziona categoria..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs text-[#71717a] w-24 shrink-0">Ticket</span>
|
|
<OptionSelect
|
|
value={macro.ticket}
|
|
options={ticketOptions}
|
|
onChange={(v) => updateMacro("ticket", v)}
|
|
onRename={handleRenameTicket}
|
|
placeholder="Seleziona ticket..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs text-[#71717a] w-24 shrink-0">Tipo</span>
|
|
<OptionMultiSelect
|
|
values={tipoTags}
|
|
options={tipoOptions}
|
|
onAdd={(v) => {
|
|
setTipoTags((prev) => [...prev, v]);
|
|
if (!tipoOptions.includes(v)) setTipoOptions((prev) => [...prev, v]);
|
|
}}
|
|
onRemove={(v) => setTipoTags((prev) => prev.filter((t) => t !== v))}
|
|
onRename={handleRenameTipo}
|
|
placeholder="+ Crea Tipo"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs text-[#71717a] w-24 shrink-0">Obiettivo</span>
|
|
<OptionMultiSelect
|
|
values={obiettivoTags}
|
|
options={obiettivoOptions}
|
|
onAdd={(v) => {
|
|
setObiettivoTags((prev) => [...prev, v]);
|
|
if (!obiettivoOptions.includes(v)) setObiettivoOptions((prev) => [...prev, v]);
|
|
}}
|
|
onRemove={(v) => setObiettivoTags((prev) => prev.filter((t) => t !== v))}
|
|
onRename={handleRenameObiettivo}
|
|
placeholder="+ Crea Obiettivo"
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Services matrix */}
|
|
<section className="space-y-3">
|
|
<h3 className="text-base font-semibold text-[#1a1a1a]">Servizi Inclusi</h3>
|
|
|
|
{/* Offer filter chips */}
|
|
<div className="flex flex-wrap gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setOfferFilter(null)}
|
|
className={`text-xs px-3 py-1 rounded-full border transition-colors duration-150 ${
|
|
offerFilter === null
|
|
? "bg-[#1A463C] text-white border-[#1A463C]"
|
|
: "border-[#e5e7eb] text-[#71717a] hover:border-[#1A463C] hover:text-[#1A463C]"
|
|
}`}
|
|
>
|
|
Tutti
|
|
</button>
|
|
{offerFilterOptions.map((opt) => (
|
|
<button
|
|
key={opt}
|
|
type="button"
|
|
onClick={() => setOfferFilter(offerFilter === opt ? null : opt)}
|
|
className={`text-xs px-3 py-1 rounded-full border transition-colors duration-150 ${
|
|
offerFilter === opt
|
|
? "bg-[#1A463C] text-white border-[#1A463C]"
|
|
: "border-[#e5e7eb] text-[#71717a] hover:border-[#1A463C] hover:text-[#1A463C]"
|
|
}`}
|
|
>
|
|
{opt}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="bg-white rounded-xl border border-[#e5e7eb] overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-[#f9f9f9] border-b border-[#e5e7eb]">
|
|
<tr>
|
|
<th className="text-left py-2 px-3 font-semibold text-[#71717a]">Servizio</th>
|
|
<th className="text-right py-2 px-3 font-semibold text-[#71717a]">Prezzo</th>
|
|
{TIER_LETTERS.map((letter) => (
|
|
<th
|
|
key={letter}
|
|
className="text-center py-2 px-3 font-semibold text-[#71717a] w-12"
|
|
>
|
|
{letter}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filteredServices.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={2 + TIER_LETTERS.length} className="py-3 px-3 text-[#71717a]">
|
|
Nessun servizio disponibile per questa categoria
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
filteredServices.map((service) => (
|
|
<tr
|
|
key={service.id}
|
|
className="border-b border-[#e5e7eb] hover:bg-[#f9f9f9] transition-colors duration-150 h-10"
|
|
>
|
|
<td className="py-2 px-3 text-[#1a1a1a]">{service.name}</td>
|
|
<td className="py-2 px-3 text-right tabular-nums text-[#1a1a1a]">
|
|
{formatUnitPrice(service.unit_price)}
|
|
</td>
|
|
{tiers.map((tier, tierIdx) => (
|
|
<td key={tier.tier_letter} className="py-2 px-3 text-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={tier.assignedServiceIds.includes(service.id)}
|
|
onChange={() => toggleService(tierIdx, service.id)}
|
|
style={{ accentColor: "#1A463C" }}
|
|
className="h-4 w-4 cursor-pointer"
|
|
/>
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr className="border-t border-[#e5e7eb]">
|
|
<td className="py-2 px-3 font-semibold text-[#1a1a1a]" colSpan={2}>
|
|
Totale Servizi
|
|
</td>
|
|
{tierTotals.map((total, idx) => (
|
|
<td
|
|
key={tiers[idx].tier_letter}
|
|
className="py-2 px-3 text-center font-semibold text-[#1a1a1a] tabular-nums"
|
|
>
|
|
{formatEuro(total)}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
<tr>
|
|
<td className="py-2 px-3 font-semibold text-[#1a1a1a]" colSpan={2}>
|
|
Prezzo Pubblico
|
|
</td>
|
|
{tiers.map((tier, tierIdx) => (
|
|
<td key={tier.tier_letter} className="py-2 px-3 text-center">
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
step="0.01"
|
|
value={tier.public_price ?? ""}
|
|
onChange={(e) => updateTierPublicPrice(tierIdx, e.target.value)}
|
|
placeholder="€0,00"
|
|
className="w-20 text-center text-sm border border-[#e5e7eb] rounded px-1 py-1 tabular-nums focus-visible:ring-1 focus-visible:ring-primary focus:outline-none"
|
|
/>
|
|
</td>
|
|
))}
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Transformation promise block */}
|
|
<section className="space-y-4">
|
|
<h3 className="text-base font-semibold text-[#1a1a1a]">Promessa di Trasformazione</h3>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs text-[#71717a] w-24 shrink-0">Aiuto</span>
|
|
<div className="flex-1">
|
|
<EditableCell
|
|
value={macro.cliente_ideale}
|
|
type="text"
|
|
placeholder="Aggiungi cliente ideale"
|
|
onSave={(v) => updateMacro("cliente_ideale", v)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs text-[#71717a] w-24 shrink-0">A ottenere</span>
|
|
<div className="flex-1">
|
|
<EditableCell
|
|
value={macro.risultato}
|
|
type="text"
|
|
placeholder="Aggiungi risultato"
|
|
onSave={(v) => updateMacro("risultato", v)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs text-[#71717a] w-24 shrink-0">In</span>
|
|
<div className="flex-1">
|
|
<EditableCell
|
|
value={macro.tempo}
|
|
type="text"
|
|
placeholder="Aggiungi durata (es. 3 mesi)"
|
|
onSave={(v) => updateMacro("tempo", v)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs text-[#71717a] w-24 shrink-0">Senza</span>
|
|
<div className="flex-1">
|
|
<EditableCell
|
|
value={macro.pain}
|
|
type="text"
|
|
placeholder="Aggiungi pain point"
|
|
onSave={(v) => updateMacro("pain", v)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs text-[#71717a] w-24 shrink-0">Grazie a</span>
|
|
<div className="flex-1">
|
|
<EditableCell
|
|
value={macro.metodo}
|
|
type="text"
|
|
placeholder="Aggiungi metodo"
|
|
onSave={(v) => updateMacro("metodo", v)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 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">
|
|
{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"
|
|
>
|
|
Annulla
|
|
</Link>
|
|
{!initialArchived && (
|
|
<button
|
|
type="button"
|
|
onClick={handleArchive}
|
|
disabled={isPending}
|
|
className="text-[#dc2626] text-sm font-semibold px-6 py-4 rounded hover:bg-red-50 transition-colors duration-150 disabled:opacity-50"
|
|
>
|
|
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>
|
|
);
|
|
}
|