first commit
This commit is contained in:
commit
c9fb67e751
423 files changed
+53287
No files matched your search
@@ -0,0 +1,611 @@
|
||||
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 {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset,
|
||||
setValue,
|
||||
watch,
|
||||
} = useForm();
|
||||
const { createCliente, updateCliente } = useClientes();
|
||||
const formPopulated = useRef(false);
|
||||
|
||||
// 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">
|
||||
<FormInput
|
||||
label="CEP"
|
||||
{...register("cep", {
|
||||
required: "CEP é obrigatório",
|
||||
})}
|
||||
error={errors.cep?.message}
|
||||
/>
|
||||
<FormInput
|
||||
className="md:col-span-2"
|
||||
label="Endereço"
|
||||
{...register("endereco", {
|
||||
required: "Endereço é obrigatório",
|
||||
})}
|
||||
error={errors.endereco?.message}
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
<FormInput
|
||||
label="Cidade"
|
||||
{...register("cidade", {
|
||||
required: "Cidade é obrigatória",
|
||||
})}
|
||||
error={errors.cidade?.message}
|
||||
/>
|
||||
<FormSelect
|
||||
label="Estado"
|
||||
{...register("estado", {
|
||||
required: "Estado é obrigatório",
|
||||
})}
|
||||
options={estadosOptions}
|
||||
error={errors.estado?.message}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user