feat: versão 1.5 - CRM Beta com leads, funis, campanhas e portal do cliente

This commit is contained in:
Erik Silva
2025-12-24 17:36:52 -03:00
parent 99d828869a
commit dfb91c8ba5
98 changed files with 18255 additions and 1465 deletions

View File

@@ -0,0 +1,226 @@
"use client";
import { useEffect, useState, Fragment } from 'react';
import { useCRMFilter } from '@/contexts/CRMFilterContext';
import { Combobox, Transition } from '@headlessui/react';
import {
FunnelIcon,
XMarkIcon,
CheckIcon,
ChevronUpDownIcon,
MagnifyingGlassIcon
} from '@heroicons/react/24/outline';
interface Customer {
id: string;
name: string;
company?: string;
logo_url?: string;
}
export function CRMCustomerFilter() {
const { selectedCustomerId, setSelectedCustomerId, customers, setCustomers, loading, setLoading } = useCRMFilter();
const [query, setQuery] = useState('');
console.log('🔍 CRMCustomerFilter render, selectedCustomerId:', selectedCustomerId);
useEffect(() => {
fetchCustomers();
}, []);
const fetchCustomers = async () => {
try {
setLoading(true);
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);
} finally {
setLoading(false);
}
};
const handleClearFilter = () => {
setSelectedCustomerId(null);
setQuery('');
};
const selectedCustomer = customers.find(c => c.id === selectedCustomerId);
const filteredCustomers =
query === ''
? customers
: customers.filter((customer: Customer) => {
const nameMatch = customer.name.toLowerCase().includes(query.toLowerCase());
const companyMatch = customer.company?.toLowerCase().includes(query.toLowerCase());
return nameMatch || companyMatch;
});
return (
<div className="flex items-center gap-2">
<div className="hidden md:flex items-center gap-1 text-gray-400 mr-1">
<FunnelIcon className="w-4 h-4" />
<span className="text-xs font-medium uppercase tracking-wider">Filtro CRM</span>
</div>
<Combobox
value={selectedCustomerId}
onChange={(value) => {
console.log('🎯 CRMCustomerFilter: Selecting customer ID:', value);
setSelectedCustomerId(value);
setQuery('');
}}
disabled={loading}
>
<div className="relative">
<div className="relative w-full min-w-[320px]">
<Combobox.Input
className="w-full rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50 py-2.5 pl-10 pr-10 text-sm leading-5 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:bg-white dark:focus:bg-gray-800 transition-all duration-200"
displayValue={(customerId: string) => {
const customer = customers.find(c => c.id === customerId);
if (!customer) return '';
return customer.company
? `${customer.name} (${customer.company})`
: customer.name;
}}
onChange={(event) => setQuery(event.target.value)}
placeholder="Pesquisar por nome ou empresa..."
/>
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
{selectedCustomer?.logo_url ? (
<img
src={selectedCustomer.logo_url}
className="h-5 w-5 rounded-full object-cover border border-gray-200 dark:border-gray-700"
alt=""
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
) : (
<MagnifyingGlassIcon
className="h-4 w-4 text-gray-400"
aria-hidden="true"
/>
)}
</div>
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400 hover:text-gray-600 transition-colors"
aria-hidden="true"
/>
</Combobox.Button>
</div>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
afterLeave={() => setQuery('')}
>
<Combobox.Options className="absolute z-50 mt-2 max-h-80 w-full overflow-auto rounded-xl bg-white dark:bg-gray-800 py-1 text-base shadow-2xl ring-1 ring-black/5 dark:ring-white/10 focus:outline-none sm:text-sm border border-gray-100 dark:border-gray-700">
<Combobox.Option
value={null}
className={({ active }) =>
`relative cursor-pointer select-none py-3 pl-10 pr-4 ${active
? 'bg-brand-50 dark:bg-brand-900/20 text-brand-900 dark:text-brand-100'
: 'text-gray-900 dark:text-white'
}`
}
>
{({ selected, active }) => (
<>
<span className={`block truncate ${selected ? 'font-semibold' : 'font-normal'}`}>
Todos os Clientes (Visão Geral)
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-brand-600">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Combobox.Option>
<div className="px-3 py-2 text-[10px] font-bold text-gray-400 uppercase tracking-widest border-t border-gray-50 dark:border-gray-700/50 mt-1">
Clientes Disponíveis
</div>
{filteredCustomers.length === 0 && query !== '' ? (
<div className="relative cursor-default select-none py-4 px-4 text-center text-gray-500 dark:text-gray-400">
<p className="text-sm">Nenhum cliente encontrado</p>
<p className="text-xs mt-1">Tente outro termo de busca</p>
</div>
) : (
filteredCustomers.map((customer: Customer) => (
<Combobox.Option
key={customer.id}
value={customer.id}
className={({ active }) =>
`relative cursor-pointer select-none py-3 pl-10 pr-4 transition-colors ${active
? 'bg-brand-50 dark:bg-brand-900/20 text-brand-900 dark:text-brand-100'
: 'text-gray-900 dark:text-white'
}`
}
>
{({ selected, active }) => (
<>
<div className="flex items-center gap-3">
{customer.logo_url ? (
<img
src={customer.logo_url}
alt={customer.name}
className="w-8 h-8 rounded-full object-cover border border-gray-200 dark:border-gray-700"
onError={(e) => {
(e.target as HTMLImageElement).src = `https://ui-avatars.com/api/?name=${encodeURIComponent(customer.name)}&background=random`;
}}
/>
) : (
<div className="w-8 h-8 rounded-full bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center text-brand-700 dark:text-brand-300 text-xs font-bold">
{customer.name.substring(0, 2).toUpperCase()}
</div>
)}
<div className="flex flex-col">
<span className={`block truncate ${selected ? 'font-semibold text-brand-700 dark:text-brand-400' : 'font-medium'}`}>
{customer.name}
</span>
{customer.company && (
<span className={`block truncate text-xs ${active ? 'text-brand-600/70 dark:text-brand-400/70' : 'text-gray-500 dark:text-gray-400'}`}>
{customer.company}
</span>
)}
</div>
</div>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-brand-600">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Combobox.Option>
))
)}
</Combobox.Options>
</Transition>
</div>
</Combobox>
{selectedCustomerId && (
<button
onClick={handleClearFilter}
className="p-2 hover:bg-red-50 dark:hover:bg-red-900/20 text-gray-400 hover:text-red-600 rounded-xl transition-all duration-200 flex-shrink-0 border border-transparent hover:border-red-100 dark:hover:border-red-900/30"
title="Limpar filtro"
>
<XMarkIcon className="w-5 h-5" />
</button>
)}
</div>
);
}

View File

@@ -0,0 +1,545 @@
"use client";
import { useState, useEffect } from 'react';
import { useToast } from '@/components/layout/ToastContext';
import Modal from '@/components/layout/Modal';
import {
EllipsisVerticalIcon,
PlusIcon,
UserIcon,
EnvelopeIcon,
PhoneIcon,
Bars2Icon,
TagIcon,
ChatBubbleLeftRightIcon,
CalendarIcon,
ClockIcon
} from '@heroicons/react/24/outline';
interface Stage {
id: string;
name: string;
color: string;
order_index: number;
}
interface Lead {
id: string;
name: string;
email: string;
phone: string;
stage_id: string;
funnel_id: string;
notes?: string;
tags?: string[];
status?: string;
created_at?: string;
}
interface KanbanBoardProps {
funnelId: string;
campaignId?: string;
}
export default function KanbanBoard({ funnelId, campaignId }: KanbanBoardProps) {
const [stages, setStages] = useState<Stage[]>([]);
const [leads, setLeads] = useState<Lead[]>([]);
const [loading, setLoading] = useState(true);
const [draggedLeadId, setDraggedLeadId] = useState<string | null>(null);
const [dropTargetStageId, setDropTargetStageId] = useState<string | null>(null);
const [movingLeadId, setMovingLeadId] = useState<string | null>(null);
// Modal states
const [isLeadModalOpen, setIsLeadModalOpen] = useState(false);
const [selectedLead, setSelectedLead] = useState<Lead | null>(null);
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [targetStageId, setTargetStageId] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
notes: '',
tags: ''
});
const toast = useToast();
useEffect(() => {
if (funnelId) {
fetchData();
}
}, [funnelId, campaignId]);
// Refetch quando houver alterações externas (ex: criação de etapa no modal de configurações)
useEffect(() => {
const handleRefresh = () => {
console.log('KanbanBoard: External refresh triggered');
fetchData();
};
window.addEventListener('kanban-refresh', handleRefresh);
return () => window.removeEventListener('kanban-refresh', handleRefresh);
}, []);
const fetchData = async () => {
console.log('KanbanBoard: Fetching data for funnel:', funnelId, 'campaign:', campaignId);
setLoading(true);
try {
const token = localStorage.getItem('token');
const headers = { 'Authorization': `Bearer ${token}` };
const [stagesRes, leadsRes] = await Promise.all([
fetch(`/api/crm/funnels/${funnelId}/stages`, { headers }),
campaignId
? fetch(`/api/crm/lists/${campaignId}/leads`, { headers })
: fetch(`/api/crm/leads`, { headers })
]);
if (stagesRes.ok && leadsRes.ok) {
const stagesData = await stagesRes.json();
const leadsData = await leadsRes.json();
console.log('KanbanBoard: Received stages:', stagesData.stages?.length);
console.log('KanbanBoard: Received leads:', leadsData.leads?.length);
setStages(stagesData.stages || []);
setLeads(leadsData.leads || []);
} else {
console.error('KanbanBoard: API Error', stagesRes.status, leadsRes.status);
toast.error('Erro ao carregar dados do servidor');
}
} catch (error) {
console.error('Error fetching kanban data:', error);
toast.error('Erro de conexão ao carregar monitoramento');
} finally {
setLoading(false);
}
};
const moveLead = async (leadId: string, newStageId: string) => {
setMovingLeadId(leadId);
// Optimistic update
const originalLeads = [...leads];
setLeads(prev => prev.map(l => l.id === leadId ? { ...l, stage_id: newStageId } : l));
try {
const response = await fetch(`/api/crm/leads/${leadId}/stage`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ funnel_id: funnelId, stage_id: newStageId })
});
console.log('KanbanBoard: Move lead response:', response.status);
if (!response.ok) {
setLeads(originalLeads);
toast.error('Erro ao mover lead');
}
} catch (error) {
console.error('Error moving lead:', error);
setLeads(originalLeads);
toast.error('Erro ao mover lead');
} finally {
setMovingLeadId(null);
}
};
const handleDragStart = (e: React.DragEvent, leadId: string) => {
console.log('KanbanBoard: Drag Start', leadId);
setDraggedLeadId(leadId);
e.dataTransfer.setData('text/plain', leadId);
e.dataTransfer.effectAllowed = 'move';
// Add a slight delay to make the original item semi-transparent
const currentTarget = e.currentTarget as HTMLElement;
setTimeout(() => {
if (currentTarget) currentTarget.style.opacity = '0.4';
}, 0);
};
const handleDragEnd = (e: React.DragEvent) => {
console.log('KanbanBoard: Drag End');
const currentTarget = e.currentTarget as HTMLElement;
if (currentTarget) currentTarget.style.opacity = '1';
setDraggedLeadId(null);
setDropTargetStageId(null);
};
const handleDragOver = (e: React.DragEvent, stageId: string) => {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'move';
if (dropTargetStageId !== stageId) {
setDropTargetStageId(stageId);
}
return false;
};
const handleDrop = (e: React.DragEvent, stageId: string) => {
e.preventDefault();
e.stopPropagation();
// Use state if dataTransfer is empty (fallback)
const leadId = e.dataTransfer.getData('text/plain') || draggedLeadId;
console.log('KanbanBoard: Drop', { leadId, stageId });
setDropTargetStageId(null);
if (!leadId) {
console.error('KanbanBoard: No leadId found');
return;
}
const lead = leads.find(l => l.id === leadId);
if (lead && lead.stage_id !== stageId) {
console.log('KanbanBoard: Moving lead', leadId, 'to stage', stageId);
moveLead(leadId, stageId);
} else {
console.log('KanbanBoard: Lead already in stage or not found', { lead, stageId });
}
};
const handleAddLead = (stageId: string) => {
setTargetStageId(stageId);
setFormData({
name: '',
email: '',
phone: '',
notes: '',
tags: ''
});
setIsAddModalOpen(true);
};
const handleEditLead = (lead: Lead) => {
setSelectedLead(lead);
setFormData({
name: lead.name || '',
email: lead.email || '',
phone: lead.phone || '',
notes: lead.notes || '',
tags: lead.tags?.join(', ') || ''
});
setIsLeadModalOpen(true);
};
const saveLead = async (e: React.FormEvent) => {
e.preventDefault();
setIsSaving(true);
try {
const token = localStorage.getItem('token');
const isEditing = !!selectedLead;
const url = isEditing ? `/api/crm/leads/${selectedLead.id}` : '/api/crm/leads';
const method = isEditing ? 'PUT' : 'POST';
const payload = {
...formData,
tags: formData.tags.split(',').map(t => t.trim()).filter(t => t),
funnel_id: funnelId,
stage_id: isEditing ? selectedLead.stage_id : targetStageId,
status: isEditing ? selectedLead.status : 'novo'
};
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
if (response.ok) {
toast.success(isEditing ? 'Lead atualizado' : 'Lead criado');
setIsAddModalOpen(false);
setIsLeadModalOpen(false);
fetchData();
} else {
toast.error('Erro ao salvar lead');
}
} catch (error) {
console.error('Error saving lead:', error);
toast.error('Erro de conexão');
} finally {
setIsSaving(false);
}
};
if (loading) {
return (
<div className="flex h-64 items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-500"></div>
</div>
);
}
return (
<div
className="flex gap-6 overflow-x-auto pb-4 h-full scrollbar-thin scrollbar-thumb-zinc-300"
onDragOver={(e) => e.preventDefault()}
>
{stages.map(stage => (
<div
key={stage.id}
className={`flex-shrink-0 w-80 flex flex-col rounded-2xl transition-all duration-200 h-full border border-zinc-200/50 ${dropTargetStageId === stage.id
? 'bg-brand-50/50 ring-2 ring-brand-500/30'
: 'bg-white'
}`}
onDragOver={(e) => handleDragOver(e, stage.id)}
onDragEnter={(e) => {
e.preventDefault();
setDropTargetStageId(stage.id);
}}
onDrop={(e) => handleDrop(e, stage.id)}
>
{/* Header da Coluna */}
<div className="p-4 flex items-center justify-between sticky top-0 z-10">
<div className="flex items-center gap-3">
<div
className="w-1.5 h-5 rounded-full"
style={{ backgroundColor: stage.color }}
></div>
<div>
<h3 className="font-bold text-zinc-900 text-xs uppercase tracking-widest">
{stage.name}
</h3>
<p className="text-[10px] text-zinc-400 font-bold">
{leads.filter(l => l.stage_id === stage.id).length}
</p>
</div>
</div>
<button className="p-1.5 text-zinc-400 hover:text-zinc-600 hover:bg-zinc-50 rounded-lg transition-colors">
<EllipsisVerticalIcon className="h-4 w-4" />
</button>
</div>
{/* Lista de Cards */}
<div className="px-3 pb-3 flex-1 overflow-y-auto space-y-3 scrollbar-thin scrollbar-thumb-zinc-200">
{leads.filter(l => l.stage_id === stage.id).map(lead => (
<div
key={lead.id}
draggable
onDragStart={(e) => handleDragStart(e, lead.id)}
onDragEnd={handleDragEnd}
onClick={() => handleEditLead(lead)}
className={`bg-white p-4 rounded-xl shadow-sm border border-zinc-200 hover:shadow-md hover:border-brand-300 transition-all duration-200 cursor-grab active:cursor-grabbing group relative select-none ${draggedLeadId === lead.id ? 'ring-2 ring-brand-500 ring-offset-2' : ''
} ${movingLeadId === lead.id ? 'opacity-50 grayscale' : ''}`}
>
{movingLeadId === lead.id && (
<div className="absolute inset-0 flex items-center justify-center bg-white/80 rounded-xl z-10">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-brand-500"></div>
</div>
)}
<div className="flex justify-between items-start mb-3">
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-lg bg-zinc-100 flex items-center justify-center">
<UserIcon className="w-3.5 h-3.5 text-zinc-500" />
</div>
<h4 className="font-bold text-zinc-900 text-sm leading-tight">
{lead.name || 'Sem nome'}
</h4>
</div>
<Bars2Icon className="w-4 h-4 text-zinc-300 group-hover:text-zinc-400 transition-colors" />
</div>
<div className="space-y-1.5">
{lead.email && (
<div className="flex items-center gap-2 text-[11px] text-zinc-500">
<EnvelopeIcon className="h-3 w-3" />
<span className="truncate">{lead.email}</span>
</div>
)}
{lead.phone && (
<div className="flex items-center gap-2 text-[11px] text-zinc-500">
<PhoneIcon className="h-3 w-3" />
<span>{lead.phone}</span>
</div>
)}
</div>
{lead.tags && lead.tags.length > 0 && (
<div className="mt-3 flex flex-wrap gap-1">
{lead.tags.slice(0, 2).map((tag, i) => (
<span key={i} className="px-1.5 py-0.5 bg-zinc-100 text-zinc-600 text-[9px] font-bold rounded uppercase tracking-wider">
{tag}
</span>
))}
{lead.tags.length > 2 && (
<span className="text-[9px] font-bold text-zinc-400">+{lead.tags.length - 2}</span>
)}
</div>
)}
{/* Badge de Status (Opcional) */}
<div className="mt-4 pt-3 border-t border-zinc-100 flex items-center justify-between">
<span className="text-[9px] font-bold text-zinc-400 uppercase tracking-tighter">
#{lead.id.slice(0, 6)}
</span>
<div className="flex items-center gap-1.5">
{lead.notes && (
<ChatBubbleLeftRightIcon className="h-3 w-3 text-brand-500" />
)}
<div className="w-5 h-5 rounded-full border border-white bg-brand-100 flex items-center justify-center">
<span className="text-[7px] font-bold text-brand-600">AG</span>
</div>
</div>
</div>
</div>
))}
{leads.filter(l => l.stage_id === stage.id).length === 0 && (
<div className="py-8 flex flex-col items-center justify-center border-2 border-dashed border-zinc-200 rounded-xl">
<span className="text-[9px] font-bold text-zinc-400 uppercase tracking-widest">
Vazio
</span>
</div>
)}
</div>
{/* Footer da Coluna */}
{campaignId && (
<div className="p-3 sticky bottom-0">
<button
onClick={(e) => {
e.stopPropagation();
handleAddLead(stage.id);
}}
className="w-full py-2 text-[10px] font-bold text-zinc-400 dark:text-zinc-500 hover:text-brand-600 dark:hover:text-brand-400 hover:bg-white dark:hover:bg-zinc-800 rounded-xl flex items-center justify-center gap-2 transition-all duration-200 border border-transparent hover:border-zinc-200 dark:hover:border-zinc-700"
>
<PlusIcon className="h-3.5 w-3.5" />
NOVO LEAD
</button>
</div>
)}
</div>
))}
{/* Modal de Adicionar/Editar Lead */}
<Modal
isOpen={isAddModalOpen || isLeadModalOpen}
onClose={() => {
setIsAddModalOpen(false);
setIsLeadModalOpen(false);
setSelectedLead(null);
}}
title={isAddModalOpen ? 'Novo Lead' : 'Detalhes do Lead'}
maxWidth="lg"
>
<form onSubmit={saveLead} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-xs font-bold text-zinc-500 uppercase ml-1">Nome</label>
<div className="relative">
<UserIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-400" />
<input
type="text"
required
className="w-full pl-10 pr-4 py-2.5 bg-white border border-zinc-200 rounded-xl text-sm focus:ring-2 focus:ring-brand-500/20 outline-none"
placeholder="Nome do lead"
value={formData.name}
onChange={e => setFormData({ ...formData, name: e.target.value })}
/>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-zinc-500 uppercase ml-1">E-mail</label>
<div className="relative">
<EnvelopeIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-400" />
<input
type="email"
className="w-full pl-10 pr-4 py-2.5 bg-white border border-zinc-200 rounded-xl text-sm focus:ring-2 focus:ring-brand-500/20 outline-none"
placeholder="email@exemplo.com"
value={formData.email}
onChange={e => setFormData({ ...formData, email: e.target.value })}
/>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-zinc-500 uppercase ml-1">Telefone</label>
<div className="relative">
<PhoneIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-400" />
<input
type="text"
className="w-full pl-10 pr-4 py-2.5 bg-white border border-zinc-200 rounded-xl text-sm focus:ring-2 focus:ring-brand-500/20 outline-none"
placeholder="(00) 00000-0000"
value={formData.phone}
onChange={e => setFormData({ ...formData, phone: e.target.value })}
/>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-zinc-500 uppercase ml-1">Tags (separadas por vírgula)</label>
<div className="relative">
<TagIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-400" />
<input
type="text"
className="w-full pl-10 pr-4 py-2.5 bg-white border border-zinc-200 rounded-xl text-sm focus:ring-2 focus:ring-brand-500/20 outline-none"
placeholder="vendas, urgente, frio"
value={formData.tags}
onChange={e => setFormData({ ...formData, tags: e.target.value })}
/>
</div>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-zinc-500 uppercase ml-1">Notas de Acompanhamento</label>
<div className="relative">
<ChatBubbleLeftRightIcon className="absolute left-3 top-3 h-4 w-4 text-zinc-400" />
<textarea
rows={4}
className="w-full pl-10 pr-4 py-2.5 bg-white border border-zinc-200 rounded-xl text-sm focus:ring-2 focus:ring-brand-500/20 outline-none resize-none"
placeholder="Descreva o histórico ou próximas ações..."
value={formData.notes}
onChange={e => setFormData({ ...formData, notes: e.target.value })}
/>
</div>
</div>
{selectedLead && (
<div className="p-4 bg-white rounded-xl border border-zinc-100 grid grid-cols-2 gap-4">
<div className="flex items-center gap-2 text-xs text-zinc-500">
<CalendarIcon className="h-4 w-4" />
<span>Criado em: {new Date(selectedLead.created_at || '').toLocaleDateString()}</span>
</div>
<div className="flex items-center gap-2 text-xs text-zinc-500">
<ClockIcon className="h-4 w-4" />
<span>ID: {selectedLead.id.slice(0, 8)}</span>
</div>
</div>
)}
<div className="flex justify-end gap-3 pt-4 border-t border-zinc-100 dark:border-zinc-800">
<button
type="button"
onClick={() => {
setIsAddModalOpen(false);
setIsLeadModalOpen(false);
setSelectedLead(null);
}}
className="px-6 py-2.5 text-sm font-bold text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 transition-colors"
>
CANCELAR
</button>
<button
type="submit"
disabled={isSaving}
className="px-8 py-2.5 bg-brand-600 hover:bg-brand-700 text-white text-sm font-bold rounded-xl shadow-lg shadow-brand-500/20 transition-all disabled:opacity-50 flex items-center gap-2"
>
{isSaving && <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>}
{isAddModalOpen ? 'CRIAR LEAD' : 'SALVAR ALTERAÇÕES'}
</button>
</div>
</form>
</Modal>
</div>
);
}