import { useState, useMemo } from 'react'; import { Plus, RefreshCw, Search, Filter, List, } from 'lucide-react'; import { Button } from '@components/atoms/Button'; import { Breadcrumbs } from '@components/organisms/Breadcrumbs'; import { DateRangePicker, type DateRange } from '@components/organisms/DatePicker'; import { Select, type SelectOption } from '@components/organisms/Select'; import { useToast } from '@components/organisms/Toast'; import { KanbanColumn, type StageKey } from '@components/crm/KanbanColumn'; import type { KanbanOpportunity } from '@components/crm/KanbanCard'; import { formatNumber } from '@utils/formatters'; import { Link } from 'react-router-dom'; // Stage labels for display const stageLabels: Record = { new: 'Nuevo', qualified: 'Calificado', proposition: 'Propuesta', won: 'Ganado', lost: 'Perdido', }; // Mock users for filter const mockUsers: SelectOption[] = [ { value: 'user-1', label: 'Juan Garcia' }, { value: 'user-2', label: 'Maria Lopez' }, { value: 'user-3', label: 'Carlos Rodriguez' }, { value: 'user-4', label: 'Ana Martinez' }, ]; // Mock data for opportunities const generateMockOpportunities = (): Record => ({ new: [ { id: '1', name: 'Proyecto ERP para Constructora ABC', expectedRevenue: 150000, expectedCloseDate: '2026-02-15', assignedTo: { id: 'user-1', name: 'Juan Garcia' }, probability: 20, partnerName: 'Constructora ABC S.A.', }, { id: '2', name: 'Sistema de Inventarios - Retail Plus', expectedRevenue: 75000, expectedCloseDate: '2026-02-28', assignedTo: { id: 'user-2', name: 'Maria Lopez' }, probability: 15, partnerName: 'Retail Plus', }, { id: '3', name: 'Integracion CRM - TechCorp', expectedRevenue: 45000, probability: 10, partnerName: 'TechCorp Internacional', }, ], qualified: [ { id: '4', name: 'Modulo de Facturacion Electronica', expectedRevenue: 85000, expectedCloseDate: '2026-02-10', assignedTo: { id: 'user-1', name: 'Juan Garcia' }, probability: 40, partnerName: 'Distribuidora Nacional', }, { id: '5', name: 'Dashboard Ejecutivo - Financiera', expectedRevenue: 120000, expectedCloseDate: '2026-03-01', assignedTo: { id: 'user-3', name: 'Carlos Rodriguez' }, probability: 50, partnerName: 'Financiera del Norte', }, ], proposition: [ { id: '6', name: 'Sistema de Gestion de Proyectos', expectedRevenue: 200000, expectedCloseDate: '2026-01-30', assignedTo: { id: 'user-2', name: 'Maria Lopez' }, probability: 65, partnerName: 'Consultoria Global', }, { id: '7', name: 'Automatizacion de Procesos - Manufactura', expectedRevenue: 180000, expectedCloseDate: '2026-02-05', assignedTo: { id: 'user-4', name: 'Ana Martinez' }, probability: 70, partnerName: 'Manufactura Industrial', }, { id: '8', name: 'Portal de Clientes Web', expectedRevenue: 95000, expectedCloseDate: '2026-02-20', assignedTo: { id: 'user-1', name: 'Juan Garcia' }, probability: 60, partnerName: 'Servicios Express', }, ], won: [ { id: '9', name: 'Implementacion ERP Completo', expectedRevenue: 350000, expectedCloseDate: '2026-01-15', assignedTo: { id: 'user-3', name: 'Carlos Rodriguez' }, probability: 100, partnerName: 'Grupo Industrial MX', }, { id: '10', name: 'Modulo de Recursos Humanos', expectedRevenue: 65000, expectedCloseDate: '2026-01-10', assignedTo: { id: 'user-2', name: 'Maria Lopez' }, probability: 100, partnerName: 'Corporativo Sur', }, ], lost: [ { id: '11', name: 'Migracion de Sistema Legacy', expectedRevenue: 125000, expectedCloseDate: '2026-01-05', assignedTo: { id: 'user-4', name: 'Ana Martinez' }, probability: 0, partnerName: 'Empresa Tradicional', }, ], }); const formatCurrency = (value: number): string => { return formatNumber(value, 'es-MX', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); }; export function PipelineKanbanPage() { const { success } = useToast(); // State for filters const [searchTerm, setSearchTerm] = useState(''); const [dateRange, setDateRange] = useState({ start: null, end: null }); const [selectedUser, setSelectedUser] = useState(''); const [showFilters, setShowFilters] = useState(false); // State for opportunities by stage const [opportunitiesByStage, setOpportunitiesByStage] = useState>( generateMockOpportunities ); // Filter opportunities based on search and filters const filteredOpportunities = useMemo(() => { const result: Record = { new: [], qualified: [], proposition: [], won: [], lost: [], }; (Object.keys(opportunitiesByStage) as StageKey[]).forEach((stage) => { result[stage] = opportunitiesByStage[stage].filter((opp) => { // Search filter if (searchTerm) { const searchLower = searchTerm.toLowerCase(); if ( !opp.name.toLowerCase().includes(searchLower) && !opp.partnerName?.toLowerCase().includes(searchLower) ) { return false; } } // User filter if (selectedUser && opp.assignedTo?.id !== selectedUser) { return false; } // Date range filter if (dateRange.start && dateRange.end && opp.expectedCloseDate) { const closeDate = new Date(opp.expectedCloseDate); if (closeDate < dateRange.start || closeDate > dateRange.end) { return false; } } return true; }); }); return result; }, [opportunitiesByStage, searchTerm, selectedUser, dateRange]); // Calculate totals const totals = useMemo(() => { const allOpps = Object.values(filteredOpportunities).flat(); const openOpps = [...filteredOpportunities.new, ...filteredOpportunities.qualified, ...filteredOpportunities.proposition]; return { totalOpportunities: allOpps.length, totalValue: allOpps.reduce((sum, o) => sum + o.expectedRevenue, 0), openValue: openOpps.reduce((sum, o) => sum + o.expectedRevenue, 0), weightedValue: openOpps.reduce((sum, o) => sum + (o.expectedRevenue * o.probability / 100), 0), wonValue: filteredOpportunities.won.reduce((sum, o) => sum + o.expectedRevenue, 0), }; }, [filteredOpportunities]); // Handle card drop (stage change) const handleDrop = (opportunity: KanbanOpportunity, targetStage: StageKey) => { // Find current stage let currentStage: StageKey | null = null; for (const [stage, items] of Object.entries(opportunitiesByStage)) { if (items.some((item) => item.id === opportunity.id)) { currentStage = stage as StageKey; break; } } if (!currentStage || currentStage === targetStage) return; // Update state setOpportunitiesByStage((prev) => { const newState = { ...prev }; // Remove from current stage newState[currentStage!] = prev[currentStage!].filter((item) => item.id !== opportunity.id); // Update probability based on stage const updatedOpp = { ...opportunity }; if (targetStage === 'won') { updatedOpp.probability = 100; } else if (targetStage === 'lost') { updatedOpp.probability = 0; } // Add to target stage newState[targetStage] = [...prev[targetStage], updatedOpp]; return newState; }); // Show toast notification success( 'Etapa actualizada', `"${opportunity.name}" movida a ${stageLabels[targetStage]}` ); }; // Handle card click const handleCardClick = (opportunity: KanbanOpportunity) => { console.log('Card clicked:', opportunity); // Here you could navigate to opportunity detail or open a modal }; // Refresh data const handleRefresh = () => { setOpportunitiesByStage(generateMockOpportunities()); success('Datos actualizados', 'El pipeline ha sido actualizado'); }; // Clear filters const handleClearFilters = () => { setSearchTerm(''); setDateRange({ start: null, end: null }); setSelectedUser(''); }; const hasActiveFilters = searchTerm || selectedUser || dateRange.start; return (
{/* Header */}

Pipeline de Ventas

Gestiona oportunidades arrastrando tarjetas entre etapas

{/* Summary Stats */}
Oportunidades
{totals.totalOpportunities}
Pipeline Total
${formatCurrency(totals.openValue)}
Ponderado
${formatCurrency(totals.weightedValue)}
Ganado
${formatCurrency(totals.wonValue)}
Valor Total
${formatCurrency(totals.totalValue)}
{/* Filters */}
{/* Search */}
setSearchTerm(e.target.value)} className="w-full rounded-md border border-gray-300 py-2 pl-10 pr-4 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
{/* Toggle Filters */} {hasActiveFilters && ( )}
{/* Expanded Filters */} {showFilters && (