"use client"; import { Fragment, useEffect, useState, Suspense } from 'react'; import { Menu, Transition, Tab } from '@headlessui/react'; import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; import { SolutionGuard } from '@/components/auth/SolutionGuard'; import { useCRMFilter } from '@/contexts/CRMFilterContext'; import { useToast } from '@/components/layout/ToastContext'; import { UserPlusIcon, TrashIcon, PencilIcon, EllipsisVerticalIcon, MagnifyingGlassIcon, PlusIcon, XMarkIcon, TagIcon, PhoneIcon, EnvelopeIcon, ChartBarIcon, RectangleStackIcon, UsersIcon, FunnelIcon, ArrowTrendingUpIcon, ArrowTrendingDownIcon, ShareIcon, ClipboardDocumentCheckIcon, LinkIcon, ArrowUpTrayIcon, ArrowDownTrayIcon, } from '@heroicons/react/24/outline'; interface Lead { id: string; tenant_id: string; customer_id?: string; name: string; email: string; phone: string; source: string; source_meta: any; status: string; notes: string; tags: string[]; is_active: boolean; created_by: string; created_at: string; updated_at: string; lists?: List[]; } interface Customer { id: string; name: string; email: string; company: string; } interface List { id: string; name: string; description: string; lead_count?: number; } interface LeadStats { total: number; novo: number; qualificado: number; negociacao: number; convertido: number; perdido: number; bySource: Record; conversionRate: number; thisMonth: number; lastMonth: number; } const STATUS_OPTIONS = [ { value: 'novo', label: 'Novo', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' }, { value: 'qualificado', label: 'Qualificado', color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' }, { value: 'negociacao', label: 'Em Negociação', color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' }, { value: 'convertido', label: 'Convertido', color: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' }, { value: 'perdido', label: 'Perdido', color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' }, ]; function classNames(...classes: string[]) { return classes.filter(Boolean).join(' '); } // Componente para exibir card de lead function LeadCard({ lead, getStatusColor, getCustomerName, handleEdit, handleDelete }: { lead: Lead; getStatusColor: (status: string) => string; getCustomerName: (customerId?: string) => string | null; handleEdit: (lead: Lead) => void; handleDelete: (id: string) => void; }) { return (

{lead.name || 'Sem nome'}

{STATUS_OPTIONS.find(s => s.value === lead.status)?.label || lead.status}
{({ active }) => ( )} {({ active }) => ( )}
{lead.email && (
{lead.email}
)} {lead.phone && (
{lead.phone}
)} {lead.tags && lead.tags.length > 0 && (
{lead.tags.map((tag, idx) => ( {tag} ))}
)}
{lead.customer_id && (
Cliente: {getCustomerName(lead.customer_id)}
)} {lead.lists && lead.lists.length > 0 && (
Campanhas: {lead.lists.map(list => ( {list.name} ))}
)}
Origem: {lead.source || 'manual'}
); } function LeadsContent() { const { selectedCustomerId, customers, setCustomers } = useCRMFilter(); const searchParams = useSearchParams(); const campaignIdFromUrl = searchParams.get('campaign'); const toast = useToast(); const [leads, setLeads] = useState([]); const [campaigns, setCampaigns] = useState([]); const [selectedCampaign, setSelectedCampaign] = useState(campaignIdFromUrl || ''); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [isModalOpen, setIsModalOpen] = useState(false); const [editingLead, setEditingLead] = useState(null); const [shareModalOpen, setShareModalOpen] = useState(false); const [selectedCustomerForShare, setSelectedCustomerForShare] = useState(null); const [shareUrl, setShareUrl] = useState(''); const [copiedToClipboard, setCopiedToClipboard] = useState(false); const [formData, setFormData] = useState({ customer_id: '', name: '', email: '', phone: '', source: 'manual', status: 'novo', notes: '', tags: [] as string[], }); useEffect(() => { fetchCustomers(); }, []); // Recarrega dados quando o filtro de cliente ou campanha mudar useEffect(() => { console.log('🔄 LeadsContent: Filtros alterados', { selectedCustomerId, selectedCampaign }); if (selectedCampaign) { fetchLeadsByList(selectedCampaign); } else { fetchLeads(); } fetchCampaigns(); }, [selectedCustomerId, selectedCampaign]); useEffect(() => { calculateStats(); }, [leads]); const calculateStats = () => { if (leads.length === 0) { setStats({ total: 0, novo: 0, qualificado: 0, negociacao: 0, convertido: 0, perdido: 0, bySource: {}, conversionRate: 0, thisMonth: 0, lastMonth: 0, }); return; } const now = new Date(); const currentMonth = now.getMonth(); const currentYear = now.getFullYear(); const statsByStatus = leads.reduce((acc, lead) => { acc[lead.status] = (acc[lead.status] || 0) + 1; return acc; }, {} as Record); const bySource = leads.reduce((acc, lead) => { const source = lead.source || 'manual'; acc[source] = (acc[source] || 0) + 1; return acc; }, {} as Record); const thisMonth = leads.filter(lead => { const createdDate = new Date(lead.created_at); return createdDate.getMonth() === currentMonth && createdDate.getFullYear() === currentYear; }).length; const lastMonth = leads.filter(lead => { const createdDate = new Date(lead.created_at); const lastMonthDate = new Date(currentYear, currentMonth - 1); return createdDate.getMonth() === lastMonthDate.getMonth() && createdDate.getFullYear() === lastMonthDate.getFullYear(); }).length; const convertidos = statsByStatus['convertido'] || 0; const conversionRate = leads.length > 0 ? (convertidos / leads.length) * 100 : 0; setStats({ total: leads.length, novo: statsByStatus['novo'] || 0, qualificado: statsByStatus['qualificado'] || 0, negociacao: statsByStatus['negociacao'] || 0, convertido: statsByStatus['convertido'] || 0, perdido: statsByStatus['perdido'] || 0, bySource, conversionRate, thisMonth, lastMonth, }); }; const fetchCampaigns = async () => { try { const timestamp = new Date().getTime(); const url = selectedCustomerId ? `/api/crm/lists?customer_id=${selectedCustomerId}&t=${timestamp}` : `/api/crm/lists?t=${timestamp}`; console.log(`📡 Fetching campaigns from: ${url}`); const response = await fetch(url, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, 'Cache-Control': 'no-cache' }, }); if (response.ok) { const data = await response.json(); const newCampaigns = data.lists || []; setCampaigns(newCampaigns); // Se a campanha selecionada não estiver na nova lista, limpa a seleção if (selectedCampaign && !newCampaigns.find((c: any) => c.id === selectedCampaign)) { console.log('🧹 Limpando campanha selecionada pois não pertence ao novo cliente'); setSelectedCampaign(''); } } } catch (error) { console.error('Error fetching campaigns:', error); } }; const fetchLeadsByList = async (listId: string) => { try { setLoading(true); const timestamp = new Date().getTime(); const url = `/api/crm/lists/${listId}/leads?t=${timestamp}`; console.log(`📡 Fetching leads by list from: ${url}`); const response = await fetch(url, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, 'Cache-Control': 'no-cache' }, }); if (response.ok) { const data = await response.json(); console.log('✅ Leads by list received:', data.leads?.length || 0, 'leads'); setLeads(data.leads || []); } } catch (error) { console.error('Error fetching leads by list:', error); } finally { setLoading(false); } }; const fetchCustomers = async () => { try { const response = await fetch('/api/crm/customers', { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, }); if (response.ok) { const data = await response.json(); setCustomers(data.customers || []); } } catch (error) { console.error('Error fetching customers:', error); } }; const fetchLeads = async () => { try { setLoading(true); const timestamp = new Date().getTime(); const url = selectedCustomerId ? `/api/crm/leads?customer_id=${selectedCustomerId}&t=${timestamp}` : `/api/crm/leads?t=${timestamp}`; console.log(`📡 Fetching leads from: ${url}`); const response = await fetch(url, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, 'Cache-Control': 'no-cache' }, }); if (response.ok) { const data = await response.json(); console.log('✅ Leads received:', data.leads?.length || 0, 'leads'); setLeads(data.leads || []); } } catch (error) { console.error('Error fetching leads:', error); } finally { setLoading(false); } }; const handleExport = async (format: 'csv' | 'xlsx' | 'json') => { try { const token = localStorage.getItem('token'); let url = `/api/crm/leads/export?format=${format}`; if (selectedCustomerId) { url += `&customer_id=${selectedCustomerId}`; } if (selectedCampaign) { url += `&campaign_id=${selectedCampaign}`; } const response = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { const blob = await response.blob(); const downloadUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = downloadUrl; a.download = `leads.${format === 'xlsx' ? 'xlsx' : format}`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(downloadUrl); document.body.removeChild(a); toast.success('Exportado com sucesso!'); } else { toast.error('Erro ao exportar leads'); } } catch (error) { console.error('Export error:', error); toast.error('Erro ao exportar'); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const url = editingLead ? `/api/crm/leads/${editingLead.id}` : '/api/crm/leads'; const method = editingLead ? 'PUT' : 'POST'; try { const response = await fetch(url, { method, headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json', }, body: JSON.stringify(formData), }); if (response.ok) { fetchLeads(); handleCloseModal(); } } catch (error) { console.error('Error saving lead:', error); } }; const handleNewLead = () => { setEditingLead(null); setFormData({ customer_id: selectedCustomerId || '', name: '', email: '', phone: '', source: 'manual', status: 'novo', notes: '', tags: [] as string[], }); setIsModalOpen(true); }; const handleEdit = (lead: Lead) => { setEditingLead(lead); setFormData({ customer_id: lead.customer_id || '', name: lead.name, email: lead.email, phone: lead.phone, source: lead.source, status: lead.status, notes: lead.notes, tags: lead.tags || [], }); setIsModalOpen(true); }; const handleDelete = async (id: string) => { if (!confirm('Tem certeza que deseja excluir este lead?')) return; try { const response = await fetch(`/api/crm/leads/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, }); if (response.ok) { fetchLeads(); } } catch (error) { console.error('Error deleting lead:', error); } }; const handleCloseModal = () => { setIsModalOpen(false); setEditingLead(null); setFormData({ customer_id: '', name: '', email: '', phone: '', source: 'manual', status: 'novo', notes: '', tags: [], }); }; const handleShareCustomerLeads = async (customer: Customer) => { setSelectedCustomerForShare(customer); try { // Gera um token de compartilhamento para este cliente const response = await fetch('/api/crm/customers/share-token', { method: 'POST', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ customer_id: customer.id }), }); if (response.ok) { const data = await response.json(); const baseUrl = window.location.origin; const shareLink = `${baseUrl}/share/leads/${data.token}`; setShareUrl(shareLink); setShareModalOpen(true); } } catch (error) { console.error('Error generating share token:', error); alert('Erro ao gerar link de compartilhamento'); } }; const copyShareLink = () => { navigator.clipboard.writeText(shareUrl); setCopiedToClipboard(true); setTimeout(() => setCopiedToClipboard(false), 2000); }; const getCustomerName = (customerId?: string) => { if (!customerId) return null; const customer = customers.find(c => c.id === customerId); return customer?.company || customer?.name || 'Cliente'; }; const filteredLeads = leads.filter(lead => lead.name?.toLowerCase().includes(searchTerm.toLowerCase()) || lead.email?.toLowerCase().includes(searchTerm.toLowerCase()) || lead.phone?.includes(searchTerm) ); const getStatusColor = (status: string) => { return STATUS_OPTIONS.find(s => s.value === status)?.color || 'bg-gray-100 text-gray-800'; }; return (
{/* Header */}

Leads

Central completa de gerenciamento de leads e funil de vendas

Exportar
{({ active }) => ( )} {({ active }) => ( )} {({ active }) => ( )}
Importar Compartilhar

Compartilhar leads com cliente

Selecione um cliente para gerar link

{selectedCustomerId && customers.find(c => c.id === selectedCustomerId) && (

Cliente Selecionado

{({ active }) => ( )}
)} {customers.length === 0 ? (
Nenhum cliente cadastrado
) : ( customers .filter(c => c.id !== selectedCustomerId) .map(customer => ( {({ active }) => ( )} )) )}
Ver no Funil
{/* Tabs */} classNames( 'w-full rounded-lg py-2.5 text-sm font-medium leading-5', 'ring-white ring-opacity-60 ring-offset-2 ring-offset-brand-400 focus:outline-none focus:ring-2', selected ? 'bg-brand-500 text-white shadow' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800' ) } >
Visão Geral
classNames( 'w-full rounded-lg py-2.5 text-sm font-medium leading-5', 'ring-white ring-opacity-60 ring-offset-2 ring-offset-brand-400 focus:outline-none focus:ring-2', selected ? 'bg-brand-500 text-white shadow' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800' ) } >
Todos os Leads
classNames( 'w-full rounded-lg py-2.5 text-sm font-medium leading-5', 'ring-white ring-opacity-60 ring-offset-2 ring-offset-brand-400 focus:outline-none focus:ring-2', selected ? 'bg-brand-500 text-white shadow' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800' ) } >
Por Campanha
{/* Painel de Visão Geral */} {stats && (
{/* Cards de Métricas Principais */}

Total de Leads

{stats.total}

{stats.thisMonth >= stats.lastMonth ? ( ) : ( )} = stats.lastMonth ? 'text-green-600' : 'text-red-600'}> {stats.thisMonth} este mês

Taxa de Conversão

{stats.conversionRate.toFixed(1)}%

{stats.convertido} convertidos

Novos Leads

{stats.novo}

Aguardando qualificação

Em Negociação

{stats.negociacao}

Potencial de conversão

{/* Leads por Status */}

Distribuição por Status

{STATUS_OPTIONS.map(status => { const count = stats[status.value as keyof LeadStats] as number || 0; const percentage = stats.total > 0 ? (count / stats.total) * 100 : 0; return (
{status.label} {count} ({percentage.toFixed(1)}%)
); })}
{/* Leads por Origem */}

Leads por Origem

{Object.entries(stats.bySource).map(([source, count]) => (

{source}

{count}

))}
)}
{/* Painel de Todos os Leads */} {/* Search */}
setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white" />
{/* Leads Grid */} {loading ? (
) : filteredLeads.length === 0 ? (

{searchTerm ? 'Nenhum lead encontrado' : 'Nenhum lead cadastrado'}

) : (
{filteredLeads.map((lead) => ( ))}
)}
{/* Painel Por Campanha */}
{/* Seletor de Campanha */}
{/* Leads da Campanha */} {loading ? (
) : selectedCampaign ? ( filteredLeads.length === 0 ? (

Nenhum lead nesta campanha

) : (
{filteredLeads.map((lead) => ( ))}
) ) : (

Selecione uma lista para ver os leads

)}
{/* Modal */} { isModalOpen && (

{editingLead ? 'Editar Lead' : 'Novo Lead'}

Vincule este lead a um cliente específico (ex: DH Projects)

setFormData({ ...formData, name: e.target.value })} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white" />
setFormData({ ...formData, email: e.target.value })} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white" />
setFormData({ ...formData, phone: e.target.value })} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white" />
setFormData({ ...formData, source: e.target.value })} placeholder="manual, site, meta, etc" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white" />