From 5e4b2239cb717fa47b0411cc145a4300d77cd015 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Fri, 19 Jun 2026 17:37:32 +0200 Subject: [PATCH] =?UTF-8?q?docs(phase-19):=20create=20phase=20plan=20?= =?UTF-8?q?=E2=80=94=20LeadsKanbanBoard=20+=20view=20toggle=20(PIPE-01,=20?= =?UTF-8?q?PIPE-02)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .planning/ROADMAP.md | 5 +- .../19-pipeline-crm-kanban/19-01-PLAN.md | 553 ++++++++++++++++++ 2 files changed, 557 insertions(+), 1 deletion(-) create mode 100644 .planning/phases/19-pipeline-crm-kanban/19-01-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index de14b1c..d8f4a64 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -176,7 +176,10 @@ Plans: 1. I lead sono mostrati in colonne per stage (contacted → qualified → proposal_sent → negotiating → won/lost) con drag-drop che aggiorna `leads.status` 2. La tabella inline-edit esistente resta disponibile come vista alternativa 3. Spostare un lead su "Vinto"/"Perso" è l'azione manuale che ne registra l'esito -**Status**: Pending planning — `/gsd-plan-phase 19` +**Plans**: 1 plan +**Plan list**: + - [ ] 19-01-PLAN.md — LeadsKanbanBoard + LeadsViewToggle + page wiring (PIPE-01, PIPE-02) +**Status**: Planned — ready for `/gsd-execute-phase 19` ### Phase 20: Knowledge Base Cliente **Goal**: Memorizzare i transcript datati delle call (multipli per lead/cliente) e mostrarli nel profilo, pronti per essere dati in pasto all'AI. diff --git a/.planning/phases/19-pipeline-crm-kanban/19-01-PLAN.md b/.planning/phases/19-pipeline-crm-kanban/19-01-PLAN.md new file mode 100644 index 0000000..22e2512 --- /dev/null +++ b/.planning/phases/19-pipeline-crm-kanban/19-01-PLAN.md @@ -0,0 +1,553 @@ +--- +phase: 19-pipeline-crm-kanban +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/components/admin/leads/LeadsKanbanBoard.tsx + - src/components/admin/leads/LeadsViewToggle.tsx + - src/app/admin/leads/LeadsSearch.tsx + - src/app/admin/leads/page.tsx +autonomous: true +requirements: + - PIPE-01 + - PIPE-02 + +must_haves: + truths: + - "I lead sono visibili in una board Kanban con 6 colonne per stage (contacted, qualified, proposal_sent, negotiating, won, lost)" + - "Trascinare un lead da una colonna a un'altra aggiorna leads.status in modo persistente" + - "Spostare un lead nella colonna Vinto o Perso registra l'esito cambiando il campo status" + - "La vista tabella inline-edit (LeadTable) resta disponibile e funzionante come vista alternativa" + - "Il toggle Lista/Kanban è visibile sopra il contenuto e preserva lo stato della ricerca quando si cambia vista" + artifacts: + - path: "src/components/admin/leads/LeadsKanbanBoard.tsx" + provides: "Board Kanban con DndContext, 6 DroppableColumn, DraggableLeadCard, DragOverlay, ottimistic update" + exports: ["LeadsKanbanBoard"] + - path: "src/components/admin/leads/LeadsViewToggle.tsx" + provides: "Client wrapper con useState<'list' | 'kanban'> e pill toggle" + exports: ["LeadsViewToggle"] + - path: "src/app/admin/leads/LeadsSearch.tsx" + provides: "Search + toggle integrati; passa filtered leads sia a LeadTable che a LeadsKanbanBoard" + - path: "src/app/admin/leads/page.tsx" + provides: "Server component aggiornato che non renderizza più LeadsSearch direttamente ma LeadsViewToggle" + key_links: + - from: "LeadsKanbanBoard.tsx — handleDragEnd" + to: "src/app/admin/leads/actions.ts — updateLeadField" + via: "startTransition async + router.refresh()" + pattern: "updateLeadField\\(leadId.*status" + - from: "LeadsSearch.tsx — filtered" + to: "LeadsKanbanBoard — leads prop" + via: "filtered array passato come prop" + pattern: "LeadsKanbanBoard.*leads=\\{filtered\\}" + - from: "LeadsViewToggle / LeadsSearch" + to: "LeadTable + LeadsKanbanBoard" + via: "view state (list | kanban)" + pattern: "useState<.list.*kanban" +--- + + +Aggiunge una vista Kanban stile Pipedrive alla pagina `/admin/leads`, affiancata alla tabella esistente via toggle Lista/Kanban. + +Purpose: Completare PIPE-01 (board drag-drop per stage) e PIPE-02 (vinto/perso come cambio-colonna manuale) senza toccare il data layer e senza rimuovere la LeadTable esistente. + +Output: +- `LeadsKanbanBoard.tsx` — nuovo componente client con @dnd-kit drag-drop tra 6 colonne stage +- `LeadsViewToggle.tsx` — nuovo wrapper client con pill toggle Lista/Kanban +- `LeadsSearch.tsx` — modificato per ospitare il toggle e passare `filtered` leads a entrambe le viste +- `page.tsx` — modificato per rimuovere il render diretto di `` e invece renderizzare `` + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/phases/19-pipeline-crm-kanban/19-RESEARCH.md + + + + +Da src/lib/admin-queries.ts (VERIFIED): +```typescript +// LeadWithTags = leads row + tags array +export type LeadWithTags = { + id: string; + name: string; + email: string | null; + phone: string | null; + company: string | null; + status: string; // one of LEAD_STAGES values + next_action: string | null; + created_at: Date; + updated_at: Date; + tags: string[]; +}; + +export type LeadFieldOptions = { + tags: string[]; +}; +``` + +Da src/lib/lead-validators.ts (VERIFIED): +```typescript +export const LEAD_STAGES = [ + "contacted", + "qualified", + "proposal_sent", + "negotiating", + "won", + "lost", +] as const; + +export type LeadStage = typeof LEAD_STAGES[number]; +``` + +Da src/app/admin/leads/actions.ts (VERIFIED, line 174): +```typescript +export async function updateLeadField( + leadId: string, + fieldName: "name" | "email" | "phone" | "company" | "status" | "next_action", + value: string +): Promise +// Valida: status deve essere in LEAD_STAGES; throws su valore invalido +// Side effects: revalidatePath("/admin/leads") + revalidatePath(`/admin/leads/${leadId}`) +``` + +Da src/components/admin/kanban/KanbanBoard.tsx (analog esatto, VERIFIED): +```typescript +// Pattern da replicare per LeadsKanbanBoard: +// - useState>() per ottimistic update +// - DndContext con sensors (PointerSensor distance:5 + KeyboardSensor) +// - onDragStart: setActiveId(e.active.id as string) +// - onDragEnd: setActiveId(null); if (!over) return; guard su valore valido; +// setTaskStatuses(...); startTransition(async () => { await action; router.refresh(); }) +// - DroppableColumn: useDroppable({ id }) → setNodeRef, isOver +// - DraggableCard: useDraggable({ id }) → setNodeRef, transform, isDragging, listeners, attributes +// - DragOverlay dropAnimation={null} con ghost card +``` + +Da src/components/admin/kanban/PhasesViewToggle.tsx (analog esatto, VERIFIED): +```typescript +// Toggle pill pattern: +// className pill: "flex items-center gap-1 mb-5 bg-[#f4f4f5] rounded-lg p-1 w-fit" +// Active button: "bg-white text-[#1A463C] shadow-sm" +// Inactive button: "text-[#71717a] hover:text-[#1a1a1a]" +// Testo: "Lista" / "Kanban" +``` + +Da src/components/admin/leads/LeadTable.tsx (VERIFIED, line 20): +```typescript +// STAGE_COLOR — reusare per headerClass/dotClass nella board +const STAGE_COLOR: Record = { + contacted: "bg-blue-100 text-blue-800", + qualified: "bg-purple-100 text-purple-800", + proposal_sent: "bg-amber-100 text-amber-800", + negotiating: "bg-orange-100 text-orange-800", + won: "bg-green-100 text-green-800", + lost: "bg-red-100 text-red-800", +}; +``` + +Da src/app/admin/leads/LeadsSearch.tsx (VERIFIED — STATO ATTUALE): +```typescript +// Attualmente: search input + LeadTable(filtered, options) +// Dopo questa fase: search input + LeadsViewToggle (che gestisce il rendering di LeadTable o LeadsKanbanBoard) +// Alternativa più pulita: LeadsSearch mantiene la search, ma il toggle vive in LeadsViewToggle +// Approccio scelto (vedi task 1): LeadsSearch riceve `view` + `onViewChange` da LeadsViewToggle +``` + + + + + + + Task 1: LeadsKanbanBoard.tsx — board Kanban con drag-drop tra 6 stage + src/components/admin/leads/LeadsKanbanBoard.tsx + + + - src/components/admin/kanban/KanbanBoard.tsx — analog esatto da replicare strutturalmente + - src/lib/lead-validators.ts — LEAD_STAGES canonical values + - src/app/admin/leads/actions.ts — firma updateLeadField (già letta, riportata in interfaces) + + + +Creare `src/components/admin/leads/LeadsKanbanBoard.tsx` come componente client. Seguire la struttura esatta di `KanbanBoard.tsx`, adattata per i lead. + +**Struttura richiesta:** + +1. `"use client"` in cima. + +2. Definire `type LeadStage` e `LEAD_COLUMNS` come costante di modulo (NON importare LEAD_STAGES da lead-validators — definire il tipo locale per evitare dipendenze circolari con il barrel del server): + +```typescript +type LeadStage = "contacted" | "qualified" | "proposal_sent" | "negotiating" | "won" | "lost"; + +const LEAD_COLUMNS: { + id: LeadStage; + label: string; + headerClass: string; + dotClass: string; +}[] = [ + { id: "contacted", label: "Contattato", headerClass: "text-[#71717a]", dotClass: "bg-[#d4d4d8]" }, + { id: "qualified", label: "Qualificato", headerClass: "text-purple-700", dotClass: "bg-purple-400" }, + { id: "proposal_sent", label: "Offerta inviata", headerClass: "text-amber-700", dotClass: "bg-amber-400" }, + { id: "negotiating", label: "Trattativa", headerClass: "text-orange-700", dotClass: "bg-orange-400" }, + { id: "won", label: "Vinto", headerClass: "text-green-700", dotClass: "bg-green-500" }, + { id: "lost", label: "Perso", headerClass: "text-red-700", dotClass: "bg-red-400" }, +]; +``` + +3. Componente `DroppableColumn` (analogo al `DroppableColumn` di KanbanBoard.tsx): + - Props: `{ id: LeadStage; label: string; headerClass: string; dotClass: string; leads: LeadWithTags[]; activeId: string | null }` + - `const { setNodeRef, isOver } = useDroppable({ id })` + - Border color isOver: `border-[#1A463C] bg-[#1A463C]/5`, default: `border-[#e5e7eb] bg-[#f9f9f9]` + - Header pill count badge identico al modello + - Empty state: `

