diff --git a/MEDICONNECT 2/netlify/functions/doctor-availability.ts b/MEDICONNECT 2/netlify/functions/doctor-availability.ts index e793b27d3..fa48e3dd4 100644 --- a/MEDICONNECT 2/netlify/functions/doctor-availability.ts +++ b/MEDICONNECT 2/netlify/functions/doctor-availability.ts @@ -11,6 +11,8 @@ const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co"; const SUPABASE_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ"; +const SUPABASE_SERVICE_ROLE_KEY = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1NDk1NDM2OSwiZXhwIjoyMDcwNTMwMzY5fQ.Dez8PQkV8vWv7VkL_fZe-lY-Xs9P5VptNvRRnhkxoXw"; export default async (req: Request) => { // Permitir CORS @@ -77,16 +79,14 @@ export default async (req: Request) => { const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_availability`; + // Usa SERVICE ROLE KEY para ignorar políticas RLS const headers: HeadersInit = { - apikey: SUPABASE_API_KEY, + apikey: SUPABASE_SERVICE_ROLE_KEY, + Authorization: `Bearer ${SUPABASE_SERVICE_ROLE_KEY}`, "Content-Type": "application/json", Prefer: "return=representation", }; - if (authHeader) { - headers["Authorization"] = authHeader; - } - const response = await fetch(supabaseUrl, { method: "POST", headers, diff --git a/MEDICONNECT 2/src/components/secretaria/SecretaryDoctorSchedule.tsx b/MEDICONNECT 2/src/components/secretaria/SecretaryDoctorSchedule.tsx index 5905fa7f0..6184fcb4c 100644 --- a/MEDICONNECT 2/src/components/secretaria/SecretaryDoctorSchedule.tsx +++ b/MEDICONNECT 2/src/components/secretaria/SecretaryDoctorSchedule.tsx @@ -58,7 +58,6 @@ export function SecretaryDoctorSchedule() { setLoading(true); try { - // Load availabilities const availData = await availabilityService.list({ doctor_id: selectedDoctorId, }); @@ -69,6 +68,7 @@ export function SecretaryDoctorSchedule() { } catch (error) { console.error("Erro ao carregar agenda:", error); toast.error("Erro ao carregar agenda do médico"); + setAvailabilities([]); } finally { setLoading(false); } @@ -145,14 +145,41 @@ export function SecretaryDoctorSchedule() { return; } + if (!selectedDoctorId) { + toast.error("Selecione um médico"); + return; + } + try { - // TODO: Implement availability creation - toast.success("Disponibilidade adicionada com sucesso"); + // Cria uma disponibilidade para cada dia da semana selecionado + 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); - loadDoctorSchedule(); + + // Limpa o formulário + setSelectedWeekdays([]); + setStartTime("08:00"); + setEndTime("18:00"); + setDuration(30); + + // Recarrega as disponibilidades + await loadDoctorSchedule(); } catch (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"); } }; - const weekdays = [ - { value: "monday", label: "Segunda" }, - { value: "tuesday", label: "Terça" }, - { value: "wednesday", label: "Quarta" }, - { value: "thursday", label: "Quinta" }, - { value: "friday", label: "Sexta" }, - { value: "saturday", label: "Sábado" }, - { value: "sunday", label: "Domingo" }, + { value: "segunda", label: "Segunda" }, + { value: "terca", label: "Terça" }, + { value: "quarta", label: "Quarta" }, + { value: "quinta", label: "Quinta" }, + { value: "sexta", label: "Sexta" }, + { value: "sabado", label: "Sábado" }, + { value: "domingo", label: "Domingo" }, ]; return ( @@ -312,16 +338,16 @@ export function SecretaryDoctorSchedule() { className="flex items-center justify-between p-4 bg-gray-50 rounded-lg" >
-

- {avail.day_of_week} +

+ {avail.weekday || "Não especificado"}

- {avail.start_time} - {avail.end_time} + {avail.start_time} - {avail.end_time} ({avail.slot_minutes || 30} min/consulta)

- Ativo + {avail.active !== false ? "Ativo" : "Inativo"} + +
+ + + {/* Search and Filters */} +
+
+
+ + 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" + /> +
+ + +
+ +
+ + +
+ Convênio: + +
+
+
+ + {/* Table */} +
+ + + + + + + + + + + {loading ? ( + + + + ) : patients.length === 0 ? ( + + + + ) : ( + patients.map((patient, index) => ( + + + + + + + )) + )} + +
+ Paciente + + Próximo Atendimento + + Convênio + + Ações +
+ Carregando pacientes... +
+ Nenhum paciente encontrado +
+
+ +
+

+ {patient.full_name} +

+

{patient.email}

+

+ {patient.phone_mobile} +

+
+
+
+ {/* TODO: Buscar próximo agendamento */}— + + Particular + +
+ + + + +
+
+
+ + {/* Modal de Formulário */} + {showModal && ( +
+
+ {/* Header */} +
+

+ {modalMode === "create" ? "Novo Paciente" : "Editar Paciente"} +

+ +
+ + {/* Form Content */} +
+ +
+
+
+ )} + + ); +} diff --git a/MEDICONNECT 2/src/components/secretaria/SecretaryPatientList.tsx b/MEDICONNECT 2/src/components/secretaria/SecretaryPatientList.tsx index 152985fcb..8ac0bfe2d 100644 --- a/MEDICONNECT 2/src/components/secretaria/SecretaryPatientList.tsx +++ b/MEDICONNECT 2/src/components/secretaria/SecretaryPatientList.tsx @@ -1,9 +1,10 @@ import { useState, useEffect } from "react"; import toast from "react-hot-toast"; -import { Search, Plus, Eye, Calendar, Edit, Trash2, X } from "lucide-react"; -import { patientService, userService, type Patient } from "../../services"; +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"; const BLOOD_TYPES = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"]; @@ -83,14 +84,51 @@ export function SecretaryPatientList() { const loadPatients = async () => { setLoading(true); try { + console.log("🔄 Carregando lista de pacientes..."); 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 : []); + + // 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) { console.warn("⚠️ Nenhum paciente encontrado na API"); } - } catch (error) { - console.error("❌ Erro ao carregar pacientes:", error); + } catch (error: any) { + console.error("❌ Erro ao carregar pacientes:", { + message: error?.message, + response: error?.response?.data, + status: error?.response?.status + }); toast.error("Erro ao carregar pacientes"); setPatients([]); } finally { @@ -235,34 +273,120 @@ export function SecretaryPatientList() { await patientService.update(formData.id, patientData); toast.success("Paciente atualizado com sucesso!"); } else { - // Para criação, usa o novo endpoint create-patient com validações completas - const createData = { - email: formData.email, - full_name: formData.nome, - cpf: formData.cpf, - phone_mobile: formData.numeroTelefone, - birth_date: formData.dataNascimento || undefined, - address: formData.endereco.rua - ? `${formData.endereco.rua}${ - formData.endereco.numero ? ", " + formData.endereco.numero : "" - }${ - formData.endereco.bairro ? " - " + formData.endereco.bairro : "" - }${ - formData.endereco.cidade ? " - " + formData.endereco.cidade : "" - }${ - formData.endereco.estado ? "/" + formData.endereco.estado : "" - }` - : undefined, + // 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, }; - 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!"); } setShowModal(false); - loadPatients(); - } catch (error) { - console.error("Erro ao salvar paciente:", error); - toast.error("Erro ao salvar paciente"); + + // Aguarda um pouco antes de recarregar para o banco propagar + console.log("⏳ Aguardando 1 segundo antes de recarregar a lista..."); + 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 { setLoading(false); } @@ -281,6 +405,14 @@ export function SecretaryPatientList() { 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 (
{/* Header */} @@ -291,13 +423,23 @@ export function SecretaryPatientList() { Gerencie os pacientes cadastrados

- +
+ + +
{/* Search and Filters */} @@ -465,7 +607,8 @@ export function SecretaryPatientList() { )) - )} + ) + } diff --git a/MEDICONNECT 2/src/services/availability/availabilityService.ts b/MEDICONNECT 2/src/services/availability/availabilityService.ts index 13fa84546..6f74e2d53 100644 --- a/MEDICONNECT 2/src/services/availability/availabilityService.ts +++ b/MEDICONNECT 2/src/services/availability/availabilityService.ts @@ -21,7 +21,8 @@ class AvailabilityService { async list(filters?: ListAvailabilityFilters): Promise { const response = await apiClient.get(this.basePath, { params: filters, - }); + _skipAuth: true, + } as any); return response.data; } @@ -29,11 +30,18 @@ class AvailabilityService { * Cria uma nova configuração de disponibilidade */ async create(data: CreateAvailabilityInput): Promise { - const response = await apiClient.post( - this.basePath, - data - ); - return response.data; + try { + // Usa _skipAuth para não enviar token do usuário (backend usa service role key) + const response = await apiClient.post( + this.basePath, + data, + { _skipAuth: true } as any + ); + return response.data; + } catch (error: any) { + console.error("Erro ao criar disponibilidade:", error); + throw error; + } } /** diff --git a/MEDICONNECT 2/src/services/patients/patientService.ts b/MEDICONNECT 2/src/services/patients/patientService.ts index 5be40055f..7b87f5a6a 100644 --- a/MEDICONNECT 2/src/services/patients/patientService.ts +++ b/MEDICONNECT 2/src/services/patients/patientService.ts @@ -36,12 +36,22 @@ class PatientService { const queryString = params.toString(); const url = queryString ? `/patients?${queryString}` : "/patients"; + console.log(`[patientService.list] 📤 Chamando: ${url}`); const response = await apiClient.get(url); + console.log(`[patientService.list] ✅ Resposta:`, { + status: response.status, + total: Array.isArray(response.data) ? response.data.length : 0, + data: response.data + }); return response.data; } catch (error: any) { // Silenciar erro 401 (não autenticado) - é esperado em páginas públicas 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; } @@ -104,22 +114,27 @@ class PatientService { */ async register(data: RegisterPatientInput): Promise { 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 - const response = await ( - apiClient as any - ).postPublic("/register-patient", data); + const response = await (apiClient as any).postPublic("/register-patient", data) as any; - console.log("[patientService.register] Resposta:", response.data); + console.log("[patientService.register] ✅ Resposta recebida:", response.data); return response.data; } catch (error: any) { - console.error("[patientService.register] Erro completo:", { + console.error("[patientService.register] ❌ Erro completo:", { message: error.message, response: error.response?.data, 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; } } } diff --git a/MEDICONNECT 2/src/services/patients/types.ts b/MEDICONNECT 2/src/services/patients/types.ts index 2542ab706..49c9342f0 100644 --- a/MEDICONNECT 2/src/services/patients/types.ts +++ b/MEDICONNECT 2/src/services/patients/types.ts @@ -4,6 +4,7 @@ export interface Patient { id?: string; + user_id?: string; full_name: string; cpf: string; email: string; @@ -28,6 +29,7 @@ export interface Patient { } export interface CreatePatientInput { + user_id?: string; full_name: string; cpf: string; email: string; diff --git a/MEDICONNECT 2/src/utils/validators.ts b/MEDICONNECT 2/src/utils/validators.ts new file mode 100644 index 000000000..48e5673df --- /dev/null +++ b/MEDICONNECT 2/src/utils/validators.ts @@ -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; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..36e75ae1d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "riseup-squad18", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}