import type { FC } from 'react'; import { useEffect, useState, useCallback, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { RefreshCw, Target, Search, TrendingUp, CheckCircle, XCircle, Circle, LayoutGrid, List, MoreHorizontal, User, ArrowRightCircle, UserPlus, MessageSquare, } from 'lucide-react'; import Layout from '../components/Layout'; import { Card, Button, Input, Select, EmptyState, Skeleton, toast, Badge, Modal } from '../components/ui'; import { odooApi, type WacrmLead, type WacrmPipeline } from '../services/odoo'; interface Member { id: string; name: string; role: string; } const statusOptions = [ { value: '', label: 'Todos los estados' }, { value: 'open', label: 'Abierto' }, { value: 'won', label: 'Ganado' }, { value: 'lost', label: 'Perdido' }, ]; const statusBadge = (status: string) => { switch (status) { case 'won': return 'success'; case 'lost': return 'danger'; default: return 'primary'; } }; const statusIcon = (status: string) => { switch (status) { case 'won': return ; case 'lost': return ; default: return ; } }; const currencySymbol = (currency: string) => { if (currency === 'MXN') return '$'; if (currency === 'USD') return 'US$'; return currency; }; const WacrmLeads: FC = () => { const [leads, setLeads] = useState([]); const [pipelines, setPipelines] = useState([]); const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); const [search, setSearch] = useState(''); const [status, setStatus] = useState(''); const [pipelineId, setPipelineId] = useState(''); const [view, setView] = useState<'list' | 'kanban'>('kanban'); const [lastSync, setLastSync] = useState(null); const [selectedLead, setSelectedLead] = useState(null); const [stageModalOpen, setStageModalOpen] = useState(false); const [movingStage, setMovingStage] = useState(null); const [members, setMembers] = useState([]); const [filtroAsign, setFiltroAsign] = useState<'todos' | 'sin_asignar' | 'mios'>('todos'); const [convirtiendo, setConvirtiendo] = useState(false); const [currentAgentId] = useState(() => typeof window !== 'undefined' ? localStorage.getItem('skeen_current_agent_id') || '' : '' ); const load = useCallback(async () => { try { setLoading(true); const [leadsRes, pipelinesRes] = await Promise.all([ odooApi.getWacrmLeads(search || undefined, status || undefined, pipelineId || undefined), odooApi.getWacrmPipelines(), ]); if (leadsRes.status === 'success') setLeads(leadsRes.leads); if (pipelinesRes.status === 'success') setPipelines(pipelinesRes.pipelines); setLastSync(new Date()); } catch (err) { toast.error('Error al cargar leads'); console.error(err); } finally { setLoading(false); } }, [search, status, pipelineId]); const sync = async () => { try { setSyncing(true); const res = await odooApi.syncWacrm(); if (res.status === 'success') { toast.success(`Sincronizado: ${res.result.deals || 0} deals`); await load(); } } catch (err) { toast.error('Error al sincronizar con WACRM'); console.error(err); } finally { setSyncing(false); } }; const updateStatus = async (lead: WacrmLead, newStatus: 'open' | 'won' | 'lost') => { try { const res = await odooApi.updateWacrmLeadStatus(lead.id, { status: newStatus }); if (res.status === 'success') { toast.success(`Lead marcado como ${newStatus}`); setLeads((prev) => prev.map((l) => (l.id === lead.id ? res.lead : l))); } } catch (err) { toast.error('Error al actualizar lead'); console.error(err); } }; const changeStage = async (lead: WacrmLead, stageExternalId: string) => { try { setMovingStage(stageExternalId); const res = await odooApi.updateWacrmLeadStatus(lead.id, { stage_id: stageExternalId }); if (res.status === 'success') { toast.success('Etapa actualizada'); setLeads((prev) => prev.map((l) => (l.id === lead.id ? res.lead : l))); setStageModalOpen(false); setSelectedLead(null); } } catch (err) { toast.error('Error al cambiar etapa'); console.error(err); } finally { setMovingStage(null); } }; useEffect(() => { load(); }, [load]); useEffect(() => { odooApi.getWacrmMembers() .then((res) => { if (res.status === 'success') setMembers(res.members); }) .catch(() => {}); }, []); const assignLead = async (lead: WacrmLead, memberId: string | null) => { try { const res = await odooApi.updateWacrmLeadStatus(lead.id, { assigned_to: memberId }); if (res.status === 'success') { setLeads((prev) => prev.map((l) => (l.id === lead.id ? res.lead : l))); setSelectedLead(res.lead); toast.success(memberId ? 'Lead asignado' : 'Lead desasignado'); } } catch (err) { toast.error('Error al asignar lead'); console.error(err); } }; const convertirLead = async (lead: WacrmLead) => { try { setConvirtiendo(true); const res = await odooApi.convertWacrmLead(lead.id); if (res.status === 'success') { setLeads((prev) => prev.map((l) => (l.id === lead.id ? res.lead : l))); setSelectedLead(res.lead); toast.success(res.created ? 'Paciente creado y ligado' : 'Lead ligado a paciente existente'); } } catch (err) { const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message; toast.error(msg || 'Error al convertir lead'); console.error(err); } finally { setConvirtiendo(false); } }; const memberName = (id?: string) => (id ? members.find((m) => m.id === id)?.name : undefined); // Auto-sync cada 15 segundos useEffect(() => { const interval = setInterval(() => { load(); }, 15000); return () => clearInterval(interval); }, [load]); const pipelineOptions = useMemo( () => [{ value: '', label: 'Todos los pipelines' }, ...pipelines.map((p) => ({ value: p.external_id, label: p.name }))], [pipelines] ); const activePipeline = useMemo( () => pipelines.find((p) => p.external_id === pipelineId) || pipelines[0], [pipelines, pipelineId] ); const filteredLeads = useMemo(() => { let out = leads; if (activePipeline) out = out.filter((l) => l.pipeline_id === activePipeline.id); if (filtroAsign === 'sin_asignar') out = out.filter((l) => !l.assigned_to); if (filtroAsign === 'mios') out = out.filter((l) => !!currentAgentId && l.assigned_to === currentAgentId); return out; }, [leads, activePipeline, filtroAsign, currentAgentId]); const stats = useMemo(() => { const open = leads.filter((l) => l.status === 'open'); const won = leads.filter((l) => l.status === 'won'); const lost = leads.filter((l) => l.status === 'lost'); return { total: leads.length, openValue: open.reduce((sum, l) => sum + (l.value || 0), 0), wonValue: won.reduce((sum, l) => sum + (l.value || 0), 0), lostValue: lost.reduce((sum, l) => sum + (l.value || 0), 0), openCount: open.length, wonCount: won.length, lostCount: lost.length, }; }, [leads]); const renderList = () => (
{filteredLeads.map((l) => ( ))}
Lead Contacto Pipeline / Etapa Valor Estado Acciones

{l.title}

{l.notes || 'Sin notas'}

{l.contact_name || '—'}

{l.contact_phone || '—'}

{l.pipeline || '—'}

{currencySymbol(l.currency)}{l.value.toLocaleString()}

{statusIcon(l.status)} {l.status}
{l.status !== 'won' && ( )} {l.status !== 'lost' && ( )} {l.status !== 'open' && ( )}
); const renderKanban = () => { if (!activePipeline) return ; return (
{activePipeline.stages.map((stage) => { const stageLeads = filteredLeads.filter((l) => l.stage_id === stage.id); const stageValue = stageLeads.reduce((sum, l) => sum + (l.value || 0), 0); return (

{stage.name}

{stageLeads.length}
{stageLeads.map((l) => (

{l.title}

{statusIcon(l.status)}
{l.contact_name || l.contact_phone || '—'} {l.assigned_to && ( {memberName(l.assigned_to) || 'Asignado'} )} {l.partner_id && ( e.stopPropagation()} > Paciente ✓ )}

{currencySymbol(l.currency)}{l.value.toLocaleString()}

{l.status !== 'won' && ( )} {l.status !== 'lost' && ( )}
))} {stageLeads.length === 0 && (
Sin leads en esta etapa
)}
{stageValue > 0 && (

Total: {currencySymbol(activePipeline.stages[0]?.color ? 'MXN' : 'MXN')}{stageValue.toLocaleString()}

)}
); })}
); }; return ( {/* Métricas */}

Total leads

{stats.total}

Valor abierto

${stats.openValue.toLocaleString()}

Ganados

{stats.wonCount} (${stats.wonValue.toLocaleString()})

Perdidos

{stats.lostCount}

{/* Filtros */}
setSearch(e.target.value)} className="pl-10" />
setPipelineId(e.target.value)} options={pipelineOptions} className="w-full lg:w-52" />
{([ { key: 'todos', label: 'Todos' }, { key: 'sin_asignar', label: 'Sin asignar' }, { key: 'mios', label: 'Míos' }, ] as const).map((f) => ( ))}
{lastSync && (

Última sincronización: {lastSync.toLocaleTimeString()} · se actualiza automáticamente cada 15s

)} {loading && leads.length === 0 ? ( ) : filteredLeads.length === 0 ? ( } actionLabel="Sincronizar" onAction={sync} /> ) : view === 'list' ? ( renderList() ) : ( renderKanban() )}
{/* Modal detalle del lead */} setStageModalOpen(false)} title={selectedLead ? `Lead — ${selectedLead.title}` : 'Lead'}> {selectedLead && (
{/* Asignación y acciones */}