Nessun lead

` + +4. Componente `DraggableLeadCard` (analogo a `DraggableCard` di KanbanBoard.tsx): + - Props: `{ lead: LeadWithTags; isActive: boolean }` + - `const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: lead.id })` + - Contenuto card: riga primaria `lead.name` (text-sm font-medium text-[#1a1a1a]), riga secondaria `lead.company` se presente (text-xs text-[#71717a]), riga hint `lead.next_action` se presente (text-xs text-[#71717a] mt-1 line-clamp-1) + - Stili drag: opacity-30 quando isDragging, hover:border-[#1A463C]/40 quando non in drag + +5. Componente esportato `LeadsKanbanBoard`: + - Props: `{ leads: LeadWithTags[] }` + - `useState>` inizializzato da `leads.map(l => [l.id, l.status as LeadStage])` + - `useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor))` + - `leadsByStage`: `LEAD_COLUMNS.reduce` che raggruppa leads per stage corrente (legge `leadStatuses[l.id] ?? l.status`) + - `handleDragEnd`: `setActiveId(null)` → guard `if (!over) return` → guard `if (!(LEAD_COLUMNS.map(c => c.id) as string[]).includes(over.id as string)) return` → guard stesso stage → ottimistic `setLeadStatuses` → `startTransition(async () => { await updateLeadField(leadId, "status", newStage); router.refresh(); })` + - Layout: wrapper `overflow-x-auto` → griglia `grid grid-cols-6 gap-3 min-w-[1080px]` (6 colonne × 180px min) + - `DragOverlay dropAnimation={null}`: ghost card con `border-2 border-[#1A463C] shadow-xl rotate-1`, mostra `name` e `company` del lead attivo + +6. Imports richiesti: +```typescript +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { + DndContext, DragEndEvent, DragOverlay, + PointerSensor, KeyboardSensor, useSensor, useSensors, + useDroppable, useDraggable, +} from "@dnd-kit/core"; +import { updateLeadField } from "@/app/admin/leads/actions"; +import type { LeadWithTags } from "@/lib/admin-queries"; +``` +
+ + + grep -n "useDroppable\|useDraggable\|DragOverlay\|DndContext\|updateLeadField\|LeadsKanbanBoard" /Users/simonecavalli/Vault/IAMCAVALLI/src/components/admin/leads/LeadsKanbanBoard.tsx | head -20 + + + + - `LeadsKanbanBoard.tsx` esiste in `src/components/admin/leads/` + - Contiene `"use client"` alla prima riga + - Contiene `export function LeadsKanbanBoard(` + - Contiene `useDroppable(` e `useDraggable(` + - Contiene `DragOverlay` + - Contiene `updateLeadField(leadId, "status", newStage)` + - Contiene `LEAD_COLUMNS` con tutti e 6 gli stage: `contacted`, `qualified`, `proposal_sent`, `negotiating`, `won`, `lost` + - Contiene `overflow-x-auto` nel wrapper della griglia + - Contiene `grid-cols-6` + - Contiene `startTransition` + - `npx tsc --noEmit` non emette errori su questo file (verificabile dopo task 3) + + + + LeadsKanbanBoard.tsx esiste, esporta LeadsKanbanBoard, implementa drag-drop tra 6 colonne lead stage via @dnd-kit primitives, persiste su updateLeadField con ottimistic update, mostra card con name/company/next_action. + +
+ + + Task 2: LeadsViewToggle.tsx — wrapper client con pill toggle Lista/Kanban + search integrata + + src/components/admin/leads/LeadsViewToggle.tsx + src/app/admin/leads/LeadsSearch.tsx + + + + - src/components/admin/kanban/PhasesViewToggle.tsx — analog esatto del pill toggle (già letto) + - src/app/admin/leads/LeadsSearch.tsx — stato attuale (già letto) + + + +**Approccio scelto (Pitfall 5 dalla research):** Il toggle e la search vivono insieme in modo che la ricerca filtra entrambe le viste. Si implementa così: + +**File 1 — `src/components/admin/leads/LeadsViewToggle.tsx` (NUOVO):** + +`LeadsViewToggle` è il nuovo client wrapper che: +- Contiene `useState<"list" | "kanban">("list")` +- Contiene `useState("")` per la query di ricerca +- Calcola `filtered` con `useMemo` (stessa logica di LeadsSearch attuale: filtra per name/email/company/status/tags) +- Renderizza: + 1. Barra superiore: search input (a sinistra) + pill toggle Lista/Kanban (a destra), su una singola riga `flex justify-between items-center mb-4` + 2. Se `view === "list"`: `` + 3. Se `view === "kanban"`: `` + +```typescript +"use client"; + +import { useState, useMemo } from "react"; +import { Input } from "@/components/ui/input"; +import { Search } from "lucide-react"; +import { LeadTable } from "@/components/admin/leads/LeadTable"; +import { LeadsKanbanBoard } from "@/components/admin/leads/LeadsKanbanBoard"; +import type { LeadWithTags, LeadFieldOptions } from "@/lib/admin-queries"; + +export function LeadsViewToggle({ + leads, + options, +}: { + leads: LeadWithTags[]; + options: LeadFieldOptions; +}) { + const [view, setView] = useState<"list" | "kanban">("list"); + const [query, setQuery] = useState(""); + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) return leads; + return leads.filter((l) => { + if (l.name.toLowerCase().includes(q)) return true; + if (l.email?.toLowerCase().includes(q)) return true; + if (l.company?.toLowerCase().includes(q)) return true; + if (l.status.toLowerCase().includes(q)) return true; + if (l.tags.some((t) => t.toLowerCase().includes(q))) return true; + return false; + }); + }, [leads, query]); + + return ( +
+
+ {/* Search */} +
+ + setQuery(e.target.value)} + className="pl-9 h-9" + /> +
+ {/* Toggle pill */} +
+ + +
+
+ + {view === "list" ? ( + + ) : ( + + )} +
+ ); +} +``` + +**File 2 — `src/app/admin/leads/LeadsSearch.tsx` (MODIFICATO):** + +`LeadsSearch` non è più necessario: la sua logica (search + filtered + LeadTable) è stata trasferita in `LeadsViewToggle`. Svuotare il file sostituendo il contenuto con un re-export di `LeadsViewToggle` per non rompere eventuali import esistenti: + +```typescript +// LeadsSearch is superseded by LeadsViewToggle (Phase 19). +// Re-exported here to avoid breaking any existing import. +export { LeadsViewToggle as LeadsSearch } from "@/components/admin/leads/LeadsViewToggle"; +``` + +Nota: se page.tsx verrà aggiornato nel task 3 ad importare direttamente `LeadsViewToggle`, questo re-export è solo una rete di sicurezza e non causa conflitti. +
+ + + grep -n "useState\|LeadsKanbanBoard\|LeadTable\|LeadsViewToggle\|list.*kanban\|kanban.*list" /Users/simonecavalli/Vault/IAMCAVALLI/src/components/admin/leads/LeadsViewToggle.tsx | head -20 + + + + - `src/components/admin/leads/LeadsViewToggle.tsx` esiste + - Contiene `"use client"` + - Contiene `useState<"list" | "kanban">("list")` + - Contiene `useState("")` per la query + - Contiene `useMemo(` per `filtered` + - Contiene `LeadsKanbanBoard` importato da `@/components/admin/leads/LeadsKanbanBoard` + - Contiene `LeadTable` importato da `@/components/admin/leads/LeadTable` + - Contiene `export function LeadsViewToggle(` + - Contiene il pill toggle con classi `bg-[#f4f4f5] rounded-lg p-1` + - `src/app/admin/leads/LeadsSearch.tsx` contiene `LeadsViewToggle as LeadsSearch` + + + + LeadsViewToggle.tsx esiste con search integrata + toggle Lista/Kanban. LeadsSearch.tsx ri-esporta LeadsViewToggle per retrocompatibilità. La ricerca filtra entrambe le viste simultaneamente. + +
+ + + Task 3: page.tsx — cablaggio LeadsViewToggle + build check + src/app/admin/leads/page.tsx + + + - src/app/admin/leads/page.tsx — stato attuale (già letto: 23 righe, renderizza LeadsSearch) + + + +Aggiornare `src/app/admin/leads/page.tsx` per usare `LeadsViewToggle` al posto di `LeadsSearch`. + +Stato attuale: +```typescript +import { LeadsSearch } from "./LeadsSearch"; +// ... + +``` + +Nuovo stato — sostituire l'import e il render: +```typescript +import { LeadsViewToggle } from "@/components/admin/leads/LeadsViewToggle"; +// ... + +``` + +Il file completo aggiornato: +```typescript +import { getLeadsWithTags, getLeadFieldOptions } from "@/lib/admin-queries"; +import { LeadsViewToggle } from "@/components/admin/leads/LeadsViewToggle"; +import { CreateLeadModal } from "@/components/admin/leads/LeadForm"; + +export const revalidate = 0; + +export default async function LeadsPage() { + const [leads, options] = await Promise.all([ + getLeadsWithTags(), + getLeadFieldOptions(), + ]); + + return ( +
+
+

Lead Pipeline

+ +
+ + +
+ ); +} +``` + +Dopo aver scritto il file, eseguire il build per verificare zero errori TypeScript: +```bash +cd /Users/simonecavalli/Vault/IAMCAVALLI && npx next build 2>&1 | tail -30 +``` + +Se il build fallisce per errori TypeScript, correggerli prima di considerare il task completo. +
+ + + grep -n "LeadsViewToggle\|LeadsSearch" /Users/simonecavalli/Vault/IAMCAVALLI/src/app/admin/leads/page.tsx + + + + - `page.tsx` importa `LeadsViewToggle` da `@/components/admin/leads/LeadsViewToggle` + - `page.tsx` NON importa più `LeadsSearch` direttamente (o se lo importa, è solo tramite il re-export che risolve in LeadsViewToggle) + - `page.tsx` renderizza `` + - `npx next build` completa senza errori TypeScript (warning accettabili, errori no) + + + + page.tsx aggiornato. Build Next.js verde. La pagina /admin/leads mostra il toggle Lista/Kanban con la board drag-drop funzionante. + +
+ + + + LeadsKanbanBoard con 6 colonne stage, drag-drop che persiste su updateLeadField, LeadsViewToggle con search integrata e pill toggle, page.tsx aggiornato. + + + 1. Aprire http://localhost:3000/admin/leads (avviare il dev server con `npm run dev` se non già attivo) + 2. Verificare che la pagina mostri: barra di ricerca a sinistra + pill toggle "Lista / Kanban" a destra + 3. La vista Lista deve mostrare la tabella esistente (identica a prima della fase) + 4. Cliccare "Kanban": deve apparire una board con 6 colonne (Contattato, Qualificato, Offerta inviata, Trattativa, Vinto, Perso) e i lead nelle colonne corrispondenti al loro stage + 5. Trascinare un lead da una colonna a un'altra: la card deve spostarsi ottimisticamente; dopo pochi secondi la pagina si aggiorna e il lead rimane nella nuova colonna + 6. Spostare un lead nella colonna "Vinto" o "Perso": verificare che il lead rimanga lì dopo il refresh + 7. Digitare un nome nella barra di ricerca mentre si è in vista Kanban: la board deve filtrare le card in tempo reale + 8. Tornare alla vista Lista: la ricerca deve ancora essere attiva con lo stesso testo + + Digita "approvato" se tutto funziona, oppure descrivi i problemi riscontrati + + +
+ + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Browser → Server Action | `handleDragEnd` invia `(leadId, "status", newStage)` via `updateLeadField`; newStage arriva dal DOM (over.id) | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-19-01 | Tampering | `handleDragEnd` — over.id come colonna di destinazione | mitigate | Guard client-side: `if (!(LEAD_COLUMNS.map(c => c.id) as string[]).includes(over.id as string)) return` prima di chiamare updateLeadField. Guard server-side: `updateLeadField` valida già `LEAD_STAGES.includes(value)` e lancia errore su valore invalido (Phase 14 pattern invariato). | +| T-19-02 | Elevation of Privilege | `updateLeadField` server action | accept | `requireAdmin()` già presente nell'azione (Phase 14); il kanban chiama la stessa azione della tabella inline-edit, stessa protezione. | + + + +## Verifica fase completa + +```bash +# 1. Build pulito +cd /Users/simonecavalli/Vault/IAMCAVALLI && npx next build 2>&1 | grep -E "error|Error|✓ Compiled" + +# 2. File creati +ls /Users/simonecavalli/Vault/IAMCAVALLI/src/components/admin/leads/LeadsKanbanBoard.tsx +ls /Users/simonecavalli/Vault/IAMCAVALLI/src/components/admin/leads/LeadsViewToggle.tsx + +# 3. Struttura corretta LeadsKanbanBoard +grep -c "useDroppable\|useDraggable\|DragOverlay\|updateLeadField" /Users/simonecavalli/Vault/IAMCAVALLI/src/components/admin/leads/LeadsKanbanBoard.tsx + +# 4. Tutti e 6 gli stage nella board +grep -v '^//' /Users/simonecavalli/Vault/IAMCAVALLI/src/components/admin/leads/LeadsKanbanBoard.tsx | grep -c '"contacted"\|"qualified"\|"proposal_sent"\|"negotiating"\|"won"\|"lost"' + +# 5. Toggle presente in LeadsViewToggle +grep -c 'useState<"list" | "kanban">' /Users/simonecavalli/Vault/IAMCAVALLI/src/components/admin/leads/LeadsViewToggle.tsx + +# 6. page.tsx non importa più LeadsSearch direttamente +grep "LeadsSearch" /Users/simonecavalli/Vault/IAMCAVALLI/src/app/admin/leads/page.tsx | wc -l +# deve essere 0 +``` + + + +**PIPE-01:** La board Kanban con 6 colonne (contacted → lost) è visibile a `/admin/leads` dopo aver cliccato "Kanban". Drag-drop aggiorna `leads.status` in modo persistente (il lead rimane nella nuova colonna dopo refresh). + +**PIPE-02:** Spostare un lead nella colonna "Vinto" o "Perso" è il gesto manuale che registra l'esito — nessun modale, nessuna conferma, solo il drag-drop su quella colonna. + +**Retrocompatibilità:** La vista Lista (LeadTable con inline edit) resta invariata e accessibile tramite il toggle "Lista". + + + +Dopo il completamento, creare `.planning/phases/19-pipeline-crm-kanban/19-01-SUMMARY.md` seguendo il template in `@$HOME/.claude/get-shit-done/templates/summary.md`. +