- Reemplazar colores hardcoded del tema café con variables --brand-* - #c08040 → var(--brand-primary-strong) - #d99a56 → var(--brand-primary) - #f0c07c → var(--brand-accent) - #1c140c → var(--brand-surface) - #3a2a16 → var(--brand-border) - #1b1209, #14100b → var(--brand-bg) - Reemplazar colores de tipos de café con variables --coffee-* - #a855f7 → var(--coffee-uva) - #f97316 → var(--coffee-oreado) - #06b6d4 → var(--coffee-mojado) - #22c55e → var(--coffee-verde) - Reemplazar clases gray-scale de Tailwind con variables de tema - text-gray-400, text-gray-500 → text-[var(--brand-text-muted)] - bg-gray-700/30 → bg-[var(--brand-surface)] - Todos los componentes ahora responden dinámicamente a cambios de tema Archivos adaptados: - Páginas: error, informe-ingresos, panorama, explorer, metabase-debug, profile, notifications, settings - Componentes de ingresos: GraficaSerieIngresos, GraficaSerieInversion, GraficaDinamicaPagadoDeposito, GraficaAcumuladoresUva, TotalesIngresoCompra, TotalesMonetarios, TotalesVerde, SecosVendidos, TopClientes, VistaTablaIngresos, VistaTablaIngresosConClientes, FiltrosActivos - Componentes de comparativa: CosechasHeatmap, CosechasPorTipo, CosechasEvolucion, CosechasTotales - Componentes de UI: ClienteSelector, DateRangeSelector, MetadatosCard, MaintenanceMode - Componentes de auth: UserAvatar, UserMetadata - Componentes de clientes: ClienteCard, VistaTablaClientes - Componentes de rechazos: RechazoCard, RechazosRechazoCard, RechazosSubproductos - Componentes de metabase: MetabaseCardDisplay, MetabaseCardsTable
293 lines
9.3 KiB
Vue
293 lines
9.3 KiB
Vue
<template>
|
|
<div class="space-y-4">
|
|
<!-- Filters and Search -->
|
|
<div class="flex flex-col sm:flex-row gap-3">
|
|
<UInput
|
|
v-model="search"
|
|
placeholder="Buscar por nombre o ID..."
|
|
icon="i-heroicons-magnifying-glass"
|
|
class="flex-1"
|
|
:ui="{
|
|
base: 'bg-[var(--brand-bg)] text-[var(--brand-text)] border border-[var(--brand-border)] focus:ring-2 focus:ring-[var(--brand-primary-strong)] focus:border-[var(--brand-primary-strong)]',
|
|
placeholder: 'placeholder-[var(--brand-text-muted)]',
|
|
icon: {
|
|
base: 'text-[var(--brand-text-muted)]'
|
|
}
|
|
}"
|
|
/>
|
|
<USelectMenu
|
|
v-model="selectedFilter"
|
|
:options="filterOptions"
|
|
placeholder="Filtrar..."
|
|
class="w-full sm:w-auto"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<UTable
|
|
:rows="filteredCards"
|
|
:columns="columns"
|
|
:loading="loading"
|
|
@select="selectCard"
|
|
>
|
|
<template #id-data="{ row }">
|
|
<UBadge color="gray" variant="subtle">{{ row.id }}</UBadge>
|
|
</template>
|
|
|
|
<template #name-data="{ row }">
|
|
<div>
|
|
<div class="font-medium text-[var(--brand-text)]">{{ row.name }}</div>
|
|
<div v-if="row.description" class="text-xs text-[var(--brand-text-muted)] truncate max-w-md">
|
|
{{ row.description }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #query_type-data="{ row }">
|
|
<UBadge :color="getQueryTypeColor(row.query_type || row.dataset_query?.type)">
|
|
{{ row.query_type || row.dataset_query?.type || 'N/A' }}
|
|
</UBadge>
|
|
</template>
|
|
|
|
<template #database_id-data="{ row }">
|
|
<span class="text-sm text-[var(--brand-text)]">{{ row.database_id || 'N/A' }}</span>
|
|
</template>
|
|
|
|
<template #actions-data="{ row }">
|
|
<div class="flex flex-wrap gap-1">
|
|
<UButton
|
|
@click.stop="executeCard(row)"
|
|
:loading="executingCards.has(row.id)"
|
|
:ui="{ base: 'bg-[var(--brand-primary-strong)] text-[var(--brand-bg)] border border-[var(--brand-primary)] hover:bg-[var(--brand-primary)] hover:border-[var(--brand-accent)] disabled:opacity-50 disabled:cursor-not-allowed' }"
|
|
size="xs"
|
|
>
|
|
Ejecutar
|
|
</UButton>
|
|
|
|
<UButton
|
|
@click.stop="executeCachedCard(row)"
|
|
:loading="executingCachedCards.has(row.id)"
|
|
color="gray"
|
|
variant="ghost"
|
|
size="xs"
|
|
>
|
|
Caché
|
|
</UButton>
|
|
|
|
<UDropdown :items="getExportItems(row)">
|
|
<UButton
|
|
color="gray"
|
|
variant="ghost"
|
|
size="xs"
|
|
icon="i-heroicons-arrow-down-tray"
|
|
/>
|
|
</UDropdown>
|
|
</div>
|
|
</template>
|
|
</UTable>
|
|
|
|
<!-- Results Modal -->
|
|
<UModal v-model="showResults">
|
|
<UCard class="brand-card border border-transparent">
|
|
<template #header>
|
|
<div class="flex justify-between items-center">
|
|
<h3 class="text-lg font-semibold brand-section-title">Resultados: {{ selectedCardForResults?.name }}</h3>
|
|
<UButton
|
|
color="gray"
|
|
variant="ghost"
|
|
icon="i-heroicons-x-mark"
|
|
@click="showResults = false"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<div v-if="currentResult" class="space-y-3">
|
|
<div class="flex flex-wrap gap-4 text-sm text-[var(--brand-text)]">
|
|
<div>
|
|
<span class="font-medium text-[var(--brand-text-muted)]">Filas:</span>
|
|
<span class="ml-2">{{ currentResult.data?.rows?.length || 0 }}</span>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-[var(--brand-text-muted)]">Tiempo:</span>
|
|
<span class="ml-2">{{ currentResult.running_time || 0 }}ms</span>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-[var(--brand-text-muted)]">Estado:</span>
|
|
<UBadge :color="currentResult.status === 'completed' ? 'green' : 'yellow'">
|
|
{{ currentResult.status }}
|
|
</UBadge>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-if="!currentResult.data?.rows || currentResult.data.rows.length === 0" class="p-8 text-center rounded-lg border border-[var(--brand-border)] bg-[var(--brand-surface)]">
|
|
<div class="mx-auto w-16 h-16 mb-4 flex items-center justify-center bg-[#3a2a16] rounded-full">
|
|
<svg class="w-8 h-8 text-[var(--brand-text-muted)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
|
|
</svg>
|
|
</div>
|
|
<h3 class="text-sm font-medium text-[var(--brand-text)] mb-1">No hay datos</h3>
|
|
<p class="text-xs text-[var(--brand-text-muted)]">La consulta se ejecutó correctamente pero no devolvió resultados.</p>
|
|
</div>
|
|
|
|
<!-- Data Display -->
|
|
<div v-else class="overflow-x-auto max-h-96">
|
|
<pre class="p-3 rounded-lg border border-[var(--brand-border)] bg-[var(--brand-surface)] text-xs whitespace-pre-wrap break-words text-[var(--brand-text)]">{{ JSON.stringify(currentResult.data, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<UAlert
|
|
v-if="currentError"
|
|
color="red"
|
|
variant="soft"
|
|
:title="currentError"
|
|
class="mt-3"
|
|
/>
|
|
</UCard>
|
|
</UModal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const props = defineProps<{
|
|
cards: any[]
|
|
loading?: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
select: [card: any]
|
|
}>()
|
|
|
|
const search = ref('')
|
|
const selectedFilter = ref('all')
|
|
const executingCards = ref(new Set<number>())
|
|
const executingCachedCards = ref(new Set<number>())
|
|
const showResults = ref(false)
|
|
const currentResult = ref<any>(null)
|
|
const currentError = ref<string | null>(null)
|
|
const selectedCardForResults = ref<any>(null)
|
|
|
|
const filterOptions = [
|
|
{ value: 'all', label: 'Todas' },
|
|
{ value: 'native', label: 'SQL Nativo' },
|
|
{ value: 'query', label: 'Query Builder' }
|
|
]
|
|
|
|
const columns = [
|
|
{ key: 'id', label: 'ID' },
|
|
{ key: 'name', label: 'Nombre' },
|
|
{ key: 'query_type', label: 'Tipo' },
|
|
{ key: 'database_id', label: 'DB' },
|
|
{ key: 'actions', label: 'Acciones' }
|
|
]
|
|
|
|
const filteredCards = computed(() => {
|
|
let result = props.cards
|
|
|
|
// Filter by search
|
|
if (search.value) {
|
|
const searchLower = search.value.toLowerCase()
|
|
result = result.filter(card =>
|
|
card.name?.toLowerCase().includes(searchLower) ||
|
|
card.id?.toString().includes(searchLower) ||
|
|
card.description?.toLowerCase().includes(searchLower)
|
|
)
|
|
}
|
|
|
|
// Filter by type
|
|
if (selectedFilter.value !== 'all') {
|
|
result = result.filter(card => {
|
|
const type = card.query_type || card.dataset_query?.type
|
|
return type === selectedFilter.value
|
|
})
|
|
}
|
|
|
|
return result
|
|
})
|
|
|
|
function getQueryTypeColor(type: string) {
|
|
switch (type) {
|
|
case 'native':
|
|
return 'blue'
|
|
case 'query':
|
|
return 'green'
|
|
default:
|
|
return 'gray'
|
|
}
|
|
}
|
|
|
|
function selectCard(card: any) {
|
|
emit('select', card)
|
|
}
|
|
|
|
async function executeCard(card: any) {
|
|
executingCards.value.add(card.id)
|
|
currentError.value = null
|
|
currentResult.value = null
|
|
selectedCardForResults.value = card
|
|
|
|
try {
|
|
const result = await $fetch(`/api/metabase/cards/${card.id}/query`, {
|
|
method: 'POST',
|
|
body: {
|
|
parameters: [
|
|
{ type: 'category', target: ['variable', ['template-tag', 'incluir_anulados']], value: [false] },
|
|
{ type: 'date/single', target: ['variable', ['template-tag', 'fecha_desde']], value: null },
|
|
{ type: 'date/single', target: ['variable', ['template-tag', 'fecha_hasta']], value: null }
|
|
]
|
|
}
|
|
})
|
|
currentResult.value = result
|
|
showResults.value = true
|
|
} catch (e: any) {
|
|
currentError.value = e.message || 'Error al ejecutar la query'
|
|
showResults.value = true
|
|
} finally {
|
|
executingCards.value.delete(card.id)
|
|
}
|
|
}
|
|
|
|
async function executeCachedCard(card: any) {
|
|
executingCachedCards.value.add(card.id)
|
|
currentError.value = null
|
|
currentResult.value = null
|
|
selectedCardForResults.value = card
|
|
|
|
try {
|
|
const result = await $fetch(`/api/metabase/cards/${card.id}/query`)
|
|
currentResult.value = result
|
|
showResults.value = true
|
|
} catch (e: any) {
|
|
currentError.value = e.message || 'Error al ejecutar la query con caché'
|
|
showResults.value = true
|
|
} finally {
|
|
executingCachedCards.value.delete(card.id)
|
|
}
|
|
}
|
|
|
|
function getExportItems(card: any) {
|
|
return [[
|
|
{
|
|
label: 'CSV',
|
|
icon: 'i-heroicons-document-text',
|
|
click: () => downloadExport(card, 'csv')
|
|
},
|
|
{
|
|
label: 'JSON',
|
|
icon: 'i-heroicons-code-bracket',
|
|
click: () => downloadExport(card, 'json')
|
|
},
|
|
{
|
|
label: 'XLSX',
|
|
icon: 'i-heroicons-table-cells',
|
|
click: () => downloadExport(card, 'xlsx')
|
|
}
|
|
]]
|
|
}
|
|
|
|
function downloadExport(card: any, format: 'csv' | 'json' | 'xlsx') {
|
|
const url = `${window.location.origin}/api/metabase/cards/${card.id}/query/${format}`
|
|
window.open(url, '_blank')
|
|
}
|
|
</script>
|