249 lines
8.6 KiB
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> |