feat(20-02): add clientTranscripts data layer
schema.ts: clientTranscripts table (id, lead_id/client_id nullable FKs, title, content NOT NULL, call_date, created_at) + leadsRelations/ clientsRelations updated + clientTranscriptsRelations + TS types. lead-service.ts: getTranscripts(leadId) — call_date DESC ordering. actions.ts: addTranscript + deleteTranscript with requireAdmin guard, Zod validation, and revalidatePath. TypeScript: no errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { leads, quotes, tags } from "@/db/schema";
|
import { leads, quotes, tags, clientTranscripts } from "@/db/schema";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
createLeadSchema,
|
createLeadSchema,
|
||||||
@@ -252,3 +253,58 @@ export async function renameLeadTag(oldValue: string, newValue: string) {
|
|||||||
|
|
||||||
revalidatePath("/admin/leads");
|
revalidatePath("/admin/leads");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Transcript actions (Phase 20 — KB-02) ────────────────────────────────────
|
||||||
|
|
||||||
|
const addTranscriptSchema = z.object({
|
||||||
|
lead_id: z.string().min(1),
|
||||||
|
title: z.string().optional(),
|
||||||
|
content: z.string().min(1, "Il testo del transcript è obbligatorio"),
|
||||||
|
call_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Data non valida (YYYY-MM-DD)"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function addTranscript(data: z.infer<typeof addTranscriptSchema>) {
|
||||||
|
await requireAdmin();
|
||||||
|
|
||||||
|
const parsed = addTranscriptSchema.safeParse(data);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return { success: false, error: parsed.error.issues[0].message };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [transcript] = await db
|
||||||
|
.insert(clientTranscripts)
|
||||||
|
.values({
|
||||||
|
id: nanoid(),
|
||||||
|
lead_id: parsed.data.lead_id,
|
||||||
|
title: parsed.data.title || null,
|
||||||
|
content: parsed.data.content,
|
||||||
|
call_date: parsed.data.call_date,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
revalidatePath(`/admin/leads/${parsed.data.lead_id}`);
|
||||||
|
return { success: true, transcript };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("addTranscript error:", error);
|
||||||
|
return { success: false, error: "Errore nel salvataggio del transcript" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTranscript(transcriptId: string, leadId: string) {
|
||||||
|
await requireAdmin();
|
||||||
|
|
||||||
|
if (!transcriptId) throw new Error("ID transcript richiesto");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.delete(clientTranscripts)
|
||||||
|
.where(eq(clientTranscripts.id, transcriptId));
|
||||||
|
|
||||||
|
revalidatePath(`/admin/leads/${leadId}`);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("deleteTranscript error:", error);
|
||||||
|
return { success: false, error: "Errore nell'eliminazione del transcript" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -498,10 +498,31 @@ export const reminders = pgTable("reminders", {
|
|||||||
.defaultNow(),
|
.defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============ CLIENT TRANSCRIPTS TABLE (Knowledge Base — Phase 20) ============
|
||||||
|
// Transcript datati delle call, multipli per lead o cliente.
|
||||||
|
// lead_id e client_id sono entrambi nullable (D-01): un transcript può appartenere
|
||||||
|
// a un lead pre-conversione (Phase 20 UI) o a un cliente post-conversione (futuro).
|
||||||
|
export const clientTranscripts = pgTable("client_transcripts", {
|
||||||
|
id: text("id")
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => nanoid()),
|
||||||
|
lead_id: text("lead_id")
|
||||||
|
.references(() => leads.id, { onDelete: "cascade" }),
|
||||||
|
client_id: text("client_id")
|
||||||
|
.references(() => clients.id, { onDelete: "cascade" }),
|
||||||
|
title: text("title"),
|
||||||
|
content: text("content").notNull(),
|
||||||
|
call_date: text("call_date").notNull(), // "YYYY-MM-DD" — coerente con <input type="date">
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
// ============ RELATIONS ============
|
// ============ RELATIONS ============
|
||||||
|
|
||||||
export const clientsRelations = relations(clients, ({ many }) => ({
|
export const clientsRelations = relations(clients, ({ many }) => ({
|
||||||
projects: many(projects),
|
projects: many(projects),
|
||||||
|
transcripts: many(clientTranscripts),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const projectsRelations = relations(projects, ({ one, many }) => ({
|
export const projectsRelations = relations(projects, ({ one, many }) => ({
|
||||||
@@ -632,6 +653,7 @@ export const leadsRelations = relations(leads, ({ many }) => ({
|
|||||||
quotes: many(quotes),
|
quotes: many(quotes),
|
||||||
activities: many(activities),
|
activities: many(activities),
|
||||||
reminders: many(reminders),
|
reminders: many(reminders),
|
||||||
|
transcripts: many(clientTranscripts),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const activitiesRelations = relations(activities, ({ one }) => ({
|
export const activitiesRelations = relations(activities, ({ one }) => ({
|
||||||
@@ -642,6 +664,17 @@ export const remindersRelations = relations(reminders, ({ one }) => ({
|
|||||||
lead: one(leads, { fields: [reminders.lead_id], references: [leads.id] }),
|
lead: one(leads, { fields: [reminders.lead_id], references: [leads.id] }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const clientTranscriptsRelations = relations(clientTranscripts, ({ one }) => ({
|
||||||
|
lead: one(leads, {
|
||||||
|
fields: [clientTranscripts.lead_id],
|
||||||
|
references: [leads.id],
|
||||||
|
}),
|
||||||
|
client: one(clients, {
|
||||||
|
fields: [clientTranscripts.client_id],
|
||||||
|
references: [clients.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
export const quotesRelations = relations(quotes, ({ one, many }) => ({
|
export const quotesRelations = relations(quotes, ({ one, many }) => ({
|
||||||
lead: one(leads, { fields: [quotes.lead_id], references: [leads.id] }),
|
lead: one(leads, { fields: [quotes.lead_id], references: [leads.id] }),
|
||||||
client: one(clients, { fields: [quotes.client_id], references: [clients.id] }),
|
client: one(clients, { fields: [quotes.client_id], references: [clients.id] }),
|
||||||
@@ -705,3 +738,5 @@ export type Activity = typeof activities.$inferSelect;
|
|||||||
export type NewActivity = typeof activities.$inferInsert;
|
export type NewActivity = typeof activities.$inferInsert;
|
||||||
export type Reminder = typeof reminders.$inferSelect;
|
export type Reminder = typeof reminders.$inferSelect;
|
||||||
export type NewReminder = typeof reminders.$inferInsert;
|
export type NewReminder = typeof reminders.$inferInsert;
|
||||||
|
export type ClientTranscript = typeof clientTranscripts.$inferSelect;
|
||||||
|
export type NewClientTranscript = typeof clientTranscripts.$inferInsert;
|
||||||
+11
-1
@@ -1,6 +1,6 @@
|
|||||||
import { eq, and, isNull, desc, gte, lte, ilike, or } from "drizzle-orm";
|
import { eq, and, isNull, desc, gte, lte, ilike, or } from "drizzle-orm";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { leads, activities, reminders, quotes } from "@/db/schema";
|
import { leads, activities, reminders, quotes, clientTranscripts } from "@/db/schema";
|
||||||
|
|
||||||
// Get all leads
|
// Get all leads
|
||||||
export async function getAllLeads(limit: number = 100, offset: number = 0) {
|
export async function getAllLeads(limit: number = 100, offset: number = 0) {
|
||||||
@@ -209,3 +209,13 @@ export async function getQuotesByLeadId(leadId: string) {
|
|||||||
.where(eq(quotes.lead_id, leadId))
|
.where(eq(quotes.lead_id, leadId))
|
||||||
.orderBy(desc(quotes.created_at));
|
.orderBy(desc(quotes.created_at));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get transcript log for a lead — call_date DESC (D-05)
|
||||||
|
// Returns all fields including full content (Phase 21 reads them in bulk).
|
||||||
|
export async function getTranscripts(leadId: string) {
|
||||||
|
return await db
|
||||||
|
.select()
|
||||||
|
.from(clientTranscripts)
|
||||||
|
.where(eq(clientTranscripts.lead_id, leadId))
|
||||||
|
.orderBy(desc(clientTranscripts.call_date));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user