fix(erp): enable erp pages and menu items

This commit is contained in:
Erik Silva
2025-12-29 17:23:59 -03:00
parent e124a64a5d
commit adbff9bb1e
13990 changed files with 1110936 additions and 59 deletions

View File

@@ -2,7 +2,7 @@
import { Fragment, useEffect, useMemo, useState } from 'react';
import { Menu, Transition, Tab } from '@headlessui/react';
import ConfirmDialog from '@/components/layout/ConfirmDialog';
import { ConfirmDialog, BulkActionBar } from "@/components/ui";
import { useToast } from '@/components/layout/ToastContext';
import {
UserIcon,
@@ -83,12 +83,14 @@ export default function CustomersPage() {
const [editingCustomer, setEditingCustomer] = useState<Customer | null>(null);
const [confirmOpen, setConfirmOpen] = useState(false);
const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false);
const [customerToDelete, setCustomerToDelete] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [showPendingOnly, setShowPendingOnly] = useState(false);
const [detailsCustomer, setDetailsCustomer] = useState<Customer | null>(null);
const [isApprovingId, setIsApprovingId] = useState<string | null>(null);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
// Portal Access States
const [isPortalModalOpen, setIsPortalModalOpen] = useState(false);
@@ -179,6 +181,93 @@ export default function CustomersPage() {
}
} catch (error) {
console.error('[Clientes] Error fetching customers:', error);
} finally {
setLoading(false);
setSelectedIds([]);
}
};
const handleBulkDelete = async () => {
if (selectedIds.length === 0) return;
setBulkConfirmOpen(true);
};
const handleConfirmBulkDelete = async () => {
try {
setLoading(true);
const results = await Promise.all(selectedIds.map(async (id) => {
const response = await fetch(`/api/crm/customers/${id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
});
return { id, ok: response.ok };
}));
const successful = results.filter(r => r.ok).length;
if (successful > 0) {
toast.success('Exclusão completa', `${successful} clientes excluídos com sucesso.`);
}
if (successful < selectedIds.length) {
toast.error('Erro', 'Não foi possível excluir alguns clientes.');
}
await fetchCustomers();
} catch (error) {
toast.error('Erro', 'Ocorreu um erro na exclusão em lote.');
} finally {
setLoading(false);
setBulkConfirmOpen(false);
setSelectedIds([]);
}
};
const handleBulkApprove = async () => {
const pendingToApprove = customers.filter(c => selectedIds.includes(c.id) && isPendingApproval(c));
if (pendingToApprove.length === 0) {
toast.error('Nenhum dos clientes selecionados está pendente de aprovação.');
return;
}
try {
setLoading(true);
await Promise.all(pendingToApprove.map(async (customer) => {
const updatedTags = (customer.tags || []).filter(tag => tag !== PENDING_APPROVAL_TAG);
let logoUrl = customer.logo_url;
const publicData = parsePublicNotes(customer.notes);
if (publicData?.logo_path && !logoUrl) {
logoUrl = publicData.logo_path;
}
return fetch(`/api/crm/customers/${customer.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: customer.name,
email: customer.email,
phone: customer.phone,
company: customer.company,
position: customer.position,
address: customer.address,
city: customer.city,
state: customer.state,
zip_code: customer.zip_code,
country: customer.country,
notes: customer.notes,
tags: updatedTags,
is_active: customer.is_active ?? true,
logo_url: logoUrl,
}),
});
}));
toast.success('Sucesso', `${pendingToApprove.length} clientes aprovados.`);
await fetchCustomers();
} catch (error) {
toast.error('Erro ao aprovar clientes');
} finally {
setLoading(false);
}
@@ -213,7 +302,7 @@ export default function CustomersPage() {
editingCustomer ? 'Cliente atualizado' : 'Cliente criado',
editingCustomer ? 'O cliente foi atualizado com sucesso.' : 'O novo cliente foi criado com sucesso.'
);
fetchCustomers();
await fetchCustomers();
handleCloseModal();
} else {
const error = await response.json();
@@ -262,7 +351,7 @@ export default function CustomersPage() {
});
if (response.ok) {
setCustomers(customers.filter(c => c.id !== customerToDelete));
await fetchCustomers();
toast.success('Cliente excluído', 'O cliente foi excluído com sucesso.');
} else {
toast.error('Erro ao excluir', 'Não foi possível excluir o cliente.');
@@ -361,7 +450,7 @@ export default function CustomersPage() {
if (response.ok) {
toast.success('Acesso liberado!', 'O cliente já pode fazer login no portal com a senha cadastrada.');
fetchCustomers();
await fetchCustomers();
setDetailsCustomer(prev => prev && prev.id === customer.id ? { ...prev, tags: updatedTags } : prev);
} else {
const error = await response.json();
@@ -524,6 +613,22 @@ export default function CustomersPage() {
<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 w-10 text-left">
<div className="flex items-center">
<input
type="checkbox"
checked={filteredCustomers.length > 0 && selectedIds.length === filteredCustomers.length}
onChange={(e) => {
if (e.target.checked) {
setSelectedIds(filteredCustomers.map(c => c.id));
} else {
setSelectedIds([]);
}
}}
className="w-4 h-4 rounded border-zinc-300 text-brand-600 focus:ring-brand-500 cursor-pointer"
/>
</div>
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Cliente</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Empresa</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Contato</th>
@@ -540,11 +645,28 @@ export default function CustomersPage() {
return (
<tr
key={customer.id}
className={`group transition-colors ${pendingApproval
onClick={() => openDetails(customer)}
className={`group transition-colors cursor-pointer ${pendingApproval
? 'bg-amber-50/40 dark:bg-amber-900/10 hover:bg-amber-50/70 dark:hover:bg-amber-900/20'
: 'hover:bg-zinc-50 dark:hover:bg-zinc-800/50'
}`}
} ${selectedIds.includes(customer.id) ? 'bg-brand-50/30 dark:bg-brand-500/5' : ''}`}
>
<td className="px-6 py-4 w-10" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center">
<input
type="checkbox"
checked={selectedIds.includes(customer.id)}
onChange={() => {
if (selectedIds.includes(customer.id)) {
setSelectedIds(selectedIds.filter(id => id !== customer.id));
} else {
setSelectedIds([...selectedIds, customer.id]);
}
}}
className="w-4 h-4 rounded border-zinc-300 text-brand-600 focus:ring-brand-500 cursor-pointer"
/>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-3">
{customer.logo_url ? (
@@ -1010,20 +1132,38 @@ export default function CustomersPage() {
</div>
)}
<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" cancelText="Cancelar" variant="danger" />
<ConfirmDialog
isOpen={confirmOpen}
onClose={() => {
setConfirmOpen(false);
setCustomerToDelete(null);
}}
onConfirm={handleConfirmDelete}
title="Excluir Cliente"
message="Tem certeza que deseja excluir este cliente? Esta ação não pode ser desfeita."
confirmText="Excluir"
isOpen={bulkConfirmOpen}
onClose={() => setBulkConfirmOpen(false)}
onConfirm={handleConfirmBulkDelete}
title="Excluir Clientes Selecionados"
message={`Tem certeza que deseja excluir os ${selectedIds.length} clientes selecionados? Esta ação não pode ser desfeita.`}
confirmText="Excluir Tudo"
cancelText="Cancelar"
variant="danger"
/>
<BulkActionBar
selectedCount={selectedIds.length}
onClearSelection={() => setSelectedIds([])}
actions={[
...(customers.some(c => selectedIds.includes(c.id) && isPendingApproval(c)) ? [{
label: "Aprovar Selecionados",
icon: <CheckCircleIcon className="w-5 h-5" />,
onClick: handleBulkApprove,
variant: 'primary' as const
}] : []),
{
label: "Excluir Selecionados",
icon: <TrashIcon className="w-5 h-5" />,
onClick: handleBulkDelete,
variant: 'danger'
}
]}
/>
{/* Modal de Acesso ao Portal */}
{isPortalModalOpen && selectedCustomerForPortal && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">