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:
@@ -0,0 +1,88 @@
|
||||
import type { ProposalContent } from "@/lib/proposal/schema";
|
||||
|
||||
type Props = { timeline: ProposalContent["timeline"] };
|
||||
|
||||
const COLOR_MAP = {
|
||||
cyan: { bg: "bg-cyan-400", text: "text-cyan-900" },
|
||||
purple: { bg: "bg-violet-400", text: "text-violet-900" },
|
||||
green: { bg: "bg-emerald-400",text: "text-emerald-900" },
|
||||
};
|
||||
|
||||
export function TimelineSection({ timeline }: Props) {
|
||||
const weeks = Array.from({ length: timeline.totalWeeks }, (_, i) => i + 1);
|
||||
|
||||
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">Timeline</p>
|
||||
<h2 className="text-5xl font-light text-foreground mt-4">
|
||||
{timeline.totalWeeks} settimane di <span className="text-primary">progressione.</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Header settimane */}
|
||||
<div className="grid gap-2" style={{ gridTemplateColumns: `140px repeat(${timeline.totalWeeks}, 1fr)` }}>
|
||||
<div />
|
||||
{weeks.map((w) => (
|
||||
<div key={w} className="text-center text-xs font-mono text-muted-foreground">W{w}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Righe fase */}
|
||||
{timeline.phases.map((phase, i) => {
|
||||
const colors = COLOR_MAP[phase.color];
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="grid gap-2 items-center"
|
||||
style={{ gridTemplateColumns: `140px repeat(${timeline.totalWeeks}, 1fr)` }}
|
||||
>
|
||||
<div className="text-xs text-muted-foreground font-mono truncate pr-2">
|
||||
{i === 0 ? "OPZIONE A" : i === 1 ? "OPZIONE B" : "OPZIONE C"}
|
||||
<br />
|
||||
<span className="text-[10px] text-muted-foreground/60">
|
||||
{phase.endWeek - phase.startWeek + 1} settimane
|
||||
</span>
|
||||
</div>
|
||||
{weeks.map((w) => {
|
||||
const active = w >= phase.startWeek && w <= phase.endWeek;
|
||||
const isFirst = w === phase.startWeek;
|
||||
const isLast = w === phase.endWeek;
|
||||
return (
|
||||
<div
|
||||
key={w}
|
||||
className={`h-10 ${active ? `${colors.bg} ${isFirst ? "rounded-l-md" : ""} ${isLast ? "rounded-r-md" : ""}` : ""} flex items-center ${isFirst ? "justify-start pl-3" : ""}`}
|
||||
>
|
||||
{isFirst && (
|
||||
<span className={`text-xs font-medium ${colors.text} whitespace-nowrap`}>
|
||||
{phase.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Legenda */}
|
||||
<div className="flex items-center gap-6">
|
||||
{timeline.phases.map((phase, i) => {
|
||||
const colors = COLOR_MAP[phase.color];
|
||||
return (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-sm ${colors.bg}`} />
|
||||
<span className="text-xs text-muted-foreground">{phase.label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className="text-xs font-mono text-muted-foreground">
|
||||
› {timeline.disclaimer}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user