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:
+129
-146
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user