/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Criado e desenvolvido por Leonardo Nohama | | Tecnologia aplicada à escuta. | Estrutura para o cuidado. | | Arquivo: src/services/GruposPacientes.service.js | Data: 2026 | Local: São Carlos/SP — Brasil |-------------------------------------------------------------------------- | © 2026 — Todos os direitos reservados |-------------------------------------------------------------------------- */ import { supabase } from '@/lib/supabase/client'; function pickCount(row) { return row?.patients_count ?? row?.patient_count ?? 0; } async function getOwnerId() { const { data, error } = await supabase.auth.getUser(); if (error) throw error; const uid = data?.user?.id; if (!uid) throw new Error('Sessão inválida.'); return uid; } async function getActiveTenantId(uid) { const { data, error } = await supabase.from('tenant_members').select('tenant_id').eq('user_id', uid).eq('status', 'active').order('created_at', { ascending: false }).limit(1).single(); if (error) throw error; if (!data?.tenant_id) throw new Error('Tenant não encontrado.'); return data.tenant_id; } function normalizeNome(s) { return String(s || '') .trim() .toLowerCase() .replace(/\s+/g, ' '); } function isUniqueViolation(err) { if (!err) return false; if (err.code === '23505') return true; const msg = String(err.message || ''); return /duplicate key value violates unique constraint/i.test(msg); } /** * Lista grupos do usuário + grupos do sistema, já com contagem. * Usa a view v_patient_groups_with_counts (preferencial). * Fallback: tabela patient_groups + contagem pela pivot. */ export async function listGroupsWithCounts() { const ownerId = await getOwnerId(); // 1) View (preferencial) — agora já é a fonte correta const { data: vData, error: vErr } = await supabase.from('v_patient_groups_with_counts').select('*').or(`owner_id.eq.${ownerId},is_system.eq.true`).order('nome', { ascending: true }); if (!vErr) { return (vData || []).map((r) => ({ ...r, patients_count: pickCount(r) })); } // 2) Fallback (caso view não exista / erro de schema) const { data: groups, error: gErr } = await supabase.from('patient_groups').select('id,nome,cor,is_system,is_active,owner_id,created_at,updated_at').or(`owner_id.eq.${ownerId},is_system.eq.true`).order('nome', { ascending: true }); if (gErr) throw gErr; const ids = (groups || []).map((g) => g.id).filter(Boolean); if (!ids.length) return []; // conta pacientes por grupo na pivot const { data: rel, error: rErr } = await supabase.from('patient_group_patient').select('patient_group_id').in('patient_group_id', ids); if (rErr) throw rErr; const counts = new Map(); for (const row of rel || []) { const gid = row.patient_group_id; if (!gid) continue; counts.set(gid, (counts.get(gid) || 0) + 1); } return (groups || []).map((g) => ({ ...g, patients_count: counts.get(g.id) || 0 })); } export async function createGroup(nome, cor = null) { const ownerId = await getOwnerId(); const tenantId = await getActiveTenantId(ownerId); const raw = String(nome || '').trim(); if (!raw) throw new Error('Nome do grupo é obrigatório.'); const nNorm = normalizeNome(raw); // proteção extra no front: busca por igualdade "normalizada" // (mantém RLS como autoridade final, mas evita UX ruim) const { data: existing, error: exErr } = await supabase.from('patient_groups').select('id,nome').eq('owner_id', ownerId).eq('is_system', false).limit(50); if (!exErr && (existing || []).some((r) => normalizeNome(r.nome) === nNorm)) { throw new Error('Já existe um grupo com esse nome.'); } const payload = { owner_id: ownerId, tenant_id: tenantId, nome: raw, cor: cor || null }; const { data, error } = await supabase.from('patient_groups').insert(payload).select('id,nome,cor,is_system,owner_id,is_active,created_at,updated_at').single(); if (error) { if (isUniqueViolation(error)) throw new Error('Já existe um grupo com esse nome.'); throw error; } return data; } export async function updateGroup(id, nome, cor = null) { const ownerId = await getOwnerId(); const raw = String(nome || '').trim(); if (!id) throw new Error('ID inválido.'); if (!raw) throw new Error('Nome do grupo é obrigatório.'); // (opcional) valida duplicidade entre os grupos do owner (não-system) const nNorm = normalizeNome(raw); const { data: existing, error: exErr } = await supabase.from('patient_groups').select('id,nome').eq('owner_id', ownerId).eq('is_system', false).neq('id', id).limit(80); if (!exErr && (existing || []).some((r) => normalizeNome(r.nome) === nNorm)) { throw new Error('Já existe um grupo com esse nome.'); } const { data, error } = await supabase .from('patient_groups') .update({ nome: raw, cor: cor || null, updated_at: new Date().toISOString() }) .eq('id', id) .eq('owner_id', ownerId) .eq('is_system', false) .select('id,nome,cor,is_system,owner_id,is_active,created_at,updated_at') .single(); if (error) { if (isUniqueViolation(error)) throw new Error('Já existe um grupo com esse nome.'); throw error; } return data; } export async function deleteGroup(id) { const ownerId = await getOwnerId(); if (!id) throw new Error('ID inválido.'); const { error } = await supabase.from('patient_groups').delete().eq('id', id).eq('owner_id', ownerId).eq('is_system', false); if (error) throw error; return true; }