first commit

This commit is contained in:
Leonardo
2026-02-18 22:36:45 -03:00
parent ec6b6ef53a
commit 676042268b
122 changed files with 26354 additions and 1615 deletions
+107
View File
@@ -0,0 +1,107 @@
// src/stores/entitlementsStore.js
import { defineStore } from 'pinia'
import { supabase } from '@/lib/supabase/client'
function normalizeKey(k) {
return String(k || '').trim()
}
export const useEntitlementsStore = defineStore('entitlements', {
state: () => ({
loading: false,
loadedForTenant: null,
features: [], // array reativo de feature_key liberadas
raw: [],
error: null,
loadedAt: null
}),
getters: {
can: (state) => (featureKey) => state.features.includes(featureKey),
has: (state) => (featureKey) => state.features.includes(featureKey)
},
actions: {
async fetch(tenantId, opts = {}) {
return this.loadForTenant(tenantId, opts)
},
clear() {
return this.invalidate()
},
/**
* Carrega entitlements do tenant.
* Importante: quando o plano muda, tenantId é o mesmo,
* então você DEVE chamar com { force: true }.
*
* opts:
* - force: ignora cache do tenant
* - maxAgeMs: se definido, recarrega quando loadedAt estiver velho
*/
async loadForTenant(tenantId, { force = false, maxAgeMs = 0 } = {}) {
if (!tenantId) {
this.invalidate()
return
}
const sameTenant = this.loadedForTenant === tenantId
const hasLoadedAt = typeof this.loadedAt === 'number'
const isFresh =
sameTenant &&
hasLoadedAt &&
maxAgeMs > 0 &&
Date.now() - this.loadedAt < maxAgeMs
if (!force && sameTenant && (maxAgeMs === 0 || isFresh)) return
this.loading = true
this.error = null
try {
// ✅ Modelo B: entitlements por tenant (view)
const { data, error } = await supabase
.from('v_tenant_entitlements')
.select('feature_key')
.eq('tenant_id', tenantId)
if (error) throw error
const rows = data ?? []
// normaliza, remove vazios e duplicados
const list = []
const seen = new Set()
for (const r of rows) {
const key = normalizeKey(r?.feature_key)
if (!key) continue
if (seen.has(key)) continue
seen.add(key)
list.push(key)
}
this.raw = rows
this.features = list
this.loadedForTenant = tenantId
this.loadedAt = Date.now()
} catch (e) {
this.error = e
this.raw = []
this.features = []
this.loadedForTenant = tenantId
this.loadedAt = Date.now()
} finally {
this.loading = false
}
},
invalidate() {
this.loadedForTenant = null
this.features = []
this.raw = []
this.error = null
this.loadedAt = null
this.loading = false
}
}
})
+31
View File
@@ -0,0 +1,31 @@
// src/stores/saasHealthStore.js
import { defineStore } from 'pinia'
import { supabase } from '@/lib/supabase/client'
export const useSaasHealthStore = defineStore('saasHealth', {
state: () => ({
mismatchCount: 0,
loading: false,
lastLoadedAt: null
}),
actions: {
async loadMismatchCount ({ force = false } = {}) {
if (this.loading) return
if (!force && this.lastLoadedAt && (Date.now() - this.lastLoadedAt) < 30_000) return // cache 30s
this.loading = true
try {
const { count, error } = await supabase
.from('v_subscription_feature_mismatch')
.select('*', { count: 'exact', head: true })
if (error) throw error
this.mismatchCount = Number(count || 0)
this.lastLoadedAt = Date.now()
} finally {
this.loading = false
}
}
}
})
+86
View File
@@ -0,0 +1,86 @@
// src/stores/tenantStore.js
import { defineStore } from 'pinia'
import { supabase } from '@/lib/supabase/client'
export const useTenantStore = defineStore('tenant', {
state: () => ({
loading: false,
loaded: false,
user: null, // auth user
memberships: [], // [{ tenant_id, role, status }]
activeTenantId: null,
activeRole: null,
needsTenantLink: false,
error: null
}),
actions: {
async loadSessionAndTenant () {
if (this.loading) return
this.loading = true
this.error = null
try {
// 1) auth user (estável)
const { data, error } = await supabase.auth.getSession()
if (error) throw error
this.user = data?.session?.user ?? null
// sem sessão -> não chama RPC, só marca estado
if (!this.user) {
this.memberships = []
this.activeTenantId = null
this.activeRole = null
this.needsTenantLink = false
this.loaded = true
return
}
// 2) memberships via RPC
const { data: mem, error: mErr } = await supabase.rpc('my_tenants')
if (mErr) throw mErr
this.memberships = Array.isArray(mem) ? mem : []
// 3) define active tenant (primeiro active)
const firstActive = this.memberships.find(x => x.status === 'active')
this.activeTenantId = firstActive?.tenant_id ?? null
this.activeRole = firstActive?.role ?? null
// se logou mas não tem vínculo ativo
this.needsTenantLink = !this.activeTenantId
this.loaded = true
} catch (e) {
console.warn('[tenantStore] loadSessionAndTenant falhou:', e)
this.error = e
// ⚠️ NÃO zera tudo agressivamente por erro transitório.
// Mantém o que já tinha (se tiver), mas marca loaded pra não travar o app.
// Se você preferir ser mais “duro”, só zere quando não houver sessão:
// (a sessão já foi lida acima; se der erro antes, user pode estar null)
if (!this.user) {
this.memberships = []
this.activeTenantId = null
this.activeRole = null
this.needsTenantLink = false
}
this.loaded = true
} finally {
this.loading = false
}
}
,
setActiveTenant (tenantId) {
const found = this.memberships.find(x => x.tenant_id === tenantId && x.status === 'active')
this.activeTenantId = found?.tenant_id ?? null
this.activeRole = found?.role ?? null
this.needsTenantLink = !this.activeTenantId
}
}
})