Files
advx/src/components/clientes/ClienteModal.tsx
T

790 lines
29 KiB
TypeScript

import React, {
useState,
useEffect,
useCallback,
forwardRef,
useRef,
} from "react";
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { useForm } from "react-hook-form";
import axios from "axios";
import { Cliente } from "../../types/entities";
import { useClientes } from "../../hooks/useClientes";
import { ORGAOS_EMISSORES, PREVIDENCIA_OPTIONS } from "../../utils/constants";
const estadosCivis = [
{ value: "solteiro", label: "Solteiro(a)" },
{ value: "casado", label: "Casado(a)" },
{ value: "divorciado", label: "Divorciado(a)" },
{ value: "uniao_estavel", label: "União Estável" },
{ value: "viuvo", label: "Viúvo(a)" },
];
const estados = [
"AC",
"AL",
"AP",
"AM",
"BA",
"CE",
"DF",
"ES",
"GO",
"MA",
"MT",
"MS",
"MG",
"PA",
"PB",
"PR",
"PE",
"PI",
"RJ",
"RN",
"RS",
"RO",
"RR",
"SC",
"SP",
"SE",
"TO",
];
const VINCULOS_EMPREGATICIO = [
{ value: "mei", label: "MEI" },
{ value: "pj", label: "Pessoa Jurídica" },
{ value: "clt", label: "CLT" },
{ value: "autonomo", label: "Autônomo" },
{ value: "servidor_publico", label: "Servidor Público" },
];
// Atualizar estados para o formato de options
const estadosOptions = estados.map((uf) => ({ value: uf, label: uf }));
// Componente de input com forwardRef
const FormInput = forwardRef<HTMLInputElement, any>(
({ label, error, className = "", ...props }, ref) => (
<div className={`${className} relative`}>
<label className="block text-sm font-semibold text-gray-700 mb-2">
{label}
{props.required && <span className="text-red-500 ml-1">*</span>}
</label>
<input
ref={ref}
className={`
w-full px-4 py-3 rounded-lg
border border-gray-300
bg-white text-gray-900
transition duration-150 ease-in-out
placeholder-gray-400
focus:ring-2 focus:ring-blue-500 focus:border-transparent
hover:border-gray-400
${error ? "border-red-300 focus:ring-red-500" : ""}
`}
{...props}
/>
{error && (
<div className="absolute -bottom-5 left-0">
<p className="text-xs text-red-500">{error}</p>
</div>
)}
</div>
),
);
// Componente de select com forwardRef
const FormSelect = forwardRef<HTMLSelectElement, any>(
({ label, error, options, className = "", ...props }, ref) => (
<div className={`${className} relative`}>
<label className="block text-sm font-semibold text-gray-700 mb-2">
{label}
{props.required && <span className="text-red-500 ml-1">*</span>}
</label>
<select
ref={ref}
className={`
w-full px-4 py-3 rounded-lg
border border-gray-300
bg-white text-gray-900
transition duration-150 ease-in-out
focus:ring-2 focus:ring-blue-500 focus:border-transparent
hover:border-gray-400
appearance-none
${error ? "border-red-300 focus:ring-red-500" : ""}
`}
{...props}
>
<option value="">Selecione...</option>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
{error && (
<div className="absolute -bottom-5 left-0">
<p className="text-xs text-red-500">{error}</p>
</div>
)}
</div>
),
);
// Componente de checkbox com forwardRef
const FormCheckbox = forwardRef<HTMLInputElement, any>(
({ label, ...props }, ref) => (
<label className="flex items-center space-x-3 cursor-pointer group">
<input
type="checkbox"
ref={ref}
className="
w-5 h-5 rounded
border-gray-300 text-blue-600
focus:ring-2 focus:ring-blue-500
transition duration-150 ease-in-out
cursor-pointer
"
{...props}
/>
<span className="text-sm text-gray-700 group-hover:text-gray-900">
{label}
</span>
</label>
),
);
// Adicionar displayName para os componentes
FormInput.displayName = "FormInput";
FormSelect.displayName = "FormSelect";
FormCheckbox.displayName = "FormCheckbox";
interface ClienteModalProps {
isOpen: boolean;
onClose: () => void;
cliente: Cliente | null;
onSuccess?: () => void;
initialTab?: number;
}
export const ClienteModal: React.FC<ClienteModalProps> = ({
isOpen,
onClose,
cliente,
onSuccess,
initialTab = 0,
}) => {
const [activeTab, setActiveTab] = useState(initialTab);
const [isLoadingCep, setIsLoadingCep] = useState(false);
// Novo estado para controlar os valores do endereço
const [enderecoState, setEnderecoState] = useState({
endereco: "",
bairro: "",
cidade: "",
estado: "",
preenchido: false,
});
const {
register,
handleSubmit,
formState: { errors },
reset,
setValue,
watch,
getValues,
} = useForm();
const { createCliente, updateCliente } = useClientes();
const formPopulated = useRef(false);
// Monitorar o valor do CEP para autopreenchimento
const cepValue = watch("cep");
// Função para buscar dados do CEP - versão corrigida
const fetchAddressByCep = useCallback(async (cep: string) => {
if (!cep || cep.length !== 8) return;
try {
setIsLoadingCep(true);
const formattedCep = cep.replace(/\D/g, ""); // Remove caracteres não numéricos
const response = await axios.get(
`https://viacep.com.br/ws/${formattedCep}/json/`,
);
if (response.data && !response.data.erro) {
const endereco = response.data.logradouro || "";
const bairro = response.data.bairro || "";
const cidade = response.data.localidade || "";
const estado = response.data.uf || "";
// Só preencher endereço/bairro se vier valor da API
if (endereco) setValue("endereco", endereco, { shouldValidate: true });
if (bairro) setValue("bairro", bairro, { shouldValidate: true });
setValue("cidade", cidade, { shouldValidate: true });
setValue("estado", estado, { shouldValidate: true });
// Atualizar estado apenas para cidade/estado (não controlar endereço/bairro)
setEnderecoState({
endereco: endereco,
bairro: bairro,
cidade: cidade,
estado: estado,
preenchido: false, // Não usar mais para travar campos
});
// Focar no próximo campo apenas se o usuário não estiver digitando ativamente
// Esta alteração evita interromper o fluxo natural de preenchimento
const activeElement = document.activeElement;
const isUserTyping =
activeElement &&
(activeElement.tagName === 'INPUT' || activeElement.tagName === 'SELECT') &&
activeElement !== document.querySelector('input[name="cep"]');
// Só focar no campo número se o usuário não estiver digitando em outro campo
if (!isUserTyping) {
setTimeout(() => {
const numeroInput = document.querySelector(
'input[name="numero"]'
) as HTMLInputElement;
if (numeroInput) numeroInput.focus();
}, 100);
}
} else {
// Se CEP não encontrado, limpar campos e permitir edição manual
setEnderecoState({
endereco: "",
bairro: "",
cidade: "",
estado: "",
preenchido: false,
});
setValue("endereco", "");
setValue("bairro", "");
setValue("cidade", "");
setValue("estado", "");
console.error("CEP não encontrado");
alert("CEP não encontrado. Por favor, verifique o CEP informado.");
}
} catch (error) {
console.error("Erro ao buscar CEP:", error);
alert("Erro ao buscar informações do CEP. Por favor, tente novamente.");
} finally {
setIsLoadingCep(false);
}
}, [setValue]);
// Efeito para monitorar mudanças no CEP com debounce otimizado
const [autoFetchEnabled, setAutoFetchEnabled] = useState(true);
useEffect(() => {
if (!autoFetchEnabled) return;
// Limpar qualquer timer existente
const timer = setTimeout(() => {
const cleanCep = cepValue?.replace(/\D/g, "");
if (cleanCep && cleanCep.length === 8) {
fetchAddressByCep(cleanCep);
setAutoFetchEnabled(false); // Desativar busca automática após primeiro preenchimento
setTimeout(() => setAutoFetchEnabled(true), 1000); // Reativar depois de 1 segundo
}
}, 200); // Reduzir debounce para resposta mais rápida
return () => clearTimeout(timer);
}, [cepValue, fetchAddressByCep, autoFetchEnabled]);
// Função para preencher o formulário com os dados do cliente
const populateForm = useCallback(
(clienteData: Cliente) => {
console.log("Populando formulário com:", clienteData);
Object.keys(clienteData).forEach((key) => {
if (key === "dataNascimento" && clienteData[key]) {
// Formatar a data para o formato YYYY-MM-DD esperado pelo input type="date"
const data = new Date(clienteData[key]);
setValue(key, data.toISOString().split("T")[0]);
} else if (
key !== "id" &&
key !== "dataCriacao" &&
key !== "processos" &&
key !== "filhos"
) {
setValue(key, clienteData[key]);
}
});
},
[setValue],
);
// Efeito para carregar os dados quando o modal abrir ou o cliente mudar
useEffect(() => {
if (isOpen && cliente && !formPopulated.current) {
console.log("Populando formulário com:", cliente);
Object.keys(cliente).forEach((key) => {
if (key === "dataNascimento" && cliente[key]) {
const data = new Date(cliente[key]);
setValue(key, data.toISOString().split("T")[0]);
} else if (
key !== "id" &&
key !== "dataCriacao" &&
key !== "processos" &&
key !== "filhos"
) {
setValue(key, cliente[key]);
}
});
formPopulated.current = true;
}
if (!isOpen) {
formPopulated.current = false;
reset();
}
}, [isOpen, cliente, setValue, reset]);
const onSubmit = async (data: Partial<Cliente>) => {
try {
// Converter nome para maiúsculas
const cleanData = Object.fromEntries(
Object.entries(data).filter(([_, value]) => {
if (value === "") return false;
if (value === null) return false;
if (value === undefined) return false;
return true;
}),
);
// Formatar campos específicos e converter nome para maiúsculas
const formattedData = {
...cleanData,
nome: cleanData.nome?.toUpperCase(),
nomeMae: cleanData.nomeMae?.toUpperCase(),
nomePai: cleanData.nomePai?.toUpperCase(),
conjugeNome: cleanData.conjugeNome?.toUpperCase(),
// Converter campos numéricos
renda: cleanData.renda ? Number(cleanData.renda) : null,
// Formatar data de nascimento para ISO String
dataNascimento: cleanData.dataNascimento
? new Date(cleanData.dataNascimento).toISOString()
: null,
// Garantir campos obrigatórios
email: cleanData.email,
telefone: cleanData.telefone,
cpfCnpj: cleanData.cpfCnpj,
endereco: cleanData.endereco,
cep: cleanData.cep,
numero: cleanData.numero,
bairro: cleanData.bairro,
cidade: cleanData.cidade,
estado: cleanData.estado,
// Converter booleanos
declaraIR: Boolean(cleanData.declaraIR),
contribuiPrevidencia: Boolean(cleanData.contribuiPrevidencia),
usaDNI: Boolean(cleanData.usaDNI),
possuiFilhos: Boolean(cleanData.possuiFilhos),
};
console.log("Dados enviados:", formattedData); // Debug
if (cliente) {
await updateCliente({
id: cliente.id,
data: formattedData,
});
} else {
await createCliente(formattedData);
}
onSuccess?.();
onClose();
} catch (error) {
console.error("Erro detalhado:", error);
if (axios.isAxiosError(error)) {
alert(error.response?.data?.error || "Erro ao salvar cliente");
} else {
alert("Erro ao salvar cliente");
}
}
};
// Reset form on modal close
const handleClose = () => {
reset();
onClose();
};
const tabs = [
{ id: "pessoal", label: "Dados Pessoais" },
{ id: "endereco", label: "Endereço" },
{ id: "financeiro", label: "Dados Financeiros" },
{ id: "familia", label: "Filiação" },
];
return (
<Transition show={isOpen} as={React.Fragment}>
<Dialog onClose={handleClose} className="relative z-50">
<Transition.Child
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/30 backdrop-blur-sm" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<Transition.Child
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"
>
<Dialog.Panel className="mx-auto max-w-4xl w-full bg-white dark:bg-gray-800 rounded-xl shadow-2xl">
{/* Header com abas */}
<div className="border-b border-gray-200 dark:border-gray-700 p-6 bg-gray-50 dark:bg-gray-900 rounded-t-xl">
<div className="flex justify-between items-center mb-6">
<Dialog.Title className="text-2xl font-bold text-gray-900 dark:text-white">
{cliente ? "Editar Cliente" : "Novo Cliente"}
</Dialog.Title>
<button
onClick={handleClose}
className="text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
<div className="flex space-x-1">
{tabs.map((tab, idx) => (
<button
key={tab.id}
onClick={() => setActiveTab(idx)}
className={`
flex items-center px-6 py-3 text-sm font-medium rounded-t-lg
transition-all duration-200 ease-in-out
${
activeTab === idx
? "bg-white text-blue-600 border-t-2 border-blue-600 shadow-sm"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}
`}
>
{tab.label}
</button>
))}
</div>
</div>
{/* Formulário */}
<form
onSubmit={handleSubmit(onSubmit)}
className="p-6 bg-white dark:bg-gray-800"
>
<div className="space-y-8 mb-8">
{/* Conteúdo das abas */}
{activeTab === 0 && (
<div className="grid grid-cols-2 gap-4">
<FormInput
label="Nome"
{...register("nome", {
required: "Nome é obrigatório",
})}
error={errors.nome?.message}
/>
<FormInput
label="CPF/CNPJ"
{...register("cpfCnpj", {
required: "CPF/CNPJ é obrigatório",
})}
error={errors.cpfCnpj?.message}
/>
<div className="col-span-2 mb-4">
<FormCheckbox
label="Usar DNI (Documento Nacional de Identidade)"
{...register("usaDNI")}
onChange={(e) => {
if (e.target.checked) {
setValue("rg", watch("cpfCnpj"));
}
}}
/>
</div>
<FormInput
label="RG"
{...register("rg")}
disabled={watch("usaDNI")}
/>
<div className="col-span-2 grid grid-cols-2 gap-4">
<FormSelect
label="Órgão Emissor"
{...register("orgaoEmissor")}
options={ORGAOS_EMISSORES}
/>
<FormSelect
label="Estado do RG"
{...register("estadoRg")}
options={estadosOptions}
/>
</div>
<FormInput
label="Email"
type="email"
{...register("email", {
required: "Email é obrigatório",
})}
error={errors.email?.message}
/>
<FormInput
label="Telefone"
{...register("telefone", {
required: "Telefone é obrigatório",
})}
error={errors.telefone?.message}
/>
<FormSelect
label="Estado Civil"
{...register("estadoCivil")}
options={estadosCivis}
/>
<FormInput
label="Instagram"
{...register("instagram")}
placeholder="@"
/>
<FormInput
label="Profissão"
{...register("profissao")}
/>
<FormInput
label="Data de Nascimento"
type="date"
{...register("dataNascimento")}
max={new Date().toISOString().split("T")[0]} // Impede datas futuras
/>
</div>
)}
{activeTab === 1 && (
<div className="grid grid-cols-2 gap-4">
<div className="relative">
<FormInput
label="CEP"
{...register("cep", {
required: "CEP é obrigatório",
pattern: {
value: /^[0-9]{5}-?[0-9]{3}$/,
message: "CEP inválido",
},
})}
error={errors.cep?.message}
placeholder="00000-000"
maxLength={9}
value={watch("cep") || ""}
onChange={(e) => {
// Formatação automática do CEP (00000-000)
let value = e.target.value.replace(/\D/g, "");
if (value.length > 5) {
value =
value.substring(0, 5) +
"-" +
value.substring(5, 8);
}
setValue("cep", value, { shouldValidate: true });
}}
onBlur={(e) => {
// Forçar busca ao sair do campo
const cleanCep = e.target.value.replace(/\D/g, "");
if (cleanCep && cleanCep.length === 8) {
fetchAddressByCep(cleanCep);
}
}}
/>
{isLoadingCep && (
<div className="absolute right-3 top-9">
<svg
className="animate-spin h-5 w-5 text-blue-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
)}
</div>
<FormInput
className="md:col-span-2"
label="Endereço"
{...register("endereco", {
required: "Endereço é obrigatório",
})}
error={errors.endereco?.message}
// Remover value/controlado por enderecoState, deixar react-hook-form controlar
onChange={(e) => {
setValue("endereco", e.target.value, { shouldValidate: true });
}}
/>
<FormInput
label="Número"
{...register("numero", {
required: "Número é obrigatório",
})}
error={errors.numero?.message}
/>
<FormInput
label="Complemento"
{...register("complemento")}
/>
<FormInput
label="Bairro"
{...register("bairro", {
required: "Bairro é obrigatório",
})}
error={errors.bairro?.message}
// Remover value/controlado por enderecoState, deixar react-hook-form controlar
onChange={(e) => {
setValue("bairro", e.target.value, { shouldValidate: true });
}}
/>
<FormInput
label="Cidade"
{...register("cidade", {
required: "Cidade é obrigatória",
})}
error={errors.cidade?.message}
value={typeof watch("cidade") === "string" ? watch("cidade") : enderecoState.cidade}
onChange={(e) => {
setValue("cidade", e.target.value, { shouldValidate: true });
setEnderecoState(prev => ({...prev, cidade: e.target.value}));
}}
/>
<FormSelect
label="Estado"
{...register("estado", {
required: "Estado é obrigatório",
})}
options={estadosOptions}
error={errors.estado?.message}
value={typeof watch("estado") === "string" ? watch("estado") : enderecoState.estado}
onChange={(e) => {
setValue("estado", e.target.value, { shouldValidate: true });
setEnderecoState(prev => ({...prev, estado: e.target.value}));
}}
/>
</div>
)}
{activeTab === 2 && (
<div className="grid grid-cols-2 gap-4">
<FormSelect
label="Vínculo Empregatício"
{...register("vinculoEmpregaticio")}
options={VINCULOS_EMPREGATICIO}
/>
<FormInput
label="Renda Mensal"
type="number"
step="0.01"
{...register("renda")}
/>
<div className="col-span-2">
<FormCheckbox
label="Declara Imposto de Renda"
{...register("declaraIR")}
/>
</div>
<FormSelect
label="Tipo de Previdência"
{...register("tipoPrevidencia")}
options={PREVIDENCIA_OPTIONS}
/>
<div className="col-span-2">
<FormCheckbox
label="Contribui para Previdência"
{...register("contribuiPrevidencia")}
/>
</div>
</div>
)}
{activeTab === 3 && (
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2 mb-4">
<FormCheckbox
label="Possui Filhos?"
{...register("possuiFilhos")}
/>
</div>
<FormInput
label="Nome do Pai"
{...register("nomePai")}
/>
<FormInput
label="Nome da Mãe"
{...register("nomeMae")}
/>
<FormInput
label="Nome do Cônjuge"
{...register("conjugeNome")}
/>
<FormInput
label="CPF do Cônjuge"
{...register("conjugeCpf")}
/>
</div>
)}
</div>
{/* Botões de ação */}
<div className="flex justify-end space-x-3 pt-6 border-t">
<button
type="button"
onClick={handleClose}
className="
px-6 py-2.5 text-sm font-medium
text-gray-700 bg-white
border border-gray-300 rounded-lg
hover:bg-gray-50
focus:outline-none focus:ring-2 focus:ring-gray-200
transition duration-200
"
>
Cancelar
</button>
<button
type="submit"
className="
px-6 py-2.5 text-sm font-medium
text-white bg-blue-600 rounded-lg
hover:bg-blue-700
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
transition duration-200
"
>
{cliente ? "Salvar" : "Criar"}
</button>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
};