diff --git a/nuxt4-app/app/components/MetricCard.vue b/nuxt4-app/app/components/MetricCard.vue new file mode 100644 index 0000000..c8cb632 --- /dev/null +++ b/nuxt4-app/app/components/MetricCard.vue @@ -0,0 +1,41 @@ + + + + {{ label }} + + {{ value }} + {{ unit }} + + + + + + + \ No newline at end of file diff --git a/nuxt4-app/app/components/ingresos/InventariosDeposito.vue b/nuxt4-app/app/components/ingresos/InventariosDeposito.vue new file mode 100644 index 0000000..3c3a259 --- /dev/null +++ b/nuxt4-app/app/components/ingresos/InventariosDeposito.vue @@ -0,0 +1,42 @@ + + + + Inventarios en Depósito + + + + + + + + + + + + \ No newline at end of file diff --git a/nuxt4-app/app/components/ingresos/InversionRestante.vue b/nuxt4-app/app/components/ingresos/InversionRestante.vue new file mode 100644 index 0000000..fefecc7 --- /dev/null +++ b/nuxt4-app/app/components/ingresos/InversionRestante.vue @@ -0,0 +1,46 @@ + + + + Inversión Restante a Realizar + + + + + + + + + + + + \ No newline at end of file diff --git a/nuxt4-app/app/components/ingresos/InversionTotal.vue b/nuxt4-app/app/components/ingresos/InversionTotal.vue new file mode 100644 index 0000000..e61742f --- /dev/null +++ b/nuxt4-app/app/components/ingresos/InversionTotal.vue @@ -0,0 +1,46 @@ + + + + Inversión Hasta la Fecha + + + + + + + + + + + + \ No newline at end of file diff --git a/nuxt4-app/app/components/ingresos/SecosVendidos.vue b/nuxt4-app/app/components/ingresos/SecosVendidos.vue new file mode 100644 index 0000000..0c3272c --- /dev/null +++ b/nuxt4-app/app/components/ingresos/SecosVendidos.vue @@ -0,0 +1,44 @@ + + + + Secos Vendidos y Pérdidas + + + + + + + + + + + + \ No newline at end of file diff --git a/nuxt4-app/app/components/ingresos/TotalesIngresoCompra.vue b/nuxt4-app/app/components/ingresos/TotalesIngresoCompra.vue new file mode 100644 index 0000000..c452f09 --- /dev/null +++ b/nuxt4-app/app/components/ingresos/TotalesIngresoCompra.vue @@ -0,0 +1,45 @@ + + + + Totales de Ingreso y Compra + + + + + + + + + + + + + \ No newline at end of file diff --git a/nuxt4-app/app/components/ingresos/TotalesVerde.vue b/nuxt4-app/app/components/ingresos/TotalesVerde.vue new file mode 100644 index 0000000..f6467c0 --- /dev/null +++ b/nuxt4-app/app/components/ingresos/TotalesVerde.vue @@ -0,0 +1,56 @@ + + + + Totales Netos de Verde + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nuxt4-app/app/components/rechazos/RechazoCard.vue b/nuxt4-app/app/components/rechazos/RechazoCard.vue new file mode 100644 index 0000000..a50eb5b --- /dev/null +++ b/nuxt4-app/app/components/rechazos/RechazoCard.vue @@ -0,0 +1,53 @@ + + + {{ title }} + + + Total {{ unidad }}: + {{ metrics.value.totalCantidad.toFixed(2) }} {{ unidad }} + + + Precio promedio: + {{ formatCurrency(metrics.value.precioPromedio) }} + + + Total cobrado: + {{ formatCurrency(metrics.value.totalCobrado) }} + + + + + + \ No newline at end of file diff --git a/nuxt4-app/app/components/rechazos/RechazosSubproductos.vue b/nuxt4-app/app/components/rechazos/RechazosSubproductos.vue new file mode 100644 index 0000000..a294309 --- /dev/null +++ b/nuxt4-app/app/components/rechazos/RechazosSubproductos.vue @@ -0,0 +1,69 @@ + + + + + Rechazos y Subproductos + + Total Rechazos + {{ formatCurrency(totalRechazos.value) }} + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nuxt4-app/app/composables/useIngresosMetrics.ts b/nuxt4-app/app/composables/useIngresosMetrics.ts new file mode 100644 index 0000000..74ccab8 --- /dev/null +++ b/nuxt4-app/app/composables/useIngresosMetrics.ts @@ -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 + totalQqSecoComprado: ComputedRef + precioPromedioUvaPorQqLb: ComputedRef + precioPromedioOreadoPorQq: ComputedRef + precioPromedioMojadoPorQq: ComputedRef + + // Inversión + inversionUva: ComputedRef + inversionOreado: ComputedRef + inversionMojado: ComputedRef + totalInvertido: ComputedRef + + // Inventarios en depósito + totalQqSecoDeposito: ComputedRef + totalQqMojadoDeposito: ComputedRef + totalQqOreadoDeposito: ComputedRef + totalLbUvaDeposito: ComputedRef + + // Inversión restante + inversionRestanteOreado: ComputedRef + inversionRestanteMojado: ComputedRef + inversionRestanteUva: ComputedRef + inversionRestanteEsperada: ComputedRef + + // Totales netos de verde + totalLbNetoVerde: ComputedRef + precioPromedioVerdePagado: ComputedRef + totalLbNetoVerdeDeposito: ComputedRef + inversionVerdeHastaFecha: ComputedRef + inversionRestanteVerde: ComputedRef + totalLbNetoCompradoVerde: ComputedRef + + // Secos vendidos y pérdidas (placeholder) + totalQqSecoPorVender: ComputedRef + precioVentaPromedioPorQq: ComputedRef + precioCompraPromedioPorQq: ComputedRef + margenGananciaPorQq: ComputedRef +} + +export function useIngresosMetrics(ingresos: ComputedRef) { + // 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 + } +} \ No newline at end of file diff --git a/nuxt4-app/app/composables/useRechazosMetrics.ts b/nuxt4-app/app/composables/useRechazosMetrics.ts new file mode 100644 index 0000000..9252f13 --- /dev/null +++ b/nuxt4-app/app/composables/useRechazosMetrics.ts @@ -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 + perico: ComputedRef + vano: ComputedRef + picadillo: ComputedRef + magalla: ComputedRef + pinta: ComputedRef + totalRechazos: ComputedRef +} + +export function useRechazosMetrics(rechazos: ComputedRef) { + const calcularMetricasPorTipo = (tipo: RechazoRecord['tipo']): ComputedRef => { + 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 + } +} \ No newline at end of file diff --git a/nuxt4-app/app/layouts/dashboard.vue b/nuxt4-app/app/layouts/dashboard.vue index 8c434ff..e04a541 100644 --- a/nuxt4-app/app/layouts/dashboard.vue +++ b/nuxt4-app/app/layouts/dashboard.vue @@ -118,6 +118,12 @@ const navigationPrimary = computed(() => [ to: '/', active: route.path === '/' }, + { + label: 'Panorama Facturador', + icon: 'i-lucide-bar-chart-3', + to: '/panorama', + active: route.path === '/panorama' + }, { label: 'Explorador de datos', icon: 'i-lucide-table', diff --git a/nuxt4-app/app/pages/panorama.vue b/nuxt4-app/app/pages/panorama.vue new file mode 100644 index 0000000..dcfa8d2 --- /dev/null +++ b/nuxt4-app/app/pages/panorama.vue @@ -0,0 +1,249 @@ + + + + + + + + Cargando datos... + + + + {{ Math.round(loadingProgress) }}% + + + + + + + Error al cargar datos: {{ error }} + + + + + + + + + + + + + + + + Totales Financieros + Vista general de ingresos, inversiones y rechazos + + + + + + Actualizar + + + + + + + Total Invertido en Café + + {{ formatCurrency(ingresosMetrics.totalInvertido.value + ingresosMetrics.inversionVerdeHastaFecha.value) }} + + + + Total Rechazos + + {{ formatCurrency(rechazosMetrics.totalRechazos.value) }} + + + + Balance Neto + + {{ formatCurrency(ingresosMetrics.totalInvertido.value + ingresosMetrics.inversionVerdeHastaFecha.value - rechazosMetrics.totalRechazos.value) }} + + + + + + + Última actualización: {{ lastUpdated }} + + + + + + + + + + + + + + + + + + + \ No newline at end of file
Error al cargar datos: {{ error }}
Vista general de ingresos, inversiones y rechazos