/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Arquivo: src/features/patients/services/patientsRepository.js | V#3 — fundação: queries de patients centralizadas. | | Mesmo padrão de feature/agenda/services/agendaRepository.js. Pages devem | chamar este repo em vez de fazer supabase.from('patients') direto. |-------------------------------------------------------------------------- */ import { supabase } from '@/lib/supabase/client'; import { assertTenantId, getUid } from '@/features/agenda/services/_tenantGuards'; const PATIENTS_SELECT_BASE = ` id, tenant_id, owner_id, user_id, responsible_member_id, therapist_member_id, nome_completo, email_principal, email_alternativo, telefone, telefone_alternativo, cpf, rg, data_nascimento, naturalidade, genero, estado_civil, profissao, escolaridade, status, cep, endereco, numero, bairro, complemento, cidade, estado, pais, nome_responsavel, telefone_responsavel, cpf_responsavel, observacao_responsavel, cobranca_no_responsavel, onde_nos_conheceu, encaminhado_por, observacoes, last_attended_at, created_at, updated_at, risco_sinalizado_por, convenio_id, patient_scope `; // ───────────────────────────────────────────────────────────────────────── // Patients core // ----------------------------------------------------------------------------- /** * Lista pacientes do tenant ativo. Aceita filtros opcionais. * @param {object} opts - { tenantId, ownerId?, includeInactive?, limit? } */ export async function listPatients({ tenantId, ownerId = null, includeInactive = true, limit = null } = {}) { assertTenantId(tenantId); let q = supabase.from('patients').select(PATIENTS_SELECT_BASE).eq('tenant_id', tenantId); if (ownerId) q = q.eq('owner_id', ownerId); if (!includeInactive) q = q.neq('status', 'Inativo'); if (limit) q = q.limit(limit); q = q.order('created_at', { ascending: false }); const { data, error } = await q; if (error) throw error; return data || []; } export async function getPatientById(id, { tenantId } = {}) { if (!id) throw new Error('id obrigatório'); assertTenantId(tenantId); const { data, error } = await supabase .from('patients') .select(PATIENTS_SELECT_BASE) .eq('id', id) .eq('tenant_id', tenantId) .maybeSingle(); if (error) throw error; return data; } export async function createPatient(payload) { const tenantId = payload?.tenant_id; assertTenantId(tenantId); const ownerId = payload?.owner_id || (await getUid()); const row = { ...payload, tenant_id: tenantId, owner_id: ownerId }; const { data, error } = await supabase.from('patients').insert(row).select(PATIENTS_SELECT_BASE).single(); if (error) throw error; return data; } export async function updatePatient(id, patch, { tenantId } = {}) { if (!id) throw new Error('id obrigatório'); assertTenantId(tenantId); const { data, error } = await supabase .from('patients') .update(patch) .eq('id', id) .eq('tenant_id', tenantId) .select(PATIENTS_SELECT_BASE) .single(); if (error) throw error; return data; } export async function softDeletePatient(id, { tenantId } = {}) { if (!id) throw new Error('id obrigatório'); assertTenantId(tenantId); const { error } = await supabase .from('patients') .update({ status: 'Arquivado' }) .eq('id', id) .eq('tenant_id', tenantId); if (error) throw error; } // ───────────────────────────────────────────────────────────────────────── // Groups // ----------------------------------------------------------------------------- export async function listGroups({ tenantId, ownerId = null } = {}) { assertTenantId(tenantId); let q = supabase.from('patient_groups').select('id, nome, cor, is_system, owner_id, is_active').eq('tenant_id', tenantId).eq('is_active', true); if (ownerId) q = q.or(`is_system.eq.true,owner_id.eq.${ownerId}`); q = q.order('nome', { ascending: true }); const { data, error } = await q; if (error) throw error; return (data || []).map((g) => ({ id: g.id, name: g.nome, color: g.cor, isSystem: g.is_system })); } export async function listGroupsByPatient(patientIds, { tenantId } = {}) { if (!patientIds?.length) return []; assertTenantId(tenantId); const { data, error } = await supabase .from('patient_group_patient') .select('patient_id, patient_group_id') .eq('tenant_id', tenantId) .in('patient_id', patientIds); if (error) throw error; return data || []; } // ───────────────────────────────────────────────────────────────────────── // Tags // ----------------------------------------------------------------------------- export async function listTags({ tenantId, ownerId = null } = {}) { assertTenantId(tenantId); let q = supabase.from('patient_tags').select('id, nome, cor, owner_id').eq('tenant_id', tenantId); if (ownerId) q = q.eq('owner_id', ownerId); const { data, error } = await q; if (error) throw error; return (data || []).map((t) => ({ id: t.id, name: t.nome, color: t.cor })); } export async function listTagsByPatient(patientIds, { tenantId } = {}) { if (!patientIds?.length) return []; assertTenantId(tenantId); const { data, error } = await supabase .from('patient_patient_tag') .select('patient_id, tag_id') .eq('tenant_id', tenantId) .in('patient_id', patientIds); if (error) throw error; return data || []; } /** * Retorna {groupIds, tagIds} de um paciente. */ export async function getPatientRelations(patientId) { if (!patientId) return { groupIds: [], tagIds: [] }; const [{ data: g, error: ge }, { data: t, error: te }] = await Promise.all([ supabase.from('patient_group_patient').select('patient_group_id').eq('patient_id', patientId), supabase.from('patient_patient_tag').select('tag_id').eq('patient_id', patientId) ]); if (ge) throw ge; if (te) throw te; return { groupIds: (g || []).map((x) => x.patient_group_id).filter(Boolean), tagIds: (t || []).map((x) => x.tag_id).filter(Boolean) }; } /** * Substitui o grupo do paciente (1:1 — sistema atual). */ export async function replacePatientGroup(patientId, groupId, { tenantId } = {}) { if (!patientId) throw new Error('patientId obrigatório'); const { error: del } = await supabase.from('patient_group_patient').delete().eq('patient_id', patientId); if (del) throw del; if (!groupId) return; const { error: ins } = await supabase .from('patient_group_patient') .insert({ patient_id: patientId, patient_group_id: groupId, tenant_id: tenantId }); if (ins) throw ins; } /** * Substitui as tags do paciente (lista). Limpa antigas do owner + inserta as novas. */ export async function replacePatientTags(patientId, tagIds, { tenantId, ownerId } = {}) { if (!patientId) throw new Error('patientId obrigatório'); if (!ownerId) throw new Error('ownerId obrigatório'); const { error: del } = await supabase .from('patient_patient_tag') .delete() .eq('patient_id', patientId) .eq('owner_id', ownerId); if (del) throw del; const clean = Array.from(new Set((tagIds || []).filter(Boolean))); if (!clean.length) return; const { error: ins } = await supabase .from('patient_patient_tag') .insert(clean.map((tag_id) => ({ owner_id: ownerId, patient_id: patientId, tag_id, tenant_id: tenantId }))); if (ins) throw ins; } // ───────────────────────────────────────────────────────────────────────── // Sessões agregadas (V#8 — get_patient_session_counts RPC) // ----------------------------------------------------------------------------- /** * Retorna contagem + última sessão por paciente. Usa RPC SECURITY DEFINER. * @param {string[]} patientIds * @returns {Array<{patient_id, session_count, last_session_at}>} */ export async function getSessionCounts(patientIds) { if (!patientIds?.length) return []; const { data, error } = await supabase.rpc('get_patient_session_counts', { p_patient_ids: patientIds }); if (error) throw error; return data || []; }