Compare commits
2 Commits
fca789662d
...
e5f55c1f69
| Author | SHA1 | Date | |
|---|---|---|---|
| e5f55c1f69 | |||
| 627035c879 |
@ -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">
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user