Files
agenciapsilmno/src/services/GruposPacientes.service.js
T

170 lines
5.8 KiB
JavaScript

/*
|--------------------------------------------------------------------------
| 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;
}