Files
analiticaNucleo/nuxt4-app/app/pages/panorama.vue

249 lines
8.6 KiB
Vue

<template>
<div class="flex flex-col gap-8">
<!-- Loading State -->
<UCard v-if="loading && !ingresosStore.hasData" class="brand-card border border-transparent">
<div class="flex flex-col items-center justify-center gap-4 py-10 text-[var(--brand-text-muted)]">
<div class="flex items-center gap-3">
<span class="inline-flex h-8 w-8 animate-spin rounded-full border-2 border-[#c08040] border-t-transparent align-middle" aria-hidden="true" />
<span class="text-sm uppercase tracking-[0.3em]">Cargando datos...</span>
</div>
<UProgress
v-if="loadingProgress > 0"
:model-value="loadingProgress"
:max="100"
size="sm"
class="w-64"
/>
<span v-if="loadingProgress > 0" class="text-xs text-[var(--brand-text-muted)]">
{{ Math.round(loadingProgress) }}%
</span>
</div>
</UCard>
<!-- Error State -->
<div v-else-if="error" class="rounded-lg border border-red-500/40 bg-red-500/18 p-4 text-sm text-red-200">
<p>Error al cargar datos: {{ error }}</p>
</div>
<!-- Main Content -->
<template v-else>
<!-- Metadatos Cards de Ingresos y Rechazos -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<MetadatosCard v-if="ingresosMetadata" :metadata="ingresosMetadata" />
<MetadatosCard v-if="rechazosMetadata" :metadata="rechazosMetadata" />
</div>
<!-- Totales Financieros - Resumen Principal -->
<UCard class="brand-card border border-transparent">
<template #header>
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold brand-section-title">Totales Financieros</h2>
<p class="text-sm text-[var(--brand-text-muted)] mt-1">Vista general de ingresos, inversiones y rechazos</p>
</div>
<UButton
:loading="refreshing"
:ui="{ base: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c]' }"
size="sm"
@click="refreshData"
>
<template #leading>
<UIcon name="i-lucide-refresh-cw" :class="{ 'animate-spin': refreshing }" />
</template>
Actualizar
</UButton>
</div>
</template>
<div class="grid grid-cols-1 md:grid-cols-3 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 Invertido en Café</div>
<div class="text-3xl font-bold text-[var(--brand-primary)]">
{{ formatCurrency(ingresosMetrics.totalInvertido.value + ingresosMetrics.inversionVerdeHastaFecha.value) }}
</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">Total Rechazos</div>
<div class="text-3xl font-bold text-green-400">
{{ formatCurrency(rechazosMetrics.totalRechazos.value) }}
</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">Balance Neto</div>
<div class="text-3xl font-bold text-[var(--brand-text)]">
{{ formatCurrency(ingresosMetrics.totalInvertido.value + ingresosMetrics.inversionVerdeHastaFecha.value - rechazosMetrics.totalRechazos.value) }}
</div>
</div>
</div>
<template #footer>
<div class="text-xs text-[var(--brand-text-muted)]">
Última actualización: {{ lastUpdated }}
</div>
</template>
</UCard>
<!-- Ingresos Sections -->
<IngresosTotalesIngresoCompra :metrics="ingresosMetrics" />
<IngresosInversionTotal :metrics="ingresosMetrics" />
<IngresosInventariosDeposito :metrics="ingresosMetrics" />
<IngresosInversionRestante :metrics="ingresosMetrics" />
<IngresosTotalesVerde :metrics="ingresosMetrics" />
<IngresosSecosVendidos :metrics="ingresosMetrics" />
<!-- Rechazos Section -->
<RechazosSubproductos :metrics="rechazosMetrics" />
</template>
</div>
</template>
<script setup lang="ts">
import { useTableDataStore } from '~/stores/tableDataFactory'
import { useMetadataStore } from '~/stores/metadata'
import { useIngresosMetrics } from '~/composables/useIngresosMetrics'
import { useRechazosMetrics } from '~/composables/useRechazosMetrics'
import type { IngresoRecord } from '~/composables/useIngresosMetrics'
import type { RechazoRecord } from '~/composables/useRechazosMetrics'
// Define page metadata
definePageMeta({
layout: 'dashboard',
title: 'Panorama Facturador'
})
// Initialize stores
const ingresosStore = useTableDataStore<IngresoRecord>('ingresos')
const rechazosStore = useTableDataStore<RechazoRecord>('rechazos')
// Reactive data from stores
const ingresos = computed(() => ingresosStore.allRecords as IngresoRecord[])
const rechazos = computed(() => rechazosStore.allRecords as RechazoRecord[])
// Calculate metrics using composables
const ingresosMetrics = useIngresosMetrics(ingresos)
const rechazosMetrics = useRechazosMetrics(rechazos)
// Loading and error states
const loading = computed(() => ingresosStore.isLoading || rechazosStore.isLoading)
const error = computed(() => ingresosStore.error || rechazosStore.error)
const refreshing = ref(false)
const loadingProgress = ref(0)
// Last updated time
const lastUpdated = computed(() => {
const ingresosDate = ingresosStore.lastUpdated
const rechazosDate = rechazosStore.lastUpdated
if (!ingresosDate && !rechazosDate) return 'Nunca'
const latest = [ingresosDate, rechazosDate]
.filter(Boolean)
.sort()
.reverse()[0]
if (!latest) return 'Nunca'
return new Date(latest).toLocaleString('es-ES', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
})
// Format currency helper
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('es-HN', {
style: 'currency',
currency: 'HNL',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(value).replace('HNL', 'L')
}
// Metadatos desde el store de metadata
const metadataStore = useMetadataStore()
const ingresosMetadata = computed(() => {
// Buscar por el nombre de la vista
const meta = metadataStore.allTables.find(t => t.table === 'vista_detalle_ingresos')
return meta ? {
...meta,
name: 'ingresos'
} : null
})
const rechazosMetadata = computed(() => {
// Buscar por el nombre de la tabla de rechazos
const meta = metadataStore.allTables.find(t => t.table === 'rechazos')
return meta ? {
...meta,
name: 'rechazos'
} : null
})
// Refresh data
async function refreshData() {
refreshing.value = true
loadingProgress.value = 0
try {
let ingresosProgress = 0
let rechazosProgress = 0
await Promise.all([
ingresosStore.loadAllDataInBatches((progress) => {
ingresosProgress = progress
loadingProgress.value = (ingresosProgress + rechazosProgress) / 2
}),
rechazosStore.loadAllDataInBatches((progress) => {
rechazosProgress = progress
loadingProgress.value = (ingresosProgress + rechazosProgress) / 2
})
])
} catch (err) {
console.error('Error refreshing data:', err)
} finally {
refreshing.value = false
loadingProgress.value = 0
}
}
// Load data on mount
onMounted(async () => {
try {
// Cargar metadatos primero
if (!metadataStore.hasMetadata) {
await metadataStore.loadMetadata()
}
// Primero cargamos del cache para mostrar datos inmediatamente
await Promise.all([
ingresosStore.loadFromCache(),
rechazosStore.loadFromCache()
])
// Si no hay datos en cache o están desactualizados, cargamos todos los datos
if (!ingresosStore.hasData || !rechazosStore.hasData) {
loadingProgress.value = 0
let ingresosProgress = 0
let rechazosProgress = 0
await Promise.all([
ingresosStore.loadAllDataInBatches((progress) => {
ingresosProgress = progress
loadingProgress.value = (ingresosProgress + rechazosProgress) / 2
}),
rechazosStore.loadAllDataInBatches((progress) => {
rechazosProgress = progress
loadingProgress.value = (ingresosProgress + rechazosProgress) / 2
})
])
loadingProgress.value = 0
}
} catch (err) {
console.error('Error loading data:', err)
}
})
</script>