Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras

This commit is contained in:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions
+106 -141
View File
@@ -14,43 +14,39 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client'
import { supabase } from '@/lib/supabase/client';
function pickCount (row) {
return row?.patients_count ?? row?.patient_count ?? 0
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 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
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 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)
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);
}
/**
@@ -58,147 +54,116 @@ function isUniqueViolation (err) {
* Usa a view v_patient_groups_with_counts (preferencial).
* Fallback: tabela patient_groups + contagem pela pivot.
*/
export async function listGroupsWithCounts () {
const ownerId = await getOwnerId()
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 })
// 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)
}))
}
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
// 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 []
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
// 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)
}
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
}))
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)
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 raw = String(nome || '').trim();
if (!raw) throw new Error('Nome do grupo é obrigatório.');
const nNorm = normalizeNome(raw)
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)
// 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.')
}
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 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()
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
}
if (error) {
if (isUniqueViolation(error)) throw new Error('Já existe um grupo com esse nome.');
throw error;
}
return data
return data;
}
export async function updateGroup (id, nome, cor = null) {
const ownerId = await getOwnerId()
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.')
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)
// (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.')
}
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()
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
}
if (error) {
if (isUniqueViolation(error)) throw new Error('Já existe um grupo com esse nome.');
throw error;
}
return data
return data;
}
export async function deleteGroup (id) {
const ownerId = await getOwnerId()
export async function deleteGroup(id) {
const ownerId = await getOwnerId();
if (!id) throw new Error('ID inválido.')
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)
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
if (error) throw error;
return true;
}