feat: offer_type (una tantum/retainer), dedup tiers, redesigned Offerte tab

- offer_macros.offer_type ('una_tantum'|'retainer') + editor "Modalità" toggle
  (migration 0012, applied to prod)
- migration 0012 also adds UNIQUE(macro_id, tier_letter) — prevents duplicate
  tiers; ran after a one-time dedup of Web Domination's duplicate A/B/C tiers
- OffersTab redesigned: two-step assign (Offer → Tier cards with price), type
  badge "Una tantum/Ricorrente" instead of misleading "X mesi", no redundant
  "· A" when public_name == tier letter, cleaner active-offers cards
- getProjectFullDetail: availableMicros grouped by macro + defensive dedup;
  projectOffers/availableMicros carry offer_type/category/price
- proposal deck PricingSection shows offer type (fallback to duration for
  pre-existing proposals)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 14:32:59 +02:00
parent 1824cb643f
commit f98828f75e
9 changed files with 333 additions and 132 deletions
@@ -45,12 +45,15 @@ function formatUnitPrice(raw: string): string {
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;
@@ -75,6 +78,7 @@ export function OfferEditorClient({
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 ?? "",
@@ -166,6 +170,7 @@ export function OfferEditorClient({
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,
@@ -319,6 +324,29 @@ export function OfferEditorClient({
/>
</div>
<div className="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>
<div className="flex items-center gap-3">
<span className="text-xs text-[#71717a] w-24 shrink-0">Tipo</span>
<OptionMultiSelect