fix(erp): enable erp pages and menu items
This commit is contained in:
358
front-end-agency/app/(agency)/erp/entidades/page.tsx
Normal file
358
front-end-agency/app/(agency)/erp/entidades/page.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user