- LeadsViewToggle: client wrapper with useState<list|kanban>, useMemo filtered
- Search input (left) + pill toggle Lista/Kanban (right) on single row
- Filters leads by name/email/company/status/tags across both views
- Search state preserved when switching between Lista and Kanban
- LeadsSearch.tsx: replaced with re-export of LeadsViewToggle (backward compat)
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
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"
---
<objective>
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 `<LeadsSearch>` e invece renderizzare `<LeadsViewToggle>`
- src/app/admin/leads/actions.ts — firma updateLeadField (già letta, riportata in interfaces)
</read_first>
<action>
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):
- 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<Record<string, LeadStage>>` inizializzato da `leads.map(l => [l.id, l.status as LeadStage])`
- 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)
</acceptance_criteria>
<done>
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.
- src/components/admin/kanban/PhasesViewToggle.tsx — analog esatto del pill toggle (già letto)
- src/app/admin/leads/LeadsSearch.tsx — stato attuale (già letto)
</read_first>
<action>
**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ì:
`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.
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.
</action>
<verify>
<automated>grep -n "useState\|LeadsKanbanBoard\|LeadTable\|LeadsViewToggle\|list.*kanban\|kanban.*list" /Users/simonecavalli/Vault/IAMCAVALLI/src/components/admin/leads/LeadsViewToggle.tsx | head -20</automated>
- 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`
</acceptance_criteria>
<done>
LeadsViewToggle.tsx esiste con search integrata + toggle Lista/Kanban. LeadsSearch.tsx ri-esporta LeadsViewToggle per retrocompatibilità. La ricerca filtra entrambe le viste simultaneamente.
LeadsKanbanBoard con 6 colonne stage, drag-drop che persiste su updateLeadField, LeadsViewToggle con search integrata e pill toggle, page.tsx aggiornato.
</what-built>
<how-to-verify>
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
</how-to-verify>
<resume-signal>Digita "approvato" se tutto funziona, oppure descrivi i problemi riscontrati</resume-signal>
</task>
</tasks>
<threat_model>
## 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. |
</threat_model>
<verification>
## 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
**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".
</success_criteria>
<output>
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`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.