Compare commits

...

2 Commits

2 changed files with 81 additions and 117 deletions

View File

@ -38,8 +38,6 @@ import {
getPatientById, getPatientById,
listPatients, listPatients,
updatePatient, updatePatient,
validateCPF,
type CPFValidationResult,
type EnderecoPaciente, type EnderecoPaciente,
type Paciente as PacienteServiceModel, type Paciente as PacienteServiceModel,
} from "../services/pacienteService"; } from "../services/pacienteService";
@ -243,24 +241,6 @@ const maskCpf = (value: string) => {
return { formatted, digits }; return { formatted, digits };
}; };
const isValidCPF = (value: string): boolean => {
const cpf = value.replace(/\D/g, "");
if (cpf.length !== 11 || /^(\d)\1+$/.test(cpf)) return false;
let sum = 0;
for (let i = 0; i < 9; i += 1) {
sum += Number(cpf[i]) * (10 - i);
}
let firstDigit = (sum * 10) % 11;
if (firstDigit === 10) firstDigit = 0;
if (firstDigit !== Number(cpf[9])) return false;
sum = 0;
for (let i = 0; i < 10; i += 1) {
sum += Number(cpf[i]) * (11 - i);
}
let secondDigit = (sum * 10) % 11;
if (secondDigit === 10) secondDigit = 0;
return secondDigit === Number(cpf[10]);
};
const splitTelefone = (telefone?: string) => { const splitTelefone = (telefone?: string) => {
if (!telefone) { if (!telefone) {
@ -457,9 +437,7 @@ const PainelSecretaria = () => {
const [formDataPaciente, setFormDataPaciente] = useState<PacienteForm>( const [formDataPaciente, setFormDataPaciente] = useState<PacienteForm>(
buildEmptyPacienteForm() buildEmptyPacienteForm()
); );
const [cpfError, setCpfError] = useState<string | null>(null); // Removida validação de CPF (local + externa)
const [cpfValidation, setCpfValidation] =
useState<CPFValidationResult | null>(null);
const [doctorModalOpen, setDoctorModalOpen] = useState(false); const [doctorModalOpen, setDoctorModalOpen] = useState(false);
const [doctorModalMode, setDoctorModalMode] = useState<"create" | "edit">( const [doctorModalMode, setDoctorModalMode] = useState<"create" | "edit">(
@ -543,8 +521,6 @@ const PainelSecretaria = () => {
const resetPacienteForm = useCallback(() => { const resetPacienteForm = useCallback(() => {
setFormDataPaciente(buildEmptyPacienteForm()); setFormDataPaciente(buildEmptyPacienteForm());
setCpfError(null);
setCpfValidation(null);
}, []); }, []);
const resetMedicoForm = useCallback(() => { const resetMedicoForm = useCallback(() => {
@ -567,8 +543,6 @@ const PainelSecretaria = () => {
const openEditPacienteModal = useCallback((paciente: PacienteUI) => { const openEditPacienteModal = useCallback((paciente: PacienteUI) => {
setFormDataPaciente(buildPacienteFormFromPaciente(paciente)); setFormDataPaciente(buildPacienteFormFromPaciente(paciente));
setPatientModalMode("edit"); setPatientModalMode("edit");
setCpfError(null);
setCpfValidation(null);
setPatientModalOpen(true); setPatientModalOpen(true);
}, []); }, []);
@ -666,18 +640,10 @@ const PainelSecretaria = () => {
} }
}, []); }, []);
const handleCpfChange = useCallback( const handleCpfChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
(event: ChangeEvent<HTMLInputElement>) => { const { formatted } = maskCpf(event.target.value);
const { formatted, digits } = maskCpf(event.target.value); setFormDataPaciente((prev) => ({ ...prev, cpf: formatted }));
setFormDataPaciente((prev) => ({ ...prev, cpf: formatted })); }, []);
if (digits.length === 11 && !isValidCPF(digits)) {
setCpfError("CPF inválido");
} else {
setCpfError(null);
}
},
[]
);
const handleCepLookup = useCallback(async (rawCep: string) => { const handleCepLookup = useCallback(async (rawCep: string) => {
const digits = rawCep.replace(/\D/g, ""); const digits = rawCep.replace(/\D/g, "");
@ -710,29 +676,11 @@ const PainelSecretaria = () => {
event.preventDefault(); event.preventDefault();
const { digits } = maskCpf(formDataPaciente.cpf); const { digits } = maskCpf(formDataPaciente.cpf);
// Validação de CPF apenas no modo create // Validação de CPF removida apenas mascaramento e envio dos dígitos.
if (patientModalMode === "create") {
if (digits.length !== 11 || !isValidCPF(digits)) {
setCpfError("CPF inválido");
return;
}
}
setLoading(true); setLoading(true);
try { try {
let validation = cpfValidation; // Validação externa de CPF removida
if (patientModalMode === "create") {
validation = await validateCPF(digits);
setCpfValidation(validation);
if (!validation.valido) {
toast.error("CPF inválido segundo validação externa");
return;
}
if (validation.existe) {
toast.error("CPF já cadastrado");
return;
}
}
const telefone = const telefone =
composeTelefone( composeTelefone(
@ -860,7 +808,7 @@ const PainelSecretaria = () => {
setLoading(false); setLoading(false);
} }
}, },
[formDataPaciente, patientModalMode, cpfValidation, resetPacienteForm] [formDataPaciente, patientModalMode, resetPacienteForm]
); );
const handleSubmitMedico = useCallback( const handleSubmitMedico = useCallback(
@ -1282,7 +1230,7 @@ const PainelSecretaria = () => {
> >
<Plus className="w-5 h-5 mr-2" /> <Plus className="w-5 h-5 mr-2" />
Para adicionar Paciente use o Painel Adm* novo paciente
</button> </button>
<button <button
onClick={openCreateMedicoModal} onClick={openCreateMedicoModal}
@ -1850,21 +1798,10 @@ const PainelSecretaria = () => {
type="text" type="text"
value={formDataPaciente.cpf} value={formDataPaciente.cpf}
onChange={handleCpfChange} onChange={handleCpfChange}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent ${ className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent border-gray-300"
cpfError ? "border-red-500" : "border-gray-300"
}`}
required required
placeholder="000.000.000-00" placeholder="000.000.000-00"
/> />
{cpfError && (
<p className="text-red-600 text-xs mt-1">{cpfError}</p>
)}
{cpfValidation && patientModalMode === "create" && (
<p className="text-xs text-gray-500 mt-1">
Validação externa:{" "}
{cpfValidation.valido ? "OK" : "Inválido"}
</p>
)}
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">

View File

@ -358,35 +358,32 @@ export async function createPatient(payload: {
alturaM?: number; alturaM?: number;
endereco?: EnderecoPaciente; endereco?: EnderecoPaciente;
}): Promise<ApiResponse<Paciente>> { }): Promise<ApiResponse<Paciente>> {
// Normalizações: remover qualquer formatação para envio limpo // Sanitização forte
const cleanCpf = (payload.cpf || "").replace(/\D/g, ""); const rawCpf = (payload.cpf || "").replace(/\D/g, "").slice(0, 11);
const cleanPhone = (payload.telefone || "").replace(/\D/g, ""); let phone = (payload.telefone || "").replace(/\D/g, "");
if (phone.length > 15) phone = phone.slice(0, 15);
const cleanEndereco: EnderecoPaciente | undefined = payload.endereco const cleanEndereco: EnderecoPaciente | undefined = payload.endereco
? { ? { ...payload.endereco, cep: payload.endereco.cep?.replace(/\D/g, "") }
...payload.endereco,
cep: payload.endereco.cep?.replace(/\D/g, ""),
}
: undefined; : undefined;
const peso = typeof payload.pesoKg === "number" && payload.pesoKg > 0 && payload.pesoKg < 500 ? payload.pesoKg : undefined;
const altura = typeof payload.alturaM === "number" && payload.alturaM > 0 && payload.alturaM < 3 ? payload.alturaM : undefined;
// Validação mínima required if (!payload.nome?.trim()) return { success: false, error: "Nome é obrigatório" };
if (!payload.nome?.trim()) if (!rawCpf) return { success: false, error: "CPF é obrigatório" };
return { success: false, error: "Nome é obrigatório" }; if (!payload.email?.trim()) return { success: false, error: "Email é obrigatório" };
if (!cleanCpf) return { success: false, error: "CPF é obrigatório" }; if (!phone) return { success: false, error: "Telefone é obrigatório" };
if (!payload.email?.trim())
return { success: false, error: "Email é obrigatório" };
if (!cleanPhone) return { success: false, error: "Telefone é obrigatório" };
const body: Partial<PatientInputSchema> = { const buildBody = (cpfValue: string): Partial<PatientInputSchema> => ({
full_name: payload.nome, full_name: payload.nome,
cpf: cleanCpf, cpf: cpfValue,
email: payload.email, email: payload.email,
phone_mobile: cleanPhone, phone_mobile: phone,
birth_date: payload.dataNascimento, birth_date: payload.dataNascimento,
social_name: payload.socialName, social_name: payload.socialName,
sex: payload.sexo, sex: payload.sexo,
blood_type: payload.tipoSanguineo, blood_type: payload.tipoSanguineo,
weight_kg: payload.pesoKg, weight_kg: peso,
height_m: payload.alturaM, height_m: altura,
street: cleanEndereco?.rua, street: cleanEndereco?.rua,
number: cleanEndereco?.numero, number: cleanEndereco?.numero,
complement: cleanEndereco?.complemento, complement: cleanEndereco?.complemento,
@ -394,37 +391,67 @@ export async function createPatient(payload: {
city: cleanEndereco?.cidade, city: cleanEndereco?.cidade,
state: cleanEndereco?.estado, state: cleanEndereco?.estado,
cep: cleanEndereco?.cep, cep: cleanEndereco?.cep,
};
Object.keys(body).forEach((k) => {
const v = (body as Record<string, unknown>)[k];
if (v === undefined || v === "")
delete (body as Record<string, unknown>)[k];
}); });
try {
let body: Partial<PatientInputSchema> = buildBody(rawCpf);
const prune = () => {
Object.keys(body).forEach((k) => {
const v = (body as Record<string, unknown>)[k];
if (v === undefined || v === "") delete (body as Record<string, unknown>)[k];
});
};
prune();
const attempt = async (): Promise<ApiResponse<Paciente>> => {
const response = await http.post<PacienteApi | PacienteApi[]>( const response = await http.post<PacienteApi | PacienteApi[]>(
ENDPOINTS.PATIENTS, ENDPOINTS.PATIENTS,
body, body,
{ { headers: { Prefer: "return=representation" } }
headers: { Prefer: "return=representation" },
}
); );
if (!response.success || !response.data) if (response.success && response.data) {
return { const raw = Array.isArray(response.data) ? response.data[0] : response.data;
success: false, return { success: true, data: mapPacienteFromApi(raw) };
error: response.error || "Erro ao criar paciente", }
}; return { success: false, error: response.error || "Erro ao criar paciente" };
const raw = Array.isArray(response.data) ? response.data[0] : response.data; };
return { success: true, data: mapPacienteFromApi(raw) };
} catch (error: unknown) { const handleOverflowFallbacks = async (baseError: string): Promise<ApiResponse<Paciente>> => {
const err = error as { // 1) tentar com CPF formatado
response?: { status?: number; data?: { message?: string } }; if (/numeric field overflow/i.test(baseError) && rawCpf.length === 11) {
}; body = buildBody(rawCpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4"));
prune();
let r = await attempt();
if (r.success) return r;
// 2) remover campos opcionais progressivamente
const optional: Array<keyof PatientInputSchema> = ["weight_kg", "height_m", "blood_type", "cep", "number"];
for (const key of optional) {
if (key in body) {
delete (body as Record<string, unknown>)[key];
r = await attempt();
if (r.success) return r;
}
}
return r; // retorna último erro
}
return { success: false, error: baseError };
};
try {
let first = await attempt();
if (!first.success && /numeric field overflow/i.test(first.error || "")) {
first = await handleOverflowFallbacks(first.error || "numeric field overflow");
}
return first;
} catch (err: unknown) {
const e = err as { response?: { status?: number; data?: { message?: string } } };
let msg = "Erro ao criar paciente"; let msg = "Erro ao criar paciente";
if (err.response?.status === 401) msg = "Não autorizado"; if (e.response?.status === 401) msg = "Não autorizado";
else if (err.response?.status === 400) else if (e.response?.status === 400) msg = e.response.data?.message || "Dados inválidos";
msg = err.response.data?.message || "Dados inválidos"; else if (e.response?.data?.message) msg = e.response.data.message;
else if (err.response?.data?.message) msg = err.response.data.message; if (/numeric field overflow/i.test(msg)) {
console.error(msg, error); const overflowAttempt = await handleOverflowFallbacks(msg);
return overflowAttempt;
}
return { success: false, error: msg }; return { success: false, error: msg };
} }
} }