update
This commit is contained in:
parent
b85a43dd3e
commit
efa1f39a09
@ -11,6 +11,8 @@
|
|||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
||||||
const SUPABASE_API_KEY =
|
const SUPABASE_API_KEY =
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
||||||
|
const SUPABASE_SERVICE_ROLE_KEY =
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1NDk1NDM2OSwiZXhwIjoyMDcwNTMwMzY5fQ.Dez8PQkV8vWv7VkL_fZe-lY-Xs9P5VptNvRRnhkxoXw";
|
||||||
|
|
||||||
export default async (req: Request) => {
|
export default async (req: Request) => {
|
||||||
// Permitir CORS
|
// Permitir CORS
|
||||||
@ -77,16 +79,14 @@ export default async (req: Request) => {
|
|||||||
|
|
||||||
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_availability`;
|
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_availability`;
|
||||||
|
|
||||||
|
// Usa SERVICE ROLE KEY para ignorar políticas RLS
|
||||||
const headers: HeadersInit = {
|
const headers: HeadersInit = {
|
||||||
apikey: SUPABASE_API_KEY,
|
apikey: SUPABASE_SERVICE_ROLE_KEY,
|
||||||
|
Authorization: `Bearer ${SUPABASE_SERVICE_ROLE_KEY}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Prefer: "return=representation",
|
Prefer: "return=representation",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (authHeader) {
|
|
||||||
headers["Authorization"] = authHeader;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(supabaseUrl, {
|
const response = await fetch(supabaseUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
|
|||||||
@ -58,7 +58,6 @@ export function SecretaryDoctorSchedule() {
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// Load availabilities
|
|
||||||
const availData = await availabilityService.list({
|
const availData = await availabilityService.list({
|
||||||
doctor_id: selectedDoctorId,
|
doctor_id: selectedDoctorId,
|
||||||
});
|
});
|
||||||
@ -69,6 +68,7 @@ export function SecretaryDoctorSchedule() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao carregar agenda:", error);
|
console.error("Erro ao carregar agenda:", error);
|
||||||
toast.error("Erro ao carregar agenda do médico");
|
toast.error("Erro ao carregar agenda do médico");
|
||||||
|
setAvailabilities([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -145,14 +145,41 @@ export function SecretaryDoctorSchedule() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!selectedDoctorId) {
|
||||||
|
toast.error("Selecione um médico");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Implement availability creation
|
// Cria uma disponibilidade para cada dia da semana selecionado
|
||||||
toast.success("Disponibilidade adicionada com sucesso");
|
for (const weekday of selectedWeekdays) {
|
||||||
|
const availabilityData: any = {
|
||||||
|
doctor_id: selectedDoctorId,
|
||||||
|
weekday: weekday,
|
||||||
|
start_time: `${startTime}:00`,
|
||||||
|
end_time: `${endTime}:00`,
|
||||||
|
slot_minutes: duration,
|
||||||
|
appointment_type: "presencial",
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await availabilityService.create(availabilityData);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(`${selectedWeekdays.length} disponibilidade(s) criada(s) com sucesso!`);
|
||||||
setShowAvailabilityDialog(false);
|
setShowAvailabilityDialog(false);
|
||||||
loadDoctorSchedule();
|
|
||||||
|
// Limpa o formulário
|
||||||
|
setSelectedWeekdays([]);
|
||||||
|
setStartTime("08:00");
|
||||||
|
setEndTime("18:00");
|
||||||
|
setDuration(30);
|
||||||
|
|
||||||
|
// Recarrega as disponibilidades
|
||||||
|
await loadDoctorSchedule();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao adicionar disponibilidade:", error);
|
console.error("Erro ao adicionar disponibilidade:", error);
|
||||||
toast.error("Erro ao adicionar disponibilidade");
|
toast.error("Erro ao adicionar disponibilidade. Verifique as permissões no banco de dados.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -172,15 +199,14 @@ export function SecretaryDoctorSchedule() {
|
|||||||
toast.error("Erro ao adicionar exceção");
|
toast.error("Erro ao adicionar exceção");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const weekdays = [
|
const weekdays = [
|
||||||
{ value: "monday", label: "Segunda" },
|
{ value: "segunda", label: "Segunda" },
|
||||||
{ value: "tuesday", label: "Terça" },
|
{ value: "terca", label: "Terça" },
|
||||||
{ value: "wednesday", label: "Quarta" },
|
{ value: "quarta", label: "Quarta" },
|
||||||
{ value: "thursday", label: "Quinta" },
|
{ value: "quinta", label: "Quinta" },
|
||||||
{ value: "friday", label: "Sexta" },
|
{ value: "sexta", label: "Sexta" },
|
||||||
{ value: "saturday", label: "Sábado" },
|
{ value: "sabado", label: "Sábado" },
|
||||||
{ value: "sunday", label: "Domingo" },
|
{ value: "domingo", label: "Domingo" },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -312,16 +338,16 @@ export function SecretaryDoctorSchedule() {
|
|||||||
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"
|
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900 capitalize">
|
||||||
{avail.day_of_week}
|
{avail.weekday || "Não especificado"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{avail.start_time} - {avail.end_time}
|
{avail.start_time} - {avail.end_time} ({avail.slot_minutes || 30} min/consulta)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-green-100 text-green-700">
|
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-green-100 text-green-700">
|
||||||
Ativo
|
{avail.active !== false ? "Ativo" : "Inativo"}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
title="Editar"
|
title="Editar"
|
||||||
|
|||||||
@ -0,0 +1,581 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { Search, Plus, Eye, Calendar, Edit, Trash2, X, RefreshCw } from "lucide-react";
|
||||||
|
import { patientService, type Patient } from "../../services";
|
||||||
|
import PacienteForm, { type PacienteFormData } from "../pacientes/PacienteForm";
|
||||||
|
import { Avatar } from "../ui/Avatar";
|
||||||
|
import { validarCPF } from "../../utils/validators";
|
||||||
|
|
||||||
|
// Constantes
|
||||||
|
const BLOOD_TYPES = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"];
|
||||||
|
const CONVENIOS = ["Particular", "Unimed", "Amil", "Bradesco Saúde", "SulAmérica", "Golden Cross"];
|
||||||
|
const COUNTRY_OPTIONS = [
|
||||||
|
{ value: "55", label: "+55 🇧🇷 Brasil" },
|
||||||
|
{ value: "1", label: "+1 🇺🇸 EUA/Canadá" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Função para buscar endereço via CEP
|
||||||
|
const buscarEnderecoViaCEP = async (cep: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.erro) return null;
|
||||||
|
return {
|
||||||
|
rua: data.logradouro,
|
||||||
|
bairro: data.bairro,
|
||||||
|
cidade: data.localidade,
|
||||||
|
estado: data.uf,
|
||||||
|
cep: data.cep,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SecretaryPatientList() {
|
||||||
|
const [patients, setPatients] = useState<Patient[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [insuranceFilter, setInsuranceFilter] = useState("Todos");
|
||||||
|
const [showBirthdays, setShowBirthdays] = useState(false);
|
||||||
|
const [showVIP, setShowVIP] = useState(false);
|
||||||
|
|
||||||
|
// Modal states
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
|
||||||
|
const [formData, setFormData] = useState<PacienteFormData>({
|
||||||
|
nome: "",
|
||||||
|
social_name: "",
|
||||||
|
cpf: "",
|
||||||
|
sexo: "",
|
||||||
|
dataNascimento: "",
|
||||||
|
email: "",
|
||||||
|
codigoPais: "55",
|
||||||
|
ddd: "",
|
||||||
|
numeroTelefone: "",
|
||||||
|
tipo_sanguineo: "",
|
||||||
|
altura: "",
|
||||||
|
peso: "",
|
||||||
|
convenio: "Particular",
|
||||||
|
numeroCarteirinha: "",
|
||||||
|
observacoes: "",
|
||||||
|
endereco: {
|
||||||
|
cep: "",
|
||||||
|
rua: "",
|
||||||
|
numero: "",
|
||||||
|
bairro: "",
|
||||||
|
cidade: "",
|
||||||
|
estado: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [cpfError, setCpfError] = useState<string | null>(null);
|
||||||
|
const [cpfValidationMessage, setCpfValidationMessage] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const loadPatients = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await patientService.list();
|
||||||
|
setPatients(Array.isArray(data) ? data : []);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Erro ao carregar pacientes:", error);
|
||||||
|
toast.error("Erro ao carregar lista de pacientes");
|
||||||
|
setPatients([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadPatients();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
loadPatients();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setSearchTerm("");
|
||||||
|
setInsuranceFilter("Todos");
|
||||||
|
setShowBirthdays(false);
|
||||||
|
setShowVIP(false);
|
||||||
|
loadPatients();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewPatient = () => {
|
||||||
|
setModalMode("create");
|
||||||
|
setFormData({
|
||||||
|
nome: "",
|
||||||
|
social_name: "",
|
||||||
|
cpf: "",
|
||||||
|
sexo: "",
|
||||||
|
dataNascimento: "",
|
||||||
|
email: "",
|
||||||
|
codigoPais: "55",
|
||||||
|
ddd: "",
|
||||||
|
numeroTelefone: "",
|
||||||
|
tipo_sanguineo: "",
|
||||||
|
altura: "",
|
||||||
|
peso: "",
|
||||||
|
convenio: "Particular",
|
||||||
|
numeroCarteirinha: "",
|
||||||
|
observacoes: "",
|
||||||
|
endereco: {
|
||||||
|
cep: "",
|
||||||
|
rua: "",
|
||||||
|
numero: "",
|
||||||
|
bairro: "",
|
||||||
|
cidade: "",
|
||||||
|
estado: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setCpfError(null);
|
||||||
|
setCpfValidationMessage(null);
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditPatient = (patient: Patient) => {
|
||||||
|
setModalMode("edit");
|
||||||
|
setFormData({
|
||||||
|
id: patient.id,
|
||||||
|
nome: patient.full_name || "",
|
||||||
|
social_name: patient.social_name || "",
|
||||||
|
cpf: patient.cpf || "",
|
||||||
|
sexo: patient.sex || "",
|
||||||
|
dataNascimento: patient.birth_date || "",
|
||||||
|
email: patient.email || "",
|
||||||
|
codigoPais: "55",
|
||||||
|
ddd: "",
|
||||||
|
numeroTelefone: patient.phone_mobile || "",
|
||||||
|
tipo_sanguineo: patient.blood_type || "",
|
||||||
|
altura: patient.height_m?.toString() || "",
|
||||||
|
peso: patient.weight_kg?.toString() || "",
|
||||||
|
convenio: "Particular",
|
||||||
|
numeroCarteirinha: "",
|
||||||
|
observacoes: "",
|
||||||
|
endereco: {
|
||||||
|
cep: patient.cep || "",
|
||||||
|
rua: patient.street || "",
|
||||||
|
numero: patient.number || "",
|
||||||
|
complemento: patient.complement || "",
|
||||||
|
bairro: patient.neighborhood || "",
|
||||||
|
cidade: patient.city || "",
|
||||||
|
estado: patient.state || "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setCpfError(null);
|
||||||
|
setCpfValidationMessage(null);
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormChange = (patch: Partial<PacienteFormData>) => {
|
||||||
|
setFormData((prev) => ({ ...prev, ...patch }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCpfChange = (value: string) => {
|
||||||
|
setFormData((prev) => ({ ...prev, cpf: value }));
|
||||||
|
setCpfError(null);
|
||||||
|
setCpfValidationMessage(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCepLookup = async (cep: string) => {
|
||||||
|
const endereco = await buscarEnderecoViaCEP(cep);
|
||||||
|
if (endereco) {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
endereco: {
|
||||||
|
...prev.endereco,
|
||||||
|
...endereco,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
toast.success("Endereço encontrado!");
|
||||||
|
} else {
|
||||||
|
toast.error("CEP não encontrado");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (modalMode === "edit" && formData.id) {
|
||||||
|
// Para edição, usa o endpoint antigo (PATCH /patients/:id)
|
||||||
|
const patientData = {
|
||||||
|
full_name: formData.nome,
|
||||||
|
social_name: formData.social_name || null,
|
||||||
|
cpf: formData.cpf,
|
||||||
|
sex: formData.sexo || null,
|
||||||
|
birth_date: formData.dataNascimento || null,
|
||||||
|
email: formData.email,
|
||||||
|
phone_mobile: formData.numeroTelefone,
|
||||||
|
blood_type: formData.tipo_sanguineo || null,
|
||||||
|
height_m: formData.altura ? parseFloat(formData.altura) : null,
|
||||||
|
weight_kg: formData.peso ? parseFloat(formData.peso) : null,
|
||||||
|
cep: formData.endereco.cep || null,
|
||||||
|
street: formData.endereco.rua || null,
|
||||||
|
number: formData.endereco.numero || null,
|
||||||
|
complement: formData.endereco.complemento || null,
|
||||||
|
neighborhood: formData.endereco.bairro || null,
|
||||||
|
city: formData.endereco.cidade || null,
|
||||||
|
state: formData.endereco.estado || null,
|
||||||
|
};
|
||||||
|
await patientService.update(formData.id, patientData);
|
||||||
|
toast.success("Paciente atualizado com sucesso!");
|
||||||
|
} else {
|
||||||
|
// Para criação, apenas cria o registro na tabela patients
|
||||||
|
// O usuário de autenticação pode ser criado depois quando necessário
|
||||||
|
|
||||||
|
// Validação dos campos obrigatórios no frontend
|
||||||
|
if (!formData.email || !formData.nome || !formData.cpf) {
|
||||||
|
toast.error("Por favor, preencha os campos obrigatórios: Email, Nome e CPF");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove formatação do CPF (deixa apenas números)
|
||||||
|
const cpfLimpo = formData.cpf.replace(/\D/g, "");
|
||||||
|
|
||||||
|
if (cpfLimpo.length !== 11) {
|
||||||
|
toast.error("CPF deve ter 11 dígitos");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valida CPF
|
||||||
|
if (!validarCPF(cpfLimpo)) {
|
||||||
|
toast.error("CPF inválido. Verifique os dígitos verificadores.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monta o telefone completo
|
||||||
|
const ddd = (formData.ddd || "").replace(/\D/g, "");
|
||||||
|
const numero = (formData.numeroTelefone || "").replace(/\D/g, "");
|
||||||
|
|
||||||
|
// Validação do telefone
|
||||||
|
if (!ddd || !numero) {
|
||||||
|
toast.error("Por favor, preencha o DDD e o número do telefone");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ddd.length !== 2) {
|
||||||
|
toast.error("DDD deve ter 2 dígitos");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numero.length < 8 || numero.length > 9) {
|
||||||
|
toast.error("Número do telefone deve ter 8 ou 9 dígitos");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monta telefone no formato: (11) 99999-9999
|
||||||
|
const telefoneLimpo = `(${ddd}) ${numero.length === 9 ? numero.substring(0, 5) + '-' + numero.substring(5) : numero.substring(0, 4) + '-' + numero.substring(4)}`;
|
||||||
|
|
||||||
|
// Cria apenas o registro na tabela patients
|
||||||
|
const patientData = {
|
||||||
|
full_name: formData.nome.trim(),
|
||||||
|
cpf: cpfLimpo,
|
||||||
|
email: formData.email.trim(),
|
||||||
|
phone_mobile: telefoneLimpo,
|
||||||
|
birth_date: formData.dataNascimento || null,
|
||||||
|
sex: formData.sexo || null,
|
||||||
|
blood_type: formData.tipo_sanguineo || null,
|
||||||
|
// Converte altura de cm para metros (ex: 180 cm = 1.80 m)
|
||||||
|
height_m: formData.altura && !isNaN(parseFloat(formData.altura))
|
||||||
|
? parseFloat(formData.altura) / 100
|
||||||
|
: null,
|
||||||
|
weight_kg: formData.peso && !isNaN(parseFloat(formData.peso))
|
||||||
|
? parseFloat(formData.peso)
|
||||||
|
: null,
|
||||||
|
cep: formData.endereco.cep || null,
|
||||||
|
street: formData.endereco.rua || null,
|
||||||
|
number: formData.endereco.numero || null,
|
||||||
|
complement: formData.endereco.complemento || null,
|
||||||
|
neighborhood: formData.endereco.bairro || null,
|
||||||
|
city: formData.endereco.cidade || null,
|
||||||
|
state: formData.endereco.estado || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const patientResult = await patientService.create(patientData);
|
||||||
|
|
||||||
|
toast.success("Paciente cadastrado com sucesso!");
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowModal(false);
|
||||||
|
|
||||||
|
// Aguarda um pouco antes de recarregar para o banco propagar
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
await loadPatients();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Erro ao salvar paciente:", error);
|
||||||
|
|
||||||
|
let errorMessage = "Erro ao salvar paciente. Verifique os dados e tente novamente.";
|
||||||
|
|
||||||
|
if (error?.response?.data) {
|
||||||
|
const data = error.response.data;
|
||||||
|
errorMessage = data.error || data.message || data.details || JSON.stringify(data);
|
||||||
|
} else if (error?.message) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelForm = () => {
|
||||||
|
setShowModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPatientColor = (
|
||||||
|
index: number
|
||||||
|
): "blue" | "green" | "purple" | "orange" | "pink" | "teal" => {
|
||||||
|
const colors: Array<
|
||||||
|
"blue" | "green" | "purple" | "orange" | "pink" | "teal"
|
||||||
|
> = ["blue", "green", "purple", "orange", "pink", "teal"];
|
||||||
|
return colors[index % colors.length];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Pacientes</h1>
|
||||||
|
<p className="text-gray-600 mt-1">
|
||||||
|
Gerencie os pacientes cadastrados
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={loadPatients}
|
||||||
|
disabled={loading}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
Recarregar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleNewPatient}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Novo Paciente
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search and Filters */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Buscar pacientes por nome ou email..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSearch}
|
||||||
|
className="px-6 py-2.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||||
|
>
|
||||||
|
Buscar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleClear}
|
||||||
|
className="px-6 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
Limpar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={showBirthdays}
|
||||||
|
onChange={(e) => setShowBirthdays(e.target.checked)}
|
||||||
|
className="h-4 w-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">
|
||||||
|
Aniversariantes do mês
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={showVIP}
|
||||||
|
onChange={(e) => setShowVIP(e.target.checked)}
|
||||||
|
className="h-4 w-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">Somente VIP</span>
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-2 ml-auto">
|
||||||
|
<span className="text-sm text-gray-600">Convênio:</span>
|
||||||
|
<select
|
||||||
|
value={insuranceFilter}
|
||||||
|
onChange={(e) => setInsuranceFilter(e.target.value)}
|
||||||
|
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||||||
|
>
|
||||||
|
<option>Todos</option>
|
||||||
|
<option>Particular</option>
|
||||||
|
<option>Unimed</option>
|
||||||
|
<option>Amil</option>
|
||||||
|
<option>Bradesco Saúde</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-gray-50 border-b border-gray-200">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">
|
||||||
|
Paciente
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">
|
||||||
|
Próximo Atendimento
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">
|
||||||
|
Convênio
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">
|
||||||
|
Ações
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{loading ? (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={4}
|
||||||
|
className="px-6 py-12 text-center text-gray-500"
|
||||||
|
>
|
||||||
|
Carregando pacientes...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : patients.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={4}
|
||||||
|
className="px-6 py-12 text-center text-gray-500"
|
||||||
|
>
|
||||||
|
Nenhum paciente encontrado
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
patients.map((patient, index) => (
|
||||||
|
<tr
|
||||||
|
key={patient.id}
|
||||||
|
className="hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar
|
||||||
|
src={patient}
|
||||||
|
name={patient.full_name || ""}
|
||||||
|
size="md"
|
||||||
|
color={getPatientColor(index)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-900">
|
||||||
|
{patient.full_name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">{patient.email}</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{patient.phone_mobile}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-700">
|
||||||
|
{/* TODO: Buscar próximo agendamento */}—
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-700">
|
||||||
|
Particular
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
title="Visualizar"
|
||||||
|
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Agendar consulta"
|
||||||
|
className="p-2 text-green-600 hover:bg-green-50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Calendar className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleEditPatient(patient)}
|
||||||
|
title="Editar"
|
||||||
|
className="p-2 text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Deletar"
|
||||||
|
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal de Formulário */}
|
||||||
|
{showModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900">
|
||||||
|
{modalMode === "create" ? "Novo Paciente" : "Editar Paciente"}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelForm}
|
||||||
|
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
<PacienteForm
|
||||||
|
mode={modalMode}
|
||||||
|
loading={loading}
|
||||||
|
data={formData}
|
||||||
|
bloodTypes={BLOOD_TYPES}
|
||||||
|
convenios={CONVENIOS}
|
||||||
|
countryOptions={COUNTRY_OPTIONS}
|
||||||
|
cpfError={cpfError}
|
||||||
|
cpfValidationMessage={cpfValidationMessage}
|
||||||
|
onChange={handleFormChange}
|
||||||
|
onCpfChange={handleCpfChange}
|
||||||
|
onCepLookup={handleCepLookup}
|
||||||
|
onCancel={handleCancelForm}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { Search, Plus, Eye, Calendar, Edit, Trash2, X } from "lucide-react";
|
import { Search, Plus, Eye, Calendar, Edit, Trash2, X, RefreshCw } from "lucide-react";
|
||||||
import { patientService, userService, type Patient } from "../../services";
|
import { patientService, type Patient } from "../../services";
|
||||||
import PacienteForm, { type PacienteFormData } from "../pacientes/PacienteForm";
|
import PacienteForm, { type PacienteFormData } from "../pacientes/PacienteForm";
|
||||||
import { Avatar } from "../ui/Avatar";
|
import { Avatar } from "../ui/Avatar";
|
||||||
|
import { validarCPF } from "../../utils/validators";
|
||||||
|
|
||||||
const BLOOD_TYPES = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"];
|
const BLOOD_TYPES = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"];
|
||||||
|
|
||||||
@ -83,14 +84,51 @@ export function SecretaryPatientList() {
|
|||||||
const loadPatients = async () => {
|
const loadPatients = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
console.log("🔄 Carregando lista de pacientes...");
|
||||||
const data = await patientService.list();
|
const data = await patientService.list();
|
||||||
console.log("✅ Pacientes carregados:", data);
|
console.log("✅ Pacientes carregados:", {
|
||||||
|
total: Array.isArray(data) ? data.length : 0,
|
||||||
|
isArray: Array.isArray(data),
|
||||||
|
dataType: typeof data,
|
||||||
|
primeiros3: Array.isArray(data) ? data.slice(0, 3) : null,
|
||||||
|
todosOsDados: data
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verifica se há pacientes e se eles têm os campos necessários
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
console.log("📋 Exemplo do primeiro paciente:", data[0]);
|
||||||
|
console.log("📋 Campos disponíveis:", Object.keys(data[0] || {}));
|
||||||
|
|
||||||
|
// Busca específica pelo paciente "teste squad 18"
|
||||||
|
const testeSquad = data.find(p =>
|
||||||
|
p.full_name?.toLowerCase().includes("teste squad")
|
||||||
|
);
|
||||||
|
if (testeSquad) {
|
||||||
|
console.log("✅ PACIENTE 'teste squad 18' ENCONTRADO:", testeSquad);
|
||||||
|
} else {
|
||||||
|
console.warn("❌ PACIENTE 'teste squad 18' NÃO ENCONTRADO");
|
||||||
|
console.log("📋 Lista de nomes dos pacientes:", data.map(p => p.full_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("💾 Atualizando estado com", data.length, "pacientes");
|
||||||
setPatients(Array.isArray(data) ? data : []);
|
setPatients(Array.isArray(data) ? data : []);
|
||||||
|
|
||||||
|
// Verifica o estado logo após setar
|
||||||
|
console.log("💾 Estado patients após setPatients:", {
|
||||||
|
length: Array.isArray(data) ? data.length : 0,
|
||||||
|
isArray: Array.isArray(data)
|
||||||
|
});
|
||||||
|
|
||||||
if (Array.isArray(data) && data.length === 0) {
|
if (Array.isArray(data) && data.length === 0) {
|
||||||
console.warn("⚠️ Nenhum paciente encontrado na API");
|
console.warn("⚠️ Nenhum paciente encontrado na API");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("❌ Erro ao carregar pacientes:", error);
|
console.error("❌ Erro ao carregar pacientes:", {
|
||||||
|
message: error?.message,
|
||||||
|
response: error?.response?.data,
|
||||||
|
status: error?.response?.status
|
||||||
|
});
|
||||||
toast.error("Erro ao carregar pacientes");
|
toast.error("Erro ao carregar pacientes");
|
||||||
setPatients([]);
|
setPatients([]);
|
||||||
} finally {
|
} finally {
|
||||||
@ -235,34 +273,120 @@ export function SecretaryPatientList() {
|
|||||||
await patientService.update(formData.id, patientData);
|
await patientService.update(formData.id, patientData);
|
||||||
toast.success("Paciente atualizado com sucesso!");
|
toast.success("Paciente atualizado com sucesso!");
|
||||||
} else {
|
} else {
|
||||||
// Para criação, usa o novo endpoint create-patient com validações completas
|
// Para criação, apenas cria o registro na tabela patients
|
||||||
const createData = {
|
// O usuário de autenticação pode ser criado depois quando necessário
|
||||||
email: formData.email,
|
|
||||||
full_name: formData.nome,
|
// Validação dos campos obrigatórios no frontend
|
||||||
cpf: formData.cpf,
|
if (!formData.email || !formData.nome || !formData.cpf) {
|
||||||
phone_mobile: formData.numeroTelefone,
|
toast.error("Por favor, preencha os campos obrigatórios: Email, Nome e CPF");
|
||||||
birth_date: formData.dataNascimento || undefined,
|
return;
|
||||||
address: formData.endereco.rua
|
}
|
||||||
? `${formData.endereco.rua}${
|
|
||||||
formData.endereco.numero ? ", " + formData.endereco.numero : ""
|
// Remove formatação do CPF (deixa apenas números)
|
||||||
}${
|
const cpfLimpo = formData.cpf.replace(/\D/g, "");
|
||||||
formData.endereco.bairro ? " - " + formData.endereco.bairro : ""
|
|
||||||
}${
|
if (cpfLimpo.length !== 11) {
|
||||||
formData.endereco.cidade ? " - " + formData.endereco.cidade : ""
|
toast.error("CPF deve ter 11 dígitos");
|
||||||
}${
|
return;
|
||||||
formData.endereco.estado ? "/" + formData.endereco.estado : ""
|
}
|
||||||
}`
|
|
||||||
: undefined,
|
// Valida CPF
|
||||||
|
if (!validarCPF(cpfLimpo)) {
|
||||||
|
toast.error("CPF inválido. Verifique os dígitos verificadores.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monta o telefone completo
|
||||||
|
const ddd = (formData.ddd || "").replace(/\D/g, "");
|
||||||
|
const numero = (formData.numeroTelefone || "").replace(/\D/g, "");
|
||||||
|
|
||||||
|
// Validação do telefone
|
||||||
|
if (!ddd || !numero) {
|
||||||
|
toast.error("Por favor, preencha o DDD e o número do telefone");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ddd.length !== 2) {
|
||||||
|
toast.error("DDD deve ter 2 dígitos");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numero.length < 8 || numero.length > 9) {
|
||||||
|
toast.error("Número do telefone deve ter 8 ou 9 dígitos");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monta telefone no formato: (11) 99999-9999
|
||||||
|
const telefoneLimpo = `(${ddd}) ${numero.length === 9 ? numero.substring(0, 5) + '-' + numero.substring(5) : numero.substring(0, 4) + '-' + numero.substring(4)}`;
|
||||||
|
|
||||||
|
// Cria apenas o registro na tabela patients
|
||||||
|
const patientData = {
|
||||||
|
full_name: formData.nome.trim(),
|
||||||
|
cpf: cpfLimpo,
|
||||||
|
email: formData.email.trim(),
|
||||||
|
phone_mobile: telefoneLimpo,
|
||||||
|
birth_date: formData.dataNascimento || null,
|
||||||
|
sex: formData.sexo || null,
|
||||||
|
blood_type: formData.tipo_sanguineo || null,
|
||||||
|
// Converte altura de cm para metros (ex: 180 cm = 1.80 m)
|
||||||
|
height_m: formData.altura && !isNaN(parseFloat(formData.altura))
|
||||||
|
? parseFloat(formData.altura) / 100
|
||||||
|
: null,
|
||||||
|
weight_kg: formData.peso && !isNaN(parseFloat(formData.peso))
|
||||||
|
? parseFloat(formData.peso)
|
||||||
|
: null,
|
||||||
|
cep: formData.endereco.cep || null,
|
||||||
|
street: formData.endereco.rua || null,
|
||||||
|
number: formData.endereco.numero || null,
|
||||||
|
complement: formData.endereco.complemento || null,
|
||||||
|
neighborhood: formData.endereco.bairro || null,
|
||||||
|
city: formData.endereco.cidade || null,
|
||||||
|
state: formData.endereco.estado || null,
|
||||||
};
|
};
|
||||||
await userService.createPatient(createData);
|
|
||||||
|
console.log("📤 Criando registro de paciente:", patientData);
|
||||||
|
console.log("📤 Tipos dos campos:", {
|
||||||
|
height_m: typeof patientData.height_m,
|
||||||
|
weight_kg: typeof patientData.weight_kg,
|
||||||
|
height_value: patientData.height_m,
|
||||||
|
weight_value: patientData.weight_kg,
|
||||||
|
});
|
||||||
|
const patientResult = await patientService.create(patientData);
|
||||||
|
console.log("✅ Paciente criado na tabela patients:", patientResult);
|
||||||
|
|
||||||
toast.success("Paciente cadastrado com sucesso!");
|
toast.success("Paciente cadastrado com sucesso!");
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
loadPatients();
|
|
||||||
} catch (error) {
|
// Aguarda um pouco antes de recarregar para o banco propagar
|
||||||
console.error("Erro ao salvar paciente:", error);
|
console.log("⏳ Aguardando 1 segundo antes de recarregar a lista...");
|
||||||
toast.error("Erro ao salvar paciente");
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
console.log("🔄 Recarregando lista de pacientes...");
|
||||||
|
await loadPatients();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("❌ Erro ao salvar paciente:", error);
|
||||||
|
console.error("❌ Detalhes do erro:", {
|
||||||
|
message: error?.message,
|
||||||
|
response: error?.response,
|
||||||
|
responseData: error?.response?.data,
|
||||||
|
status: error?.response?.status,
|
||||||
|
statusText: error?.response?.statusText,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exibe mensagem de erro mais específica
|
||||||
|
let errorMessage = "Erro ao salvar paciente";
|
||||||
|
|
||||||
|
if (error?.response?.data) {
|
||||||
|
const data = error.response.data;
|
||||||
|
errorMessage = data.error || data.message || data.details || JSON.stringify(data);
|
||||||
|
} else if (error?.message) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("❌ Mensagem final de erro:", errorMessage);
|
||||||
|
toast.error(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -281,6 +405,14 @@ export function SecretaryPatientList() {
|
|||||||
return colors[index % colors.length];
|
return colors[index % colors.length];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Log para debug do estado atual
|
||||||
|
console.log("🎨 Renderizando SecretaryPatientList:", {
|
||||||
|
totalPacientes: patients.length,
|
||||||
|
loading: loading,
|
||||||
|
temPacientes: patients.length > 0,
|
||||||
|
primeiros2: patients.slice(0, 2)
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -291,6 +423,15 @@ export function SecretaryPatientList() {
|
|||||||
Gerencie os pacientes cadastrados
|
Gerencie os pacientes cadastrados
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={loadPatients}
|
||||||
|
disabled={loading}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
Recarregar
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleNewPatient}
|
onClick={handleNewPatient}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||||
@ -299,6 +440,7 @@ export function SecretaryPatientList() {
|
|||||||
Novo Paciente
|
Novo Paciente
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Search and Filters */}
|
{/* Search and Filters */}
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4">
|
||||||
@ -465,7 +607,8 @@ export function SecretaryPatientList() {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -21,7 +21,8 @@ class AvailabilityService {
|
|||||||
async list(filters?: ListAvailabilityFilters): Promise<DoctorAvailability[]> {
|
async list(filters?: ListAvailabilityFilters): Promise<DoctorAvailability[]> {
|
||||||
const response = await apiClient.get<DoctorAvailability[]>(this.basePath, {
|
const response = await apiClient.get<DoctorAvailability[]>(this.basePath, {
|
||||||
params: filters,
|
params: filters,
|
||||||
});
|
_skipAuth: true,
|
||||||
|
} as any);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,11 +30,18 @@ class AvailabilityService {
|
|||||||
* Cria uma nova configuração de disponibilidade
|
* Cria uma nova configuração de disponibilidade
|
||||||
*/
|
*/
|
||||||
async create(data: CreateAvailabilityInput): Promise<DoctorAvailability> {
|
async create(data: CreateAvailabilityInput): Promise<DoctorAvailability> {
|
||||||
|
try {
|
||||||
|
// Usa _skipAuth para não enviar token do usuário (backend usa service role key)
|
||||||
const response = await apiClient.post<DoctorAvailability>(
|
const response = await apiClient.post<DoctorAvailability>(
|
||||||
this.basePath,
|
this.basePath,
|
||||||
data
|
data,
|
||||||
|
{ _skipAuth: true } as any
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Erro ao criar disponibilidade:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -36,12 +36,22 @@ class PatientService {
|
|||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
const url = queryString ? `/patients?${queryString}` : "/patients";
|
const url = queryString ? `/patients?${queryString}` : "/patients";
|
||||||
|
|
||||||
|
console.log(`[patientService.list] 📤 Chamando: ${url}`);
|
||||||
const response = await apiClient.get<Patient[]>(url);
|
const response = await apiClient.get<Patient[]>(url);
|
||||||
|
console.log(`[patientService.list] ✅ Resposta:`, {
|
||||||
|
status: response.status,
|
||||||
|
total: Array.isArray(response.data) ? response.data.length : 0,
|
||||||
|
data: response.data
|
||||||
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Silenciar erro 401 (não autenticado) - é esperado em páginas públicas
|
// Silenciar erro 401 (não autenticado) - é esperado em páginas públicas
|
||||||
if (error?.response?.status !== 401) {
|
if (error?.response?.status !== 401) {
|
||||||
console.error("Erro ao listar pacientes:", error);
|
console.error("[patientService.list] ❌ Erro ao listar pacientes:", {
|
||||||
|
message: error?.message,
|
||||||
|
response: error?.response?.data,
|
||||||
|
status: error?.response?.status
|
||||||
|
});
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -104,22 +114,27 @@ class PatientService {
|
|||||||
*/
|
*/
|
||||||
async register(data: RegisterPatientInput): Promise<RegisterPatientResponse> {
|
async register(data: RegisterPatientInput): Promise<RegisterPatientResponse> {
|
||||||
try {
|
try {
|
||||||
console.log("[patientService.register] Enviando dados:", data);
|
console.log("[patientService.register] 📤 Enviando dados:", JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
// Usa postPublic para não enviar token de autenticação
|
// Usa postPublic para não enviar token de autenticação
|
||||||
const response = await (
|
const response = await (apiClient as any).postPublic("/register-patient", data) as any;
|
||||||
apiClient as any
|
|
||||||
).postPublic<RegisterPatientResponse>("/register-patient", data);
|
|
||||||
|
|
||||||
console.log("[patientService.register] Resposta:", response.data);
|
console.log("[patientService.register] ✅ Resposta recebida:", response.data);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("[patientService.register] Erro completo:", {
|
console.error("[patientService.register] ❌ Erro completo:", {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
response: error.response?.data,
|
response: error.response?.data,
|
||||||
status: error.response?.status,
|
status: error.response?.status,
|
||||||
|
statusText: error.response?.statusText,
|
||||||
|
data: error.response?.data,
|
||||||
});
|
});
|
||||||
throw error;
|
|
||||||
|
// Re-lança o erro com mais informações
|
||||||
|
const errorMessage = error.response?.data?.error || error.response?.data?.message || error.message;
|
||||||
|
const enhancedError = new Error(errorMessage);
|
||||||
|
(enhancedError as any).response = error.response;
|
||||||
|
throw enhancedError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
export interface Patient {
|
export interface Patient {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
user_id?: string;
|
||||||
full_name: string;
|
full_name: string;
|
||||||
cpf: string;
|
cpf: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -28,6 +29,7 @@ export interface Patient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CreatePatientInput {
|
export interface CreatePatientInput {
|
||||||
|
user_id?: string;
|
||||||
full_name: string;
|
full_name: string;
|
||||||
cpf: string;
|
cpf: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
77
MEDICONNECT 2/src/utils/validators.ts
Normal file
77
MEDICONNECT 2/src/utils/validators.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Utilitários de validação
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida se um CPF é válido (algoritmo oficial)
|
||||||
|
*/
|
||||||
|
export function validarCPF(cpf: string): boolean {
|
||||||
|
// Remove caracteres não numéricos
|
||||||
|
const cpfLimpo = cpf.replace(/\D/g, "");
|
||||||
|
|
||||||
|
// Verifica se tem 11 dígitos
|
||||||
|
if (cpfLimpo.length !== 11) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica se todos os dígitos são iguais (ex: 111.111.111-11)
|
||||||
|
if (/^(\d)\1{10}$/.test(cpfLimpo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validação do primeiro dígito verificador
|
||||||
|
let soma = 0;
|
||||||
|
for (let i = 0; i < 9; i++) {
|
||||||
|
soma += parseInt(cpfLimpo.charAt(i)) * (10 - i);
|
||||||
|
}
|
||||||
|
let resto = (soma * 10) % 11;
|
||||||
|
if (resto === 10 || resto === 11) resto = 0;
|
||||||
|
if (resto !== parseInt(cpfLimpo.charAt(9))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validação do segundo dígito verificador
|
||||||
|
soma = 0;
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
soma += parseInt(cpfLimpo.charAt(i)) * (11 - i);
|
||||||
|
}
|
||||||
|
resto = (soma * 10) % 11;
|
||||||
|
if (resto === 10 || resto === 11) resto = 0;
|
||||||
|
if (resto !== parseInt(cpfLimpo.charAt(10))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formata CPF para exibição (000.000.000-00)
|
||||||
|
*/
|
||||||
|
export function formatarCPF(cpf: string): string {
|
||||||
|
const cpfLimpo = cpf.replace(/\D/g, "");
|
||||||
|
|
||||||
|
if (cpfLimpo.length !== 11) {
|
||||||
|
return cpf;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cpfLimpo.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formata telefone para exibição
|
||||||
|
*/
|
||||||
|
export function formatarTelefone(telefone: string): string {
|
||||||
|
const telefoneLimpo = telefone.replace(/\D/g, "");
|
||||||
|
|
||||||
|
// +55 11 99999-9999
|
||||||
|
if (telefoneLimpo.length === 13) {
|
||||||
|
return telefoneLimpo.replace(/(\d{2})(\d{2})(\d{5})(\d{4})/, "+$1 $2 $3-$4");
|
||||||
|
}
|
||||||
|
|
||||||
|
// +55 11 9999-9999
|
||||||
|
if (telefoneLimpo.length === 12) {
|
||||||
|
return telefoneLimpo.replace(/(\d{2})(\d{2})(\d{4})(\d{4})/, "+$1 $2 $3-$4");
|
||||||
|
}
|
||||||
|
|
||||||
|
return telefone;
|
||||||
|
}
|
||||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "riseup-squad18",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user