pagina panorama facturador en camino
This commit is contained in:
41
nuxt4-app/app/components/MetricCard.vue
Normal file
41
nuxt4-app/app/components/MetricCard.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="p-4 rounded-lg border transition-all"
|
||||||
|
:class="[variantClasses, props.class]"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-xs uppercase tracking-wide opacity-80 mb-1">{{ label }}</span>
|
||||||
|
<div class="flex items-baseline gap-2">
|
||||||
|
<span class="text-2xl font-bold">{{ value }}</span>
|
||||||
|
<span v-if="unit" class="text-sm opacity-70">{{ unit }}</span>
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
label: string
|
||||||
|
value: string | number
|
||||||
|
unit?: string
|
||||||
|
variant?: 'default' | 'primary' | 'success' | 'danger' | 'warning' | 'info'
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
variant: 'default'
|
||||||
|
})
|
||||||
|
|
||||||
|
const variantClasses = computed(() => {
|
||||||
|
const variants = {
|
||||||
|
default: 'bg-[#1c140c] border-[#3a2a16] text-[var(--brand-text)]',
|
||||||
|
primary: 'bg-[#1c140c] border-[#c08040] text-[var(--brand-primary)]',
|
||||||
|
success: 'bg-[#1c140c] border-green-600/30 text-green-400',
|
||||||
|
danger: 'bg-[#1c140c] border-red-600/30 text-red-400',
|
||||||
|
warning: 'bg-[#1c140c] border-yellow-600/30 text-yellow-400',
|
||||||
|
info: 'bg-[#1c140c] border-cyan-600/30 text-cyan-400'
|
||||||
|
}
|
||||||
|
return variants[props.variant]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
42
nuxt4-app/app/components/ingresos/InventariosDeposito.vue
Normal file
42
nuxt4-app/app/components/ingresos/InventariosDeposito.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<UCard class="brand-card border border-transparent">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-xl font-bold brand-section-title">Inventarios en Depósito</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<MetricCard
|
||||||
|
label="Total QQ Seco en Depósito"
|
||||||
|
:value="metrics.totalQqSecoDeposito.value.toFixed(2)"
|
||||||
|
unit="QQ"
|
||||||
|
variant="warning"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Total QQ Mojado en Depósito"
|
||||||
|
:value="metrics.totalQqMojadoDeposito.value.toFixed(2)"
|
||||||
|
unit="QQ"
|
||||||
|
variant="info"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Total QQ Oreado en Depósito"
|
||||||
|
:value="metrics.totalQqOreadoDeposito.value.toFixed(2)"
|
||||||
|
unit="QQ"
|
||||||
|
variant="info"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Total Lb Uva en Depósito"
|
||||||
|
:value="metrics.totalLbUvaDeposito.value.toFixed(2)"
|
||||||
|
unit="lb"
|
||||||
|
variant="info"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { IngresosMetrics } from '~/composables/useIngresosMetrics'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
metrics: IngresosMetrics
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
46
nuxt4-app/app/components/ingresos/InversionRestante.vue
Normal file
46
nuxt4-app/app/components/ingresos/InversionRestante.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<UCard class="brand-card border border-transparent">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-xl font-bold brand-section-title">Inversión Restante a Realizar</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<MetricCard
|
||||||
|
label="Inversión Restante Oreado"
|
||||||
|
:value="formatCurrency(metrics.inversionRestanteOreado.value)"
|
||||||
|
variant="warning"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Inversión Restante Mojado"
|
||||||
|
:value="formatCurrency(metrics.inversionRestanteMojado.value)"
|
||||||
|
variant="warning"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Inversión Restante Uva"
|
||||||
|
:value="formatCurrency(metrics.inversionRestanteUva.value)"
|
||||||
|
variant="warning"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Inversión Restante Esperada"
|
||||||
|
:value="formatCurrency(metrics.inversionRestanteEsperada.value)"
|
||||||
|
variant="danger"
|
||||||
|
class="font-bold"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { IngresosMetrics } from '~/composables/useIngresosMetrics'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
metrics: IngresosMetrics
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return new Intl.NumberFormat('es-GT', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'GTQ'
|
||||||
|
}).format(value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
46
nuxt4-app/app/components/ingresos/InversionTotal.vue
Normal file
46
nuxt4-app/app/components/ingresos/InversionTotal.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<UCard class="brand-card border border-transparent">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-xl font-bold brand-section-title">Inversión Hasta la Fecha</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<MetricCard
|
||||||
|
label="Inversión en Uva"
|
||||||
|
:value="formatCurrency(metrics.inversionUva.value)"
|
||||||
|
variant="success"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Inversión en Oreado"
|
||||||
|
:value="formatCurrency(metrics.inversionOreado.value)"
|
||||||
|
variant="success"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Inversión en Mojado"
|
||||||
|
:value="formatCurrency(metrics.inversionMojado.value)"
|
||||||
|
variant="success"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Total Invertido"
|
||||||
|
:value="formatCurrency(metrics.totalInvertido.value)"
|
||||||
|
variant="primary"
|
||||||
|
class="font-bold"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { IngresosMetrics } from '~/composables/useIngresosMetrics'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
metrics: IngresosMetrics
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return new Intl.NumberFormat('es-GT', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'GTQ'
|
||||||
|
}).format(value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
44
nuxt4-app/app/components/ingresos/SecosVendidos.vue
Normal file
44
nuxt4-app/app/components/ingresos/SecosVendidos.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<UCard class="brand-card border border-transparent">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-xl font-bold brand-section-title">Secos Vendidos y Pérdidas</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<MetricCard
|
||||||
|
label="Total QQ Secos por Vender"
|
||||||
|
:value="metrics.totalQqSecoPorVender.value.toFixed(2)"
|
||||||
|
unit="QQ"
|
||||||
|
variant="info"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Precio de Venta Promedio por QQ"
|
||||||
|
:value="formatCurrency(metrics.precioVentaPromedioPorQq.value)"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Precio de Compra Promedio por QQ"
|
||||||
|
:value="formatCurrency(metrics.precioCompraPromedioPorQq.value)"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Margen de Ganancia por QQ"
|
||||||
|
:value="formatCurrency(metrics.margenGananciaPorQq.value)"
|
||||||
|
:variant="metrics.margenGananciaPorQq.value > 0 ? 'success' : 'danger'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { IngresosMetrics } from '~/composables/useIngresosMetrics'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
metrics: IngresosMetrics
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return new Intl.NumberFormat('es-GT', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'GTQ'
|
||||||
|
}).format(value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
45
nuxt4-app/app/components/ingresos/TotalesIngresoCompra.vue
Normal file
45
nuxt4-app/app/components/ingresos/TotalesIngresoCompra.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<UCard class="brand-card border border-transparent">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-xl font-bold brand-section-title">Totales de Ingreso y Compra</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<MetricCard
|
||||||
|
label="Total QQ Seco Ingresado"
|
||||||
|
:value="metrics.totalQqSecoIngresado.value.toFixed(2)"
|
||||||
|
unit="QQ"
|
||||||
|
variant="primary"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Total QQ Seco Comprado"
|
||||||
|
:value="metrics.totalQqSecoComprado.value.toFixed(2)"
|
||||||
|
unit="QQ"
|
||||||
|
variant="success"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Precio Promedio Ponderado Uva"
|
||||||
|
:value="metrics.precioPromedioUvaPorQqLb.value.toFixed(2)"
|
||||||
|
unit="$/lb"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Precio Promedio Ponderado Oreado"
|
||||||
|
:value="metrics.precioPromedioOreadoPorQq.value.toFixed(2)"
|
||||||
|
unit="$/QQ"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Precio Promedio Ponderado Mojado"
|
||||||
|
:value="metrics.precioPromedioMojadoPorQq.value.toFixed(2)"
|
||||||
|
unit="$/QQ"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { IngresosMetrics } from '~/composables/useIngresosMetrics'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
metrics: IngresosMetrics
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
56
nuxt4-app/app/components/ingresos/TotalesVerde.vue
Normal file
56
nuxt4-app/app/components/ingresos/TotalesVerde.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<UCard class="brand-card border border-transparent">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-xl font-bold brand-section-title">Totales Netos de Verde</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<MetricCard
|
||||||
|
label="Total Lb Neto de Verde"
|
||||||
|
:value="metrics.totalLbNetoVerde.value.toFixed(2)"
|
||||||
|
unit="lb"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Precio Promedio Ponderado Pagado"
|
||||||
|
:value="metrics.precioPromedioVerdePagado.value.toFixed(2)"
|
||||||
|
unit="$/lb"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Total Lb Neto de Verde en Depósito"
|
||||||
|
:value="metrics.totalLbNetoVerdeDeposito.value.toFixed(2)"
|
||||||
|
unit="lb"
|
||||||
|
variant="info"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Inversión en Verde Hasta la Fecha"
|
||||||
|
:value="formatCurrency(metrics.inversionVerdeHastaFecha.value)"
|
||||||
|
variant="success"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Inversión Restante a Realizar en Verde"
|
||||||
|
:value="formatCurrency(metrics.inversionRestanteVerde.value)"
|
||||||
|
variant="warning"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Total Lb Neto Comprado de Verde"
|
||||||
|
:value="metrics.totalLbNetoCompradoVerde.value.toFixed(2)"
|
||||||
|
unit="lb"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { IngresosMetrics } from '~/composables/useIngresosMetrics'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
metrics: IngresosMetrics
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return new Intl.NumberFormat('es-GT', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'GTQ'
|
||||||
|
}).format(value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
53
nuxt4-app/app/components/rechazos/RechazoCard.vue
Normal file
53
nuxt4-app/app/components/rechazos/RechazoCard.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-[#1c140c] rounded-lg p-4 border-l-4" :class="borderColor">
|
||||||
|
<h3 class="text-lg font-semibold mb-3 capitalize text-[var(--brand-text)]">{{ title }}</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-[var(--brand-text-muted)] text-sm">Total {{ unidad }}:</span>
|
||||||
|
<span class="font-medium text-[var(--brand-text)]">{{ metrics.value.totalCantidad.toFixed(2) }} {{ unidad }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-[var(--brand-text-muted)] text-sm">Precio promedio:</span>
|
||||||
|
<span class="font-medium text-[var(--brand-text)]">{{ formatCurrency(metrics.value.precioPromedio) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center pt-2 border-t border-[#3a2a16]">
|
||||||
|
<span class="text-[var(--brand-text)] font-semibold">Total cobrado:</span>
|
||||||
|
<span class="font-bold text-green-400">{{ formatCurrency(metrics.value.totalCobrado) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { RechazoMetrics } from '~/composables/useRechazosMetrics'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string
|
||||||
|
metrics: ComputedRef<RechazoMetrics>
|
||||||
|
unidad: 'libras' | 'galones'
|
||||||
|
color?: 'blue' | 'green' | 'yellow' | 'red' | 'purple' | 'pink'
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
color: 'blue'
|
||||||
|
})
|
||||||
|
|
||||||
|
const borderColor = computed(() => {
|
||||||
|
const colors = {
|
||||||
|
blue: 'border-blue-500',
|
||||||
|
green: 'border-green-500',
|
||||||
|
yellow: 'border-yellow-500',
|
||||||
|
red: 'border-red-500',
|
||||||
|
purple: 'border-purple-500',
|
||||||
|
pink: 'border-pink-500'
|
||||||
|
}
|
||||||
|
return colors[props.color]
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return new Intl.NumberFormat('es-GT', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'GTQ'
|
||||||
|
}).format(value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
69
nuxt4-app/app/components/rechazos/RechazosSubproductos.vue
Normal file
69
nuxt4-app/app/components/rechazos/RechazosSubproductos.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<UCard class="brand-card border border-transparent">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-xl font-bold brand-section-title">Rechazos y Subproductos</h2>
|
||||||
|
<div class="rounded-lg border border-[#3a2a16] bg-[#1c140c] px-4 py-2">
|
||||||
|
<div class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide mb-1">Total Rechazos</div>
|
||||||
|
<div class="text-2xl font-bold text-green-400">{{ formatCurrency(totalRechazos.value) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<RechazosRechazoCard
|
||||||
|
title="Chibolita"
|
||||||
|
:metrics="metrics.chibolita"
|
||||||
|
unidad="libras"
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<RechazosRechazoCard
|
||||||
|
title="Perico"
|
||||||
|
:metrics="metrics.perico"
|
||||||
|
unidad="libras"
|
||||||
|
color="green"
|
||||||
|
/>
|
||||||
|
<RechazosRechazoCard
|
||||||
|
title="Vano"
|
||||||
|
:metrics="metrics.vano"
|
||||||
|
unidad="galones"
|
||||||
|
color="yellow"
|
||||||
|
/>
|
||||||
|
<RechazosRechazoCard
|
||||||
|
title="Picadillo"
|
||||||
|
:metrics="metrics.picadillo"
|
||||||
|
unidad="galones"
|
||||||
|
color="red"
|
||||||
|
/>
|
||||||
|
<RechazosRechazoCard
|
||||||
|
title="Magalla"
|
||||||
|
:metrics="metrics.magalla"
|
||||||
|
unidad="galones"
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<RechazosRechazoCard
|
||||||
|
title="Pinta"
|
||||||
|
:metrics="metrics.pinta"
|
||||||
|
unidad="libras"
|
||||||
|
color="pink"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { RechazosMetrics } from '~/composables/useRechazosMetrics'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
metrics: RechazosMetrics
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const totalRechazos = computed(() => props.metrics.totalRechazos)
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return new Intl.NumberFormat('es-GT', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'GTQ'
|
||||||
|
}).format(value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
281
nuxt4-app/app/composables/useIngresosMetrics.ts
Normal file
281
nuxt4-app/app/composables/useIngresosMetrics.ts
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import { computed } from 'vue'
|
||||||
|
import type { ComputedRef } from 'vue'
|
||||||
|
|
||||||
|
export interface IngresoRecord {
|
||||||
|
estado: 'pagado' | 'pendiente'
|
||||||
|
tipo: 'uva' | 'oreado' | 'mojado' | 'verde'
|
||||||
|
peso_seco: number
|
||||||
|
peso_neto: number
|
||||||
|
precio: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IngresosMetrics {
|
||||||
|
// Totales de ingreso y compra
|
||||||
|
totalQqSecoIngresado: ComputedRef<number>
|
||||||
|
totalQqSecoComprado: ComputedRef<number>
|
||||||
|
precioPromedioUvaPorQqLb: ComputedRef<number>
|
||||||
|
precioPromedioOreadoPorQq: ComputedRef<number>
|
||||||
|
precioPromedioMojadoPorQq: ComputedRef<number>
|
||||||
|
|
||||||
|
// Inversión
|
||||||
|
inversionUva: ComputedRef<number>
|
||||||
|
inversionOreado: ComputedRef<number>
|
||||||
|
inversionMojado: ComputedRef<number>
|
||||||
|
totalInvertido: ComputedRef<number>
|
||||||
|
|
||||||
|
// Inventarios en depósito
|
||||||
|
totalQqSecoDeposito: ComputedRef<number>
|
||||||
|
totalQqMojadoDeposito: ComputedRef<number>
|
||||||
|
totalQqOreadoDeposito: ComputedRef<number>
|
||||||
|
totalLbUvaDeposito: ComputedRef<number>
|
||||||
|
|
||||||
|
// Inversión restante
|
||||||
|
inversionRestanteOreado: ComputedRef<number>
|
||||||
|
inversionRestanteMojado: ComputedRef<number>
|
||||||
|
inversionRestanteUva: ComputedRef<number>
|
||||||
|
inversionRestanteEsperada: ComputedRef<number>
|
||||||
|
|
||||||
|
// Totales netos de verde
|
||||||
|
totalLbNetoVerde: ComputedRef<number>
|
||||||
|
precioPromedioVerdePagado: ComputedRef<number>
|
||||||
|
totalLbNetoVerdeDeposito: ComputedRef<number>
|
||||||
|
inversionVerdeHastaFecha: ComputedRef<number>
|
||||||
|
inversionRestanteVerde: ComputedRef<number>
|
||||||
|
totalLbNetoCompradoVerde: ComputedRef<number>
|
||||||
|
|
||||||
|
// Secos vendidos y pérdidas (placeholder)
|
||||||
|
totalQqSecoPorVender: ComputedRef<number>
|
||||||
|
precioVentaPromedioPorQq: ComputedRef<number>
|
||||||
|
precioCompraPromedioPorQq: ComputedRef<number>
|
||||||
|
margenGananciaPorQq: ComputedRef<number>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIngresosMetrics(ingresos: ComputedRef<IngresoRecord[]>) {
|
||||||
|
// Función auxiliar para calcular total a pagar
|
||||||
|
const calcularTotalAPagar = (ingreso: IngresoRecord): number => {
|
||||||
|
if (ingreso.tipo === 'verde' || ingreso.tipo === 'uva') {
|
||||||
|
return ingreso.precio * ingreso.peso_neto
|
||||||
|
}
|
||||||
|
if (ingreso.tipo === 'oreado' || ingreso.tipo === 'mojado') {
|
||||||
|
return (ingreso.precio / 2) * ingreso.peso_seco
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOTALES DE INGRESO Y COMPRA
|
||||||
|
const totalQqSecoIngresado = computed(() => {
|
||||||
|
return ingresos.value
|
||||||
|
.filter(i => i.estado === 'pagado' || i.estado === 'pendiente')
|
||||||
|
.reduce((sum, i) => sum + (i.peso_seco || 0), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalQqSecoComprado = computed(() => {
|
||||||
|
return ingresos.value
|
||||||
|
.filter(i => i.estado === 'pagado')
|
||||||
|
.reduce((sum, i) => sum + (i.peso_seco || 0), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const precioPromedioUvaPorQqLb = computed(() => {
|
||||||
|
const uvasPagadas = ingresos.value.filter(i => i.tipo === 'uva' && i.estado === 'pagado')
|
||||||
|
const sumaPesoNeto = uvasPagadas.reduce((sum, i) => sum + (i.peso_neto || 0), 0)
|
||||||
|
|
||||||
|
if (sumaPesoNeto === 0) return 0
|
||||||
|
|
||||||
|
const sumaProducto = uvasPagadas.reduce((sum, i) =>
|
||||||
|
sum + (i.peso_neto || 0) * (i.precio || 0), 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return sumaProducto / sumaPesoNeto
|
||||||
|
})
|
||||||
|
|
||||||
|
const precioPromedioOreadoPorQq = computed(() => {
|
||||||
|
const oreadosPagados = ingresos.value.filter(i => i.tipo === 'oreado' && i.estado === 'pagado')
|
||||||
|
const sumaPesoSeco = oreadosPagados.reduce((sum, i) => sum + (i.peso_seco || 0), 0)
|
||||||
|
|
||||||
|
if (sumaPesoSeco === 0) return 0
|
||||||
|
|
||||||
|
const sumaProducto = oreadosPagados.reduce((sum, i) =>
|
||||||
|
sum + (i.peso_seco || 0) * (i.precio || 0), 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return (sumaProducto / sumaPesoSeco) / 2
|
||||||
|
})
|
||||||
|
|
||||||
|
const precioPromedioMojadoPorQq = computed(() => {
|
||||||
|
const mojadosPagados = ingresos.value.filter(i => i.tipo === 'mojado' && i.estado === 'pagado')
|
||||||
|
const sumaPesoSeco = mojadosPagados.reduce((sum, i) => sum + (i.peso_seco || 0), 0)
|
||||||
|
|
||||||
|
if (sumaPesoSeco === 0) return 0
|
||||||
|
|
||||||
|
const sumaProducto = mojadosPagados.reduce((sum, i) =>
|
||||||
|
sum + (i.peso_seco || 0) * (i.precio || 0), 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return (sumaProducto / sumaPesoSeco) / 2
|
||||||
|
})
|
||||||
|
|
||||||
|
// INVERSIÓN
|
||||||
|
const inversionUva = computed(() => {
|
||||||
|
return ingresos.value
|
||||||
|
.filter(i => i.tipo === 'uva' && i.estado === 'pagado')
|
||||||
|
.reduce((sum, i) => sum + calcularTotalAPagar(i), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const inversionOreado = computed(() => {
|
||||||
|
return ingresos.value
|
||||||
|
.filter(i => i.tipo === 'oreado' && i.estado === 'pagado')
|
||||||
|
.reduce((sum, i) => sum + calcularTotalAPagar(i), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const inversionMojado = computed(() => {
|
||||||
|
return ingresos.value
|
||||||
|
.filter(i => i.tipo === 'mojado' && i.estado === 'pagado')
|
||||||
|
.reduce((sum, i) => sum + calcularTotalAPagar(i), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalInvertido = computed(() => {
|
||||||
|
return inversionUva.value + inversionOreado.value + inversionMojado.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// INVENTARIOS EN DEPÓSITO
|
||||||
|
const totalQqSecoDeposito = computed(() => {
|
||||||
|
return ingresos.value
|
||||||
|
.filter(i => i.estado === 'pendiente')
|
||||||
|
.reduce((sum, i) => sum + (i.peso_seco || 0), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalQqMojadoDeposito = computed(() => {
|
||||||
|
return ingresos.value
|
||||||
|
.filter(i => i.tipo === 'mojado' && i.estado === 'pendiente')
|
||||||
|
.reduce((sum, i) => sum + (i.peso_seco || 0), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalQqOreadoDeposito = computed(() => {
|
||||||
|
return ingresos.value
|
||||||
|
.filter(i => i.tipo === 'oreado' && i.estado === 'pendiente')
|
||||||
|
.reduce((sum, i) => sum + (i.peso_seco || 0), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalLbUvaDeposito = computed(() => {
|
||||||
|
return ingresos.value
|
||||||
|
.filter(i => i.tipo === 'uva' && i.estado === 'pendiente')
|
||||||
|
.reduce((sum, i) => sum + (i.peso_neto || 0), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// INVERSIÓN RESTANTE
|
||||||
|
const inversionRestanteOreado = computed(() => {
|
||||||
|
return precioPromedioOreadoPorQq.value * totalQqOreadoDeposito.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const inversionRestanteMojado = computed(() => {
|
||||||
|
return precioPromedioMojadoPorQq.value * totalQqMojadoDeposito.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const inversionRestanteUva = computed(() => {
|
||||||
|
return precioPromedioUvaPorQqLb.value * totalLbUvaDeposito.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const inversionRestanteEsperada = computed(() => {
|
||||||
|
return inversionRestanteOreado.value + inversionRestanteMojado.value + inversionRestanteUva.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// TOTALES NETOS DE VERDE
|
||||||
|
const totalLbNetoVerde = computed(() => {
|
||||||
|
return ingresos.value
|
||||||
|
.filter(i => i.tipo === 'verde')
|
||||||
|
.reduce((sum, i) => sum + (i.peso_neto || 0), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const precioPromedioVerdePagado = computed(() => {
|
||||||
|
const verdesPagados = ingresos.value.filter(i => i.tipo === 'verde' && i.estado === 'pagado')
|
||||||
|
const sumaPesoNeto = verdesPagados.reduce((sum, i) => sum + (i.peso_neto || 0), 0)
|
||||||
|
|
||||||
|
if (sumaPesoNeto === 0) return 0
|
||||||
|
|
||||||
|
const sumaProducto = verdesPagados.reduce((sum, i) =>
|
||||||
|
sum + (i.peso_neto || 0) * (i.precio || 0), 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return sumaProducto / sumaPesoNeto
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalLbNetoVerdeDeposito = computed(() => {
|
||||||
|
return ingresos.value
|
||||||
|
.filter(i => i.tipo === 'verde' && i.estado === 'pendiente')
|
||||||
|
.reduce((sum, i) => sum + (i.peso_neto || 0), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const inversionVerdeHastaFecha = computed(() => {
|
||||||
|
return ingresos.value
|
||||||
|
.filter(i => i.tipo === 'verde' && i.estado === 'pagado')
|
||||||
|
.reduce((sum, i) => sum + calcularTotalAPagar(i), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const inversionRestanteVerde = computed(() => {
|
||||||
|
return precioPromedioVerdePagado.value * totalLbNetoVerdeDeposito.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalLbNetoCompradoVerde = computed(() => {
|
||||||
|
return ingresos.value
|
||||||
|
.filter(i => i.tipo === 'verde' && i.estado === 'pagado')
|
||||||
|
.reduce((sum, i) => sum + (i.peso_neto || 0), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// SECOS VENDIDOS Y PÉRDIDAS (placeholder - necesitan datos de ventas)
|
||||||
|
const totalQqSecoPorVender = computed(() => totalQqSecoDeposito.value)
|
||||||
|
const precioVentaPromedioPorQq = computed(() => 0) // Necesita datos de ventas
|
||||||
|
const precioCompraPromedioPorQq = computed(() => {
|
||||||
|
const totalPagado = ingresos.value.filter(i => i.estado === 'pagado')
|
||||||
|
const sumaPesoSeco = totalPagado.reduce((sum, i) => sum + (i.peso_seco || 0), 0)
|
||||||
|
|
||||||
|
if (sumaPesoSeco === 0) return 0
|
||||||
|
|
||||||
|
const sumaTotal = totalPagado.reduce((sum, i) => sum + calcularTotalAPagar(i), 0)
|
||||||
|
|
||||||
|
return sumaTotal / sumaPesoSeco
|
||||||
|
})
|
||||||
|
const margenGananciaPorQq = computed(() =>
|
||||||
|
precioVentaPromedioPorQq.value - precioCompraPromedioPorQq.value
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Totales de ingreso y compra
|
||||||
|
totalQqSecoIngresado,
|
||||||
|
totalQqSecoComprado,
|
||||||
|
precioPromedioUvaPorQqLb,
|
||||||
|
precioPromedioOreadoPorQq,
|
||||||
|
precioPromedioMojadoPorQq,
|
||||||
|
|
||||||
|
// Inversión
|
||||||
|
inversionUva,
|
||||||
|
inversionOreado,
|
||||||
|
inversionMojado,
|
||||||
|
totalInvertido,
|
||||||
|
|
||||||
|
// Inventarios en depósito
|
||||||
|
totalQqSecoDeposito,
|
||||||
|
totalQqMojadoDeposito,
|
||||||
|
totalQqOreadoDeposito,
|
||||||
|
totalLbUvaDeposito,
|
||||||
|
|
||||||
|
// Inversión restante
|
||||||
|
inversionRestanteOreado,
|
||||||
|
inversionRestanteMojado,
|
||||||
|
inversionRestanteUva,
|
||||||
|
inversionRestanteEsperada,
|
||||||
|
|
||||||
|
// Totales netos de verde
|
||||||
|
totalLbNetoVerde,
|
||||||
|
precioPromedioVerdePagado,
|
||||||
|
totalLbNetoVerdeDeposito,
|
||||||
|
inversionVerdeHastaFecha,
|
||||||
|
inversionRestanteVerde,
|
||||||
|
totalLbNetoCompradoVerde,
|
||||||
|
|
||||||
|
// Secos vendidos y pérdidas
|
||||||
|
totalQqSecoPorVender,
|
||||||
|
precioVentaPromedioPorQq,
|
||||||
|
precioCompraPromedioPorQq,
|
||||||
|
margenGananciaPorQq
|
||||||
|
}
|
||||||
|
}
|
||||||
69
nuxt4-app/app/composables/useRechazosMetrics.ts
Normal file
69
nuxt4-app/app/composables/useRechazosMetrics.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { computed } from 'vue'
|
||||||
|
import type { ComputedRef } from 'vue'
|
||||||
|
|
||||||
|
export interface RechazoRecord {
|
||||||
|
tipo: 'chibolita' | 'perico' | 'vano' | 'picadillo' | 'magalla' | 'pinta'
|
||||||
|
cantidad: number // libras para chibolita, perico, pinta; galones para vano, picadillo, magalla
|
||||||
|
precio_unitario: number
|
||||||
|
total_cobrado: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RechazoMetrics {
|
||||||
|
totalCantidad: number
|
||||||
|
precioPromedio: number
|
||||||
|
totalCobrado: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RechazosMetrics {
|
||||||
|
chibolita: ComputedRef<RechazoMetrics>
|
||||||
|
perico: ComputedRef<RechazoMetrics>
|
||||||
|
vano: ComputedRef<RechazoMetrics>
|
||||||
|
picadillo: ComputedRef<RechazoMetrics>
|
||||||
|
magalla: ComputedRef<RechazoMetrics>
|
||||||
|
pinta: ComputedRef<RechazoMetrics>
|
||||||
|
totalRechazos: ComputedRef<number>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRechazosMetrics(rechazos: ComputedRef<RechazoRecord[]>) {
|
||||||
|
const calcularMetricasPorTipo = (tipo: RechazoRecord['tipo']): ComputedRef<RechazoMetrics> => {
|
||||||
|
return computed(() => {
|
||||||
|
const registros = rechazos.value.filter(r => r.tipo === tipo)
|
||||||
|
|
||||||
|
const totalCantidad = registros.reduce((sum, r) => sum + (r.cantidad || 0), 0)
|
||||||
|
const totalCobrado = registros.reduce((sum, r) => sum + (r.total_cobrado || 0), 0)
|
||||||
|
const precioPromedio = totalCantidad > 0 ? totalCobrado / totalCantidad : 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalCantidad,
|
||||||
|
precioPromedio,
|
||||||
|
totalCobrado
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const chibolita = calcularMetricasPorTipo('chibolita')
|
||||||
|
const perico = calcularMetricasPorTipo('perico')
|
||||||
|
const vano = calcularMetricasPorTipo('vano')
|
||||||
|
const picadillo = calcularMetricasPorTipo('picadillo')
|
||||||
|
const magalla = calcularMetricasPorTipo('magalla')
|
||||||
|
const pinta = calcularMetricasPorTipo('pinta')
|
||||||
|
|
||||||
|
const totalRechazos = computed(() => {
|
||||||
|
return chibolita.value.totalCobrado +
|
||||||
|
perico.value.totalCobrado +
|
||||||
|
vano.value.totalCobrado +
|
||||||
|
picadillo.value.totalCobrado +
|
||||||
|
magalla.value.totalCobrado +
|
||||||
|
pinta.value.totalCobrado
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
chibolita,
|
||||||
|
perico,
|
||||||
|
vano,
|
||||||
|
picadillo,
|
||||||
|
magalla,
|
||||||
|
pinta,
|
||||||
|
totalRechazos
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -118,6 +118,12 @@ const navigationPrimary = computed<NavigationMenuItem[]>(() => [
|
|||||||
to: '/',
|
to: '/',
|
||||||
active: route.path === '/'
|
active: route.path === '/'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Panorama Facturador',
|
||||||
|
icon: 'i-lucide-bar-chart-3',
|
||||||
|
to: '/panorama',
|
||||||
|
active: route.path === '/panorama'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Explorador de datos',
|
label: 'Explorador de datos',
|
||||||
icon: 'i-lucide-table',
|
icon: 'i-lucide-table',
|
||||||
|
|||||||
249
nuxt4-app/app/pages/panorama.vue
Normal file
249
nuxt4-app/app/pages/panorama.vue
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
<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>
|
||||||
Reference in New Issue
Block a user