---
phase: 20-knowledge-base-cliente
plan: 02
type: execute
wave: 2
depends_on:
  - 20-01
files_modified:
  - src/db/schema.ts
  - src/lib/lead-service.ts
  - src/app/admin/leads/actions.ts
autonomous: true
requirements:
  - KB-01
  - KB-02
must_haves:
  truths:
    - "La tabella clientTranscripts è definita in schema.ts con le relazioni Drizzle corrette"
    - "getTranscripts(leadId) restituisce i transcript in ordine call_date DESC"
    - "addTranscript e deleteTranscript sono server actions con requireAdmin guard"
  artifacts:
    - path: "src/db/schema.ts"
      provides: "Definizione tabella clientTranscripts + relazioni Drizzle per leads e clients"
      contains: "export const clientTranscripts = pgTable"
    - path: "src/lib/lead-service.ts"
      provides: "Query getTranscripts(leadId) — tutti i campi incluso content completo"
      exports: ["getTranscripts"]
    - path: "src/app/admin/leads/actions.ts"
      provides: "Server actions addTranscript e deleteTranscript con requireAdmin"
      exports: ["addTranscript", "deleteTranscript"]
  key_links:
    - from: "src/app/admin/leads/actions.ts"
      to: "src/db/schema.ts"
      via: "import clientTranscripts"
      pattern: "clientTranscripts"
    - from: "src/lib/lead-service.ts"
      to: "src/db/schema.ts"
      via: "import clientTranscripts"
      pattern: "getTranscripts"
---

<objective>
Aggiungere la definizione Drizzle della tabella `clientTranscripts` in schema.ts, la query `getTranscripts` in lead-service.ts e le server actions `addTranscript` / `deleteTranscript` in actions.ts.

Purpose: Fornisce il layer dati completo per la UI del piano 20-03. Phase 21 (AI) leggerà i transcript via `getTranscripts(leadId)` — la firma deve restituire tutti i campi incluso `content` completo.

Output: Schema Drizzle + query + actions pronte, build TypeScript senza errori.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/20-knowledge-base-cliente/20-CONTEXT.md
@.planning/phases/20-knowledge-base-cliente/20-01-SUMMARY.md

<interfaces>
<!-- Pattern esistenti estratti da src/db/schema.ts (linee 464-643) -->

Pattern tabella CRM con FK lead_id (activities, linee 464-481):
```typescript
export const activities = pgTable("activities", {
  id: text("id").primaryKey().$defaultFn(() => nanoid()),
  lead_id: text("lead_id").notNull().references(() => leads.id, { onDelete: "cascade" }),
  type: text("type").notNull(),
  notes: text("notes").notNull(),
  activity_date: timestamp("activity_date", { withTimezone: true }).notNull(),
  created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
```

Pattern relazioni (leadsRelations, linea 631-635):
```typescript
export const leadsRelations = relations(leads, ({ many }) => ({
  quotes: many(quotes),
  activities: many(activities),
  reminders: many(reminders),
}));

export const activitiesRelations = relations(activities, ({ one }) => ({
  lead: one(leads, { fields: [activities.lead_id], references: [leads.id] }),
}));
```

Pattern TypeScript types (fine file):
```typescript
export type Activity = typeof activities.$inferSelect;
export type NewActivity = typeof activities.$inferInsert;
```

Pattern query (lead-service.ts linee 51-57):
```typescript
export async function getActivityLog(leadId: string) {
  return await db
    .select()
    .from(activities)
    .where(eq(activities.lead_id, leadId))
    .orderBy(desc(activities.activity_date));
}
```

Pattern requireAdmin + server action (actions.ts linee 164-205):
```typescript
async function requireAdmin() {
  const session = await getServerSession(authOptions);
  if (!session) throw new Error("Non autorizzato");
}

export async function updateLeadField(leadId: string, ...) {
  await requireAdmin();
  // ... validazione ...
  await db.update(leads).set({...}).where(eq(leads.id, leadId));
  revalidatePath("/admin/leads");
  revalidatePath(`/admin/leads/${leadId}`);
}
```

Import correnti in actions.ts (linee 1-16):
```typescript
"use server";
import { z } from "zod";
import { db } from "@/db";
import { leads, quotes, tags } from "@/db/schema";
import { eq, and } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
```

