fix(erp): enable erp pages and menu items
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user