Compare commits

...

2 Commits

2 changed files with 81 additions and 117 deletions

View File

@ -38,8 +38,6 @@ import {
getPatientById,
listPatients,
updatePatient,
validateCPF,
type CPFValidationResult,
type EnderecoPaciente,
type Paciente as PacienteServiceModel,
} from "../services/pacienteService";
@ -243,24 +241,6 @@ const maskCpf = (value: string) => {
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) => {
if (!telefone) {
@ -457,9 +437,7 @@ const PainelSecretaria = () => {
const [formDataPaciente, setFormDataPaciente] = useState<PacienteForm>(
buildEmptyPacienteForm()
);
const [cpfError, setCpfError] = useState<string | null>(null);
const [cpfValidation, setCpfValidation] =
useState<CPFValidationResult | null>(null);
// Removida validação de CPF (local + externa)
const [doctorModalOpen, setDoctorModalOpen] = useState(false);
const [doctorModalMode, setDoctorModalMode] = useState<"create" | "edit">(
@ -543,8 +521,6 @@ const PainelSecretaria = () => {
const resetPacienteForm = useCallback(() => {
setFormDataPaciente(buildEmptyPacienteForm());
setCpfError(null);
setCpfValidation(null);
}, []);
const resetMedicoForm = useCallback(() => {
@ -567,8 +543,6 @@ const PainelSecretaria = () => {
const openEditPacienteModal = useCallback((paciente: PacienteUI) => {
setFormDataPaciente(buildPacienteFormFromPaciente(paciente));
setPatientModalMode("edit");
setCpfError(null);
setCpfValidation(null);
setPatientModalOpen(true);
}, []);
@ -666,18 +640,10 @@ const PainelSecretaria = () => {
}
}, []);
const handleCpfChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const { formatted, digits } = maskCpf(event.target.value);
const handleCpfChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const { formatted } = maskCpf(event.target.value);
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 digits = rawCep.replace(/\D/g, "");
@ -710,29 +676,11 @@ const PainelSecretaria = () => {
event.preventDefault();
const { digits } = maskCpf(formDataPaciente.cpf);
// Validação de CPF apenas no modo create
if (patientModalMode === "create") {
if (digits.length !== 11 || !isValidCPF(digits)) {
setCpfError("CPF inválido");
return;
}
}
// Validação de CPF removida apenas mascaramento e envio dos dígitos.
setLoading(true);
try {
let validation = cpfValidation;
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;
}
}
// Validação externa de CPF removida
const telefone =
composeTelefone(
@ -860,7 +808,7 @@ const PainelSecretaria = () => {
setLoading(false);
}
},
[formDataPaciente, patientModalMode, cpfValidation, resetPacienteForm]
[formDataPaciente, patientModalMode, resetPacienteForm]
);
const handleSubmitMedico = useCallback(
@ -1282,7 +1230,7 @@ const PainelSecretaria = () => {
>
<Plus className="w-5 h-5 mr-2" />
Para adicionar Paciente use o Painel Adm*
novo paciente
</button>
<button
onClick={openCreateMedicoModal}
@ -1850,21 +1798,10 @@ const PainelSecretaria = () => {
type="text"
value={formDataPaciente.cpf}
onChange={handleCpfChange}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent ${
cpfError ? "border-red-500" : "border-gray-300"
}`}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent border-gray-300"
required
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>
<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;
endereco?: EnderecoPaciente;
}): Promise<ApiResponse<Paciente>> {
// Normalizações: remover qualquer formatação para envio limpo
const cleanCpf = (payload.cpf || "").replace(/\D/g, "");
const cleanPhone = (payload.telefone || "").replace(/\D/g, "");
// Sanitização forte
const rawCpf = (payload.cpf || "").replace(/\D/g, "").slice(0, 11);
let phone = (payload.telefone || "").replace(/\D/g, "");
if (phone.length > 15) phone = phone.slice(0, 15);
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;
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 (!cleanCpf) return { success: false, error: "CPF é obrigatório" };
if (!payload.email?.trim())
return { success: false, error: "Email é obrigatório" };
if (!cleanPhone) return { success: false, error: "Telefone é obrigatório" };
if (!payload.nome?.trim()) return { success: false, error: "Nome é obrigatório" };
if (!rawCpf) return { success: false, error: "CPF é obrigatório" };
if (!payload.email?.trim()) return { success: false, error: "Email é obrigatório" };
if (!phone) return { success: false, error: "Telefone é obrigatório" };
const body: Partial<PatientInputSchema> = {
const buildBody = (cpfValue: string): Partial<PatientInputSchema> => ({
full_name: payload.nome,
cpf: cleanCpf,
cpf: cpfValue,
email: payload.email,
phone_mobile: cleanPhone,
phone_mobile: phone,
birth_date: payload.dataNascimento,
social_name: payload.socialName,
sex: payload.sexo,
blood_type: payload.tipoSanguineo,
weight_kg: payload.pesoKg,
height_m: payload.alturaM,
weight_kg: peso,
height_m: altura,
street: cleanEndereco?.rua,
number: cleanEndereco?.numero,
complement: cleanEndereco?.complemento,
@ -394,37 +391,67 @@ export async function createPatient(payload: {
city: cleanEndereco?.cidade,
state: cleanEndereco?.estado,
cep: cleanEndereco?.cep,
};
});
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];
if (v === undefined || v === "") delete (body as Record<string, unknown>)[k];
});
try {
};
prune();
const attempt = async (): Promise<ApiResponse<Paciente>> => {
const response = await http.post<PacienteApi | PacienteApi[]>(
ENDPOINTS.PATIENTS,
body,
{
headers: { Prefer: "return=representation" },
}
{ headers: { Prefer: "return=representation" } }
);
if (!response.success || !response.data)
return {
success: false,
error: response.error || "Erro ao criar paciente",
};
if (response.success && response.data) {
const raw = Array.isArray(response.data) ? response.data[0] : response.data;
return { success: true, data: mapPacienteFromApi(raw) };
} catch (error: unknown) {
const err = error as {
response?: { status?: number; data?: { message?: string } };
}
return { success: false, error: response.error || "Erro ao criar paciente" };
};
const handleOverflowFallbacks = async (baseError: string): Promise<ApiResponse<Paciente>> => {
// 1) tentar com CPF formatado
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";
if (err.response?.status === 401) msg = "Não autorizado";
else if (err.response?.status === 400)
msg = err.response.data?.message || "Dados inválidos";
else if (err.response?.data?.message) msg = err.response.data.message;
console.error(msg, error);
if (e.response?.status === 401) msg = "Não autorizado";
else if (e.response?.status === 400) msg = e.response.data?.message || "Dados inválidos";
else if (e.response?.data?.message) msg = e.response.data.message;
if (/numeric field overflow/i.test(msg)) {
const overflowAttempt = await handleOverflowFallbacks(msg);
return overflowAttempt;
}
return { success: false, error: msg };
}
}