diff --git a/src/components/admin/leads/LeadsKanbanBoard.tsx b/src/components/admin/leads/LeadsKanbanBoard.tsx new file mode 100644 index 0000000..e5d17ca --- /dev/null +++ b/src/components/admin/leads/LeadsKanbanBoard.tsx @@ -0,0 +1,183 @@ +"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"; + +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" }, +]; + +const VALID_STAGES = LEAD_COLUMNS.map((c) => c.id) as string[]; + +function DroppableColumn({ + id, label, headerClass, dotClass, leads, activeId, +}: { + id: LeadStage; + label: string; + headerClass: string; + dotClass: string; + leads: LeadWithTags[]; + activeId: string | null; +}) { + const { setNodeRef, isOver } = useDroppable({ id }); + + return ( +
+
+
+ + {label} +
+ + {leads.length} + +
+ +
+ {leads.map((lead) => ( + + ))} + {leads.length === 0 && ( +

+ Nessun lead +

+ )} +
+
+ ); +} + +function DraggableLeadCard({ lead, isActive }: { lead: LeadWithTags; isActive: boolean }) { + const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: lead.id }); + + const style = transform + ? { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)` } + : undefined; + + return ( +
+

{lead.name}

+ {lead.company && ( +

{lead.company}

+ )} + {lead.next_action && ( +

{lead.next_action}

+ )} +
+ ); +} + +export function LeadsKanbanBoard({ leads }: { leads: LeadWithTags[] }) { + const router = useRouter(); + const [, startTransition] = useTransition(); + const [activeId, setActiveId] = useState(null); + + const [leadStatuses, setLeadStatuses] = useState>(() => + Object.fromEntries(leads.map((l) => [l.id, l.status as LeadStage])) + ); + + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), + useSensor(KeyboardSensor) + ); + + const leadsByStage = LEAD_COLUMNS.reduce( + (acc, col) => { + acc[col.id] = leads.filter((l) => (leadStatuses[l.id] ?? l.status) === col.id); + return acc; + }, + {} as Record + ); + + const activeLead = activeId ? leads.find((l) => l.id === activeId) : null; + + 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; + const currentStage = leadStatuses[leadId]; + + if (!VALID_STAGES.includes(newStage)) return; + if (newStage === currentStage) return; + + setLeadStatuses((prev) => ({ ...prev, [leadId]: newStage })); + + startTransition(async () => { + await updateLeadField(leadId, "status", newStage); + router.refresh(); + }); + } + + return ( + setActiveId(e.active.id as string)} + onDragEnd={handleDragEnd} + > +
+
+ {LEAD_COLUMNS.map((col) => ( + + ))} +
+
+ + + {activeLead && ( +
+

{activeLead.name}

+ {activeLead.company && ( +

{activeLead.company}

+ )} +
+ )} +
+
+ ); +}