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