import { apiConfig, getAnonHeaders, getAuthenticatedHeaders } from '../config/api.js' import { getResponseError } from './repositoryUtils.js' export const patientRepository = { // 1. Listar pacientes async getAll() { const response = await fetch(`${apiConfig.restUrl}/patients?select=*`, { headers: getAuthenticatedHeaders() }) if (!response.ok) throw new Error(await getResponseError(response, 'Erro ao buscar pacientes.')) return response.json() }, async getById(patientId) { const [patient, appointments] = await Promise.all([ getPatientById(patientId), getAppointments().catch(() => []), ]) return patient ? mapPatientToDetail(patient, appointments) : null }, async getDirectoryRows({ doctorId } = {}) { const [patients, appointments] = await Promise.all([ this.getAll(), getAppointments({ doctorId }).catch(() => []), ]) const visiblePatients = doctorId ? getPatientsFromDoctorAppointments(patients, appointments) : patients return visiblePatients.map((patient) => mapPatientToDirectory(patient, appointments)) }, // 2. Criar paciente (direto) async create(data) { const body = { full_name: data.name, cpf: data.cpf, email: data.email, phone_mobile: data.phone, birth_date: data.birthDate || data.birth_date || null, created_by: data.createdBy || '00000000-0000-0000-0000-000000000000', } const response = await fetch(`${apiConfig.restUrl}/patients`, { method: 'POST', headers: getAuthenticatedHeaders({ Prefer: 'return=representation' }), body: JSON.stringify(body), }) if (!response.ok) { if ([401, 403].includes(response.status)) { return this.createWithValidation(data) } throw new Error(await getResponseError(response, 'Erro ao criar paciente.')) } return response.json() }, // 3. Criar paciente com validação de CPF (Edge Function) async createWithValidation(data) { const body = { full_name: data.name, cpf: data.cpf, email: data.email, phone_mobile: data.phone, birth_date: data.birthDate || data.birth_date || null, created_by: data.createdBy || '00000000-0000-0000-0000-000000000000', } const response = await fetch(`${apiConfig.functionsUrl}/create-patient`, { method: 'POST', headers: getAuthenticatedHeaders(), body: JSON.stringify(body), }) if (!response.ok) { throw new Error(await getResponseError(response, 'Erro ao criar paciente com validação.')) } return response.json() }, async registerPublic(data) { const body = cleanPayload({ full_name: data.name || data.full_name, cpf: data.cpf, email: data.email, phone_mobile: data.phone || data.phone_mobile, birth_date: data.birthDate || data.birth_date || null, redirect_url: data.redirectUrl || data.redirect_url, }) const response = await fetch(`${apiConfig.functionsUrl}/register-patient`, { method: 'POST', headers: getAnonHeaders(), body: JSON.stringify(body), }) if (!response.ok) { throw new Error(await getResponseError(response, 'Erro ao realizar auto-cadastro de paciente.')) } return response.json() }, // 4. Atualizar paciente async update(patientId, data) { const body = { full_name: data.name, cpf: data.cpf, email: data.email, phone_mobile: data.phone, birth_date: data.birthDate || data.birth_date || null, } const response = await fetch(`${apiConfig.restUrl}/patients?id=eq.${patientId}`, { method: 'PATCH', headers: getAuthenticatedHeaders({ Prefer: 'return=representation' }), body: JSON.stringify(body), }) if (!response.ok) throw new Error(await getResponseError(response, 'Erro ao atualizar paciente.')) return response.json() }, async uploadAvatar(patientId, file) { if (!patientId) { throw new Error('Não foi possível identificar o paciente para enviar o avatar.') } const extension = file.name?.split('.').pop() || 'jpg' const objectPath = `patients/${patientId}/avatar.${extension}` const avatarUrl = `${apiConfig.storageUrl}/object/avatars/${objectPath}` const response = await fetch(avatarUrl, { method: 'POST', headers: getAuthenticatedHeaders({ 'Content-Type': file.type || 'application/octet-stream', 'x-upsert': 'true', }), body: file, }) if (!response.ok) { throw new Error(await getResponseError(response, 'Falha ao enviar avatar do paciente.')) } await updatePatientAvatarUrl(patientId, avatarUrl).catch(() => null) return { avatarUrl, path: objectPath, } }, // 5. Deletar paciente async remove(patientId) { const response = await fetch(`${apiConfig.restUrl}/patients?id=eq.${patientId}`, { method: 'DELETE', headers: getAuthenticatedHeaders(), }) if (!response.ok) throw new Error(await getResponseError(response, 'Erro ao deletar paciente.')) return true }, } async function getPatientById(patientId) { const query = new URLSearchParams({ select: '*', id: `eq.${patientId}`, limit: '1', }) const response = await fetch(`${apiConfig.restUrl}/patients?${query.toString()}`, { headers: getAuthenticatedHeaders() }) if (!response.ok) throw new Error(await getResponseError(response, 'Erro ao buscar paciente.')) const data = await response.json() return Array.isArray(data) ? data[0] || null : data } function mapPatientToDirectory(patient, appointments = []) { const appointmentSummary = summarizeAppointments(patient.id, appointments) const city = getFirstValue(patient, ['city', 'cidade', 'address_city', 'municipio'], patient.address?.city) const state = getFirstValue(patient, ['state', 'uf', 'address_state', 'estado'], patient.address?.state) const insurance = getFirstValue(patient, ['insurance', 'convenio', 'health_insurance', 'insurance_name']) return { ...patient, name: patient.name || patient.full_name || patient.nome || 'Paciente', phone: patient.phone || patient.phone_mobile || patient.telefone || '', avatarUrl: normalizeAvatarUrl(patient.avatarUrl || patient.avatar_url || patient.avatar_path), detailId: patient.id, insurance: normalizeInsurance(insurance), city, state, vip: Boolean(patient.vip), birthDate: patient.birthDate || patient.birth_date || '', motherName: patient.motherName || patient.mother_name || patient.nome_mae || '', fatherName: patient.fatherName || patient.father_name || patient.nome_pai || '', ethnicity: patient.ethnicity || patient.etnia || '', maritalStatus: patient.maritalStatus || patient.marital_status || patient.estado_civil || '', phoneSecondary: patient.phoneSecondary || patient.phone_secondary || patient.phone_home || '', zipCode: patient.zipCode || patient.zip_code || patient.cep || '', addressStreet: patient.addressStreet || patient.address_street || patient.street || patient.logradouro || patient.address || '', addressNumber: patient.addressNumber || patient.address_number || patient.numero || '', addressComplement: patient.addressComplement || patient.address_complement || patient.complemento || '', plan: patient.plan || patient.plano || patient.insurance_plan || '', notesText: patient.notesText || patient.notes_text || patient.observations || patient.observacoes || '', lastVisitIso: patient.lastVisitIso || patient.last_visit_iso || appointmentSummary.lastVisitIso || null, lastVisit: patient.lastVisit || patient.last_visit || appointmentSummary.lastVisit || '', nextVisit: patient.nextVisit || patient.next_visit || appointmentSummary.nextVisit || '', } } function mapPatientToDetail(patient, appointments = []) { const directory = mapPatientToDirectory(patient, appointments) return { ...directory, age: patient.age || patient.idade || calculateAge(patient.birth_date), document: patient.document || patient.cpf || 'CPF não informado', plan: directory.plan || directory.insurance, condition: patient.condition || patient.condicao || 'Sem condicao principal', status: patient.status || 'Acompanhamento', risk: patient.risk || patient.risco || 'Baixo', email: patient.email || '', avatarUrl: directory.avatarUrl, address: formatAddress(directory) || patient.address || patient.endereco || 'Endereço não informado', team: patient.team || patient.equipe || [], notes: normalizeNotes(patient.notes || patient.observacoes || directory.notesText), exams: patient.exams || patient.exames || [], } } async function getAppointments({ doctorId } = {}) { const query = new URLSearchParams() query.set('select', '*,patients(*)') if (doctorId) { query.set('doctor_id', `eq.${doctorId}`) } const response = await fetch(`${apiConfig.restUrl}/appointments?${query.toString()}`, { headers: getAuthenticatedHeaders(), }) if (!response.ok) return [] return response.json() } function getPatientsFromDoctorAppointments(patients, appointments) { const patientById = new Map( patients .map((patient) => [normalizeId(patient.id), patient]) .filter(([id]) => id), ) const visibleIds = new Set() for (const appointment of appointments) { const patientId = normalizeId( appointment.patient_id || appointment.patientId || appointment.paciente_id || appointment.patients?.id || appointment.patient?.id || appointment.paciente?.id, ) if (!patientId) continue visibleIds.add(patientId) if (!patientById.has(patientId)) { const embeddedPatient = appointment.patients || appointment.patient || appointment.paciente if (embeddedPatient) { patientById.set(patientId, { ...embeddedPatient, id: embeddedPatient.id || patientId }) } } } return [...visibleIds] .map((patientId) => patientById.get(patientId)) .filter(Boolean) } function summarizeAppointments(patientId, appointments) { const now = new Date() const normalizedPatientId = String(patientId) const patientAppointments = appointments .filter((appointment) => String(appointment.patient_id || appointment.patientId || appointment.paciente_id || '') === normalizedPatientId) .map((appointment) => ({ ...appointment, date: getAppointmentDate(appointment), })) .filter((appointment) => appointment.date) .sort((a, b) => a.date - b.date) const past = patientAppointments.filter((appointment) => appointment.date < now) const future = patientAppointments.filter((appointment) => appointment.date >= now) const last = past.at(-1) const next = future[0] return { lastVisitIso: last ? formatDateInput(last.date) : null, lastVisit: last ? formatAppointmentLabel(last.date) : '', nextVisit: next ? formatAppointmentLabel(next.date) : '', } } function getAppointmentDate(appointment) { if (appointment.scheduled_at) { const date = new Date(appointment.scheduled_at) return Number.isNaN(date.getTime()) ? null : date } const dateValue = appointment.date || appointment.appointment_date || appointment.data const timeValue = appointment.time || appointment.appointment_time || appointment.hora || '00:00' if (!dateValue) return null const date = new Date(`${dateValue}T${timeValue}`) return Number.isNaN(date.getTime()) ? null : date } function formatAppointmentLabel(date) { return new Intl.DateTimeFormat('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }).format(date) } function formatDateInput(date) { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } function getFirstValue(source, keys, fallback = '') { for (const key of keys) { if (source?.[key]) return source[key] } return fallback || '' } function normalizeId(value) { return String(value || '').trim() } function formatAddress(patient) { return [ patient.addressStreet, patient.addressNumber, patient.addressComplement, patient.city, patient.state, patient.zipCode, ] .filter(Boolean) .join(', ') } function normalizeNotes(notes) { if (Array.isArray(notes)) return notes if (!notes) return [] return [String(notes)] } function normalizeInsurance(value) { const normalized = String(value || '').trim() if (normalized.toLowerCase() === 'bradesco saude') return 'Bradesco Saúde' return normalized } function normalizeAvatarUrl(value) { const avatar = String(value || '').trim() if (!avatar) return '' if (/^https?:\/\//i.test(avatar)) return avatar return `${apiConfig.storageUrl}/object/avatars/${avatar.replace(/^\/+/, '')}` } async function updatePatientAvatarUrl(patientId, avatarUrl) { const response = await fetch(`${apiConfig.restUrl}/patients?id=eq.${patientId}`, { method: 'PATCH', headers: getAuthenticatedHeaders({ Prefer: 'return=minimal' }), body: JSON.stringify({ avatar_url: avatarUrl }), }) if (!response.ok) { throw new Error(await getResponseError(response, 'Falha ao salvar avatar do paciente.')) } } function calculateAge(birthDate) { if (!birthDate) return 0 const birth = new Date(birthDate) if (Number.isNaN(birth.getTime())) return 0 const today = new Date() let age = today.getFullYear() - birth.getFullYear() const monthDiff = today.getMonth() - birth.getMonth() if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) { age -= 1 } return age } function cleanPayload(payload) { return Object.fromEntries( Object.entries(payload).filter(([, value]) => value !== undefined && value !== null && value !== ''), ) }