first commit
This commit is contained in:
commit
c9fb67e751
423 files changed
+53287
No files matched your search
@@ -0,0 +1,390 @@
|
||||
import React, { useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useClientes } from "../../hooks/useClientes";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
PencilIcon,
|
||||
UserIcon,
|
||||
HomeIcon,
|
||||
BanknotesIcon,
|
||||
UserGroupIcon,
|
||||
DocumentTextIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ClienteModal } from "./ClienteModal";
|
||||
import {
|
||||
formatCPF,
|
||||
formatTelefone,
|
||||
formatNome,
|
||||
formatEmail,
|
||||
formatMoeda,
|
||||
formatTextoSimples,
|
||||
formatData,
|
||||
} from "../../utils/formatters";
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ id: "info", name: "Informações Básicas", icon: UserIcon },
|
||||
{ id: "endereco", name: "Endereço", icon: HomeIcon },
|
||||
{ id: "financeiro", name: "Informações Financeiras", icon: BanknotesIcon },
|
||||
{ id: "familia", name: "Informações Familiares", icon: UserGroupIcon },
|
||||
{ id: "processos", name: "Processos", icon: DocumentTextIcon },
|
||||
];
|
||||
|
||||
interface InfoItemProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onEdit?: () => void;
|
||||
isEditable?: boolean;
|
||||
}
|
||||
|
||||
const InfoItem: React.FC<InfoItemProps> = ({
|
||||
label,
|
||||
value,
|
||||
onEdit,
|
||||
isEditable = true,
|
||||
}) => (
|
||||
<div className="relative group">
|
||||
<dt className="text-sm font-medium text-gray-500">{label}</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900 flex items-center justify-between">
|
||||
<span>{value}</span>
|
||||
{isEditable && (
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<PencilIcon className="h-4 w-4 text-blue-600" />
|
||||
</button>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ClienteDetalhes: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { clientes, isLoading } = useClientes();
|
||||
const [selectedTab, setSelectedTab] = useState(0);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [editingField, setEditingField] = useState<string | null>(null);
|
||||
const cliente = clientes.find((c) => c.id === id);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!cliente) {
|
||||
return <div>Cliente não encontrado</div>;
|
||||
}
|
||||
|
||||
// Funções de formatação simplificadas
|
||||
const formataCurrency = (valor: any): string => {
|
||||
if (!valor) return "Não informado";
|
||||
try {
|
||||
return new Intl.NumberFormat("pt-BR", {
|
||||
style: "currency",
|
||||
currency: "BRL",
|
||||
}).format(Number(valor));
|
||||
} catch {
|
||||
return "Não informado";
|
||||
}
|
||||
};
|
||||
|
||||
const formataTexto = (texto: any): string => {
|
||||
if (!texto && texto !== false) return "Não informado";
|
||||
if (typeof texto === "boolean") return texto ? "Sim" : "Não";
|
||||
return String(texto);
|
||||
};
|
||||
|
||||
const getTabForField = (field: string | null) => {
|
||||
switch (field) {
|
||||
case "nome":
|
||||
case "cpfCnpj":
|
||||
case "rg":
|
||||
case "email":
|
||||
case "telefone":
|
||||
case "instagram":
|
||||
case "estadoCivil":
|
||||
case "dataNascimento":
|
||||
return 0;
|
||||
case "cep":
|
||||
case "endereco":
|
||||
case "complemento":
|
||||
case "bairro":
|
||||
case "cidade":
|
||||
case "estado":
|
||||
return 1;
|
||||
case "profissao":
|
||||
case "vinculoEmpregaticio":
|
||||
case "renda":
|
||||
case "declaraIR":
|
||||
case "tipoPrevidencia":
|
||||
case "contribuiPrevidencia":
|
||||
return 2;
|
||||
case "nomePai":
|
||||
case "nomeMae":
|
||||
case "conjugeNome":
|
||||
case "conjugeCpf":
|
||||
return 3;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => navigate("/clientes")}
|
||||
className="p-2 hover:bg-gray-100 rounded-full"
|
||||
>
|
||||
<ArrowLeftIcon className="h-5 w-5 text-gray-500" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-800">
|
||||
{formatNome(cliente.nome)}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
Cadastrado em {new Date(cliente.dataCriacao).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsEditModalOpen(true)}
|
||||
className="flex items-center px-4 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-lg"
|
||||
>
|
||||
<PencilIcon className="h-4 w-4 mr-2" />
|
||||
Editar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tab.Group selectedIndex={selectedTab} onChange={setSelectedTab}>
|
||||
<Tab.List className="flex space-x-1 rounded-xl bg-blue-900/20 p-1">
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab.id}
|
||||
className={({ selected }) =>
|
||||
classNames(
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5",
|
||||
"flex items-center justify-center",
|
||||
selected
|
||||
? "bg-white text-blue-700 shadow"
|
||||
: "text-gray-600 hover:bg-white/[0.12] hover:text-blue-600",
|
||||
)
|
||||
}
|
||||
>
|
||||
<tab.icon className="h-4 w-4 mr-2" />
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels className="mt-2">
|
||||
{/* Informações Básicas */}
|
||||
<Tab.Panel className="bg-white rounded-xl p-6">
|
||||
<dl className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<InfoItem
|
||||
label="Nome"
|
||||
value={formatNome(cliente.nome)}
|
||||
onEdit={() => {
|
||||
setEditingField("nome");
|
||||
setIsEditModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
<InfoItem
|
||||
label="CPF/CNPJ"
|
||||
value={formatCPF(cliente.cpfCnpj)}
|
||||
onEdit={() => {
|
||||
setEditingField("cpfCnpj");
|
||||
setIsEditModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
<InfoItem label="RG" value={formatTextoSimples(cliente.rg)} />
|
||||
<InfoItem label="Email" value={formatEmail(cliente.email)} />
|
||||
<InfoItem
|
||||
label="Telefone"
|
||||
value={formatTelefone(cliente.telefone)}
|
||||
/>
|
||||
<InfoItem
|
||||
label="Instagram"
|
||||
value={formatTextoSimples(cliente.instagram)}
|
||||
/>
|
||||
<InfoItem
|
||||
label="Estado Civil"
|
||||
value={formatTextoSimples(cliente.estadoCivil)}
|
||||
/>
|
||||
<InfoItem
|
||||
label="Data de Nascimento"
|
||||
value={formatData(cliente.dataNascimento)}
|
||||
/>
|
||||
</dl>
|
||||
</Tab.Panel>
|
||||
|
||||
{/* Endereço */}
|
||||
<Tab.Panel className="bg-white rounded-xl p-6">
|
||||
<dl className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<InfoItem label="CEP" value={formataTexto(cliente.cep)} />
|
||||
<InfoItem
|
||||
label="Endereço"
|
||||
value={formataTexto(`${cliente.endereco}, ${cliente.numero}`)}
|
||||
/>
|
||||
<InfoItem
|
||||
label="Complemento"
|
||||
value={formataTexto(cliente.complemento)}
|
||||
/>
|
||||
<InfoItem label="Bairro" value={formataTexto(cliente.bairro)} />
|
||||
<InfoItem label="Cidade" value={formataTexto(cliente.cidade)} />
|
||||
<InfoItem label="Estado" value={formataTexto(cliente.estado)} />
|
||||
</dl>
|
||||
</Tab.Panel>
|
||||
|
||||
{/* Informações Financeiras */}
|
||||
<Tab.Panel className="bg-white rounded-xl p-6">
|
||||
<dl className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<InfoItem
|
||||
label="Profissão"
|
||||
value={formatTextoSimples(cliente.profissao)}
|
||||
/>
|
||||
<InfoItem
|
||||
label="Vínculo"
|
||||
value={formatTextoSimples(cliente.vinculoEmpregaticio)}
|
||||
/>
|
||||
<InfoItem
|
||||
label="Renda Mensal"
|
||||
value={formatMoeda(Number(cliente.renda))}
|
||||
/>
|
||||
<InfoItem
|
||||
label="Declara IR"
|
||||
value={cliente.declaraIR ? "Sim" : "Não"}
|
||||
/>
|
||||
<InfoItem
|
||||
label="Tipo Previdência"
|
||||
value={formatTextoSimples(cliente.tipoPrevidencia)}
|
||||
/>
|
||||
<InfoItem
|
||||
label="Contribui Previdência"
|
||||
value={cliente.contribuiPrevidencia ? "Sim" : "Não"}
|
||||
/>
|
||||
</dl>
|
||||
</Tab.Panel>
|
||||
|
||||
{/* Informações Familiares */}
|
||||
<Tab.Panel className="bg-white rounded-xl p-6">
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<InfoItem
|
||||
label="Nome do Pai"
|
||||
value={formatNome(cliente.nomePai || "")}
|
||||
/>
|
||||
<InfoItem
|
||||
label="Nome da Mãe"
|
||||
value={formatNome(cliente.nomeMae || "")}
|
||||
/>
|
||||
<InfoItem
|
||||
label="Nome do Cônjuge"
|
||||
value={formatNome(cliente.conjugeNome || "")}
|
||||
/>
|
||||
<InfoItem
|
||||
label="CPF do Cônjuge"
|
||||
value={
|
||||
cliente.conjugeCpf
|
||||
? formatCPF(cliente.conjugeCpf)
|
||||
: "Não informado"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Lista de Filhos */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||||
Filhos
|
||||
</h3>
|
||||
{cliente.filhos && cliente.filhos.length > 0 ? (
|
||||
<ul className="divide-y divide-gray-200">
|
||||
{cliente.filhos.map((filho) => (
|
||||
<li key={filho.id} className="py-3">
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{filho.nome}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Nascimento:{" "}
|
||||
{new Date(filho.dataNascimento).toLocaleDateString()}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">
|
||||
Nenhum filho cadastrado
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
|
||||
{/* Processos */}
|
||||
<Tab.Panel className="bg-white rounded-xl p-6">
|
||||
{cliente.processos && cliente.processos.length > 0 ? (
|
||||
<ul className="divide-y divide-gray-200">
|
||||
{cliente.processos.map((processo) => (
|
||||
<li key={processo.id} className="py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{processo.titulo}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Nº {processo.numero}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={classNames(
|
||||
"px-2 py-1 text-xs rounded-full",
|
||||
processo.status === "em_andamento"
|
||||
? "bg-blue-100 text-blue-800"
|
||||
: processo.status === "concluido"
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-gray-100 text-gray-800",
|
||||
)}
|
||||
>
|
||||
{processo.status}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">
|
||||
Nenhum processo cadastrado
|
||||
</p>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
|
||||
{/* Modal de Edição */}
|
||||
<ClienteModal
|
||||
isOpen={isEditModalOpen}
|
||||
cliente={cliente}
|
||||
onClose={() => {
|
||||
setIsEditModalOpen(false);
|
||||
setEditingField(null);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setIsEditModalOpen(false);
|
||||
setEditingField(null);
|
||||
}}
|
||||
initialTab={getTabForField(editingField)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user