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

View File

@@ -0,0 +1,231 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { supabase } from '@/lib/supabase/client'
import { useToast } from 'primevue/usetoast'
import Toolbar from 'primevue/toolbar'
import Button from 'primevue/button'
import Toast from 'primevue/toast'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
import Tag from 'primevue/tag'
import InputText from 'primevue/inputtext'
import Divider from 'primevue/divider'
const router = useRouter()
const toast = useToast()
const loading = ref(false)
const fixing = ref(false)
const fixingOwner = ref(null)
const rows = ref([])
const q = ref('')
const total = computed(() => rows.value.length)
const totalMissing = computed(() => rows.value.filter(r => r.mismatch_type === 'missing_entitlement').length)
const totalUnexpected = computed(() => rows.value.filter(r => r.mismatch_type === 'unexpected_entitlement').length)
function severityForMismatch (t) {
if (t === 'missing_entitlement') return 'danger'
if (t === 'unexpected_entitlement') return 'warning'
return 'secondary'
}
function labelForMismatch (t) {
if (t === 'missing_entitlement') return 'Missing'
if (t === 'unexpected_entitlement') return 'Unexpected'
return t || '-'
}
async function fetchAll () {
loading.value = true
try {
const { data, error } = await supabase
.from('v_subscription_feature_mismatch')
.select('*')
if (error) throw error
rows.value = data || []
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e.message || String(e), life: 5000 })
} finally {
loading.value = false
}
}
function filteredRows () {
const term = String(q.value || '').trim().toLowerCase()
if (!term) return rows.value
return rows.value.filter(r =>
String(r.owner_id || '').toLowerCase().includes(term) ||
String(r.feature_key || '').toLowerCase().includes(term) ||
String(r.mismatch_type || '').toLowerCase().includes(term)
)
}
function openOwnerSubscriptions (ownerId) {
if (!ownerId) return
router.push({ path: '/saas/subscriptions', query: { q: ownerId } })
}
async function fixOwner (ownerId) {
if (!ownerId) return
fixing.value = true
fixingOwner.value = ownerId
try {
const { error } = await supabase.rpc('rebuild_owner_entitlements', {
p_owner_id: ownerId
})
if (error) throw error
toast.add({
severity: 'success',
summary: 'Corrigido',
detail: 'Entitlements reconstruídos para este owner.',
life: 2500
})
await fetchAll()
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e.message || String(e), life: 5000 })
} finally {
fixing.value = false
fixingOwner.value = null
}
}
async function fixAll () {
fixing.value = true
try {
const { error } = await supabase.rpc('fix_all_subscription_mismatches')
if (error) throw error
toast.add({
severity: 'success',
summary: 'Sistema corrigido',
detail: 'Entitlements reconstruídos para todos os owners com divergência.',
life: 3000
})
await fetchAll()
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e.message || String(e), life: 5000 })
} finally {
fixing.value = false
}
}
onMounted(fetchAll)
</script>
<template>
<Toast />
<div class="p-4">
<Toolbar class="mb-4">
<template #start>
<div class="flex flex-col">
<div class="text-xl font-semibold leading-none">Subscription Health</div>
<small class="text-color-secondary mt-1">
Divergências entre Plano (esperado) e Entitlements (atual). Use Fix para reconstruir.
</small>
</div>
</template>
<template #end>
<div class="flex items-center gap-2 flex-wrap justify-end">
<span class="p-input-icon-left">
<i class="pi pi-search" />
<InputText v-model="q" placeholder="Buscar owner_id, feature_key..." />
</span>
<Button
label="Recarregar"
icon="pi pi-refresh"
severity="secondary"
outlined
:loading="loading"
@click="fetchAll"
/>
<Button
v-if="total > 0"
label="Fix All"
icon="pi pi-refresh"
severity="danger"
:loading="fixing"
@click="fixAll"
/>
</div>
</template>
</Toolbar>
<!-- resumo conceitual -->
<div class="surface-100 border-round p-3 mb-4">
<div class="flex flex-wrap gap-2 items-center justify-content-between">
<div class="flex gap-2 items-center flex-wrap">
<Tag :value="`Total: ${total}`" severity="secondary" />
<Tag :value="`Missing: ${totalMissing}`" severity="danger" />
<Tag :value="`Unexpected: ${totalUnexpected}`" severity="warning" />
</div>
<div class="text-color-secondary text-sm">
Missing = plano exige, mas não está ativo · Unexpected = ativo sem constar no plano
</div>
</div>
</div>
<DataTable
:value="filteredRows()"
dataKey="owner_id"
:loading="loading"
stripedRows
responsiveLayout="scroll"
sortField="owner_id"
:sortOrder="1"
>
<Column field="owner_id" header="Owner" style="min-width: 22rem" />
<Column field="feature_key" header="Feature" style="min-width: 18rem" />
<Column header="Tipo" style="width: 12rem">
<template #body="{ data }">
<Tag :severity="severityForMismatch(data.mismatch_type)" :value="labelForMismatch(data.mismatch_type)" />
</template>
</Column>
<Column header="Ações" style="min-width: 18rem">
<template #body="{ data }">
<div class="flex gap-2 flex-wrap">
<Button
icon="pi pi-external-link"
severity="secondary"
outlined
v-tooltip.top="'Abrir subscriptions deste owner (filtro)'"
@click="openOwnerSubscriptions(data.owner_id)"
/>
<Button
label="Fix owner"
icon="pi pi-wrench"
severity="danger"
outlined
:loading="fixing && fixingOwner === data.owner_id"
@click="fixOwner(data.owner_id)"
/>
</div>
</template>
</Column>
</DataTable>
<Divider class="my-5" />
<div class="text-color-secondary text-sm">
Dica: se você trocar plano e o cliente não refletir de imediato, essa página te mostra exatamente o que ficou divergente.
</div>
</div>
</template>