Import correnti in lead-service.ts (linee 1-4):
```typescript
import { eq, and, isNull, desc, gte, lte, ilike, or } from "drizzle-orm";
import { db } from "@/db";
import { leads, activities, reminders, quotes } from "@/db/schema";
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Aggiungere clientTranscripts a src/db/schema.ts</name>
  <files>src/db/schema.ts</files>
  <read_first>
    - src/db/schema.ts (leggere tutto il file per capire dove inserire la nuova tabella e come aggiornare le relations esistenti)
    - .planning/phases/20-knowledge-base-cliente/20-CONTEXT.md (D-01, D-02 — campi esatti)
  </read_first>
  <action>
Aggiungere in `src/db/schema.ts` tre blocchi, rispettando la struttura esistente del file:

**1. Definizione tabella** — aggiungere dopo `// ============ REMINDERS TABLE` (dopo la chiusura della definizione reminders a linea ~499) e prima di `// ============ RELATIONS`:

```typescript
// ============ 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).
// onDelete cascade su entrambe le FK — se il lead/cliente viene eliminato, i transcript vengono eliminati.
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"), // opzionale — es. "Discovery call 12 giugno"
  content: text("content").notNull(), // testo grezzo illimitato (PostgreSQL text)
  call_date: text("call_date").notNull(), // DATE come text "YYYY-MM-DD" — coerente col pattern date nel progetto
  created_at: timestamp("created_at", { withTimezone: true })
    .notNull()
    .defaultNow(),
});
```

Nota su `call_date`: usare `text` invece di un tipo `date` Drizzle — il progetto gestisce le date come string ISO nei form e nel DB (vedi `activity_date` che è timestamp, ma `call_date` è date pura). Se Drizzle pg-core espone `date` nativo, usarlo; altrimenti usare `text("call_date").notNull()` per coerenza con i pattern di input da `<input type="date">` che restituisce stringhe "YYYY-MM-DD".

**2. Relazioni** — aggiungere nei blocchi relations esistenti:

Aggiornare `leadsRelations` (attuale linea ~631):
```typescript
export const leadsRelations = relations(leads, ({ many }) => ({
  quotes: many(quotes),
  activities: many(activities),
  reminders: many(reminders),
  transcripts: many(clientTranscripts),  // ← aggiungere questa riga
}));
```

Aggiornare `clientsRelations` (attuale linea ~503):
```typescript
export const clientsRelations = relations(clients, ({ many }) => ({
  projects: many(projects),
  transcripts: many(clientTranscripts),  // ← aggiungere questa riga
}));
```

Aggiungere le nuove relations dopo `remindersRelations`:
```typescript
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],
  }),
}));
```

**3. TypeScript types** — aggiungere in fondo al file dopo `export type Reminder`:
```typescript
export type ClientTranscript = typeof clientTranscripts.$inferSelect;
export type NewClientTranscript = typeof clientTranscripts.$inferInsert;
```
  </action>
  <verify>
    <automated>cd /Users/simonecavalli/Vault/IAMCAVALLI && npx tsc --noEmit 2>&1 | head -20</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c "export const clientTranscripts = pgTable" src/db/schema.ts` restituisce 1
    - `grep -c "transcripts: many(clientTranscripts)" src/db/schema.ts` restituisce 2 (leadsRelations + clientsRelations)
    - `grep -c "export const clientTranscriptsRelations" src/db/schema.ts` restituisce 1
    - `grep -c "export type ClientTranscript" src/db/schema.ts` restituisce 1
    - `npx tsc --noEmit` passa senza errori TypeScript relativi a schema.ts
  </acceptance_criteria>
  <done>clientTranscripts definita in schema.ts con relazioni Drizzle bidirezionali e TypeScript types esportati.</done>
</task>

<task type="auto">
  <name>Task 2: Aggiungere getTranscripts a lead-service.ts e addTranscript/deleteTranscript ad actions.ts</name>
  <files>src/lib/lead-service.ts, src/app/admin/leads/actions.ts</files>
  <read_first>
    - src/lib/lead-service.ts (leggere per vedere gli import correnti e dove appendere getTranscripts)
    - src/app/admin/leads/actions.ts (leggere per vedere gli import correnti e la posizione di requireAdmin)
    - src/db/schema.ts (dopo il Task 1 — per verificare il nome esatto dell'export clientTranscripts)
  </read_first>
  <action>
**In src/lib/lead-service.ts:**

Aggiungere `clientTranscripts` agli import esistenti:
```typescript
import { leads, activities, reminders, quotes, clientTranscripts } from "@/db/schema";
```

Appendere in fondo al file (dopo `getQuotesByLeadId`):
```typescript
// Get transcript log for a lead — call_date DESC (D-05)
// Restituisce tutti i campi incluso content completo (Phase 21 li legge in blocco).
export async function getTranscripts(leadId: string) {
  return await db
    .select()
    .from(clientTranscripts)
    .where(eq(clientTranscripts.lead_id, leadId))
    .orderBy(desc(clientTranscripts.call_date));
}
```

---

**In src/app/admin/leads/actions.ts:**

Aggiungere `clientTranscripts` agli import dal DB schema:
```typescript
import { leads, quotes, tags, clientTranscripts } from "@/db/schema";
```

Aggiungere `nanoid` agli import (serve per generare l'id del transcript):
```typescript
import { nanoid } from "nanoid";
```

Definire lo schema Zod per il transcript e appendere le due actions dopo le tag actions esistenti (dopo `renameLeadTag`):

```typescript
// ── 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" };
  }
}
```
  </action>
  <verify>
    <automated>cd /Users/simonecavalli/Vault/IAMCAVALLI && npx tsc --noEmit 2>&1 | grep -E "lead-service|actions" | head -10</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c "export async function getTranscripts" src/lib/lead-service.ts` restituisce 1
    - `grep -c "orderBy(desc(clientTranscripts.call_date))" src/lib/lead-service.ts` restituisce 1
    - `grep -c "export async function addTranscript" src/app/admin/leads/actions.ts` restituisce 1
    - `grep -c "export async function deleteTranscript" src/app/admin/leads/actions.ts` restituisce 1
    - `grep -c "await requireAdmin()" src/app/admin/leads/actions.ts` è >= 3 (addTranscript e deleteTranscript devono averlo, più le actions esistenti)
    - `grep -c "revalidatePath" src/app/admin/leads/actions.ts` aumenta di 2 rispetto al file originale (una per addTranscript, una per deleteTranscript)
    - `npx tsc --noEmit` passa senza errori su lead-service.ts e actions.ts
  </acceptance_criteria>
  <done>getTranscripts esportata da lead-service.ts, addTranscript e deleteTranscript aggiunte ad actions.ts con requireAdmin guard e revalidatePath.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Descrizione |
