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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user