"use client"; import { useMemo, useState } from "react"; import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, useSensor, useSensors, useDroppable, useDraggable, closestCorners, type DragEndEvent, type DragStartEvent, } from "@dnd-kit/core"; import type { Deal, PipelineStage } from "@/types"; import { DealCard } from "./deal-card"; import { Button } from "@/components/ui/button"; import { Plus } from "lucide-react"; import { useAuth } from "@/hooks/use-auth"; import { formatCurrency } from "@/lib/currency"; interface PipelineBoardProps { stages: PipelineStage[]; deals: Deal[]; onDealMoved: (dealId: string, newStageId: string) => void; onAddDeal: (stageId: string) => void; onEditDeal: (deal: Deal) => void; } export function PipelineBoard({ stages, deals, onDealMoved, onAddDeal, onEditDeal, }: PipelineBoardProps) { const { defaultCurrency } = useAuth(); const [activeDealId, setActiveDealId] = useState(null); const sortedStages = useMemo( () => [...stages].sort((a, b) => a.position - b.position), [stages], ); const dealsByStage = useMemo(() => { const map = new Map(); for (const stage of sortedStages) map.set(stage.id, []); for (const deal of deals) { const bucket = map.get(deal.stage_id); if (bucket) bucket.push(deal); } return map; }, [sortedStages, deals]); const sensors = useSensors( // 5px activation distance avoids clicks being interpreted as drags. useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), // Keyboard drag support: focus a card, Space to pick up, arrows to move, // Space to drop, Escape to cancel. useSensor(KeyboardSensor), ); const activeDeal = activeDealId ? deals.find((d) => d.id === activeDealId) ?? null : null; function handleDragStart(event: DragStartEvent) { setActiveDealId(String(event.active.id)); } function handleDragEnd(event: DragEndEvent) { setActiveDealId(null); const { active, over } = event; if (!over) return; const dealId = String(active.id); const targetStageId = String(over.id); const deal = deals.find((d) => d.id === dealId); if (!deal || deal.stage_id === targetStageId) return; if (!sortedStages.some((s) => s.id === targetStageId)) return; onDealMoved(dealId, targetStageId); } function handleDragCancel() { setActiveDealId(null); } return ( {/* snap-x + snap-mandatory on mobile so swipes land the next stage cleanly at the viewport edge instead of mid-column. Disabled on lg+ where snapping would interfere with the natural layout. The board can still overflow horizontally on lg+ once a pipeline has many stages (columns keep a 260px min-width), so a thin scrollbar stays visible on desktop. */}
{sortedStages.map((stage) => { const stageDeals = dealsByStage.get(stage.id) ?? []; const totalValue = stageDeals.reduce( (s, d) => s + Number(d.value || 0), 0, ); return ( ); })}
{activeDeal ? (
s.id === activeDeal.stage_id) ?? null } onEdit={() => {}} isOverlay />
) : null}
); } function StageColumn({ stage, deals, totalValue, currency, onAddDeal, onEditDeal, }: { stage: PipelineStage; deals: Deal[]; totalValue: number; currency: string; onAddDeal: (stageId: string) => void; onEditDeal: (deal: Deal) => void; }) { const { setNodeRef, isOver } = useDroppable({ id: stage.id }); return ( // On mobile each column is `w-[85vw]` (with a reasonable min/max) // so the next column's edge peeks in — a "there's more here" hint. // snap-start lands each column cleanly when swiping. On lg+ we // restore the flex-1 share-the-row behavior. The droppable ref is // on the inner messages region below — intentionally NOT here, so // a drag over the column header doesn't highlight the whole column.
{/* 3px colored top border — sits above the column's padding */}

{stage.name}

{deals.length}

{formatCurrency(totalValue, currency)}

{deals.length === 0 ? (
Drop a deal here
) : ( deals.map((deal) => ( )) )}
); } function DraggableDealCard({ deal, stage, onEdit, }: { deal: Deal; stage: PipelineStage; onEdit: (deal: Deal) => void; }) { const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ id: deal.id, }); return (
); }