170 lines
5.8 KiB
JavaScript
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;
|
|
}
|