From 47e42ec985c15db5a43ca9393c49e648a51d2ae7 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Tue, 30 Sep 2025 23:07:09 -0600 Subject: [PATCH] cuenta-cliente tabla pro implementada --- .../VistaTablaIngresosConClientes.vue | 320 ++++++++++++ nuxt4-app/app/pages/cuenta-cliente.vue | 486 ++++++++++++++++-- 2 files changed, 753 insertions(+), 53 deletions(-) create mode 100644 nuxt4-app/app/components/ingresos/VistaTablaIngresosConClientes.vue diff --git a/nuxt4-app/app/components/ingresos/VistaTablaIngresosConClientes.vue b/nuxt4-app/app/components/ingresos/VistaTablaIngresosConClientes.vue new file mode 100644 index 0000000..9c0f890 --- /dev/null +++ b/nuxt4-app/app/components/ingresos/VistaTablaIngresosConClientes.vue @@ -0,0 +1,320 @@ + + + + diff --git a/nuxt4-app/app/pages/cuenta-cliente.vue b/nuxt4-app/app/pages/cuenta-cliente.vue index d2f8695..b77a33e 100644 --- a/nuxt4-app/app/pages/cuenta-cliente.vue +++ b/nuxt4-app/app/pages/cuenta-cliente.vue @@ -74,39 +74,193 @@ -
- -
-

- Clientes -

- +
+ +
+
+

+ Clientes +

+ +
+ + + Limpiar +
- -
-

- Rango de Fechas -

- + +
+
+

+ Rango de Fechas +

+ +
+ + + Limpiar + +
+ + +
+
+ +
+

+ Tipos de Café +

+ +
+ + +
+

+ Estados +

+ +
+ + +
+

+ Ubicaciones +

+ +
+
+ + + + Limpiar +
@@ -124,14 +278,95 @@ {{ tableDescription }}

