docs(planning): archivia v2.1/v2.2/v2.3 e documenta v2.4
.planning/ documentava in dettaglio cio che era vecchio e per niente cio
che e in produzione: le fasi 11-22 (v2.1 e v2.2, chiuse a giugno) erano
ancora in phases/ mentre v1.0 e v2.0 stavano gia in milestones/, e il
lavoro degli ultimi due mesi - gate OTP e ciclo di vita dei retainer, cioe
quello che gira su hub.iamcavalli.net - non aveva nessuna cartella.
- phases/{11,12,14} -> milestones/v2.1-phases/, phases/{18..22} ->
milestones/v2.2-phases/. Ora phases/ contiene solo la milestone in
corso, che e quello che state.cjs conta per il progresso
- v2.1-ROADMAP.md ricostruito: era l'unica milestone senza archivio,
interrotta dal reset del 19/06 e mai chiusa formalmente
- v2.3-ROADMAP.md + v2.3-REQUIREMENTS.md: v2.3 e stata eseguita fuori dal
ciclo GSD, non esistono PLAN/SUMMARY per fase. L'archivio E la doc
- REQUIREMENTS.md riscritto per v2.4 con il backlog reale
- phases/13 e phases/26: SUMMARY ricostruiti da commit, migration e
STATUS.md. 26 e il primo numero libero
- research/: cancellate 4 varianti dello stesso PITFALLS e FEATURES/
SUMMARY, superati da PROJECT.md. Diverse anti-feature erano ormai
contraddette dai fatti (il Kanban e stato costruito in Phase 19,
l'email in v2.3, il time tracking esiste)
- cancellati UI-RULES.md e DESIGN-SYSTEM.md (CLAUDE.md li dichiara
superseded: impongono l'inverso della regola attuale) e HANDOFF.md,
fermo al 13/06
- SECURITY-*.md -> security/: audit chiuso, ma i report restano la doc di
cosa e stato ruotato e perche
- PROJECT.md/MILESTONES.md/ROADMAP.md allineati: milestone corrente v2.4,
sessione OTP 90gg non 30, migrazioni fino alla 0016
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
---
|
||||
|
||||
<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>`
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/19-pipeline-crm-kanban/19-RESEARCH.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Tipi e contratti estratti dal codebase. L'executor li usa direttamente — nessuna esplorazione necessaria. -->
|
||||
|
||||
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<void>
|
||||
// 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<Record<string, Status>>() 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<string, string> = {
|
||||
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
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: LeadsKanbanBoard.tsx — board Kanban con drag-drop tra 6 stage</name>
|
||||
<files>src/components/admin/leads/LeadsKanbanBoard.tsx</files>
|
||||
|
||||
<read_first>
|
||||
- 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)
|
||||
</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):
|
||||
|
||||
```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: `<p className="text-xs text-[#d4d4d8] italic text-center py-10 select-none">Nessun lead</p>`
|
||||
|
||||
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<Record<string, LeadStage>>` 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";
|
||||
```
|
||||
</action>
|
||||
|
||||
<verify>
|
||||
<automated>grep -n "useDroppable\|useDraggable\|DragOverlay\|DndContext\|updateLeadField\|LeadsKanbanBoard" /Users/simonecavalli/Vault/IAMCAVALLI/src/components/admin/leads/LeadsKanbanBoard.tsx | head -20</automated>
|
||||
</verify>
|
||||
|
||||
<acceptance_criteria>
|
||||
- `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)
|
||||
</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.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: LeadsViewToggle.tsx — wrapper client con pill toggle Lista/Kanban + search integrata</name>
|
||||
<files>
|
||||
src/components/admin/leads/LeadsViewToggle.tsx
|
||||
src/app/admin/leads/LeadsSearch.tsx
|
||||
</files>
|
||||
|
||||
<read_first>
|
||||
- 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ì:
|
||||
|
||||
**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"`: `<LeadTable leads={filtered} options={options} />`
|
||||
3. Se `view === "kanban"`: `<LeadsKanbanBoard leads={filtered} />`
|
||||
|
||||
```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 (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
{/* Search */}
|
||||
<div className="relative max-w-sm flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#71717a]" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Cerca lead..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="pl-9 h-9"
|
||||
/>
|
||||
</div>
|
||||
{/* Toggle pill */}
|
||||
<div className="flex items-center gap-1 bg-[#f4f4f5] rounded-lg p-1 w-fit flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setView("list")}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
|
||||
view === "list"
|
||||
? "bg-white text-[#1A463C] shadow-sm"
|
||||
: "text-[#71717a] hover:text-[#1a1a1a]"
|
||||
}`}
|
||||
>
|
||||
Lista
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView("kanban")}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
|
||||
view === "kanban"
|
||||
? "bg-white text-[#1A463C] shadow-sm"
|
||||
: "text-[#71717a] hover:text-[#1a1a1a]"
|
||||
}`}
|
||||
>
|
||||
Kanban
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{view === "list" ? (
|
||||
<LeadTable leads={filtered} options={options} />
|
||||
) : (
|
||||
<LeadsKanbanBoard leads={filtered} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**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.
|
||||
</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>
|
||||
</verify>
|
||||
|
||||
<acceptance_criteria>
|
||||
- `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`
|
||||
</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.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: page.tsx — cablaggio LeadsViewToggle + build check</name>
|
||||
<files>src/app/admin/leads/page.tsx</files>
|
||||
|
||||
<read_first>
|
||||
- src/app/admin/leads/page.tsx — stato attuale (già letto: 23 righe, renderizza LeadsSearch)
|
||||
</read_first>
|
||||
|
||||
<action>
|
||||
Aggiornare `src/app/admin/leads/page.tsx` per usare `LeadsViewToggle` al posto di `LeadsSearch`.
|
||||
|
||||
Stato attuale:
|
||||
```typescript
|
||||
import { LeadsSearch } from "./LeadsSearch";
|
||||
// ...
|
||||
<LeadsSearch leads={leads} options={options} />
|
||||
```
|
||||
|
||||
Nuovo stato — sostituire l'import e il render:
|
||||
```typescript
|
||||
import { LeadsViewToggle } from "@/components/admin/leads/LeadsViewToggle";
|
||||
// ...
|
||||
<LeadsViewToggle leads={leads} options={options} />
|
||||
```
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-[#1a1a1a]">Lead Pipeline</h1>
|
||||
<CreateLeadModal />
|
||||
</div>
|
||||
|
||||
<LeadsViewToggle leads={leads} options={options} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
</action>
|
||||
|
||||
<verify>
|
||||
<automated>grep -n "LeadsViewToggle\|LeadsSearch" /Users/simonecavalli/Vault/IAMCAVALLI/src/app/admin/leads/page.tsx</automated>
|
||||
</verify>
|
||||
|
||||
<acceptance_criteria>
|
||||
- `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 `<LeadsViewToggle leads={leads} options={options} />`
|
||||
- `npx next build` completa senza errori TypeScript (warning accettabili, errori no)
|
||||
</acceptance_criteria>
|
||||
|
||||
<done>
|
||||
page.tsx aggiornato. Build Next.js verde. La pagina /admin/leads mostra il toggle Lista/Kanban con la board drag-drop funzionante.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>
|
||||
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
|
||||
|
||||
# 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
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
**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`.
|
||||
</output>
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
phase: 19-pipeline-crm-kanban
|
||||
plan: 01
|
||||
status: complete
|
||||
completed_at: "2026-06-19"
|
||||
requirements_satisfied:
|
||||
- PIPE-01
|
||||
- PIPE-02
|
||||
commits:
|
||||
- 2c67e6f
|
||||
- 607c257
|
||||
- 34be934
|
||||
---
|
||||
|
||||
# Plan 19-01 Summary — LeadsKanbanBoard + LeadsViewToggle
|
||||
|
||||
## What Was Built
|
||||
|
||||
Plan 19-01 aggiunge una vista Kanban stile Pipedrive alla pagina `/admin/leads`, affiancata alla tabella esistente tramite un toggle Lista/Kanban.
|
||||
|
||||
### Componenti creati / modificati
|
||||
|
||||
| File | Tipo | Descrizione |
|
||||
|------|------|-------------|
|
||||
| `src/components/admin/leads/LeadsKanbanBoard.tsx` | Nuovo | Board Kanban con 6 colonne stage, drag-drop via @dnd-kit, ottimistic update |
|
||||
| `src/components/admin/leads/LeadsViewToggle.tsx` | Nuovo | Client wrapper con `useState<"list" \| "kanban">`, search integrata, pill toggle |
|
||||
| `src/app/admin/leads/LeadsSearch.tsx` | Modificato | Svuotato e ri-esporta `LeadsViewToggle as LeadsSearch` per retrocompatibilità |
|
||||
| `src/app/admin/leads/page.tsx` | Modificato | Ora renderizza `<LeadsViewToggle>` invece di `<LeadsSearch>` |
|
||||
|
||||
### Dettaglio implementazione
|
||||
|
||||
**LeadsKanbanBoard.tsx**
|
||||
- `"use client"` — componente puramente client
|
||||
- `LEAD_COLUMNS` definite come costante locale (6 stage: contacted, qualified, proposal_sent, negotiating, won, lost) con `headerClass` e `dotClass` per ogni colonna
|
||||
- `DroppableColumn` — `useDroppable({ id })`, evidenziazione `isOver`, pill badge count, empty state "Nessun lead"
|
||||
- `DraggableLeadCard` — `useDraggable({ id: lead.id })`, opacity-30 in drag, mostra name / company / next_action
|
||||
- `LeadsKanbanBoard` (export) — `useState<Record<string, LeadStage>>` ottimistic, `PointerSensor distance:5` + `KeyboardSensor`, `handleDragEnd` con guard client-side su `over.id ∈ LEAD_COLUMNS`, `startTransition(async () => { await updateLeadField(leadId, "status", newStage); router.refresh(); })`
|
||||
- Layout: `overflow-x-auto` → `grid grid-cols-6 gap-3 min-w-[1080px]`
|
||||
- `DragOverlay dropAnimation={null}` con ghost card `rotate-1 border-2 border-[#1A463C] shadow-xl`
|
||||
|
||||
**LeadsViewToggle.tsx**
|
||||
- `useState<"list" | "kanban">("list")` per il toggle
|
||||
- `useState("")` per la query di ricerca
|
||||
- `useMemo` per `filtered` — filtra su name / email / company / status / tags
|
||||
- Barra superiore: search input (max-w-sm, flex-1) + pill toggle (flex-shrink-0) su singola riga `flex justify-between items-center`
|
||||
- Pill toggle con classi `bg-[#f4f4f5] rounded-lg p-1`, active: `bg-white text-[#1A463C] shadow-sm`
|
||||
- Condizionale: `view === "list"` → `<LeadTable>` | `view === "kanban"` → `<LeadsKanbanBoard>`
|
||||
|
||||
**LeadsSearch.tsx** (ri-esportazione)
|
||||
- Ridotto a una singola riga: `export { LeadsViewToggle as LeadsSearch } from "@/components/admin/leads/LeadsViewToggle"`
|
||||
- Garantisce zero import-break su eventuali riferimenti residui
|
||||
|
||||
**page.tsx**
|
||||
- Import aggiornato: `LeadsViewToggle` da `@/components/admin/leads/LeadsViewToggle`
|
||||
- Render: `<LeadsViewToggle leads={leads} options={options} />`
|
||||
|
||||
## Commits
|
||||
|
||||
| Hash | Descrizione |
|
||||
|------|-------------|
|
||||
| `2c67e6f` | Task 1 — LeadsKanbanBoard.tsx con 6 colonne e drag-drop persistente |
|
||||
| `607c257` | Task 2 — LeadsViewToggle.tsx + re-export LeadsSearch |
|
||||
| `34be934` | Task 3 — page.tsx aggiornato + build check verde + fix overflow clipping |
|
||||
|
||||
> Nota: il commit `34be934` include anche un fix minore all'overflow del dropdown in `LeadTable` (il clipping del `overflow-x-auto` del wrapper Kanban tagliava il menu a tendina delle colonne tabella in vista Lista). Fix non bloccante, risolto nella stessa sessione.
|
||||
|
||||
## Requirements Satisfied
|
||||
|
||||
| Req | Descrizione | Verifica |
|
||||
|-----|-------------|---------|
|
||||
| PIPE-01 | Board Kanban 6 colonne con drag-drop che aggiorna `leads.status` | Drag tra colonne → `updateLeadField(id, "status", newStage)` → `router.refresh()` — persistente dopo reload |
|
||||
| PIPE-02 | Vinto/Perso come cambio-colonna manuale, nessun modale | Trascinare su colonna "won" o "lost" registra l'esito direttamente |
|
||||
|
||||
## Must-Haves Verificati
|
||||
|
||||
| Truth | Stato |
|
||||
|-------|-------|
|
||||
| Board 6 colonne (contacted → qualified → proposal_sent → negotiating → won → lost) | ✅ |
|
||||
| Drag-drop aggiorna `leads.status` in modo persistente | ✅ |
|
||||
| Colonne Vinto/Perso registrano l'esito via cambio stage | ✅ |
|
||||
| Vista Lista (LeadTable inline-edit) resta disponibile e funzionante | ✅ |
|
||||
| Toggle Lista/Kanban preserva la query di ricerca al cambio vista | ✅ |
|
||||
| Search filtra entrambe le viste (Lista e Kanban) con lo stesso `filtered` array | ✅ |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
| Item | Motivazione | Priorità |
|
||||
|------|-------------|----------|
|
||||
| Overflow clipping del dropdown `LeadTable` in vista Lista (da `overflow-x-auto` del wrapper Kanban) | Non bloccante — già parzialmente mitigato nel commit 34be934; comportamento accettabile | Bassa — da affrontare in phase futura se segnalato |
|
||||
|
||||
## Self-Check
|
||||
|
||||
**PASSED** — tutti i must_have verificati, build verde, checkpoint umano approvato ("approvato").
|
||||
|
||||
Durata esecuzione: ~1 sessione sincrona (2026-06-19).
|
||||
@@ -0,0 +1,433 @@
|
||||
# Phase 19: Pipeline CRM Kanban — Research
|
||||
|
||||
**Researched:** 2026-06-19
|
||||
**Domain:** @dnd-kit drag-drop, Next.js App Router client components, CRM leads view toggle
|
||||
**Confidence:** HIGH — all findings verified directly from codebase
|
||||
|
||||
---
|
||||
|
||||
<phase_requirements>
|
||||
## Phase Requirements
|
||||
|
||||
| ID | Description | Research Support |
|
||||
|----|-------------|------------------|
|
||||
| PIPE-01 | I lead sono visualizzabili in una board Kanban stile Pipedrive con colonne per stage e drag-drop per cambiare stage | `leads.status` enum verified (6 stages); `@dnd-kit/core` v6.3.1 already installed; exact analog in `KanbanBoard.tsx` |
|
||||
| PIPE-02 | Spostare un lead nelle colonne "Vinto"/"Perso" è il cambio-stato manuale dell'esito | `won`/`lost` are existing LEAD_STAGES values; `updateLeadField(id, "status", value)` handles this today via the table dropdown |
|
||||
</phase_requirements>
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 14 delivered a complete inline-edit table view of leads (`LeadTable.tsx`) backed by a solid data layer: `getLeadsWithTags()`, `updateLeadField()`, typed `LEAD_STAGES`, and a polymorphic tag system. Phase 19 adds a second view — Kanban — toggled from the same page, without replacing or changing any of that.
|
||||
|
||||
The project already ships a working `KanbanBoard.tsx` (for project tasks) that uses exactly the `@dnd-kit` primitives needed here. The new `LeadsKanbanBoard` is a direct structural analog: swap task status columns (`todo/in_progress/done`) for lead stage columns (`contacted/qualified/proposal_sent/negotiating/won/lost`), swap task cards for lead cards, swap `updateTaskStatus` for `updateLeadField(id, "status", newStage)`.
|
||||
|
||||
The view-toggle pattern is also ready in `PhasesViewToggle.tsx` — a client component that holds `useState<"list" | "kanban">` and renders either `listView` (a `ReactNode` passed as prop) or the kanban. The leads page only needs a `LeadsViewToggle` wrapper that receives the existing `LeadsSearch` as the `listView` slot and the new `LeadsKanbanBoard` as the kanban.
|
||||
|
||||
No schema changes. No new server actions. No new dependencies. This is a pure UI addition.
|
||||
|
||||
**Primary recommendation:** Copy the `KanbanBoard.tsx` structure exactly; adapt for 6 lead-stage columns; wire to `updateLeadField`; wrap with a `LeadsViewToggle` component in `LeadsSearch` or at the page level.
|
||||
|
||||
---
|
||||
|
||||
## Architectural Responsibility Map
|
||||
|
||||
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||||
|------------|-------------|----------------|-----------|
|
||||
| Kanban board rendering + drag state | Browser / Client | — | Drag-drop is inherently client-side; `"use client"` required |
|
||||
| Lead status persistence on drop | API / Backend (Server Action) | — | `updateLeadField` is already a `"use server"` action |
|
||||
| Lead data fetching | Frontend Server (SSR) | — | `LeadsPage` is a server component; passes data down as props |
|
||||
| View toggle state (table / kanban) | Browser / Client | — | `useState` in a client wrapper component |
|
||||
| Column definitions (stage labels, colors) | Browser / Client | — | Derived from `LEAD_STAGES` constant, purely presentational |
|
||||
|
||||
---
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core (already installed — no new installs needed)
|
||||
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| @dnd-kit/core | ^6.3.1 [VERIFIED: package.json] | DndContext, useDraggable, useDroppable, sensors | Already used in KanbanBoard.tsx |
|
||||
| @dnd-kit/sortable | ^10.0.0 [VERIFIED: package.json] | Available but NOT used by existing KanbanBoard | Not needed; existing pattern uses useDraggable + useDroppable directly |
|
||||
| @dnd-kit/utilities | ^3.2.2 [VERIFIED: package.json] | CSS.Transform helper | Imported if transform style needed |
|
||||
| React (useTransition, useState) | via Next.js 16 | Optimistic state + async server action bridging | Project pattern |
|
||||
|
||||
**Installation:** None required. All dependencies already present.
|
||||
|
||||
### Existing Primitives Used by KanbanBoard.tsx [VERIFIED: src/components/admin/kanban/KanbanBoard.tsx]
|
||||
|
||||
```typescript
|
||||
import {
|
||||
DndContext, // Root context — wraps the entire board
|
||||
DragEndEvent, // Event type for onDragEnd handler
|
||||
DragOverlay, // Ghost card rendered at cursor during drag
|
||||
PointerSensor, // Mouse/touch activation
|
||||
KeyboardSensor, // Accessibility
|
||||
useSensor,
|
||||
useSensors,
|
||||
useDroppable, // Applied to column containers
|
||||
useDraggable, // Applied to individual cards
|
||||
} from "@dnd-kit/core";
|
||||
```
|
||||
|
||||
Note: `@dnd-kit/sortable` / `SortableContext` / `useSortable` are NOT used. The existing pattern uses the lower-level `useDraggable` + `useDroppable` primitives, which is appropriate for cross-column drag (not intra-column reordering).
|
||||
|
||||
---
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### System Architecture Diagram
|
||||
|
||||
```
|
||||
LeadsPage (server component)
|
||||
├─ getLeadsWithTags() ──────────────────────────────► Postgres / leads + tags
|
||||
├─ getLeadFieldOptions() ───────────────────────────► Postgres / tags
|
||||
└─ renders LeadsViewToggle (client component)
|
||||
├─ [view="list"] → LeadsSearch → LeadTable (existing, unchanged)
|
||||
└─ [view="kanban"] → LeadsKanbanBoard (new)
|
||||
├─ DndContext (onDragEnd → updateLeadField server action)
|
||||
├─ DroppableColumn × 6 (one per LEAD_STAGES value)
|
||||
└─ DraggableLeadCard × N (one per lead)
|
||||
└─ useTransition + router.refresh() (after persist)
|
||||
```
|
||||
|
||||
### Recommended File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├─ components/admin/leads/
|
||||
│ ├─ LeadTable.tsx # EXISTING — unchanged
|
||||
│ └─ LeadsKanbanBoard.tsx # NEW — analogous to KanbanBoard.tsx
|
||||
├─ app/admin/leads/
|
||||
│ ├─ page.tsx # MODIFIED — wrap with LeadsViewToggle
|
||||
│ ├─ LeadsSearch.tsx # MODIFIED — receives view toggle or replaced by LeadsViewToggle
|
||||
│ └─ actions.ts # EXISTING — updateLeadField already handles status changes
|
||||
```
|
||||
|
||||
The view toggle can live either at the page level (simpler) or inside `LeadsSearch` (keeps search state alive across views). Recommended: extract a `LeadsViewToggle` client wrapper at the page level (same pattern as `PhasesViewToggle`), passing `<LeadsSearch leads={leads} options={options} />` as the `listView` ReactNode and `<LeadsKanbanBoard leads={leads} />` as the kanban.
|
||||
|
||||
### Pattern 1: Column definition for 6 lead stages
|
||||
|
||||
```typescript
|
||||
// Source: VERIFIED from src/lib/lead-validators.ts + src/components/admin/leads/LeadTable.tsx
|
||||
|
||||
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-[#1A463C]", 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" },
|
||||
];
|
||||
```
|
||||
|
||||
### Pattern 2: Lead Kanban Board (adapted from KanbanBoard.tsx)
|
||||
|
||||
```typescript
|
||||
// Source: VERIFIED structure from src/components/admin/kanban/KanbanBoard.tsx
|
||||
// Key adaptation: replace taskStatuses/updateTaskStatus with leadStatuses/updateLeadField
|
||||
|
||||
"use client";
|
||||
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";
|
||||
|
||||
export function LeadsKanbanBoard({ leads }: { leads: LeadWithTags[] }) {
|
||||
const router = useRouter();
|
||||
const [, startTransition] = useTransition();
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [leadStatuses, setLeadStatuses] = useState<Record<string, LeadStage>>(
|
||||
() => Object.fromEntries(leads.map((l) => [l.id, l.status as LeadStage]))
|
||||
);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||
useSensor(KeyboardSensor)
|
||||
);
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
setActiveId(null);
|
||||
if (!over) return;
|
||||
const leadId = active.id as string;
|
||||
const newStage = over.id as LeadStage;
|
||||
if (newStage === leadStatuses[leadId]) return;
|
||||
// Optimistic update
|
||||
setLeadStatuses((prev) => ({ ...prev, [leadId]: newStage }));
|
||||
// Persist
|
||||
startTransition(async () => {
|
||||
await updateLeadField(leadId, "status", newStage);
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
// ... render DndContext with LEAD_COLUMNS mapped to DroppableColumn
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: View toggle (adapted from PhasesViewToggle.tsx)
|
||||
|
||||
```typescript
|
||||
// Source: VERIFIED from src/components/admin/kanban/PhasesViewToggle.tsx
|
||||
"use client";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { LeadsKanbanBoard } from "@/components/admin/leads/LeadsKanbanBoard";
|
||||
import type { LeadWithTags, LeadFieldOptions } from "@/lib/admin-queries";
|
||||
|
||||
export function LeadsViewToggle({
|
||||
listView,
|
||||
leads,
|
||||
}: {
|
||||
listView: ReactNode;
|
||||
leads: LeadWithTags[];
|
||||
}) {
|
||||
const [view, setView] = useState<"list" | "kanban">("list");
|
||||
return (
|
||||
<div>
|
||||
{/* Toggle buttons — same pill pattern as PhasesViewToggle */}
|
||||
{view === "list" ? listView : <LeadsKanbanBoard leads={leads} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Lead card content
|
||||
|
||||
Each Kanban card should show: `name` (primary), `company` (secondary/optional), `next_action` (hint text, optional). Avoid showing `email`/`phone`/`tags` on the card to keep it compact — these are available in the table view.
|
||||
|
||||
```typescript
|
||||
// Fields available on LeadWithTags (VERIFIED: src/lib/admin-queries.ts line 890)
|
||||
// Lead & { tags: string[] }
|
||||
// Relevant for card: name, company, next_action, status
|
||||
```
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
|
||||
- **Using `useSortable` / `SortableContext`:** The existing project pattern does NOT use these. They are for intra-column reordering. Use `useDraggable` + `useDroppable` to match the established `KanbanBoard.tsx` pattern.
|
||||
- **Calling `router.refresh()` before `await updateLeadField`:** Always await the server action first, then refresh. The existing KanbanBoard does this correctly inside `startTransition`.
|
||||
- **Dropping `react-hook-form` / Zod on drag-drop:** No form validation needed for a status change — `updateLeadField` already validates via `LEAD_STAGES.includes(value)` check.
|
||||
- **Removing `LeadsSearch` / `LeadTable`:** PIPE-01 requires the table to remain as an alternative view. Do not replace it.
|
||||
|
||||
---
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| Drag detection (distance threshold) | Custom mouse event tracking | `PointerSensor` with `activationConstraint: { distance: 5 }` | Already proven in KanbanBoard.tsx; prevents accidental drag on click |
|
||||
| Keyboard accessibility for drag | Custom key handlers | `KeyboardSensor` from @dnd-kit/core | a11y for free |
|
||||
| Drag ghost/overlay | CSS clone positioning | `DragOverlay` from @dnd-kit/core | Correct portal rendering, no z-index fights |
|
||||
| Optimistic UI update | Complex local state with rollback | `useState` + `useTransition` (React pattern) | Already used in KanbanBoard.tsx and LeadTable.tsx |
|
||||
| Status validation | Re-implementing LEAD_STAGES check | `updateLeadField` server action already validates status | DRY — the action throws on invalid stage |
|
||||
|
||||
**Key insight:** The entire drag-drop + persist pattern is already implemented and tested in `KanbanBoard.tsx`. This phase is a structural copy with domain adaptation, not a new implementation.
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Columns wider than viewport on 6-stage board
|
||||
**What goes wrong:** 6 columns in `grid-cols-6` become too narrow on typical laptop screens (1280–1440px). The 3-column project kanban uses `grid-cols-3` with comfortable card width.
|
||||
**Why it happens:** 6 × min-width ≈ 720px+ is tight.
|
||||
**How to avoid:** Use `grid-cols-3 lg:grid-cols-6` or a horizontally scrollable container (`overflow-x-auto` on the grid wrapper). Alternatively, `min-w-[200px]` per column inside a scroll container.
|
||||
**Warning signs:** Cards truncate before the lead name is visible.
|
||||
|
||||
### Pitfall 2: Won/Lost columns need visual distinction
|
||||
**What goes wrong:** Dropping to "won" or "lost" looks identical to other columns — user may not notice the semantic weight of these terminal states.
|
||||
**Why it happens:** Uniform column styling.
|
||||
**How to avoid:** Use visually distinct `headerClass` (green for won, red for lost) and consider a stronger `isOver` highlight for these columns. The STAGE_COLOR map in `LeadTable.tsx` already defines these colors — reuse them.
|
||||
|
||||
### Pitfall 3: Leads not sorted consistently between views
|
||||
**What goes wrong:** Table shows leads ordered by `updated_at DESC`; kanban derived from the same array shows different visual order depending on column grouping.
|
||||
**Why it happens:** No explicit sort on the kanban card order within a column.
|
||||
**How to avoid:** Sort leads within each column by `updated_at DESC` (same as the existing query order). The `getLeadsWithTags` query already returns `orderBy(desc(leads.updated_at))` so inheriting that order is sufficient.
|
||||
|
||||
### Pitfall 4: `router.refresh()` causes full re-mount of kanban
|
||||
**What goes wrong:** After a drag-drop, `router.refresh()` rehydrates the server component, re-running `getLeadsWithTags()`. If the drag animation hasn't completed, it can cause a visual flicker.
|
||||
**Why it happens:** Next.js App Router refresh re-renders the whole tree.
|
||||
**How to avoid:** The existing `KanbanBoard.tsx` uses the same pattern without issue. The `setActiveId(null)` call in `handleDragEnd` clears the overlay before the refresh arrives, so the flicker is acceptable. This is the project's established pattern — do not deviate.
|
||||
|
||||
### Pitfall 5: Search/filter not available in kanban view
|
||||
**What goes wrong:** The search bar lives in `LeadsSearch.tsx` and only filters `LeadTable`. If the user switches to kanban, they lose the ability to filter.
|
||||
**Why it happens:** The view toggle renders either `LeadsSearch` (with its internal state) or the bare `LeadsKanbanBoard`.
|
||||
**How to avoid:** Two acceptable approaches: (a) wrap both views together inside `LeadsSearch` and pass filtered leads to both (preferred — search state persists across view switches); or (b) accept that kanban shows all leads unfiltered (simpler, acceptable for now given the single-user context). Document the choice in the plan.
|
||||
|
||||
---
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Existing updateLeadField signature (server action)
|
||||
|
||||
```typescript
|
||||
// Source: VERIFIED from src/app/admin/leads/actions.ts line 174
|
||||
// EDITABLE_FIELDS includes "status" — drag-drop can call this directly
|
||||
export async function updateLeadField(
|
||||
leadId: string,
|
||||
fieldName: "name" | "email" | "phone" | "company" | "status" | "next_action",
|
||||
value: string
|
||||
): Promise<void>
|
||||
// Validates: status must be in LEAD_STAGES; throws on invalid value
|
||||
// Side effects: revalidatePath("/admin/leads") + revalidatePath(`/admin/leads/${leadId}`)
|
||||
```
|
||||
|
||||
### LEAD_STAGES canonical values
|
||||
|
||||
```typescript
|
||||
// Source: VERIFIED from src/lib/lead-validators.ts line 4
|
||||
export const LEAD_STAGES = [
|
||||
"contacted",
|
||||
"qualified",
|
||||
"proposal_sent",
|
||||
"negotiating",
|
||||
"won",
|
||||
"lost",
|
||||
] as const;
|
||||
```
|
||||
|
||||
### Existing STAGE_COLOR map (reuse for kanban column headers)
|
||||
|
||||
```typescript
|
||||
// Source: VERIFIED from src/components/admin/leads/LeadTable.tsx line 20
|
||||
const STAGE_COLOR: Record<string, string> = {
|
||||
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",
|
||||
};
|
||||
// Move to a shared constant (e.g., src/lib/lead-constants.ts) if reused in both components
|
||||
```
|
||||
|
||||
### PhasesViewToggle pattern (exact analog)
|
||||
|
||||
```typescript
|
||||
// Source: VERIFIED from src/components/admin/kanban/PhasesViewToggle.tsx
|
||||
// State: useState<"list" | "kanban">("list")
|
||||
// Toggle: pill button group (bg-[#f4f4f5] rounded-lg p-1 w-fit)
|
||||
// Active: bg-white text-[#1A463C] shadow-sm
|
||||
// Inactive: text-[#71717a] hover:text-[#1a1a1a]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| Lead status change via modal form | Inline dropdown in table cell (`StatusCell`) | Phase 14 | Drag-drop is the third mechanism; all write to same `updateLeadField` action |
|
||||
| Separate `/admin/analytics` route | Fused into `/admin` dashboard | Phase 18 | No impact on leads page |
|
||||
| `SendQuoteModal` with dead branch | Dead branch removed | Phase 18 | No impact |
|
||||
|
||||
---
|
||||
|
||||
## Project Constraints (from CLAUDE.md)
|
||||
|
||||
| Directive | Impact on This Phase |
|
||||
|-----------|---------------------|
|
||||
| `clients.token` = rotatable, never PK | Not relevant (leads have no token) |
|
||||
| `quote_items` never exposed via client API | Not relevant (Kanban is admin-only) |
|
||||
| `deliverables.approved_at` immutable once set | Not relevant |
|
||||
| Auth: `/admin/*` → Auth.js session | Kanban lives at `/admin/leads` — already protected |
|
||||
| No file hosting v1 | Not relevant |
|
||||
| Migration safety: never drop/truncate rows | Phase 19 is UI-only — no schema changes, no migration needed |
|
||||
| Security: confirm before destructive commands | No destructive operations |
|
||||
| No package installs without showing name+version | No new packages needed |
|
||||
|
||||
---
|
||||
|
||||
## Environment Availability
|
||||
|
||||
Step 2.6: SKIPPED — Phase 19 is a pure UI addition. All required libraries (`@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`) are already installed. No external services, databases (beyond the existing Neon Postgres connection), or CLI tools are needed.
|
||||
|
||||
---
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
`nyquist_validation: false` in `.planning/config.json` — section omitted per config.
|
||||
|
||||
---
|
||||
|
||||
## Security Domain
|
||||
|
||||
Phase 19 adds a new interaction path to an existing admin-only route (`/admin/leads`). No new auth surface is introduced.
|
||||
|
||||
| ASVS Category | Applies | Standard Control |
|
||||
|---------------|---------|-----------------|
|
||||
| V2 Authentication | yes (existing) | Auth.js session via `requireAdmin()` in server action |
|
||||
| V4 Access Control | yes (existing) | `requireAdmin()` guard in `updateLeadField` — drag-drop calls same action |
|
||||
| V5 Input Validation | yes | `updateLeadField` validates status via `LEAD_STAGES.includes(value)` — no new validation needed |
|
||||
|
||||
No new threat surface beyond what Phase 14 already addressed. The drag-drop `handleDragEnd` validates the `over.id` is a known stage before calling the server action — follow the same guard pattern as in `KanbanBoard.tsx` (line 190: `if (!(["todo", "in_progress", "done"] as string[]).includes(newStatus)) return;`).
|
||||
|
||||
---
|
||||
|
||||
## Assumptions Log
|
||||
|
||||
| # | Claim | Section | Risk if Wrong |
|
||||
|---|-------|---------|---------------|
|
||||
| A1 | The `won` and `lost` columns are terminal states with no special side-effects beyond setting `leads.status` (no auto-creation of client/project, no email trigger) | Architecture Patterns | If the plan later requires auto-provisioning on "won", a new server action will be needed — but PIPE-01/02 say nothing about this, and PROP-04 (auto-provisioning) is deferred to backlog post-R5 |
|
||||
| A2 | Card content (name, company, next_action) is sufficient for the kanban view; no additional fields are needed per card | Code Examples | If the user wants tags or email visible on cards, the `LeadWithTags` type already provides them — no data-layer change, only card template change |
|
||||
| A3 | The search filter covering only the table view (not the kanban) is acceptable for v1 of this feature | Common Pitfalls | If the user wants search in kanban too, the fix is to lift filtered state into the toggle wrapper — straightforward but adds scope |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Search/filter scope in kanban view**
|
||||
- What we know: `LeadsSearch` holds the search `useState` and passes `filtered` leads to `LeadTable`. The kanban would receive all leads from the page.
|
||||
- What's unclear: Does the user want the search bar to filter the kanban board too, or is it acceptable that kanban shows all leads?
|
||||
- Recommendation: Default to wrapping both views inside a new `LeadsViewToggle` that receives `leads` (unfiltered) and `options`, manages the view toggle, and passes `filtered` leads to both `LeadTable` and `LeadsKanbanBoard`. This is a clean pattern and handles it gracefully.
|
||||
|
||||
2. **Column layout: scroll vs. wrap on 6 columns**
|
||||
- What we know: The existing kanban uses `grid-cols-3`. Six columns need more space.
|
||||
- What's unclear: Target viewport is unknown (likely 1440px+ since this is a single-admin tool).
|
||||
- Recommendation: Use `min-w-[180px]` per column inside an `overflow-x-auto` wrapper. This makes it work on any viewport without content truncation.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- `src/components/admin/kanban/KanbanBoard.tsx` — exact @dnd-kit usage pattern, drag primitives, sensors, DragOverlay, optimistic update + router.refresh()
|
||||
- `src/components/admin/kanban/PhasesViewToggle.tsx` — view toggle pattern (list/kanban state, pill button UI)
|
||||
- `src/components/admin/leads/LeadTable.tsx` — STAGE_COLOR map, LeadWithTags usage, StatusCell inline dropdown
|
||||
- `src/app/admin/leads/actions.ts` — `updateLeadField` signature, EDITABLE_FIELDS, `requireAdmin()` guard
|
||||
- `src/lib/lead-validators.ts` — canonical LEAD_STAGES array (6 values)
|
||||
- `src/lib/admin-queries.ts` lines 883–942 — `LeadWithTags` type, `getLeadsWithTags()` query (all fields), `LeadFieldOptions`
|
||||
- `src/db/schema.ts` lines 441–462 — `leads` table definition, `status` column with all 6 stage values documented
|
||||
- `src/app/admin/leads/LeadsSearch.tsx` — search filter pattern, `LeadWithTags` + `LeadFieldOptions` prop interface
|
||||
- `src/app/admin/leads/page.tsx` — server component structure, data fetching pattern, `revalidate = 0`
|
||||
- `src/components/admin/AdminSidebar.tsx` — `/admin/leads` is already in NAV_ITEMS, no sidebar change needed
|
||||
- `package.json` — @dnd-kit/core ^6.3.1, @dnd-kit/sortable ^10.0.0, @dnd-kit/utilities ^3.2.2
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- `.planning/config.json` — `nyquist_validation: false` confirmed
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH — all packages verified in package.json; exact primitives verified in KanbanBoard.tsx
|
||||
- Architecture: HIGH — all patterns verified from existing codebase analogs
|
||||
- Pitfalls: HIGH — derived from direct code inspection and known Next.js App Router behaviors
|
||||
- Data layer: HIGH — schema, actions, and query functions all read directly
|
||||
|
||||
**Research date:** 2026-06-19
|
||||
**Valid until:** Stable indefinitely (no external dependencies; codebase-derived findings)
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
phase: 19-pipeline-crm-kanban
|
||||
verified: 2026-06-19T18:09:00+02:00
|
||||
status: passed
|
||||
score: 5/5
|
||||
overrides_applied: 0
|
||||
re_verification: false
|
||||
---
|
||||
|
||||
# Phase 19: Pipeline CRM Kanban — Verification Report
|
||||
|
||||
**Phase Goal:** Aggiungere vista Kanban drag-drop alla pagina /admin/leads (PIPE-01 + PIPE-02), affiancata alla LeadTable esistente via toggle Lista/Kanban.
|
||||
**Verified:** 2026-06-19T18:09:00+02:00
|
||||
**Status:** passed
|
||||
**Re-verification:** No — initial verification
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Observable Truths
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 1 | I lead sono visibili in una board Kanban con 6 colonne per stage (contacted, qualified, proposal_sent, negotiating, won, lost) | VERIFIED | `LeadsKanbanBoard.tsx` lines 19-33: `LeadStage` type and `LEAD_COLUMNS` array define all 6 stages with labels Contattato/Qualificato/Offerta inviata/Trattativa/Vinto/Perso. `grid-cols-6` layout confirmed line 159. |
|
||||
| 2 | Trascinare un lead da una colonna a un'altra aggiorna leads.status in modo persistente | VERIFIED | `handleDragEnd` (lines 132-150): optimistic `setLeadStatuses`, then `startTransition(async () => { await updateLeadField(leadId, "status", newStage); router.refresh(); })`. `updateLeadField` confirmed in `actions.ts` line 174 with `revalidatePath` side-effect. |
|
||||
| 3 | Spostare un lead nella colonna Vinto o Perso registra l'esito cambiando il campo status | VERIFIED | `won` and `lost` are members of `LEAD_COLUMNS` (lines 31-32) and `VALID_STAGES` (line 35). `handleDragEnd` line 141: `if (!VALID_STAGES.includes(newStage)) return` — only valid stages accepted. Dropping on "Vinto"/"Perso" column calls `updateLeadField(leadId, "status", "won"/"lost")`. Server-side `actions.ts` line 190 validates against `LEAD_STAGES` before writing to DB. |
|
||||
| 4 | La vista tabella inline-edit (LeadTable) resta disponibile e funzionante come vista alternativa | VERIFIED | `LeadsViewToggle.tsx` lines 70-71: `view === "list" ? <LeadTable leads={filtered} options={options} /> : <LeadsKanbanBoard leads={filtered} />`. `LeadTable` imported from existing component (line 6). `LeadsSearch.tsx` re-exports `LeadsViewToggle as LeadsSearch` — no existing import paths broken. |
|
||||
| 5 | Il toggle Lista/Kanban è visibile sopra il contenuto e preserva lo stato della ricerca quando si cambia vista | VERIFIED | `LeadsViewToggle.tsx`: single `useState("")` for query (line 18) and single `useMemo` for `filtered` (lines 20-31) shared by both views. Toggling `view` state does not reset `query` — both `<LeadTable>` and `<LeadsKanbanBoard>` receive the same `filtered` array. Pill toggle rendered above content in `flex justify-between` bar (lines 35-67). |
|
||||
|
||||
**Score:** 5/5 truths verified
|
||||
|
||||
### Required Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `src/components/admin/leads/LeadsKanbanBoard.tsx` | Board Kanban con DndContext, 6 DroppableColumn, DraggableLeadCard, DragOverlay, ottimistic update | VERIFIED | 184 lines. `"use client"`, `DndContext`, `DragOverlay`, `useDroppable`, `useDraggable`, `useTransition`, `useState<Record<string, LeadStage>>`, all 6 LEAD_COLUMNS, `overflow-x-auto`, `grid-cols-6`, `startTransition`. Exports `LeadsKanbanBoard`. |
|
||||
| `src/components/admin/leads/LeadsViewToggle.tsx` | Client wrapper con useState<'list' \| 'kanban'> e pill toggle | VERIFIED | 77 lines. `"use client"`, `useState<"list" \| "kanban">("list")`, `useState("")`, `useMemo`, pill toggle with `bg-[#f4f4f5] rounded-lg p-1`. Exports `LeadsViewToggle`. |
|
||||
| `src/app/admin/leads/LeadsSearch.tsx` | Search + toggle integrati; passa filtered leads sia a LeadTable che a LeadsKanbanBoard | VERIFIED | Re-exports `LeadsViewToggle as LeadsSearch` — search and filtering now live in `LeadsViewToggle`. Backward compatibility preserved. |
|
||||
| `src/app/admin/leads/page.tsx` | Server component aggiornato che renderizza LeadsViewToggle | VERIFIED | 24 lines. Imports `LeadsViewToggle` from `@/components/admin/leads/LeadsViewToggle`. Renders `<LeadsViewToggle leads={leads} options={options} />`. No `LeadsSearch` import. |
|
||||
|
||||
### Key Link Verification
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|----|-----|--------|---------|
|
||||
| `LeadsKanbanBoard.tsx` — `handleDragEnd` | `actions.ts` — `updateLeadField` | `startTransition(async () => { await updateLeadField(leadId, "status", newStage); router.refresh(); })` | WIRED | Line 147: `await updateLeadField(leadId, "status", newStage)` — exact match. `startTransition` at line 146. `router.refresh()` at line 148. |
|
||||
| `LeadsViewToggle.tsx` — `filtered` | `LeadsKanbanBoard` — `leads` prop | `filtered` array passed as prop | WIRED | Line 73: `<LeadsKanbanBoard leads={filtered} />` — exact match. |
|
||||
| `LeadsViewToggle` / view state | `LeadTable` + `LeadsKanbanBoard` | `useState<"list" \| "kanban">` | WIRED | Line 17: `useState<"list" \| "kanban">("list")`. Lines 70-73: conditional render branches both views from shared state. |
|
||||
|
||||
### Data-Flow Trace (Level 4)
|
||||
|
||||
| Artifact | Data Variable | Source | Produces Real Data | Status |
|
||||
|----------|---------------|--------|--------------------|--------|
|
||||
| `LeadsKanbanBoard.tsx` | `leads: LeadWithTags[]` | `getLeadsWithTags()` in `page.tsx` | Yes — Drizzle ORM `db.select().from(leads).leftJoin(tags, ...)` (admin-queries.ts lines 893-917). Returns aggregated rows with tags array. | FLOWING |
|
||||
| `LeadsViewToggle.tsx` | `filtered` (derived from `leads` prop) | Same `getLeadsWithTags()` → passed as prop from page.tsx | Yes — `useMemo` derives from server-fetched `leads` prop. | FLOWING |
|
||||
|
||||
### Behavioral Spot-Checks
|
||||
|
||||
Runnable entry points require a live dev server. Spot-checks performed via static analysis against the call chain instead.
|
||||
|
||||
| Behavior | Check | Result | Status |
|
||||
|----------|-------|--------|--------|
|
||||
| `handleDragEnd` persists status change | `updateLeadField(leadId, "status", newStage)` call confirmed + `requireAdmin()` guard in actions.ts | Call at line 147; auth guard at actions.ts line 179 | PASS |
|
||||
| Won/Lost drag records outcome | `VALID_STAGES.includes(newStage)` guard + `won`/`lost` in LEAD_COLUMNS | Line 141 guard; lines 31-32 column defs | PASS |
|
||||
| Search state preserved across view switch | Single `query` state, single `filtered` memo, shared by both render branches | Lines 18, 20-31, 70-73 in LeadsViewToggle.tsx | PASS |
|
||||
| Commits exist in git | `git log 2c67e6f 607c257 34be934` | All 3 hashes present with correct descriptions | PASS |
|
||||
|
||||
### Requirements Coverage
|
||||
|
||||
| Requirement | Source Plan | Description | Status | Evidence |
|
||||
|-------------|-------------|-------------|--------|---------|
|
||||
| PIPE-01 | 19-01-PLAN.md | I lead sono visualizzabili in una board Kanban stile Pipedrive con colonne per stage e drag-drop per cambiare stage | SATISFIED | `LeadsKanbanBoard.tsx`: 6 `DroppableColumn` components, `DndContext` with `onDragEnd`, optimistic state update, persistent write via `updateLeadField`. |
|
||||
| PIPE-02 | 19-01-PLAN.md | Spostare un lead nelle colonne "Vinto"/"Perso" è il cambio-stato manuale dell'esito | SATISFIED | `won` and `lost` are standard columns in `LEAD_COLUMNS`. Dropping a card on either column triggers the same `updateLeadField` path — no modal, no separate confirmation. |
|
||||
|
||||
### Anti-Patterns Found
|
||||
|
||||
| File | Line | Pattern | Severity | Impact |
|
||||
|------|------|---------|----------|--------|
|
||||
| `LeadsKanbanBoard.tsx` | 110 | `const [, startTransition] = useTransition()` — unused first element of destructure | Info | No functional impact; minor lint noise. |
|
||||
|
||||
No TODOs, FIXMEs, placeholder returns, or empty implementations found in any of the four modified files.
|
||||
|
||||
### Human Verification Required
|
||||
|
||||
A human checkpoint was completed and approved prior to this verification (the task plan included a blocking `checkpoint:human-verify` gate). The developer confirmed:
|
||||
|
||||
- Board renders 6 columns with leads in correct stages
|
||||
- Drag-drop moves cards and persists after reload
|
||||
- Won/Lost columns register outcomes correctly
|
||||
- Lista/Kanban toggle works with search state preserved
|
||||
|
||||
No further human verification items are outstanding.
|
||||
|
||||
### Gaps Summary
|
||||
|
||||
No gaps. All 5 must-have truths are VERIFIED against the codebase. All 4 artifacts are substantive and wired. All 3 key links are confirmed. Data flows from a real Drizzle ORM query. Both PIPE-01 and PIPE-02 are satisfied. Build passes (0 TypeScript errors, confirmed in commit 34be934 and known context). Human checkpoint approved in-session.
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-06-19T18:09:00+02:00_
|
||||
_Verifier: Claude (gsd-verifier)_
|
||||
Reference in New Issue
Block a user