All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 54s
Agregado z-20 al icono para que herede correctamente el color del trigger activo. Ahora el icono cambia a negro (#1b1209) cuando la tab está seleccionada, igual que el label.
307 lines
9.6 KiB
Vue
307 lines
9.6 KiB
Vue
<template>
|
|
<div class="container mx-auto px-4 py-8">
|
|
<div class="space-y-6">
|
|
<!-- Header -->
|
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-4">
|
|
<div class="flex-1">
|
|
<h1 class="text-2xl sm:text-3xl font-bold">Metabase Debug</h1>
|
|
<p class="text-sm sm:text-base text-gray-600 dark:text-gray-400 mt-1">
|
|
Herramienta de debugging para queries de Metabase
|
|
</p>
|
|
</div>
|
|
|
|
<UButton
|
|
@click="refreshCards"
|
|
:loading="loading"
|
|
icon="i-heroicons-arrow-path"
|
|
color="primary"
|
|
variant="soft"
|
|
class="w-full sm:w-auto"
|
|
>
|
|
Actualizar
|
|
</UButton>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<UCard class="brand-card border border-transparent">
|
|
<template #header>
|
|
<div>
|
|
<h2 class="text-2xl font-bold brand-section-title">Estadísticas de Metabase</h2>
|
|
<p class="text-sm text-[var(--brand-text-muted)] mt-1">Vista general de queries y cards configuradas</p>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-5 gap-3 sm:gap-4">
|
|
<div class="rounded-lg border border-[#3a2a16] bg-[#1c140c] px-6 py-4">
|
|
<div class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide mb-1">Total Cards</div>
|
|
<div class="text-3xl font-bold text-[var(--brand-primary)]">{{ cards.length }}</div>
|
|
</div>
|
|
|
|
<div class="rounded-lg border border-[#3a2a16] bg-[#1c140c] px-6 py-4">
|
|
<div class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide mb-1">SQL Nativo</div>
|
|
<div class="text-3xl font-bold text-[var(--brand-primary)]">{{ nativeQueries }}</div>
|
|
</div>
|
|
|
|
<div class="rounded-lg border border-[#3a2a16] bg-[#1c140c] px-6 py-4">
|
|
<div class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide mb-1">Query Builder</div>
|
|
<div class="text-3xl font-bold text-green-400">{{ queryBuilderQueries }}</div>
|
|
</div>
|
|
|
|
<div class="rounded-lg border border-[#3a2a16] bg-[#1c140c] px-6 py-4">
|
|
<div class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide mb-1">Queries Panorama</div>
|
|
<div class="text-3xl font-bold" :class="panoramaQueries.length === 9 ? 'text-green-400' : 'text-orange-400'">{{ panoramaQueries.length }}/9</div>
|
|
</div>
|
|
|
|
<div class="rounded-lg border border-[#3a2a16] bg-[#1c140c] px-6 py-4">
|
|
<div class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide mb-1">Queries Informe</div>
|
|
<div class="text-3xl font-bold" :class="informeQueries.length === 8 ? 'text-green-400' : 'text-orange-400'">{{ informeQueries.length }}/8</div>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- Error Display -->
|
|
<UAlert
|
|
v-if="error"
|
|
color="error"
|
|
variant="soft"
|
|
:title="error"
|
|
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', color: 'error', variant: 'link' }"
|
|
@close="error = null"
|
|
/>
|
|
|
|
<!-- Tabs -->
|
|
<UTabs
|
|
v-model="selectedTab"
|
|
:items="tabs"
|
|
:ui="{
|
|
root: 'space-y-4',
|
|
list: 'bg-[#1c140c]',
|
|
indicator: 'bg-[#c08040] z-10',
|
|
trigger: 'relative data-[state=active]:text-[#1b1209] data-[state=active]:hover:text-black data-[state=inactive]:text-[var(--brand-text-muted)] data-[state=inactive]:bg-transparent data-[state=inactive]:hover:text-[var(--brand-text)] data-[state=inactive]:hover:bg-[#2a1f14]/50 focus-visible:ring-2 focus-visible:ring-[#c08040] transition-colors duration-200',
|
|
label: 'font-medium text-sm relative z-20',
|
|
icon: 'relative z-20',
|
|
content: 'py-4'
|
|
}"
|
|
>
|
|
<!-- Table View -->
|
|
<template #table>
|
|
<div class="py-4">
|
|
<MetabaseCardsTable
|
|
:cards="cards"
|
|
:loading="loading"
|
|
@select="selectCard"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Cards View -->
|
|
<template #cards>
|
|
<div class="py-4">
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<MetabaseCardDisplay
|
|
v-for="card in cards"
|
|
:key="card.id"
|
|
:card="card"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Panorama Queries -->
|
|
<template #panorama>
|
|
<div class="py-4 space-y-4">
|
|
<UAlert
|
|
color="info"
|
|
variant="soft"
|
|
title="Queries del Panorama Facturador"
|
|
description="Estas son las 9 queries documentadas en METABASE_QUERIES_PANORAMA.md y definidas en server/config/metabase-queries.ts"
|
|
/>
|
|
|
|
<div class="grid grid-cols-1 gap-4">
|
|
<MetabaseCardDisplay
|
|
v-for="card in panoramaQueries"
|
|
:key="card.id"
|
|
:card="card"
|
|
/>
|
|
</div>
|
|
|
|
<UAlert
|
|
v-if="panoramaQueries.length < 9"
|
|
color="warning"
|
|
variant="soft"
|
|
:title="`Faltan ${9 - panoramaQueries.length} queries por encontrar`"
|
|
:description="`Queries faltantes: ${missingPanoramaQueries.join(', ')}`"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Informe Queries -->
|
|
<template #informe>
|
|
<div class="py-4 space-y-4">
|
|
<UAlert
|
|
color="info"
|
|
variant="soft"
|
|
title="Queries del Informe de Ingresos"
|
|
description="Estas son las 8 queries documentadas en METABASE_QUERIES_INFORME_INGRESOS.md y definidas en server/config/metabase-queries.ts"
|
|
/>
|
|
|
|
<div class="grid grid-cols-1 gap-4">
|
|
<MetabaseCardDisplay
|
|
v-for="card in informeQueries"
|
|
:key="card.id"
|
|
:card="card"
|
|
/>
|
|
</div>
|
|
|
|
<UAlert
|
|
v-if="informeQueries.length < 8"
|
|
color="warning"
|
|
variant="soft"
|
|
:title="`Faltan ${8 - informeQueries.length} queries por encontrar`"
|
|
:description="`Queries faltantes: ${missingInformeQueries.join(', ')}`"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Selected Card Detail -->
|
|
<template #detail>
|
|
<div v-if="selectedCard" class="py-4">
|
|
<MetabaseCardDisplay :card="selectedCard" />
|
|
</div>
|
|
</template>
|
|
</UTabs>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
definePageMeta({
|
|
layout: 'informe',
|
|
title: 'Metabase Debug'
|
|
})
|
|
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const cards = ref<any[]>([])
|
|
const selectedCard = ref<any>(null)
|
|
const selectedTab = ref(0)
|
|
|
|
// Get expected query names from centralized config (via API)
|
|
const expectedPanoramaNames = ref<string[]>([])
|
|
const expectedInformeNames = ref<string[]>([])
|
|
|
|
// Fetch query config on mount
|
|
async function fetchQueryConfig() {
|
|
try {
|
|
const config = await $fetch('/api/metabase/query-config')
|
|
expectedPanoramaNames.value = config.panorama
|
|
expectedInformeNames.value = config.informe
|
|
} catch (e: any) {
|
|
console.error('Error fetching query config:', e)
|
|
}
|
|
}
|
|
|
|
const tabs = [
|
|
{
|
|
key: 'table',
|
|
label: 'Vista Tabla',
|
|
icon: 'i-heroicons-table-cells',
|
|
slot: 'table'
|
|
},
|
|
{
|
|
key: 'cards',
|
|
label: 'Vista Cards',
|
|
icon: 'i-heroicons-squares-2x2',
|
|
slot: 'cards'
|
|
},
|
|
{
|
|
key: 'panorama',
|
|
label: 'Queries Panorama',
|
|
icon: 'i-heroicons-chart-bar',
|
|
slot: 'panorama'
|
|
},
|
|
{
|
|
key: 'informe',
|
|
label: 'Queries Informe',
|
|
icon: 'i-heroicons-document-chart-bar',
|
|
slot: 'informe'
|
|
},
|
|
{
|
|
key: 'detail',
|
|
label: 'Detalle',
|
|
icon: 'i-heroicons-document-magnifying-glass',
|
|
disabled: !selectedCard.value,
|
|
slot: 'detail'
|
|
}
|
|
]
|
|
|
|
const nativeQueries = computed(() => {
|
|
return cards.value.filter(card => {
|
|
const type = card.query_type || card.dataset_query?.type
|
|
return type === 'native'
|
|
}).length
|
|
})
|
|
|
|
const queryBuilderQueries = computed(() => {
|
|
return cards.value.filter(card => {
|
|
const type = card.query_type || card.dataset_query?.type
|
|
return type === 'query'
|
|
}).length
|
|
})
|
|
|
|
// Find panorama queries using centralized config
|
|
const panoramaQueries = computed(() => {
|
|
return cards.value.filter(card =>
|
|
expectedPanoramaNames.value.includes(card.name)
|
|
)
|
|
})
|
|
|
|
// Find informe queries using centralized config
|
|
const informeQueries = computed(() => {
|
|
return cards.value.filter(card =>
|
|
expectedInformeNames.value.includes(card.name)
|
|
)
|
|
})
|
|
|
|
// Calculate missing queries
|
|
const missingPanoramaQueries = computed(() => {
|
|
const foundNames = panoramaQueries.value.map(q => q.name)
|
|
return expectedPanoramaNames.value.filter(name => !foundNames.includes(name))
|
|
})
|
|
|
|
const missingInformeQueries = computed(() => {
|
|
const foundNames = informeQueries.value.map(q => q.name)
|
|
return expectedInformeNames.value.filter(name => !foundNames.includes(name))
|
|
})
|
|
|
|
async function fetchCards() {
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const result = await $fetch('/api/metabase/cards?f=all')
|
|
cards.value = Array.isArray(result) ? result : []
|
|
} catch (e: any) {
|
|
error.value = e.message || 'Error al cargar las cards de Metabase'
|
|
console.error('Error fetching cards:', e)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function refreshCards() {
|
|
fetchCards()
|
|
}
|
|
|
|
function selectCard(card: any) {
|
|
selectedCard.value = card
|
|
selectedTab.value = 4 // Switch to detail tab (now index 4 instead of 3)
|
|
}
|
|
|
|
// Load data on mount
|
|
onMounted(async () => {
|
|
await fetchQueryConfig()
|
|
await fetchCards()
|
|
})
|
|
</script>
|