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
+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;
}