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 { 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" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user