feat: versão 1.5 - CRM Beta com leads, funis, campanhas e portal do cliente
This commit is contained in:
545
front-end-agency/components/crm/KanbanBoard.tsx
Normal file
545
front-end-agency/components/crm/KanbanBoard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user