-
- +
+
+ +
+ + +
+ + + ({{ includeClientesWithoutIngresos ? 'Mostrando todos los clientes' : 'Solo clientes con ingresos' }}) + +
- - + + + + + + + + + + +
@@ -142,7 +377,6 @@ import { useTableDataStore } from '~/stores/tableDataFactory' import { useMetadataStore } from '~/stores/metadata' import { useIngresosMetrics } from '~/composables/useIngresosMetrics' import type { IngresoRecord } from '~/composables/useIngresosMetrics' -import type { TabsItem } from '@nuxt/ui' // Define page metadata definePageMeta({ @@ -150,45 +384,109 @@ definePageMeta({ title: 'Cuenta Cliente' }) -// Tabs -const activeTab = ref<'ingresos' | 'clientes'>('ingresos') +// View modes with explicit hierarchy +type ViewMode = 'ingresos-only' | 'clientes-only' | 'ingresos-clientes' | 'clientes-ingresos' +const selectedView = ref('ingresos-only') -const tabItems: TabsItem[] = [ +// Toggle for including clients without ingresos in clientes-ingresos view +const includeClientesWithoutIngresos = ref(false) + +const viewOptions = [ { + value: 'ingresos-only' as ViewMode, label: 'Ingresos', - value: 'ingresos', - icon: 'i-lucide-trending-up' + icon: 'i-lucide-trending-up', + color: 'cyan', + gradient: 'from-cyan-500 to-cyan-600', + borderColor: 'border-cyan-500/50', + bgColor: 'bg-cyan-500/10', + shadowColor: 'shadow-cyan-500/20' }, { + value: 'clientes-only' as ViewMode, label: 'Clientes', - value: 'clientes', - icon: 'i-lucide-users' + icon: 'i-lucide-users', + color: 'yellow', + gradient: 'from-yellow-500 to-yellow-600', + borderColor: 'border-yellow-500/50', + bgColor: 'bg-yellow-500/10', + shadowColor: 'shadow-yellow-500/20' + }, + { + value: 'ingresos-clientes' as ViewMode, + label: 'Ingresos → Clientes', + icon: 'i-lucide-git-branch', + color: 'gradient', + gradient: 'from-cyan-500 via-cyan-400 to-yellow-500', + borderColor: 'border-cyan-500/50', + bgColor: 'bg-gradient-to-r from-cyan-500/10 to-yellow-500/10', + shadowColor: 'shadow-cyan-500/20' + }, + { + value: 'clientes-ingresos' as ViewMode, + label: 'Clientes → Ingresos', + icon: 'i-lucide-git-merge', + color: 'gradient', + gradient: 'from-yellow-500 via-yellow-400 to-cyan-500', + borderColor: 'border-yellow-500/50', + bgColor: 'bg-gradient-to-r from-yellow-500/10 to-cyan-500/10', + shadowColor: 'shadow-yellow-500/20' } ] // Dynamic table title and description const tableTitle = computed(() => { - return activeTab.value === 'ingresos' ? 'Tabla de Ingresos' : 'Tabla de Clientes' -}) - -const tableDescription = computed(() => { - if (activeTab.value === 'ingresos') { - return `Mostrando ${ingresosFiltrados.value.length} registros de ingresos` - } else { - return `Mostrando ${clientesFiltrados.value.length} clientes` + switch (selectedView.value) { + case 'ingresos-only': + return 'Tabla de Ingresos' + case 'clientes-only': + return 'Tabla de Clientes' + case 'ingresos-clientes': + return 'Vista Combinada: Ingresos con Clientes' + case 'clientes-ingresos': + return 'Vista Combinada: Clientes con Ingresos' + default: + return 'Tabla de Datos' } }) -// Ref for tabs element -const tabsRef = ref(null) +const tableDescription = computed(() => { + switch (selectedView.value) { + case 'ingresos-only': + return `Mostrando ${ingresosFiltrados.value.length} registros de ingresos` + case 'clientes-only': + return `Mostrando ${clientesFiltrados.value.length} clientes` + case 'ingresos-clientes': + return `${ingresosFiltrados.value.length} ingresos, cada uno con su cliente relacionado` + case 'clientes-ingresos': { + const clientesConIngresos = clientesFiltrados.value.filter(c => + ingresosFiltrados.value.some(i => i.cliente_id === c.id) + ).length + if (includeClientesWithoutIngresos.value) { + return `${clientesFiltrados.value.length} clientes (${clientesConIngresos} con ingresos, ${clientesFiltrados.value.length - clientesConIngresos} sin ingresos)` + } + return `${clientesConIngresos} clientes con ingresos relacionados` + } + default: + return 'Selecciona una vista' + } +}) -// Watch for tab changes and scroll to keep the tabs in view -watch(activeTab, () => { +// Ref for view selector element +const viewSelectorRef = ref(null) + +// Watch for view changes and scroll to keep the selector in view +watch(selectedView, (newView, oldView) => { nextTick(() => { - if (tabsRef.value) { - tabsRef.value.scrollIntoView({ behavior: 'smooth', block: 'center' }) + if (viewSelectorRef.value) { + viewSelectorRef.value.scrollIntoView({ behavior: 'smooth', block: 'center' }) } }) + + // Reset the toggle when switching away from clientes-ingresos view + if (oldView === 'clientes-ingresos' && newView !== 'clientes-ingresos') { + includeClientesWithoutIngresos.value = false + } }) interface ClienteRecord extends Record { @@ -228,6 +526,64 @@ const fechaDesde = ref(null) const fechaHasta = ref(null) const selectedClienteIds = ref([]) +// Filtros avanzados +const selectedTipos = ref([]) +const selectedEstados = ref([]) +const selectedUbicaciones = ref([]) + +// Opciones para filtros avanzados +const tiposCafeOptions = [ + { value: 'uva', label: 'Uva' }, + { value: 'oreado', label: 'Oreado' }, + { value: 'mojado', label: 'Mojado' }, + { value: 'verde', label: 'Verde' } +] + +const estadosOptions = [ + { value: 'pagado', label: 'Pagado' }, + { value: 'pendiente', label: 'Pendiente' } +] + +// Ubicaciones dinámicas basadas en los clientes +const ubicacionesOptions = computed(() => { + const ubicaciones = new Set() + clientes.value?.forEach(c => { + if (c.ubicacion) { + ubicaciones.add(c.ubicacion) + } + }) + return Array.from(ubicaciones).sort().map(u => ({ value: u, label: u })) +}) + +// Labels for selected filters +const selectedTiposLabels = computed(() => { + return selectedTipos.value + .map(v => tiposCafeOptions.find(o => o.value === v)?.label) + .filter(Boolean) + .join(', ') +}) + +const selectedEstadosLabels = computed(() => { + return selectedEstados.value + .map(v => estadosOptions.find(o => o.value === v)?.label) + .filter(Boolean) + .join(', ') +}) + +// Check if advanced filters are active +const hasAdvancedFilters = computed(() => { + return selectedTipos.value.length > 0 || + selectedEstados.value.length > 0 || + selectedUbicaciones.value.length > 0 +}) + +// Clear advanced filters +function clearAdvancedFilters() { + selectedTipos.value = [] + selectedEstados.value = [] + selectedUbicaciones.value = [] +} + async function onToggleAnulados(newValue: boolean | 'indeterminate') { if (newValue === true) { // Pedir confirmación al activar @@ -285,6 +641,22 @@ function isClienteSelected(clienteId: number): boolean { return selectedClienteIds.value.includes(clienteId) } +function matchesTipoCafe(ingreso: IngresoRecord): boolean { + if (selectedTipos.value.length === 0) return true + return selectedTipos.value.includes(ingreso.tipo) +} + +function matchesEstado(ingreso: IngresoRecord): boolean { + if (selectedEstados.value.length === 0) return true + return selectedEstados.value.includes(ingreso.estado) +} + +function matchesUbicacion(ingreso: IngresoRecord): boolean { + if (selectedUbicaciones.value.length === 0) return true + const cliente = clientes.value?.find(c => c.id === ingreso.cliente_id) + return cliente?.ubicacion ? selectedUbicaciones.value.includes(cliente.ubicacion) : false +} + // Get selected clientes for display cards const clientesSeleccionados = computed((): ClienteRecord[] => { if (selectedClienteIds.value.length === 0) return [] @@ -302,6 +674,9 @@ const ingresosFiltrados = computed(() => { .filter(r => (includeAnulados.value ? true : !isAnulado(r))) .filter(r => isWithinDate(r, fechaDesde.value, fechaHasta.value)) .filter(r => isClienteSelected(r.cliente_id)) + .filter(r => matchesTipoCafe(r)) + .filter(r => matchesEstado(r)) + .filter(r => matchesUbicacion(r)) }) const clientesFiltrados = computed((): ClienteRecord[] => { @@ -370,6 +745,11 @@ onMounted(async () => { // Default preset: cosecha 25-26 selectedPreset.value = 'cosecha-25-26' includeAnulados.value = false + + // Clear advanced filters + selectedTipos.value = [] + selectedEstados.value = [] + selectedUbicaciones.value = [] } })