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:
2026-06-20 08:55:46 +02:00
parent a46e8b0bc8
commit b96b6a2083
3 changed files with 104 additions and 3 deletions
+57 -1
View File
@@ -2,7 +2,8 @@
import { z } from "zod";
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 {
createLeadSchema,
@@ -252,3 +253,58 @@ export async function renameLeadTag(oldValue: string, newValue: string) {
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" };
}
}
+35
View File
@@ -498,10 +498,31 @@ export const reminders = pgTable("reminders", {
.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 ============
export const clientsRelations = relations(clients, ({ many }) => ({
projects: many(projects),
transcripts: many(clientTranscripts),
}));
export const projectsRelations = relations(projects, ({ one, many }) => ({
@@ -632,6 +653,7 @@ export const leadsRelations = relations(leads, ({ many }) => ({
quotes: many(quotes),
activities: many(activities),
reminders: many(reminders),
transcripts: many(clientTranscripts),
}));
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] }),
}));
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 }) => ({
lead: one(leads, { fields: [quotes.lead_id], references: [leads.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 Reminder = typeof reminders.$inferSelect;
export type NewReminder = typeof reminders.$inferInsert;
export type ClientTranscript = typeof clientTranscripts.$inferSelect;
export type NewClientTranscript = typeof clientTranscripts.$inferInsert;
+11 -1
View File
@@ -1,6 +1,6 @@
import { eq, and, isNull, desc, gte, lte, ilike, or } from "drizzle-orm";
import { db } from "@/db";
import { leads, activities, reminders, quotes } from "@/db/schema";
import { leads, activities, reminders, quotes, clientTranscripts } from "@/db/schema";
// Get all leads
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))
.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));
}