|----------|-------------|
| browser → server action | addTranscript e deleteTranscript sono "use server" — input non trusted |
| server action → database | Query Drizzle con parametri typed |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-20-04 | Elevation of Privilege | addTranscript / deleteTranscript | mitigate | `await requireAdmin()` come prima istruzione in entrambe le actions — lancia Error se sessione assente, blocca esecuzione |
| T-20-05 | Tampering | addTranscript — content | accept | Nessuna sanitization HTML richiesta (testo grezzo, non renderizzato come HTML — CONTEXT.md security note); Zod valida presenza e tipo |
| T-20-06 | Tampering | deleteTranscript — transcriptId | mitigate | L'ID viene passato dalla UI admin-only (protetta da requireAdmin); Drizzle usa query parametrizzata — no SQL injection |
| T-20-07 | Information Disclosure | getTranscripts | mitigate | Funzione usata solo in server components admin (`/admin/leads/[id]/page.tsx`); non esposta su API client (D-04 — client_id FK presente ma non UI client in Phase 20) |
</threat_model>

<verification>
```bash
cd /Users/simonecavalli/Vault/IAMCAVALLI

# Schema: tabella e relazioni presenti
grep -E "clientTranscripts|ClientTranscript" src/db/schema.ts | grep -v "^//"

# lead-service: query con ordinamento corretto
grep -A 6 "getTranscripts" src/lib/lead-service.ts

# actions: requireAdmin su entrambe le nuove actions
grep -B 1 "requireAdmin" src/app/admin/leads/actions.ts

# Build TypeScript
npx tsc --noEmit 2>&1 | tail -5
```
</verification>

<success_criteria>
- schema.ts contiene `clientTranscripts` table, relazioni bidirezionali (leads + clients), e TypeScript types (KB-01)
- lead-service.ts esporta `getTranscripts(leadId)` che ordina per `call_date DESC` (KB-02, D-05)
- actions.ts ha `addTranscript` e `deleteTranscript` entrambe con `requireAdmin()` guard (KB-02)
- `npx tsc --noEmit` passa senza errori sui file modificati
</success_criteria>

<output>
Dopo il completamento, creare `.planning/phases/20-knowledge-base-cliente/20-02-SUMMARY.md` con:
- Tabella Drizzle aggiunta: clientTranscripts con 7 campi
- Query aggiunta: getTranscripts(leadId) in lead-service.ts
- Actions aggiunte: addTranscript, deleteTranscript in actions.ts
- TypeScript check: passed/failed
</output>
