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;
}
+37 -51
View File
@@ -14,64 +14,50 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client'
import { supabase } from '@/lib/supabase/client';
export 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
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;
}
export async function fetchSlotsRegras(ownerId) {
const { data, error } = await supabase
.from('agenda_slots_regras')
.select('*')
.eq('owner_id', ownerId)
.order('dia_semana', { ascending: true })
if (error) throw error
return data || []
const { data, error } = await supabase.from('agenda_slots_regras').select('*').eq('owner_id', ownerId).order('dia_semana', { ascending: true });
if (error) throw error;
return data || [];
}
export async function upsertSlotRegra(ownerId, payload) {
const row = {
owner_id: ownerId,
dia_semana: Number(payload.dia_semana),
passo_minutos: Number(payload.passo_minutos),
offset_minutos: Number(payload.offset_minutos),
buffer_antes_min: Number(payload.buffer_antes_min || 0),
buffer_depois_min: Number(payload.buffer_depois_min || 0),
min_antecedencia_horas: Number(payload.min_antecedencia_horas || 0),
ativo: !!payload.ativo
}
const row = {
owner_id: ownerId,
dia_semana: Number(payload.dia_semana),
passo_minutos: Number(payload.passo_minutos),
offset_minutos: Number(payload.offset_minutos),
buffer_antes_min: Number(payload.buffer_antes_min || 0),
buffer_depois_min: Number(payload.buffer_depois_min || 0),
min_antecedencia_horas: Number(payload.min_antecedencia_horas || 0),
ativo: !!payload.ativo
};
const { data, error } = await supabase
.from('agenda_slots_regras')
.upsert(row, { onConflict: 'owner_id,dia_semana' })
.select('*')
.single()
const { data, error } = await supabase.from('agenda_slots_regras').upsert(row, { onConflict: 'owner_id,dia_semana' }).select('*').single();
if (error) throw error
return data
if (error) throw error;
return data;
}
export function normalizeHHMM(v) {
if (v == null) return null
const s = String(v).trim()
if (/^\d{2}:\d{2}$/.test(s)) return s
if (/^\d{2}:\d{2}:\d{2}$/.test(s)) return s.slice(0, 5)
return s
if (v == null) return null;
const s = String(v).trim();
if (/^\d{2}:\d{2}$/.test(s)) return s;
if (/^\d{2}:\d{2}:\d{2}$/.test(s)) return s.slice(0, 5);
return s;
}
export function ruleKey(r) {
return [
r.dia_semana,
normalizeHHMM(r.hora_inicio),
normalizeHHMM(r.hora_fim),
(r.modalidade || 'ambos'),
!!r.ativo
].join('|')
return [r.dia_semana, normalizeHHMM(r.hora_inicio), normalizeHHMM(r.hora_fim), r.modalidade || 'ambos', !!r.ativo].join('|');
}
/**
@@ -79,13 +65,13 @@ export function ruleKey(r) {
* (DB já tem UNIQUE, mas isso evita erro e deixa UX melhor)
*/
export function dedupeRegrasSemanais(regras) {
const seen = new Set()
const out = []
for (const r of regras || []) {
const k = ruleKey(r)
if (seen.has(k)) continue
seen.add(k)
out.push(r)
}
return out
const seen = new Set();
const out = [];
for (const r of regras || []) {
const k = ruleKey(r);
if (seen.has(k)) continue;
seen.add(k);
out.push(r);
}
return out;
}
+22 -35
View File
@@ -14,47 +14,34 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client'
import { supabase } from '@/lib/supabase/client';
export async function fetchSlotsBloqueados(ownerId, diaSemana) {
const { data, error } = await supabase
.from('agenda_slots_bloqueados_semanais')
.select('*')
.eq('owner_id', ownerId)
.eq('dia_semana', diaSemana)
.eq('ativo', true)
.order('hora_inicio', { ascending: true })
const { data, error } = await supabase.from('agenda_slots_bloqueados_semanais').select('*').eq('owner_id', ownerId).eq('dia_semana', diaSemana).eq('ativo', true).order('hora_inicio', { ascending: true });
if (error) throw error
return data || []
if (error) throw error;
return data || [];
}
export async function setSlotBloqueado(ownerId, diaSemana, horaInicio, isBloqueado, motivo = null) {
if (isBloqueado) {
const { error } = await supabase
.from('agenda_slots_bloqueados_semanais')
.upsert(
{
owner_id: ownerId,
dia_semana: diaSemana,
hora_inicio: horaInicio,
motivo: motivo || null,
ativo: true
},
{ onConflict: 'owner_id,dia_semana,hora_inicio' }
)
if (error) throw error
return true
}
if (isBloqueado) {
const { error } = await supabase.from('agenda_slots_bloqueados_semanais').upsert(
{
owner_id: ownerId,
dia_semana: diaSemana,
hora_inicio: horaInicio,
motivo: motivo || null,
ativo: true
},
{ onConflict: 'owner_id,dia_semana,hora_inicio' }
);
if (error) throw error;
return true;
}
// "desbloquear": deletar (ou marcar ativo=false; aqui vou deletar por simplicidade)
const { error } = await supabase
.from('agenda_slots_bloqueados_semanais')
.delete()
.eq('owner_id', ownerId)
.eq('dia_semana', diaSemana)
.eq('hora_inicio', horaInicio)
// "desbloquear": deletar (ou marcar ativo=false; aqui vou deletar por simplicidade)
const { error } = await supabase.from('agenda_slots_bloqueados_semanais').delete().eq('owner_id', ownerId).eq('dia_semana', diaSemana).eq('hora_inicio', horaInicio);
if (error) throw error
return true
if (error) throw error;
return true;
}
+10 -10
View File
@@ -14,21 +14,21 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client'
import { supabase } from '@/lib/supabase/client';
export const signIn = async (email, password) => {
return await supabase.auth.signInWithPassword({ email, password })
}
return await supabase.auth.signInWithPassword({ email, password });
};
export const signUp = async (email, password) => {
return await supabase.auth.signUp({ email, password })
}
return await supabase.auth.signUp({ email, password });
};
export const signOut = async () => {
return await supabase.auth.signOut()
}
return await supabase.auth.signOut();
};
export const getUser = async () => {
const { data } = await supabase.auth.getUser()
return data.user
}
const { data } = await supabase.auth.getUser();
return data.user;
};
+28 -52
View File
@@ -14,79 +14,55 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client'
import { supabase } from '@/lib/supabase/client';
async function getOwnerId() {
const { data, error } = await supabase.auth.getUser()
if (error) throw error
const user = data?.user
if (!user) throw new Error('Você precisa estar logado.')
return user.id
const { data, error } = await supabase.auth.getUser();
if (error) throw error;
const user = data?.user;
if (!user) throw new Error('Você precisa estar logado.');
return user.id;
}
export async function listTagsWithCounts() {
const ownerId = await getOwnerId()
const v = await supabase
.from('v_tag_patient_counts')
.select('*')
.eq('owner_id', ownerId)
.order('name', { ascending: true })
const ownerId = await getOwnerId();
const v = await supabase.from('v_tag_patient_counts').select('*').eq('owner_id', ownerId).order('name', { ascending: true });
if (!v.error) return v.data || []
if (!v.error) return v.data || [];
const t = await supabase
.from('patient_tags')
.select('id, owner_id, name, color, is_native, created_at, updated_at')
.eq('owner_id', ownerId)
.order('name', { ascending: true })
const t = await supabase.from('patient_tags').select('id, owner_id, name, color, is_native, created_at, updated_at').eq('owner_id', ownerId).order('name', { ascending: true });
if (t.error) throw t.error
return (t.data || []).map(r => ({ ...r, patient_count: 0 }))
if (t.error) throw t.error;
return (t.data || []).map((r) => ({ ...r, patient_count: 0 }));
}
export async function createTag({ name, color = null }) {
const ownerId = await getOwnerId()
const { error } = await supabase.from('patient_tags').insert({ owner_id: ownerId, name, color })
if (error) throw error
const ownerId = await getOwnerId();
const { error } = await supabase.from('patient_tags').insert({ owner_id: ownerId, name, color });
if (error) throw error;
}
export async function updateTag({ id, name, color = null }) {
const ownerId = await getOwnerId()
const { error } = await supabase
.from('patient_tags')
.update({ name, color, updated_at: new Date().toISOString() })
.eq('id', id)
.eq('owner_id', ownerId)
if (error) throw error
const ownerId = await getOwnerId();
const { error } = await supabase.from('patient_tags').update({ name, color, updated_at: new Date().toISOString() }).eq('id', id).eq('owner_id', ownerId);
if (error) throw error;
}
export async function deleteTagsByIds(ids = []) {
const ownerId = await getOwnerId()
if (!ids.length) return
const ownerId = await getOwnerId();
if (!ids.length) return;
const pivotDel = await supabase
.from('patient_patient_tag')
.delete()
.eq('owner_id', ownerId)
.in('tag_id', ids)
if (pivotDel.error) throw pivotDel.error
const pivotDel = await supabase.from('patient_patient_tag').delete().eq('owner_id', ownerId).in('tag_id', ids);
if (pivotDel.error) throw pivotDel.error;
const tagDel = await supabase
.from('patient_tags')
.delete()
.eq('owner_id', ownerId)
.in('id', ids)
if (tagDel.error) throw tagDel.error
const tagDel = await supabase.from('patient_tags').delete().eq('owner_id', ownerId).in('id', ids);
if (tagDel.error) throw tagDel.error;
}
export async function fetchPatientsByTagId(tagId) {
const ownerId = await getOwnerId()
const { data, error } = await supabase
.from('patient_patient_tag')
.select('patient_id, patients:patients(id, name, email, phone)')
.eq('owner_id', ownerId)
.eq('tag_id', tagId)
const ownerId = await getOwnerId();
const { data, error } = await supabase.from('patient_patient_tag').select('patient_id, patients:patients(id, name, email, phone)').eq('owner_id', ownerId).eq('tag_id', tagId);
if (error) throw error
return (data || []).map(r => r.patients).filter(Boolean)
if (error) throw error;
return (data || []).map((r) => r.patients).filter(Boolean);
}
+129 -146
View File
@@ -14,52 +14,52 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client'
import { supabase } from '@/lib/supabase/client';
// --------------------------------------
// Helpers
// --------------------------------------
function applyFilters (query, { q, status, planKey, interval }) {
if (q) query = query.ilike('email', `%${q}%`)
if (status) query = query.eq('status', status)
if (planKey) query = query.eq('plan_key', planKey)
if (interval) query = query.eq('interval', interval)
return query
function applyFilters(query, { q, status, planKey, interval }) {
if (q) query = query.ilike('email', `%${q}%`);
if (status) query = query.eq('status', status);
if (planKey) query = query.eq('plan_key', planKey);
if (interval) query = query.eq('interval', interval);
return query;
}
function getWriteTableByTarget (planTarget) {
const t = String(planTarget || '').toLowerCase()
if (t === 'clinic') return 'subscription_intents_tenant'
if (t === 'therapist') return 'subscription_intents_personal'
return null
function getWriteTableByTarget(planTarget) {
const t = String(planTarget || '').toLowerCase();
if (t === 'clinic') return 'subscription_intents_tenant';
if (t === 'therapist') return 'subscription_intents_personal';
return null;
}
async function fetchIntentFromView (intentId) {
const { data, error } = await supabase
.from('subscription_intents') // ✅ VIEW (read)
.select('*')
.eq('id', intentId)
.maybeSingle()
async function fetchIntentFromView(intentId) {
const { data, error } = await supabase
.from('subscription_intents') // ✅ VIEW (read)
.select('*')
.eq('id', intentId)
.maybeSingle();
if (error) throw error
if (!data) throw new Error('Intenção não encontrada.')
return data
if (error) throw error;
if (!data) throw new Error('Intenção não encontrada.');
return data;
}
// --------------------------------------
// Public API
// --------------------------------------
export async function listSubscriptionIntents (filters = {}) {
let query = supabase
.from('subscription_intents') // ✅ VIEW
.select('*')
.order('created_at', { ascending: false })
export async function listSubscriptionIntents(filters = {}) {
let query = supabase
.from('subscription_intents') // ✅ VIEW
.select('*')
.order('created_at', { ascending: false });
query = applyFilters(query, filters)
query = applyFilters(query, filters);
const { data, error } = await query
if (error) throw error
return data || []
const { data, error } = await query;
if (error) throw error;
return data || [];
}
/**
@@ -70,41 +70,35 @@ export async function listSubscriptionIntents (filters = {}) {
* - strictPlanKey (default true): filtra por plan_key (evita pegar plano errado)
* - onlyActive (default false): se true, busca somente status 'active'
*/
export async function findLatestSubscriptionForIntent (intentOrId, opts = {}) {
const { strictPlanKey = true, onlyActive = false } = opts
export async function findLatestSubscriptionForIntent(intentOrId, opts = {}) {
const { strictPlanKey = true, onlyActive = false } = opts;
const intent = typeof intentOrId === 'string'
? await fetchIntentFromView(intentOrId)
: intentOrId
const intent = typeof intentOrId === 'string' ? await fetchIntentFromView(intentOrId) : intentOrId;
const target = String(intent?.plan_target || '').toLowerCase()
const planKey = intent?.plan_key || null
const intentUserId = intent?.user_id || intent?.created_by_user_id || null
const tenantId = intent?.tenant_id || null
const target = String(intent?.plan_target || '').toLowerCase();
const planKey = intent?.plan_key || null;
const intentUserId = intent?.user_id || intent?.created_by_user_id || null;
const tenantId = intent?.tenant_id || null;
let query = supabase
.from('subscriptions')
.select('*')
.order('created_at', { ascending: false })
.limit(1)
let query = supabase.from('subscriptions').select('*').order('created_at', { ascending: false }).limit(1);
if (onlyActive) query = query.eq('status', 'active')
if (onlyActive) query = query.eq('status', 'active');
if (target === 'clinic') {
if (!tenantId) throw new Error('Intenção clinic sem tenant_id.')
query = query.eq('tenant_id', tenantId)
if (strictPlanKey && planKey) query = query.eq('plan_key', planKey)
} else if (target === 'therapist') {
if (!intentUserId) throw new Error('Intenção therapist sem user_id.')
query = query.eq('user_id', intentUserId).is('tenant_id', null)
if (strictPlanKey && planKey) query = query.eq('plan_key', planKey)
} else {
throw new Error('plan_target inválido na intenção (esperado clinic/therapist).')
}
if (target === 'clinic') {
if (!tenantId) throw new Error('Intenção clinic sem tenant_id.');
query = query.eq('tenant_id', tenantId);
if (strictPlanKey && planKey) query = query.eq('plan_key', planKey);
} else if (target === 'therapist') {
if (!intentUserId) throw new Error('Intenção therapist sem user_id.');
query = query.eq('user_id', intentUserId).is('tenant_id', null);
if (strictPlanKey && planKey) query = query.eq('plan_key', planKey);
} else {
throw new Error('plan_target inválido na intenção (esperado clinic/therapist).');
}
const { data, error } = await query.maybeSingle()
if (error) throw error
return data || null
const { data, error } = await query.maybeSingle();
if (error) throw error;
return data || null;
}
/**
@@ -112,105 +106,94 @@ export async function findLatestSubscriptionForIntent (intentOrId, opts = {}) {
* 1) se existir intent.subscription_id, tenta carregar ela
* 2) fallback: busca a mais recente coerente com o target
*/
export async function getSubscriptionForIntent (intentOrId, opts = {}) {
const intent = typeof intentOrId === 'string'
? await fetchIntentFromView(intentOrId)
: intentOrId
export async function getSubscriptionForIntent(intentOrId, opts = {}) {
const intent = typeof intentOrId === 'string' ? await fetchIntentFromView(intentOrId) : intentOrId;
const subId = intent?.subscription_id || null
if (subId) {
const { data, error } = await supabase
.from('subscriptions')
.select('*')
.eq('id', subId)
.maybeSingle()
if (error) throw error
if (data?.id) return data
}
const subId = intent?.subscription_id || null;
if (subId) {
const { data, error } = await supabase.from('subscriptions').select('*').eq('id', subId).maybeSingle();
if (error) throw error;
if (data?.id) return data;
}
return await findLatestSubscriptionForIntent(intent, opts)
return await findLatestSubscriptionForIntent(intent, opts);
}
export async function markIntentPaid (intentId, notes = '') {
// 0) pega intenção na VIEW (pra descobrir plan_target e validar estado)
const intent = await fetchIntentFromView(intentId)
export async function markIntentPaid(intentId, notes = '') {
// 0) pega intenção na VIEW (pra descobrir plan_target e validar estado)
const intent = await fetchIntentFromView(intentId);
if (intent.status === 'paid') {
// idempotente: ainda tenta ativar a subscription a partir do intent (caso tenha falhado antes)
if (intent.status === 'paid') {
// idempotente: ainda tenta ativar a subscription a partir do intent (caso tenha falhado antes)
const { data: sub, error: rpcErr } = await supabase.rpc('activate_subscription_from_intent', {
p_intent_id: intentId
});
if (rpcErr) throw rpcErr;
const merged = await fetchIntentFromView(intentId);
return { intent: merged || intent, subscription: sub || null };
}
if (intent.status === 'canceled') {
throw new Error('Intenção cancelada não pode ser marcada como paga.');
}
const table = getWriteTableByTarget(intent.plan_target);
if (!table) {
throw new Error('plan_target inválido na intenção (esperado clinic/therapist).');
}
// 1) marca como pago na TABELA REAL (write)
const patch = {
status: 'paid',
paid_at: new Date().toISOString(),
notes: notes || null
};
const { data: updated, error: upErr } = await supabase.from(table).update(patch).eq('id', intentId).select('*').maybeSingle();
if (upErr) throw upErr;
// 2) ativa assinatura a partir da intenção
const { data: sub, error: rpcErr } = await supabase.rpc('activate_subscription_from_intent', {
p_intent_id: intentId
})
if (rpcErr) throw rpcErr
p_intent_id: intentId
});
const merged = await fetchIntentFromView(intentId)
return { intent: merged || intent, subscription: sub || null }
}
if (rpcErr) throw rpcErr;
if (intent.status === 'canceled') {
throw new Error('Intenção cancelada não pode ser marcada como paga.')
}
const table = getWriteTableByTarget(intent.plan_target)
if (!table) {
throw new Error('plan_target inválido na intenção (esperado clinic/therapist).')
}
// 1) marca como pago na TABELA REAL (write)
const patch = {
status: 'paid',
paid_at: new Date().toISOString(),
notes: notes || null
}
const { data: updated, error: upErr } = await supabase
.from(table)
.update(patch)
.eq('id', intentId)
.select('*')
.maybeSingle()
if (upErr) throw upErr
// 2) ativa assinatura a partir da intenção
const { data: sub, error: rpcErr } = await supabase.rpc('activate_subscription_from_intent', {
p_intent_id: intentId
})
if (rpcErr) throw rpcErr
// 3) retorna visão unificada + assinatura
const merged = await fetchIntentFromView(intentId)
return { intent: merged || updated, subscription: sub || null }
// 3) retorna visão unificada + assinatura
const merged = await fetchIntentFromView(intentId);
return { intent: merged || updated, subscription: sub || null };
}
export async function cancelIntent (intentId, notes = '') {
// 0) pega intenção na VIEW (pra descobrir plan_target e validar estado)
const intent = await fetchIntentFromView(intentId)
export async function cancelIntent(intentId, notes = '') {
// 0) pega intenção na VIEW (pra descobrir plan_target e validar estado)
const intent = await fetchIntentFromView(intentId);
if (intent.status === 'canceled') return intent
if (intent.status === 'paid') {
// regra de negócio: se você quiser permitir cancelar paid, mude aqui.
throw new Error('Intenção já paga não deve ser cancelada. Cancele a assinatura, não a intenção.')
}
if (intent.status === 'canceled') return intent;
if (intent.status === 'paid') {
// regra de negócio: se você quiser permitir cancelar paid, mude aqui.
throw new Error('Intenção já paga não deve ser cancelada. Cancele a assinatura, não a intenção.');
}
const table = getWriteTableByTarget(intent.plan_target)
if (!table) {
throw new Error('plan_target inválido na intenção (esperado clinic/therapist).')
}
const table = getWriteTableByTarget(intent.plan_target);
if (!table) {
throw new Error('plan_target inválido na intenção (esperado clinic/therapist).');
}
const { data, error } = await supabase
.from(table)
.update({
status: 'canceled',
notes: notes || null
})
.eq('id', intentId)
.select('*')
.maybeSingle()
const { data, error } = await supabase
.from(table)
.update({
status: 'canceled',
notes: notes || null
})
.eq('id', intentId)
.select('*')
.maybeSingle();
if (error) throw error
if (error) throw error;
// devolve a visão unificada
const merged = await fetchIntentFromView(intentId)
return merged || data
}
// devolve a visão unificada
const merged = await fetchIntentFromView(intentId);
return merged || data;
}