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:
+49
-1
@@ -5,6 +5,7 @@ import {
|
||||
numeric,
|
||||
timestamp,
|
||||
boolean,
|
||||
jsonb,
|
||||
primaryKey,
|
||||
uniqueIndex,
|
||||
index,
|
||||
@@ -518,11 +519,46 @@ export const clientTranscripts = pgTable("client_transcripts", {
|
||||
.defaultNow(),
|
||||
});
|
||||
|
||||
// ============ PROPOSALS TABLE (Preventivo AI — Phase 21/22) ============
|
||||
// Preventivo generato dall'agente AI e pubblicato come pagina pubblica
|
||||
// navigabile su /preventivo/[slug]. content (jsonb) contiene l'intero
|
||||
// ProposalContent assemblato (sezioni AI + offerta + config consulente).
|
||||
// lead_id/client_id nullable (ON DELETE SET NULL — non perdere la proposta se
|
||||
// il lead viene rimosso). offer_macro_id RESTRICT — la proposta riferisce
|
||||
// l'offerta sorgente. accepted_at IMMUTABILE una volta valorizzato.
|
||||
export const proposals = pgTable("proposals", {
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
slug: text("slug")
|
||||
.notNull()
|
||||
.unique()
|
||||
.$defaultFn(() => nanoid()),
|
||||
lead_id: text("lead_id").references(() => leads.id, { onDelete: "set null" }),
|
||||
client_id: text("client_id").references(() => clients.id, { onDelete: "set null" }),
|
||||
offer_macro_id: text("offer_macro_id")
|
||||
.notNull()
|
||||
.references(() => offer_macros.id, { onDelete: "restrict" }),
|
||||
title: text("title"),
|
||||
// Intero ProposalContent assemblato. Validato a runtime via Zod
|
||||
// (src/lib/proposal/schema.ts); jsonb gestito da postgres-js (parse automatico).
|
||||
content: jsonb("content").notNull(),
|
||||
model: text("model"), // es. "claude-opus-4-8"
|
||||
state: text("state").notNull().default("draft"), // draft | published | accepted | rejected
|
||||
selected_tier: text("selected_tier"), // 'A' | 'B' | 'C' — CHECK a livello SQL (migration 0010)
|
||||
accepted_at: timestamp("accepted_at", { withTimezone: true }), // immutable once set
|
||||
client_email: text("client_email"),
|
||||
client_notes: text("client_notes"),
|
||||
created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updated_at: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
// ============ RELATIONS ============
|
||||
|
||||
export const clientsRelations = relations(clients, ({ many }) => ({
|
||||
projects: many(projects),
|
||||
transcripts: many(clientTranscripts),
|
||||
proposals: many(proposals),
|
||||
}));
|
||||
|
||||
export const projectsRelations = relations(projects, ({ one, many }) => ({
|
||||
@@ -654,6 +690,7 @@ export const leadsRelations = relations(leads, ({ many }) => ({
|
||||
activities: many(activities),
|
||||
reminders: many(reminders),
|
||||
transcripts: many(clientTranscripts),
|
||||
proposals: many(proposals),
|
||||
}));
|
||||
|
||||
export const activitiesRelations = relations(activities, ({ one }) => ({
|
||||
@@ -682,6 +719,15 @@ export const quotesRelations = relations(quotes, ({ one, many }) => ({
|
||||
quoteItems: many(quote_items),
|
||||
}));
|
||||
|
||||
export const proposalsRelations = relations(proposals, ({ one }) => ({
|
||||
lead: one(leads, { fields: [proposals.lead_id], references: [leads.id] }),
|
||||
client: one(clients, { fields: [proposals.client_id], references: [clients.id] }),
|
||||
offerMacro: one(offer_macros, {
|
||||
fields: [proposals.offer_macro_id],
|
||||
references: [offer_macros.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
// ============ TYPESCRIPT TYPES (for use in API routes and Server Components) ============
|
||||
|
||||
export type Client = typeof clients.$inferSelect;
|
||||
@@ -739,4 +785,6 @@ export type NewActivity = typeof activities.$inferInsert;
|
||||
export type Reminder = typeof reminders.$inferSelect;
|
||||
export type NewReminder = typeof reminders.$inferInsert;
|
||||
export type ClientTranscript = typeof clientTranscripts.$inferSelect;
|
||||
export type NewClientTranscript = typeof clientTranscripts.$inferInsert;
|
||||
export type NewClientTranscript = typeof clientTranscripts.$inferInsert;
|
||||
export type Proposal = typeof proposals.$inferSelect;
|
||||
export type NewProposal = typeof proposals.$inferInsert;
|
||||
Reference in New Issue
Block a user