Files
aggios.app/front-end-agency/app/(agency)/erp/entidades/page.tsx
2025-12-29 17:23:59 -03:00

359 lines
16 KiB
TypeScript

'use client';
import React, { useState, useEffect, Fragment } from 'react';
import {
PlusIcon,
MagnifyingGlassIcon,
UserIcon,
BriefcaseIcon,
TrashIcon,
PencilSquareIcon,
ArrowRightIcon,
XMarkIcon,
} from '@heroicons/react/24/outline';
import { erpApi, Entity } from '@/lib/api-erp';
import { useToast } from '@/components/layout/ToastContext';
import {
StatsCard,
DataTable,
Input,
Card,
CustomSelect,
PageHeader,
BulkActionBar,
ConfirmDialog,
} from "@/components/ui";
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
import { SolutionGuard } from '@/components/auth/SolutionGuard';
import Link from 'next/link';
interface CRMCustomer {
id: string;
name: string;
email: string;
company: string;
phone: string;
}
function EntidadesContent() {
const toast = useToast();
const [entities, setEntities] = useState<Entity[]>([]);
const [crmCustomers, setCrmCustomers] = useState<CRMCustomer[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false);
const [entityToDelete, setEntityToDelete] = useState<string | null>(null);
const [editingEntity, setEditingEntity] = useState<Partial<Entity> | null>(null);
const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
const [formData, setFormData] = useState<Partial<Entity>>({
name: '',
type: 'supplier',
document: '',
email: '',
phone: '',
address: '',
});
useEffect(() => {
fetchAllData();
}, []);
const fetchAllData = async (silent = false) => {
try {
if (!silent) setLoading(true);
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
const [erpData, crmResp] = await Promise.all([
erpApi.getEntities(),
fetch('/api/crm/customers', {
headers: { 'Authorization': `Bearer ${token}` }
}).then(res => res.ok ? res.json() : [])
]);
setEntities(erpData || []);
setCrmCustomers(crmResp?.customers || crmResp || []);
} catch (error) {
toast.error('Erro ao carregar', 'Não foi possível carregar os dados financeiros');
} finally {
setLoading(false);
setSelectedIds([]);
}
};
const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
try {
if (editingEntity?.id) {
await erpApi.updateEntity(editingEntity.id, formData);
toast.success('Cadastro atualizado!');
} else {
await erpApi.createEntity(formData);
toast.success('Entidade cadastrada!');
}
setIsModalOpen(false);
setEditingEntity(null);
await fetchAllData();
} catch (error) {
toast.error('Erro ao salvar');
}
};
const handleConfirmDelete = async () => {
if (!entityToDelete) return;
const originalEntities = [...entities];
const idToDelete = String(entityToDelete);
// Dynamic: remove instantly
setEntities(prev => prev.filter(e => String(e.id) !== idToDelete));
try {
await erpApi.deleteEntity(idToDelete);
toast.success('Exclusão completa', 'A entidade foi removida com sucesso.');
setTimeout(() => fetchAllData(true), 500);
} catch (error) {
setEntities(originalEntities);
toast.error('Erro ao excluir', 'Ocorreu um erro ao excluir a entidade.');
} finally {
setConfirmOpen(false);
setEntityToDelete(null);
}
};
const handleBulkDelete = async () => {
if (selectedIds.length === 0) return;
setBulkConfirmOpen(true);
};
const handleConfirmBulkDelete = async () => {
const erpIds = selectedIds.filter(id => {
const item = combinedData.find(d => d.id === id);
return item?.source === 'ERP';
}).map(String);
if (erpIds.length === 0) return;
const originalEntities = [...entities];
// Dynamic: remove instantly
setEntities(prev => prev.filter(e => !erpIds.includes(String(e.id))));
try {
await Promise.all(erpIds.map(id => erpApi.deleteEntity(id)));
toast.success('Exclusão completa', `${erpIds.length} entidades excluídas com sucesso.`);
setTimeout(() => fetchAllData(true), 500);
} catch (error) {
setEntities(originalEntities);
toast.error('Erro ao excluir', 'Ocorreu um erro ao excluir algumas entidades.');
} finally {
setBulkConfirmOpen(false);
setSelectedIds([]);
}
};
// Combine both for searching
const combinedData = [
...crmCustomers.map(c => ({
id: c.id,
name: c.name,
email: c.email,
phone: c.phone,
source: 'CRM' as const,
type: 'Cliente (CRM)',
original: c
})),
...entities.map(e => ({
id: e.id,
name: e.name,
email: e.email,
phone: e.phone,
source: 'ERP' as const,
type: e.type === 'customer' ? 'Cliente (ERP)' : (e.type === 'supplier' ? 'Fornecedor (ERP)' : 'Ambos'),
original: e
}))
];
const filteredData = combinedData.filter(d =>
d.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(d.email || '').toLowerCase().includes(searchTerm.toLowerCase())
);
if (loading && combinedData.length === 0) return (
<div className="p-6 max-w-[1600px] mx-auto">
<div className="text-center py-20 text-zinc-500">Carregando parceiros de negócio...</div>
</div>
);
return (
<div className="p-6 max-w-[1600px] mx-auto space-y-6">
<PageHeader
title="Parceiros de Negócio"
description="Gerencie seus Clientes (CRM) e Fornecedores (ERP) em um único lugar."
primaryAction={{
label: "Novo Fornecedor",
icon: <PlusIcon className="w-5 h-5" />,
onClick: () => {
setEditingEntity(null);
setFormData({ name: '', type: 'supplier', document: '', email: '', phone: '', address: '' });
setIsModalOpen(true);
}
}}
secondaryAction={{
label: "Ir para CRM Clientes",
icon: <UserIcon className="w-5 h-5" />,
onClick: () => window.location.href = '/crm/clientes'
}}
/>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<StatsCard
title="Clientes no CRM"
value={crmCustomers.length}
icon={<UserIcon className="w-6 h-6 text-emerald-500" />}
/>
<StatsCard
title="Fornecedores no ERP"
value={entities.filter(e => e.type === 'supplier' || e.type === 'both').length}
icon={<BriefcaseIcon className="w-6 h-6 text-purple-500" />}
/>
</div>
<Card noPadding>
<div className="p-4 border-b border-zinc-100 dark:border-zinc-800">
<div className="max-w-md">
<Input
placeholder="Pesquisar por nome ou e-mail em toda a base..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
leftIcon={<MagnifyingGlassIcon className="w-5 h-5 text-zinc-400" />}
/>
</div>
</div>
<DataTable
selectable
isLoading={loading}
selectedIds={selectedIds}
onSelectionChange={setSelectedIds}
data={filteredData}
columns={[
{
header: 'Nome / Razão Social',
accessor: (row) => (
<div className="flex flex-col">
<span className="font-bold text-zinc-900 dark:text-white">{row.name}</span>
<div className="flex items-center gap-2 mt-0.5">
<span className={`text-[10px] px-1.5 py-0.5 rounded font-black uppercase ${row.source === 'CRM' ? 'bg-emerald-50 text-emerald-700' : 'bg-purple-50 text-purple-700'}`}>
{row.source}
</span>
<span className="text-[10px] text-zinc-400 font-medium">{row.type}</span>
</div>
</div>
)
},
{
header: 'E-mail',
accessor: (row) => row.email || '-'
},
{
header: 'Telefone',
accessor: (row) => row.phone || '-'
},
{
header: '',
className: 'text-right',
accessor: (row) => (
<div className="flex justify-end gap-2">
{row.source === 'ERP' ? (
<>
<button onClick={(e) => { e.stopPropagation(); setEditingEntity(row.original as Entity); setFormData(row.original as Entity); setIsModalOpen(true); }} className="p-2 text-zinc-400 hover:text-brand-500">
<PencilSquareIcon className="w-5 h-5" />
</button>
<button onClick={(e) => { e.stopPropagation(); setEntityToDelete(row.id as string); setConfirmOpen(true); }} className="p-2 text-zinc-400 hover:text-rose-500">
<TrashIcon className="w-5 h-5" />
</button>
</>
) : (
<Link href={`/crm/clientes?id=${row.id}`} onClick={(e) => e.stopPropagation()} className="p-2 text-zinc-400 hover:text-brand-500 flex items-center gap-1 text-xs font-bold">
Ver no CRM <ArrowRightIcon className="w-4 h-4" />
</Link>
)}
</div>
)
}
]}
/>
</Card>
</div>
{/* Modal de Cadastro ERP (Fornecedores) */}
<Transition show={isModalOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={() => setIsModalOpen(false)}>
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0">
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm" />
</TransitionChild>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100" leave="ease-in duration-200" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95">
<DialogPanel className="w-full max-w-lg transform overflow-hidden rounded-2xl bg-white dark:bg-zinc-900 p-8 shadow-xl transition-all border border-zinc-200 dark:border-zinc-800">
<DialogTitle className="text-xl font-bold mb-6">{editingEntity ? 'Editar Fornecedor' : 'Novo Fornecedor / Outros'}</DialogTitle>
<form onSubmit={handleSave} className="space-y-4">
<Input label="Nome / Razão Social" value={formData.name} onChange={e => setFormData({ ...formData, name: e.target.value })} required />
<CustomSelect label="Tipo" options={[{ label: 'Fornecedor', value: 'supplier' }, { label: 'Cliente (ERP Avulso)', value: 'customer' }, { label: 'Ambos', value: 'both' }]} value={formData.type || 'supplier'} onChange={val => setFormData({ ...formData, type: val as any })} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input label="Documento (CNPJ/CPF)" value={formData.document} onChange={e => setFormData({ ...formData, document: e.target.value })} />
<Input label="Telefone" value={formData.phone} onChange={e => setFormData({ ...formData, phone: e.target.value })} />
</div>
<Input label="E-mail" type="email" value={formData.email} onChange={e => setFormData({ ...formData, email: e.target.value })} />
<Input label="Endereço Completo" value={formData.address} onChange={e => setFormData({ ...formData, address: e.target.value })} />
<div className="flex justify-end gap-3 mt-8 pt-4 border-t border-zinc-100 dark:border-zinc-800">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-zinc-500 font-bold">Cancelar</button>
<button type="submit" className="px-8 py-2 text-white rounded-xl font-bold" style={{ background: 'var(--gradient)' }}>Salvar Cadastro</button>
</div>
</form>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
<ConfirmDialog isOpen={confirmOpen} onClose={() => setConfirmOpen(false)} onConfirm={handleConfirmDelete} title="Excluir Cadastro" message="Tem certeza? Isso pode afetar lançamentos vinculados a esta entidade no ERP." confirmText="Excluir" />
<ConfirmDialog
isOpen={bulkConfirmOpen}
onClose={() => setBulkConfirmOpen(false)}
onConfirm={handleConfirmBulkDelete}
title="Excluir Itens Selecionados"
message={`Tem certeza que deseja excluir as entidades selecionadas? Esta ação não pode ser desfeita.`}
confirmText="Excluir Tudo"
variant="danger"
/>
<BulkActionBar
selectedCount={selectedIds.length}
onClearSelection={() => setSelectedIds([])}
actions={[
{
label: "Excluir Selecionados",
icon: <TrashIcon className="w-5 h-5" />,
onClick: handleBulkDelete,
variant: 'danger'
}
]}
/>
</div>
);
}
export default function EntidadesPage() {
return (
<SolutionGuard requiredSolution="erp">
<EntidadesContent />
</SolutionGuard>
);
}