Dashboard SaaS ganha seção de receita de créditos WhatsApp (Asaas)
Fecha o gap de analytics que faltava: MRR/ARR de assinatura já existia, mas não havia visão de receita dos créditos WhatsApp comprados via Asaas. Banco (migration 20260423000011) — 4 RPCs saas_admin only: - saas_wa_credits_revenue_stats(from, to): total arrecadado, count de compras, tenants únicos, créditos vendidos, ticket médio. - saas_wa_credits_top_packages(from, to): ranking top 10 pacotes por revenue, consolida nome atual se pacote foi renomeado. - saas_wa_credits_usage_summary(): snapshot atual de lifetime_purchased vs lifetime_used vs current_balance + taxa de consumo. - saas_wa_credits_revenue_evolution(from, to, bucket_days): série temporal pra sparkline. Todas com check is_saas_admin() no início + SECURITY DEFINER. Frontend: - useSaasCreditsAnalytics composable orquestra as 4 RPCs em paralelo com seleção de período (30d/90d/6m/12m) que ajusta bucket_days automaticamente. - SaasCreditsRevenueCard.vue: 4 KPIs (receita + ticket médio, compras + tenants, créditos vendidos, % consumo global), sparkline SVG com indicador de tendência, ranking top 5 pacotes. - Integrado no SaasDashboard logo antes da tabela "Distribuição por plano". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Arquivo: src/composables/useSaasCreditsAnalytics.js
|
||||
|
|
||||
| Wrapper das RPCs do Dashboard SaaS pra analytics de créditos WhatsApp:
|
||||
| - saas_wa_credits_revenue_stats (agregados do período)
|
||||
| - saas_wa_credits_top_packages (ranking)
|
||||
| - saas_wa_credits_usage_summary (vendidos vs usados — snapshot)
|
||||
| - saas_wa_credits_revenue_evolution (série temporal)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
const PERIOD_DAYS = { '30d': 30, '90d': 90, '180d': 180, '365d': 365 };
|
||||
const BUCKET_BY_PERIOD = { '30d': 7, '90d': 15, '180d': 30, '365d': 30 };
|
||||
|
||||
export function useSaasCreditsAnalytics() {
|
||||
const stats = ref(null);
|
||||
const topPackages = ref([]);
|
||||
const usage = ref(null);
|
||||
const evolution = ref([]);
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
async function loadAll({ period = '30d' } = {}) {
|
||||
const days = PERIOD_DAYS[period] || 30;
|
||||
const bucketDays = BUCKET_BY_PERIOD[period] || 7;
|
||||
const to = new Date();
|
||||
const from = new Date(Date.now() - days * 24 * 3600 * 1000);
|
||||
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const [sRes, tRes, uRes, eRes] = await Promise.all([
|
||||
supabase.rpc('saas_wa_credits_revenue_stats', { p_from: from.toISOString(), p_to: to.toISOString() }),
|
||||
supabase.rpc('saas_wa_credits_top_packages', { p_from: from.toISOString(), p_to: to.toISOString() }),
|
||||
supabase.rpc('saas_wa_credits_usage_summary'),
|
||||
supabase.rpc('saas_wa_credits_revenue_evolution', { p_from: from.toISOString(), p_to: to.toISOString(), p_bucket_days: bucketDays })
|
||||
]);
|
||||
|
||||
if (sRes.error) throw sRes.error;
|
||||
if (tRes.error) throw tRes.error;
|
||||
if (uRes.error) throw uRes.error;
|
||||
if (eRes.error) throw eRes.error;
|
||||
|
||||
stats.value = Array.isArray(sRes.data) ? (sRes.data[0] || null) : sRes.data;
|
||||
topPackages.value = tRes.data || [];
|
||||
usage.value = Array.isArray(uRes.data) ? (uRes.data[0] || null) : uRes.data;
|
||||
evolution.value = eRes.data || [];
|
||||
} catch (e) {
|
||||
error.value = e;
|
||||
stats.value = null;
|
||||
topPackages.value = [];
|
||||
usage.value = null;
|
||||
evolution.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatBRL(v) {
|
||||
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(v) || 0);
|
||||
}
|
||||
|
||||
const hasRevenue = computed(() => (Number(stats.value?.revenue_brl) || 0) > 0);
|
||||
|
||||
return {
|
||||
stats,
|
||||
topPackages,
|
||||
usage,
|
||||
evolution,
|
||||
loading,
|
||||
error,
|
||||
hasRevenue,
|
||||
loadAll,
|
||||
formatBRL
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user