feat: versão 1.5 - CRM Beta com leads, funis, campanhas e portal do cliente
This commit is contained in:
624
front-end-agency/app/(agency)/crm/campanhas/[id]/page.tsx
Normal file
624
front-end-agency/app/(agency)/crm/campanhas/[id]/page.tsx
Normal file
@@ -0,0 +1,624 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment, useEffect, useState, use } from 'react';
|
||||
import { Tab, Menu, Transition } from '@headlessui/react';
|
||||
import {
|
||||
UserGroupIcon,
|
||||
InformationCircleIcon,
|
||||
CreditCardIcon,
|
||||
ArrowLeftIcon,
|
||||
PlusIcon,
|
||||
MagnifyingGlassIcon,
|
||||
FunnelIcon,
|
||||
EllipsisVerticalIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
EnvelopeIcon,
|
||||
PhoneIcon,
|
||||
TagIcon,
|
||||
CalendarIcon,
|
||||
UserIcon,
|
||||
ArrowDownTrayIcon,
|
||||
BriefcaseIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import Link from 'next/link';
|
||||
import { useToast } from '@/components/layout/ToastContext';
|
||||
import KanbanBoard from '@/components/crm/KanbanBoard';
|
||||
|
||||
interface Lead {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface Campaign {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
customer_id: string;
|
||||
customer_name: string;
|
||||
lead_count: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
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(' ');
|
||||
}
|
||||
|
||||
export default function CampaignDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = use(params);
|
||||
const toast = useToast();
|
||||
const [campaign, setCampaign] = useState<Campaign | null>(null);
|
||||
const [leads, setLeads] = useState<Lead[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [funnels, setFunnels] = useState<any[]>([]);
|
||||
const [selectedFunnelId, setSelectedFunnelId] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchCampaignDetails();
|
||||
fetchCampaignLeads();
|
||||
fetchFunnels();
|
||||
}, [id]);
|
||||
|
||||
const fetchFunnels = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/crm/funnels', {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setFunnels(data.funnels || []);
|
||||
if (data.funnels?.length > 0) {
|
||||
setSelectedFunnelId(data.funnels[0].id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching funnels:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCampaignDetails = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/crm/lists`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const found = data.lists?.find((l: Campaign) => l.id === id);
|
||||
if (found) {
|
||||
setCampaign(found);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching campaign details:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCampaignLeads = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/crm/lists/${id}/leads`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setLeads(data.leads || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching leads:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredLeads = leads.filter(lead =>
|
||||
(lead.name?.toLowerCase() || '').includes(searchTerm.toLowerCase()) ||
|
||||
(lead.email?.toLowerCase() || '').includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const handleExport = async (format: 'csv' | 'xlsx' | 'json') => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch(`/api/crm/leads/export?format=${format}&campaign_id=${id}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `leads-${campaign?.name || 'campaign'}.${format === 'xlsx' ? 'xlsx' : format}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && !campaign) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!campaign) {
|
||||
return (
|
||||
<div className="p-8 text-center">
|
||||
<h2 className="text-2xl font-bold text-zinc-900 dark:text-white">Campanha não encontrada</h2>
|
||||
<Link href="/crm/campanhas" className="mt-4 inline-flex items-center text-brand-500 hover:underline">
|
||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||
Voltar para Campanhas
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-[1600px] mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<Link
|
||||
href="/crm/campanhas"
|
||||
className="inline-flex items-center text-sm text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||
Voltar para Campanhas
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className="w-14 h-14 rounded-2xl flex items-center justify-center text-white shadow-lg"
|
||||
style={{ backgroundColor: campaign.color }}
|
||||
>
|
||||
<UserGroupIcon className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-zinc-900 dark:text-white tracking-tight">
|
||||
{campaign.name}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
{campaign.customer_name ? (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-brand-50 text-brand-700 dark:bg-brand-900/20 dark:text-brand-400 border border-brand-100 dark:border-brand-800/50">
|
||||
{campaign.customer_name}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 border border-zinc-200 dark:border-zinc-700">
|
||||
Geral
|
||||
</span>
|
||||
)}
|
||||
<span className="text-zinc-400 text-xs">•</span>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{leads.length} leads vinculados
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative inline-block text-left">
|
||||
<Menu>
|
||||
<Menu.Button className="inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors">
|
||||
<ArrowDownTrayIcon className="w-4 h-4" />
|
||||
Exportar
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 mt-2 w-48 origin-top-right divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none z-50 border border-zinc-200 dark:border-zinc-800">
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => handleExport('csv')}
|
||||
className={`${active ? 'bg-zinc-50 dark:bg-zinc-800' : ''} group flex w-full items-center rounded-lg px-3 py-2 text-sm text-zinc-700 dark:text-zinc-300`}
|
||||
>
|
||||
Exportar como CSV
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => handleExport('xlsx')}
|
||||
className={`${active ? 'bg-zinc-50 dark:bg-zinc-800' : ''} group flex w-full items-center rounded-lg px-3 py-2 text-sm text-zinc-700 dark:text-zinc-300`}
|
||||
>
|
||||
Exportar como Excel
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => handleExport('json')}
|
||||
className={`${active ? 'bg-zinc-50 dark:bg-zinc-800' : ''} group flex w-full items-center rounded-lg px-3 py-2 text-sm text-zinc-700 dark:text-zinc-300`}
|
||||
>
|
||||
Exportar como JSON
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
<button className="px-4 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors">
|
||||
Editar Campanha
|
||||
</button>
|
||||
<Link
|
||||
href={`/crm/leads/importar?campaign=${campaign.id}`}
|
||||
className="inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium text-white rounded-lg hover:opacity-90 transition-opacity"
|
||||
style={{ background: 'var(--gradient)' }}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
Importar Leads
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tab.Group>
|
||||
<Tab.List className="flex space-x-1 rounded-xl bg-zinc-100 dark:bg-zinc-800/50 p-1 max-w-lg">
|
||||
<Tab className={({ selected }) =>
|
||||
classNames(
|
||||
'w-full rounded-lg py-2.5 text-sm font-medium leading-5 transition-all duration-200',
|
||||
'ring-white ring-opacity-60 ring-offset-2 ring-offset-brand-400 focus:outline-none',
|
||||
selected
|
||||
? 'bg-white dark:bg-zinc-900 text-brand-600 dark:text-brand-400 shadow-sm'
|
||||
: 'text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 hover:bg-white/[0.12]'
|
||||
)
|
||||
}>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<FunnelIcon className="w-4 h-4" />
|
||||
Monitoramento
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab className={({ selected }) =>
|
||||
classNames(
|
||||
'w-full rounded-lg py-2.5 text-sm font-medium leading-5 transition-all duration-200',
|
||||
'ring-white ring-opacity-60 ring-offset-2 ring-offset-brand-400 focus:outline-none',
|
||||
selected
|
||||
? 'bg-white dark:bg-zinc-900 text-brand-600 dark:text-brand-400 shadow-sm'
|
||||
: 'text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 hover:bg-white/[0.12]'
|
||||
)
|
||||
}>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<UserGroupIcon className="w-4 h-4" />
|
||||
Leads
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab className={({ selected }) =>
|
||||
classNames(
|
||||
'w-full rounded-lg py-2.5 text-sm font-medium leading-5 transition-all duration-200',
|
||||
'ring-white ring-opacity-60 ring-offset-2 ring-offset-brand-400 focus:outline-none',
|
||||
selected
|
||||
? 'bg-white dark:bg-zinc-900 text-brand-600 dark:text-brand-400 shadow-sm'
|
||||
: 'text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 hover:bg-white/[0.12]'
|
||||
)
|
||||
}>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<InformationCircleIcon className="w-4 h-4" />
|
||||
Informações
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab className={({ selected }) =>
|
||||
classNames(
|
||||
'w-full rounded-lg py-2.5 text-sm font-medium leading-5 transition-all duration-200',
|
||||
'ring-white ring-opacity-60 ring-offset-2 ring-offset-brand-400 focus:outline-none',
|
||||
selected
|
||||
? 'bg-white dark:bg-zinc-900 text-brand-600 dark:text-brand-400 shadow-sm'
|
||||
: 'text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 hover:bg-white/[0.12]'
|
||||
)
|
||||
}>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<CreditCardIcon className="w-4 h-4" />
|
||||
Pagamentos
|
||||
</div>
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
|
||||
<Tab.Panels className="mt-6">
|
||||
{/* Monitoramento Panel */}
|
||||
<Tab.Panel className="space-y-6">
|
||||
{funnels.length > 0 ? (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-2 bg-brand-50 dark:bg-brand-900/20 rounded-lg">
|
||||
<FunnelIcon className="h-5 w-5 text-brand-600 dark:text-brand-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-zinc-900 dark:text-white uppercase tracking-wider">Monitoramento de Leads</h3>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">Acompanhe o progresso dos leads desta campanha no funil.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="text-xs font-bold text-zinc-500 uppercase">Funil:</label>
|
||||
<select
|
||||
value={selectedFunnelId}
|
||||
onChange={(e) => setSelectedFunnelId(e.target.value)}
|
||||
className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg px-3 py-1.5 text-sm font-medium focus:ring-2 focus:ring-brand-500/20 outline-none"
|
||||
>
|
||||
{funnels.map(f => (
|
||||
<option key={f.id} value={f.id}>{f.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-[600px]">
|
||||
<KanbanBoard funnelId={selectedFunnelId} campaignId={id} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-64 bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 text-center p-8">
|
||||
<div className="w-16 h-16 bg-zinc-50 dark:bg-zinc-800 rounded-full flex items-center justify-center mb-4">
|
||||
<FunnelIcon className="w-8 h-8 text-zinc-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-zinc-900 dark:text-white mb-1">
|
||||
Nenhum funil configurado
|
||||
</h3>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 max-w-sm mx-auto">
|
||||
Configure um funil de vendas para começar a monitorar os leads desta campanha.
|
||||
</p>
|
||||
<Link href="/crm/funis" className="mt-4 text-brand-600 font-medium hover:underline">
|
||||
Configurar Funis
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
|
||||
{/* Leads Panel */}
|
||||
<Tab.Panel className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="relative w-full lg:w-96">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-zinc-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full pl-10 pr-3 py-2 border border-zinc-200 dark:border-zinc-700 rounded-lg leading-5 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-1 focus:ring-[var(--brand-color)] focus:border-[var(--brand-color)] sm:text-sm transition duration-150 ease-in-out"
|
||||
placeholder="Buscar leads nesta campanha..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors">
|
||||
<FunnelIcon className="w-4 h-4" />
|
||||
Filtros
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredLeads.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 text-center p-8">
|
||||
<div className="w-16 h-16 bg-zinc-50 dark:bg-zinc-800 rounded-full flex items-center justify-center mb-4">
|
||||
<UserGroupIcon className="w-8 h-8 text-zinc-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-zinc-900 dark:text-white mb-1">
|
||||
Nenhum lead encontrado
|
||||
</h3>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 max-w-sm mx-auto">
|
||||
{searchTerm ? 'Nenhum lead corresponde à sua busca.' : 'Esta campanha ainda não possui leads vinculados.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredLeads.map((lead) => (
|
||||
<div key={lead.id} className="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-4 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-zinc-900 dark:text-white truncate">
|
||||
{lead.name || 'Sem nome'}
|
||||
</h3>
|
||||
<span className={classNames(
|
||||
'inline-block px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider rounded-full mt-1',
|
||||
STATUS_OPTIONS.find(s => s.value === lead.status)?.color || 'bg-zinc-100 text-zinc-800'
|
||||
)}>
|
||||
{STATUS_OPTIONS.find(s => s.value === lead.status)?.label || lead.status}
|
||||
</span>
|
||||
</div>
|
||||
<button className="p-1 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded text-zinc-400">
|
||||
<EllipsisVerticalIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
{lead.email && (
|
||||
<div className="flex items-center gap-2 text-zinc-600 dark:text-zinc-400">
|
||||
<EnvelopeIcon className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="truncate">{lead.email}</span>
|
||||
</div>
|
||||
)}
|
||||
{lead.phone && (
|
||||
<div className="flex items-center gap-2 text-zinc-600 dark:text-zinc-400">
|
||||
<PhoneIcon className="w-4 h-4 flex-shrink-0" />
|
||||
<span>{lead.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-zinc-100 dark:border-zinc-800 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 text-[10px] text-zinc-400 uppercase font-bold tracking-widest">
|
||||
<CalendarIcon className="w-3 h-3" />
|
||||
{new Date(lead.created_at).toLocaleDateString('pt-BR')}
|
||||
</div>
|
||||
<button className="text-xs font-semibold text-brand-600 dark:text-brand-400 hover:underline">
|
||||
Ver Detalhes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
|
||||
{/* Info Panel */}
|
||||
<Tab.Panel>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 overflow-hidden">
|
||||
<div className="p-6 border-b border-zinc-100 dark:border-zinc-800">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Detalhes da Campanha</h3>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-zinc-400 uppercase tracking-widest mb-2">Descrição</label>
|
||||
<p className="text-zinc-600 dark:text-zinc-400">
|
||||
{campaign.description || 'Nenhuma descrição fornecida para esta campanha.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-zinc-400 uppercase tracking-widest mb-2">Data de Criação</label>
|
||||
<div className="flex items-center gap-2 text-zinc-900 dark:text-white">
|
||||
<CalendarIcon className="w-5 h-5 text-zinc-400" />
|
||||
{new Date(campaign.created_at).toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' })}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-zinc-400 uppercase tracking-widest mb-2">Cor de Identificação</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded-full shadow-sm" style={{ backgroundColor: campaign.color }}></div>
|
||||
<span className="text-zinc-900 dark:text-white font-medium">{campaign.color}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 overflow-hidden">
|
||||
<div className="p-6 border-b border-zinc-100 dark:border-zinc-800">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Configurações de Integração</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="bg-zinc-50 dark:bg-zinc-800/50 rounded-xl p-4 border border-zinc-200 dark:border-zinc-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<InformationCircleIcon className="w-5 h-5 text-brand-500 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-zinc-900 dark:text-white">Webhook de Entrada</h4>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
Use este endpoint para enviar leads automaticamente de outras plataformas (Typeform, Elementor, etc).
|
||||
</p>
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<code className="flex-1 block p-2 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded text-[10px] text-zinc-600 dark:text-zinc-400 overflow-x-auto">
|
||||
https://api.aggios.app/v1/webhooks/leads/{campaign.id}
|
||||
</code>
|
||||
<button className="p-2 text-zinc-400 hover:text-brand-500 transition-colors">
|
||||
<TagIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 p-6">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white mb-4">Cliente Responsável</h3>
|
||||
{campaign.customer_id ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center text-brand-600 dark:text-brand-400">
|
||||
<UserIcon className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-zinc-900 dark:text-white">{campaign.customer_name}</p>
|
||||
<p className="text-xs text-zinc-500">Cliente Ativo</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href={`/crm/clientes?id=${campaign.customer_id}`}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-zinc-50 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 text-xs font-bold rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
|
||||
>
|
||||
<BriefcaseIcon className="w-4 h-4" />
|
||||
Ver Perfil do Cliente
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-sm text-zinc-500">Esta é uma campanha geral da agência.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-brand-500 to-brand-600 rounded-2xl p-6 text-white shadow-lg">
|
||||
<h3 className="text-lg font-bold mb-2">Resumo de Performance</h3>
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex justify-between items-end">
|
||||
<span className="text-xs text-brand-100">Total de Leads</span>
|
||||
<span className="text-2xl font-bold">{leads.length}</span>
|
||||
</div>
|
||||
<div className="w-full bg-white/20 rounded-full h-1.5">
|
||||
<div className="bg-white h-1.5 rounded-full" style={{ width: '65%' }}></div>
|
||||
</div>
|
||||
<p className="text-[10px] text-brand-100">
|
||||
+12% em relação ao mês passado
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
|
||||
{/* Payments Panel */}
|
||||
<Tab.Panel>
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 overflow-hidden">
|
||||
<div className="p-12 text-center">
|
||||
<div className="w-20 h-20 bg-zinc-50 dark:bg-zinc-800 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<CreditCardIcon className="w-10 h-10 text-zinc-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-zinc-900 dark:text-white mb-2">Módulo de Pagamentos</h3>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 max-w-md mx-auto mb-8">
|
||||
Em breve você poderá gerenciar orçamentos, faturas e pagamentos vinculados diretamente a esta campanha.
|
||||
</p>
|
||||
<button className="px-6 py-3 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 font-bold rounded-xl hover:opacity-90 transition-opacity">
|
||||
Solicitar Acesso Antecipado
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
622
front-end-agency/app/(agency)/crm/campanhas/page.tsx
Normal file
622
front-end-agency/app/(agency)/crm/campanhas/page.tsx
Normal file
@@ -0,0 +1,622 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import ConfirmDialog from '@/components/layout/ConfirmDialog';
|
||||
import { useToast } from '@/components/layout/ToastContext';
|
||||
import Pagination from '@/components/layout/Pagination';
|
||||
import { useCRMFilter } from '@/contexts/CRMFilterContext';
|
||||
import { SolutionGuard } from '@/components/auth/SolutionGuard';
|
||||
import SearchableSelect from '@/components/form/SearchableSelect';
|
||||
import {
|
||||
ListBulletIcon,
|
||||
TrashIcon,
|
||||
PencilIcon,
|
||||
EllipsisVerticalIcon,
|
||||
MagnifyingGlassIcon,
|
||||
PlusIcon,
|
||||
XMarkIcon,
|
||||
UserGroupIcon,
|
||||
EyeIcon,
|
||||
CalendarIcon,
|
||||
RectangleStackIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
interface List {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
customer_id: string;
|
||||
customer_name: string;
|
||||
funnel_id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
customer_count: number;
|
||||
lead_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface Funnel {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Customer {
|
||||
id: string;
|
||||
name: string;
|
||||
company: string;
|
||||
}
|
||||
|
||||
const COLORS = [
|
||||
{ name: 'Azul', value: '#3B82F6' },
|
||||
{ name: 'Verde', value: '#10B981' },
|
||||
{ name: 'Roxo', value: '#8B5CF6' },
|
||||
{ name: 'Rosa', value: '#EC4899' },
|
||||
{ name: 'Laranja', value: '#F97316' },
|
||||
{ name: 'Amarelo', value: '#EAB308' },
|
||||
{ name: 'Vermelho', value: '#EF4444' },
|
||||
{ name: 'Cinza', value: '#6B7280' },
|
||||
];
|
||||
|
||||
function CampaignsContent() {
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const { selectedCustomerId } = useCRMFilter();
|
||||
console.log('📢 CampaignsPage render, selectedCustomerId:', selectedCustomerId);
|
||||
|
||||
const [lists, setLists] = useState<List[]>([]);
|
||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
||||
const [funnels, setFunnels] = useState<Funnel[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingList, setEditingList] = useState<List | null>(null);
|
||||
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [listToDelete, setListToDelete] = useState<string | null>(null);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 10;
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
color: COLORS[0].value,
|
||||
customer_id: '',
|
||||
funnel_id: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log('🔄 CampaignsPage useEffect triggered by selectedCustomerId:', selectedCustomerId);
|
||||
fetchLists();
|
||||
fetchCustomers();
|
||||
fetchFunnels();
|
||||
}, [selectedCustomerId]);
|
||||
|
||||
const fetchFunnels = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/crm/funnels', {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setFunnels(data.funnels || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching funnels:', error);
|
||||
}
|
||||
};
|
||||
|
||||
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 fetchLists = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const url = selectedCustomerId
|
||||
? `/api/crm/lists?customer_id=${selectedCustomerId}`
|
||||
: '/api/crm/lists';
|
||||
|
||||
console.log(`📊 Fetching campaigns from: ${url}`);
|
||||
|
||||
const response = await fetch(url, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('📊 Campaigns data received:', data);
|
||||
setLists(data.lists || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching campaigns:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const url = editingList
|
||||
? `/api/crm/lists/${editingList.id}`
|
||||
: '/api/crm/lists';
|
||||
|
||||
const method = editingList ? '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) {
|
||||
toast.success(
|
||||
editingList ? 'Campanha atualizada' : 'Campanha criada',
|
||||
editingList ? 'A campanha foi atualizada com sucesso.' : 'A nova campanha foi criada com sucesso.'
|
||||
);
|
||||
fetchLists();
|
||||
handleCloseModal();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
toast.error('Erro', error.message || 'Não foi possível salvar a campanha.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving campaign:', error);
|
||||
toast.error('Erro', 'Ocorreu um erro ao salvar a campanha.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewCampaign = () => {
|
||||
setEditingList(null);
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
color: COLORS[0].value,
|
||||
customer_id: selectedCustomerId || '',
|
||||
funnel_id: '',
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (list: List) => {
|
||||
setEditingList(list);
|
||||
setFormData({
|
||||
name: list.name,
|
||||
description: list.description,
|
||||
color: list.color,
|
||||
customer_id: list.customer_id || '',
|
||||
funnel_id: list.funnel_id || '',
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (id: string) => {
|
||||
setListToDelete(id);
|
||||
setConfirmOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!listToDelete) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/crm/lists/${listToDelete}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setLists(lists.filter(l => l.id !== listToDelete));
|
||||
toast.success('Campanha excluída', 'A campanha foi excluída com sucesso.');
|
||||
} else {
|
||||
toast.error('Erro ao excluir', 'Não foi possível excluir a campanha.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting campaign:', error);
|
||||
toast.error('Erro ao excluir', 'Ocorreu um erro ao excluir a campanha.');
|
||||
} finally {
|
||||
setConfirmOpen(false);
|
||||
setListToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setEditingList(null);
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
color: COLORS[0].value,
|
||||
customer_id: '',
|
||||
funnel_id: '',
|
||||
});
|
||||
};
|
||||
|
||||
const filteredLists = lists.filter((list) => {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
return (
|
||||
(list.name?.toLowerCase() || '').includes(searchLower) ||
|
||||
(list.description?.toLowerCase() || '').includes(searchLower)
|
||||
);
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(filteredLists.length / itemsPerPage);
|
||||
const paginatedLists = filteredLists.slice(
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
currentPage * itemsPerPage
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-[1600px] mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-zinc-900 dark:text-white tracking-tight">Campanhas</h1>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
Organize seus leads e rastreie a origem de cada um
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleNewCampaign}
|
||||
className="inline-flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium text-white rounded-lg hover:opacity-90 transition-opacity"
|
||||
style={{ background: 'var(--gradient)' }}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
Nova Campanha
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative w-full lg:w-96">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-zinc-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full pl-10 pr-3 py-2 border border-zinc-200 dark:border-zinc-700 rounded-lg leading-5 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-1 focus:ring-[var(--brand-color)] focus:border-[var(--brand-color)] sm:text-sm transition duration-150 ease-in-out"
|
||||
placeholder="Buscar campanhas..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64 bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--brand-color)]"></div>
|
||||
</div>
|
||||
) : filteredLists.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 text-center p-8">
|
||||
<div className="w-16 h-16 bg-zinc-50 dark:bg-zinc-800 rounded-full flex items-center justify-center mb-4">
|
||||
<ListBulletIcon className="w-8 h-8 text-zinc-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-zinc-900 dark:text-white mb-1">
|
||||
Nenhuma campanha encontrada
|
||||
</h3>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 max-w-sm mx-auto">
|
||||
{searchTerm ? 'Nenhuma campanha corresponde à sua busca.' : 'Comece criando sua primeira campanha.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-zinc-50/50 dark:bg-zinc-800/50 border-b border-zinc-200 dark:border-zinc-800">
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Campanha</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Cliente Vinculado</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Leads</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Criada em</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
{paginatedLists.map((list) => (
|
||||
<tr
|
||||
key={list.id}
|
||||
onClick={() => router.push(`/crm/campanhas/${list.id}`)}
|
||||
className="group hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors cursor-pointer"
|
||||
>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center text-white shadow-sm"
|
||||
style={{ backgroundColor: list.color }}
|
||||
>
|
||||
<ListBulletIcon className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-zinc-900 dark:text-white">
|
||||
{list.name}
|
||||
</div>
|
||||
{list.description && (
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400 truncate max-w-[200px]">
|
||||
{list.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{list.customer_name ? (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-brand-50 text-brand-700 dark:bg-brand-900/20 dark:text-brand-400 border border-brand-100 dark:border-brand-800/50">
|
||||
{list.customer_name}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 border border-zinc-200 dark:border-zinc-700">
|
||||
Geral
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<UserGroupIcon className="w-4 h-4 text-zinc-400" />
|
||||
<span className="text-sm font-bold text-zinc-900 dark:text-white">{list.lead_count || 0}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<CalendarIcon className="w-4 h-4 text-zinc-400" />
|
||||
{new Date(list.created_at).toLocaleDateString('pt-BR')}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right">
|
||||
<div className="flex items-center justify-end gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
onClick={() => router.push(`/crm/campanhas/${list.id}`)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold text-brand-600 dark:text-brand-400 bg-brand-50 dark:bg-brand-900/20 rounded-lg hover:bg-brand-100 dark:hover:bg-brand-900/40 transition-all"
|
||||
title="Monitorar Leads"
|
||||
>
|
||||
<RectangleStackIcon className="w-4 h-4" />
|
||||
MONITORAR
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push(`/crm/campanhas/${list.id}`)}
|
||||
className="p-2 text-zinc-400 hover:text-brand-500 dark:hover:text-brand-400 transition-colors"
|
||||
title="Ver Detalhes"
|
||||
>
|
||||
<EyeIcon className="w-5 h-5" />
|
||||
</button>
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<Menu.Button className="p-2 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors">
|
||||
<EllipsisVerticalIcon className="w-5 h-5" />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 mt-2 w-48 origin-top-right divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none z-50 border border-zinc-200 dark:border-zinc-800">
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => handleEdit(list)}
|
||||
className={`${active ? 'bg-zinc-50 dark:bg-zinc-800' : ''
|
||||
} group flex w-full items-center rounded-lg px-3 py-2 text-sm text-zinc-700 dark:text-zinc-300`}
|
||||
>
|
||||
<PencilIcon className="mr-2 h-4 w-4 text-zinc-400" />
|
||||
Editar
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => handleDeleteClick(list.id)}
|
||||
className={`${active ? 'bg-red-50 dark:bg-red-900/20' : ''
|
||||
} group flex w-full items-center rounded-lg px-3 py-2 text-sm text-red-600 dark:text-red-400`}
|
||||
>
|
||||
<TrashIcon className="mr-2 h-4 w-4" />
|
||||
Excluir
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
totalItems={filteredLists.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<div className="fixed inset-0 bg-zinc-900/40 backdrop-blur-sm transition-opacity" onClick={handleCloseModal}></div>
|
||||
|
||||
<div className="relative transform overflow-hidden rounded-2xl bg-white dark:bg-zinc-900 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg border border-zinc-200 dark:border-zinc-800">
|
||||
<div className="absolute right-0 top-0 pr-6 pt-6">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg p-1.5 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
onClick={handleCloseModal}
|
||||
>
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 sm:p-8">
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div
|
||||
className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-xl shadow-lg"
|
||||
style={{ backgroundColor: formData.color }}
|
||||
>
|
||||
<ListBulletIcon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-zinc-900 dark:text-white">
|
||||
{editingList ? 'Editar Campanha' : 'Nova Campanha'}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{editingList ? 'Atualize as informações da campanha.' : 'Crie uma nova campanha para organizar seus leads.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SearchableSelect
|
||||
label="Cliente Vinculado"
|
||||
options={customers.map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
subtitle: c.company || undefined
|
||||
}))}
|
||||
value={formData.customer_id}
|
||||
onChange={(value) => setFormData({ ...formData, customer_id: value || '' })}
|
||||
placeholder="Nenhum cliente (Geral)"
|
||||
emptyText="Nenhum cliente encontrado"
|
||||
helperText="Vincule esta campanha a um cliente específico para melhor organização."
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
|
||||
Nome da Campanha *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="Ex: Black Friday 2025"
|
||||
required
|
||||
className="w-full px-3 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:border-transparent transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
|
||||
Descrição
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="Descreva o propósito desta campanha"
|
||||
rows={3}
|
||||
className="w-full px-3 py-2.5 border border-zinc-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[var(--brand-color)] focus:border-transparent resize-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-3">
|
||||
Cor
|
||||
</label>
|
||||
<div className="grid grid-cols-8 gap-2">
|
||||
{COLORS.map((color) => (
|
||||
<button
|
||||
key={color.value}
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, color: color.value })}
|
||||
className={`w-10 h-10 rounded-lg transition-all ${formData.color === color.value
|
||||
? 'ring-2 ring-offset-2 ring-zinc-400 dark:ring-zinc-600 scale-110'
|
||||
: 'hover:scale-105'
|
||||
}`}
|
||||
style={{ backgroundColor: color.value }}
|
||||
title={color.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchableSelect
|
||||
label="Funil de Vendas"
|
||||
options={funnels.map(f => ({
|
||||
id: f.id,
|
||||
name: f.name
|
||||
}))}
|
||||
value={formData.funnel_id}
|
||||
onChange={(value) => setFormData({ ...formData, funnel_id: value || '' })}
|
||||
placeholder="Nenhum funil selecionado"
|
||||
emptyText="Nenhum funil encontrado. Crie um funil primeiro."
|
||||
helperText="Leads desta campanha seguirão as etapas do funil selecionado."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-zinc-200 dark:border-zinc-700 flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCloseModal}
|
||||
className="flex-1 px-4 py-2.5 border border-zinc-200 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300 font-medium rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 px-4 py-2.5 text-white font-medium rounded-lg transition-all shadow-lg hover:shadow-xl"
|
||||
style={{ background: 'var(--gradient)' }}
|
||||
>
|
||||
{editingList ? 'Atualizar' : 'Criar Campanha'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={confirmOpen}
|
||||
onClose={() => {
|
||||
setConfirmOpen(false);
|
||||
setListToDelete(null);
|
||||
}}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="Excluir Campanha"
|
||||
message="Tem certeza que deseja excluir esta campanha? Os leads não serão excluídos, apenas removidos da campanha."
|
||||
confirmText="Excluir"
|
||||
cancelText="Cancelar"
|
||||
variant="danger"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CampaignsPage() {
|
||||
return (
|
||||
<SolutionGuard requiredSolution="crm">
|
||||
<CampaignsContent />
|
||||
</SolutionGuard>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user