feat(21-22): agente AI generazione preventivo + pagina pubblica deck

- Migration 0010: tabella proposals (id, slug, lead_id, client_id,
  offer_macro_id, content jsonb, state, selected_tier, accepted_at)
  applicata a prod via SSH tunnel
- @anthropic-ai/sdk@0.105.0 installato; ANTHROPIC_API_KEY in .env.local
- src/lib/proposal/: schema Zod ProposalContent, agente Claude Opus 4.8,
  assemble (AI + offerta DB + config consulente), queries, profile.ts
- Admin: /admin/preventivi lista + /genera (pre-fill ?lead_id=X) + /[id] review
- Sidebar: voce Preventivi + CTA globale lime "Genera preventivo"
- LeadDetail: pulsante "Genera preventivo" → /admin/preventivi/genera?lead_id=X
- Pagina pubblica /preventivo/[slug]: deck 20+ slide light-mode iamcavalli,
  navigazione frecce + dot + keyboard, accept/reject con guard immutabilità
- STATE.md aggiornato (80%), 21-PLAN.md scritto nel formato GSD

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 14:37:05 +02:00
parent 86c86cd420
commit fdcc938252
41 changed files with 2818 additions and 19 deletions
@@ -0,0 +1,76 @@
import type { AssembledProposal } from "@/lib/proposal/assemble";
type Props = { tiers: AssembledProposal["offer"]["tiers"] };
function formatEur(val: string | null | undefined): string {
if (!val) return "—";
const n = parseFloat(val);
return `${n.toLocaleString("it-IT")}`;
}
export function PricingSection({ tiers }: Props) {
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-10">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Le tre opzioni</p>
<h2 className="text-5xl font-light text-foreground mt-4">
Tre tappe, <span className="text-primary">un solo risultato.</span>
</h2>
</div>
<div className="grid grid-cols-3 gap-6">
{tiers.map((tier, i) => {
const isHighlighted = i === 1; // tier B highlighted di default
const price = tier.publicPrice ?? tier.servicesTotal;
const durationLabel = `${tier.durationMonths} ${tier.durationMonths === 1 ? "mese" : "mesi"}`;
return (
<div
key={tier.id}
className={`rounded-xl p-8 space-y-6 border ${
isHighlighted
? "border-primary bg-primary/5"
: "border-border bg-white"
}`}
>
<div className="flex items-start justify-between">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">
Opzione {tier.tierLetter} / {durationLabel}
</p>
<h3 className="text-xl font-medium text-foreground mt-1">{tier.publicName}</h3>
</div>
<span className="text-xs font-mono bg-primary/10 text-primary px-2 py-1 rounded">
{tier.tierLetter}
</span>
</div>
<div className="space-y-1">
<p className="text-3xl font-light text-primary">{formatEur(price)}</p>
<p className="text-xs text-muted-foreground">
{tier.durationMonths > 1 ? `${tier.durationMonths} mesi` : "pagamento unico"}
</p>
</div>
{/* Servizi inclusi */}
{tier.services.length > 0 && (
<ul className="space-y-1.5">
{tier.services.map((s) => (
<li key={s.id} className="flex items-start gap-2 text-sm text-foreground">
<span className="text-primary mt-0.5"></span>
<span>{s.name}</span>
</li>
))}
</ul>
)}
</div>
);
})}
</div>
<p className="text-xs font-mono text-muted-foreground">
Le tre opzioni si compongono progressivamente. La C eredita tutto da B, che a sua volta eredita tutto da A.
</p>
</div>
);
}