diff --git a/nuxt4-app/app/components/clientes/VistaTablaClientes.vue b/nuxt4-app/app/components/clientes/VistaTablaClientes.vue index 42222c3..df6b56c 100644 --- a/nuxt4-app/app/components/clientes/VistaTablaClientes.vue +++ b/nuxt4-app/app/components/clientes/VistaTablaClientes.vue @@ -8,13 +8,21 @@ {{ props.records.length }} clientes registrados

- +
+ + +
@@ -46,13 +54,19 @@ @@ -95,6 +109,7 @@ import { h, ref, computed, resolveComponent } from 'vue' import { upperFirst } from 'scule' import type { TableColumn } from '@nuxt/ui' +import type { IngresoRecord } from '~/composables/useIngresosMetrics' interface ClienteRecord extends Record { id: number @@ -110,8 +125,17 @@ interface ClienteRecord extends Record { idciat?: number } +interface ClienteWithChildren extends ClienteRecord { + children?: IngresoRecord[] + peso_seco?: number + peso_neto?: number + precio?: number + estado?: string +} + interface Props { records: ClienteRecord[] + ingresos?: IngresoRecord[] } const props = defineProps() @@ -126,13 +150,53 @@ const table = useTemplateRef<{ tableApi?: any }>('table') const currentPage = ref(1) const recordsPerPage = 100 const dateFormat = ref<'short' | 'long'>('short') +const expanded = ref>({}) +const includeClientesWithoutIngresos = ref(false) function toggleDateFormat() { dateFormat.value = dateFormat.value === 'short' ? 'long' : 'short' } +// Map clientes to include ingresos as children and aggregate data +const recordsWithChildren = computed((): ClienteWithChildren[] => { + if (!props.ingresos || props.ingresos.length === 0) { + return props.records as ClienteWithChildren[] + } + + const clientesData = props.records.map(cliente => { + const clienteIngresos = props.ingresos?.filter(i => i.cliente_id === cliente.id) || [] + + // Agregar campos agregados al cliente + const peso_seco = clienteIngresos.reduce((sum, i) => sum + (i.peso_seco || 0), 0) + const peso_neto = clienteIngresos.reduce((sum, i) => sum + (i.peso_neto || 0), 0) + + // Precio promedio ponderado (por peso_neto) + const totalPrecioXPeso = clienteIngresos.reduce((sum, i) => sum + (i.precio * i.peso_neto), 0) + const precio = peso_neto > 0 ? totalPrecioXPeso / peso_neto : 0 + + // Estados únicos + const estados = [...new Set(clienteIngresos.map(i => i.estado))] + + return { + ...cliente, + peso_seco, + peso_neto, + precio, + estado: estados.length > 0 ? estados.join(', ') : '', + children: clienteIngresos + } + }) + + // Filter out clientes without ingresos if toggle is OFF + if (!includeClientesWithoutIngresos.value) { + return clientesData.filter(cliente => cliente.children && cliente.children.length > 0) + } + + return clientesData +}) + // Paginación -const totalRecords = computed(() => props.records.length) +const totalRecords = computed(() => recordsWithChildren.value.length) const totalPages = computed(() => Math.ceil(totalRecords.value / recordsPerPage)) const startRecord = computed(() => { @@ -148,7 +212,7 @@ const endRecord = computed(() => { const limitedRecords = computed(() => { const start = (currentPage.value - 1) * recordsPerPage const end = start + recordsPerPage - return (props.records || []).slice(start, end) + return recordsWithChildren.value.slice(start, end) }) function nextPage() { @@ -173,6 +237,10 @@ const selectedColumns = [ 'grupo_estudio', 'empleado', 'idciat', + 'peso_seco', + 'peso_neto', + 'precio', + 'estado', 'created_at', 'updated_at' ] @@ -183,24 +251,102 @@ const tableColumns = computed((): TableColumn>[] => { const firstRow = limitedRecords.value[0] if (!firstRow) return [] - const availableColumns = selectedColumns.filter(col => col in firstRow) + const availableColumns = selectedColumns.filter(col => { + // Siempre incluir columnas de cliente base + const baseColumns = ['id', 'name', 'cedula', 'telefono', 'ubicacion', 'grupo_estudio', 'empleado', 'idciat', 'created_at', 'updated_at'] + if (baseColumns.includes(col)) return true - return availableColumns.map((column: string) => ({ - accessorKey: column, - header: ({ column: tableColumn }) => { - const isSorted = tableColumn.getIsSorted() + // Incluir columnas agregadas si hay ingresos + if (props.ingresos && props.ingresos.length > 0 && ['peso_seco', 'peso_neto', 'precio', 'estado'].includes(col)) { + return true + } - return h(UButton, { - color: 'neutral', - variant: 'ghost', - label: upperFirst(column), - icon: isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down', - class: '-mx-2.5 text-white hover:text-yellow-100', - onClick: () => tableColumn.toggleSorting(tableColumn.getIsSorted() === 'asc') - }) - }, - cell: ({ row }) => formatCellValue(row.getValue(column), column, row.original as ClienteRecord) - })) + return col in firstRow + }) + + return availableColumns.map((column: string) => { + // Columna ID especial con botón de expansión + if (column === 'id') { + return { + accessorKey: column, + header: ({ column: tableColumn }) => { + const isSorted = tableColumn.getIsSorted() + return h(UButton, { + color: 'neutral', + variant: 'ghost', + label: '#', + icon: isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down', + class: '-mx-2.5 text-white hover:text-yellow-100', + onClick: () => tableColumn.toggleSorting(tableColumn.getIsSorted() === 'asc') + }) + }, + cell: ({ row }) => { + const original = row.original as any + const isIngreso = 'tipo' in original + const isCliente = 'name' in original && !('tipo' in original) + const isParent = row.depth === 0 + + return h( + 'div', + { + style: { + paddingLeft: `${row.depth * 2}rem` + }, + class: 'flex items-center gap-2' + }, + [ + // Botón de expansión solo en parent rows + isParent && props.ingresos && props.ingresos.length > 0 && h(UButton, { + color: 'neutral', + variant: 'outline', + size: 'xs', + icon: row.getIsExpanded() ? 'i-lucide-minus' : 'i-lucide-plus', + class: !row.getCanExpand() && 'invisible', + ui: { + base: 'p-0 rounded-sm', + leadingIcon: 'size-4' + }, + onClick: row.getToggleExpandedHandler() + }), + // Badge de ID + isCliente && h(UBadge, { + label: `C-${row.getValue('id')}`, + color: 'primary', + variant: 'subtle', + size: 'md', + class: 'rounded-full' + }), + isIngreso && h(UBadge, { + label: `I-${row.getValue('id')}`, + color: 'info', + variant: 'subtle', + size: 'md', + class: 'rounded-full' + }) + ] + ) + } + } + } + + // Otras columnas normales + return { + accessorKey: column, + header: ({ column: tableColumn }) => { + const isSorted = tableColumn.getIsSorted() + + return h(UButton, { + color: 'neutral', + variant: 'ghost', + label: upperFirst(column), + icon: isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down', + class: '-mx-2.5 text-white hover:text-yellow-100', + onClick: () => tableColumn.toggleSorting(tableColumn.getIsSorted() === 'asc') + }) + }, + cell: ({ row }) => formatCellValue(row.getValue(column), column, row.original as any, row.depth) + } + }) }) const columnVisibilityItems = computed((): any[] => { @@ -222,20 +368,56 @@ const columnVisibilityItems = computed((): any[] => { })) }) -function formatCellValue(value: unknown, column: string, row: ClienteRecord): any { - if (value === null || value === undefined) { +function formatCellValue(value: unknown, column: string, row: any, depth: number): any { + // Detectar si es ingreso (child row) + const isIngreso = 'tipo' in row + const isCliente = 'name' in row && !('tipo' in row) + + // Si es una fila de ingreso expandida, mostrar info del ingreso + if (isIngreso && depth > 0) { + if (column === 'name') { + return h('div', { class: 'flex items-center gap-2' }, [ + h('span', { + class: row.tipo === 'uva' ? 'text-purple-400' : + row.tipo === 'oreado' ? 'text-orange-400' : + row.tipo === 'mojado' ? 'text-blue-400' : 'text-green-400' + }, row.tipo?.toUpperCase()) + ]) + } + if (column === 'peso_seco' && typeof row.peso_seco === 'number') { + return h('div', { class: 'text-right font-medium text-cyan-400' }, row.peso_seco.toFixed(2)) + } + if (column === 'peso_neto' && typeof row.peso_neto === 'number') { + return h('div', { class: 'text-right font-medium text-cyan-400' }, row.peso_neto.toFixed(2)) + } + if (column === 'precio' && typeof row.precio === 'number') { + const formatted = new Intl.NumberFormat('es-HN', { + style: 'currency', + currency: 'HNL', + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }).format(row.precio).replace('HNL', 'L') + return h('div', { class: 'text-right font-medium text-cyan-400' }, formatted) + } + if (column === 'estado' && typeof row.estado === 'string') { + return h('span', { + class: row.estado === 'pagado' + ? 'inline-flex items-center gap-1 px-2 py-0.5 rounded bg-green-500/20 text-green-300 border border-green-400/30 text-xs' + : 'inline-flex items-center gap-1 px-2 py-0.5 rounded bg-yellow-500/20 text-yellow-300 border border-yellow-400/30 text-xs' + }, row.estado === 'pagado' ? '✓ Pagado' : '⏳ Pendiente') + } + if (column === 'created_at' && typeof row.created_at === 'string') { + const date = new Date(row.created_at) + const formattedDate = dateFormat.value === 'long' + ? date.toLocaleDateString('es-HN', { day: 'numeric', month: 'long', year: 'numeric' }) + : date.toLocaleDateString('es-HN') + return h('span', { class: 'font-bold text-base' }, formattedDate) + } return '—' } - // ID column - badge con formato C-#### - if (column === 'id' && typeof value === 'number') { - return h(UBadge, { - label: `C-${value}`, - color: 'primary', - variant: 'subtle', - size: 'md', - class: 'rounded-full' - }) + if (value === null || value === undefined) { + return '—' } // idciat - badge con formato CIAT-#### @@ -250,7 +432,7 @@ function formatCellValue(value: unknown, column: string, row: ClienteRecord): an } // name - con avatar si está disponible - if (column === 'name' && typeof value === 'string') { + if (column === 'name' && typeof value === 'string' && isCliente) { return h('div', { class: 'flex items-center gap-2' }, [ row.avatar_url ? h(UAvatar, { src: row.avatar_url, @@ -315,6 +497,36 @@ function formatCellValue(value: unknown, column: string, row: ClienteRecord): an }) } + // Campos agregados de ingresos + if (column === 'peso_seco' && typeof value === 'number' && depth === 0) { + return h('div', { class: 'text-right font-medium text-yellow-500' }, value.toFixed(2)) + } + + if (column === 'peso_neto' && typeof value === 'number' && depth === 0) { + return h('div', { class: 'text-right font-medium text-yellow-500' }, value.toFixed(2)) + } + + if (column === 'precio' && typeof value === 'number' && depth === 0) { + const formatted = new Intl.NumberFormat('es-HN', { + style: 'currency', + currency: 'HNL', + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }).format(value).replace('HNL', 'L') + return h('div', { class: 'text-right font-medium text-yellow-500' }, formatted) + } + + if (column === 'estado' && typeof value === 'string' && depth === 0) { + const estados = value.split(', ') + return h('div', { class: 'flex flex-wrap gap-1' }, estados.map((estado: string) => + h('span', { + class: estado === 'pagado' + ? 'inline-flex items-center gap-1 px-2 py-0.5 rounded bg-green-500/20 text-green-300 border border-green-400/30 text-xs' + : 'inline-flex items-center gap-1 px-2 py-0.5 rounded bg-yellow-500/20 text-yellow-300 border border-yellow-400/30 text-xs' + }, estado === 'pagado' ? '✓ Pagado' : '⏳ Pendiente') + )) + } + // Formatear fechas if ((column === 'created_at' || column === 'updated_at') && typeof value === 'string' && value.includes('T')) { try { diff --git a/nuxt4-app/app/components/comparativa/CosechasHeatmap.vue b/nuxt4-app/app/components/comparativa/CosechasHeatmap.vue index 83eec3f..c18c686 100644 --- a/nuxt4-app/app/components/comparativa/CosechasHeatmap.vue +++ b/nuxt4-app/app/components/comparativa/CosechasHeatmap.vue @@ -199,11 +199,11 @@ Leyenda:
- Total completo + Total completo de la cosecha
- Acumulado hasta hoy (día {{ diaActualRelativo }}) + Acumulado hasta la fecha
@@ -224,18 +224,18 @@ {{ formatTotal(cosecha.total) }}
↗ {{ formatTotal(cosecha.totalALaFecha) }}
- Sin datos + 0
@@ -272,10 +272,6 @@ :key="`${cosecha.id}-${dia}`" class="border-r border-b transition-all cursor-pointer relative" :class="[ - // Marcador del día actual - dia - 1 === diaActualRelativo - ? 'border-l-2 border-l-blue-400' - : '', // Rango seleccionado (naranja) isInSelectedRange(dia - 1) ? 'ring-2 ring-[#c08040] border-[#c08040] z-10' @@ -295,16 +291,17 @@ @mouseenter="showTooltip($event, cosecha, dia - 1)" @mouseleave="hideTooltip" > + +
+
- -
@@ -318,11 +315,11 @@ Leyenda:
- Total completo + Total completo de la cosecha
- Acumulado hasta hoy (día {{ diaActualRelativo }}) + Acumulado hasta la fecha
@@ -343,18 +340,18 @@ {{ formatTotal(cosecha.total) }}
↗ {{ formatTotal(cosecha.totalALaFecha) }}
- Sin datos + {{ formatTotal(cosecha.totalALaFecha) }}
@@ -392,10 +389,6 @@ :key="`${cosecha.id}-${dia}`" class="relative border-b cursor-pointer group" :class="[ - // Marcador del día actual - dia - 1 === diaActualRelativo - ? 'border-l-2 border-l-blue-400' - : '', // Rango seleccionado (naranja) isInSelectedRange(dia - 1) ? 'border-[#c08040] border-2 bg-[#c08040]/10' @@ -411,6 +404,12 @@ @mouseenter="showTooltip($event, cosecha, dia - 1)" @mouseleave="hideTooltip" > + +
+
- -
@@ -636,25 +630,45 @@ const tooltipX = ref(0) const tooltipY = ref(0) const tooltipData = ref({ cosecha: '', dia: 0, valor: '', fecha: '' }) -// Calcular el día actual relativo desde el inicio del año de cosecha (8 de septiembre) -const diaActualRelativo = computed(() => { +// Día y mes actual para comparar +const fechaActual = computed(() => { const hoy = new Date() + // Normalizar a medianoche para comparaciones + hoy.setHours(0, 0, 0, 0) + return { + dia: hoy.getDate(), + mes: hoy.getMonth(), // 0-11 + fecha: hoy, + formatted: hoy.toLocaleDateString('es-HN', { day: 'numeric', month: 'short' }) + } +}) - // Fecha de inicio del año de cosecha actual (8 de septiembre del año correspondiente) - let anioInicioCosecha = hoy.getFullYear() - const inicioCosechaEsteAnio = new Date(anioInicioCosecha, 8, 8) // 8 de septiembre +// Calcular cuántos días han pasado desde el 7 de septiembre de este año +const diasTranscurridosDesdeInicio = computed(() => { + const hoy = new Date() + hoy.setHours(0, 0, 0, 0) - // Si aún no hemos llegado al 8 de septiembre de este año, el inicio fue el año pasado - if (hoy < inicioCosechaEsteAnio) { - anioInicioCosecha-- + // 7 de septiembre del año actual + const inicioDelAnio = new Date(hoy.getFullYear(), 8, 7) // mes 8 = septiembre (0-indexed) + inicioDelAnio.setHours(0, 0, 0, 0) + + // Si todavía no llegamos al 7 de septiembre de este año, usar el del año pasado + if (hoy < inicioDelAnio) { + inicioDelAnio.setFullYear(inicioDelAnio.getFullYear() - 1) } - const inicioCosecha = new Date(anioInicioCosecha, 8, 8) // 8 de septiembre - const diaRelativo = Math.floor((hoy.getTime() - inicioCosecha.getTime()) / (1000 * 60 * 60 * 24)) + // Calcular diferencia en días + const diffMs = hoy.getTime() - inicioDelAnio.getTime() + const diffDias = Math.floor(diffMs / (1000 * 60 * 60 * 24)) - return diaRelativo + return diffDias }) +// Función para verificar si un día ya pasó (basado en días desde el inicio, no en año) +function isDiaPasado(cosechaId: string, diaIndex: number): boolean { + return diaIndex <= diasTranscurridosDesdeInicio.value +} + // Calcular datos por cosecha usando vista_resumen_ingresos const datosCosechas = computed(() => { return props.cosechasSeleccionadas.map(cosechaId => { @@ -747,13 +761,25 @@ const datosCosechas = computed(() => { } } - // Calcular total acumulado hasta la fecha actual (día relativo de hoy) + // Calcular total acumulado hasta la fecha actual (mismo día/mes pero del año de cada cosecha) let totalALaFecha: number | null = null - // Solo calcular si el día actual está dentro del rango de datos disponibles - if (diaActualRelativo.value >= 0 && diaActualRelativo.value < valoresPorDia.length) { - totalALaFecha = 0 - for (let i = 0; i <= diaActualRelativo.value; i++) { + // Obtener el año de inicio de esta cosecha + const anioInicioCosecha = fechaInicio.getFullYear() + + // Construir la fecha objetivo: mismo día y mes de hoy, pero del año de inicio de esta cosecha + // Ej: Si hoy es 1 oct 2025 y la cosecha inició en sep 2023, usamos 1 oct 2023 + const fechaObjetivoCosecha = new Date(anioInicioCosecha, fechaActual.value.mes, fechaActual.value.dia) + + // Calcular el día relativo desde el inicio de la cosecha hasta esa fecha objetivo + const diaObjetivo = Math.floor((fechaObjetivoCosecha.getTime() - fechaInicio.getTime()) / (1000 * 60 * 60 * 24)) + + // Acumular desde el inicio de la cosecha hasta la fecha objetivo + totalALaFecha = 0 + if (diaObjetivo >= 0) { + // Sumar todos los días desde el inicio hasta la fecha objetivo (o hasta donde haya datos) + const limiteSuperior = Math.min(diaObjetivo, valoresPorDia.length - 1) + for (let i = 0; i <= limiteSuperior; i++) { totalALaFecha += valoresPorDia[i] || 0 } } diff --git a/nuxt4-app/app/components/ingresos/VistaTablaIngresos.vue b/nuxt4-app/app/components/ingresos/VistaTablaIngresos.vue index 00e4c79..aea4d37 100644 --- a/nuxt4-app/app/components/ingresos/VistaTablaIngresos.vue +++ b/nuxt4-app/app/components/ingresos/VistaTablaIngresos.vue @@ -8,13 +8,15 @@ {{ props.records.length }} registros filtrados

- +
+ +
@@ -46,13 +48,19 @@ @@ -100,9 +108,20 @@ import type { IngresoRecord } from '~/composables/useIngresosMetrics' interface ClienteRecord { id: number name: string + cedula?: number + ubicacion?: string + telefono?: string + grupo_estudio?: string + empleado?: boolean + avatar_url?: string + idciat?: number [key: string]: any } +interface IngresoWithChildren extends IngresoRecord { + children?: ClienteRecord[] +} + interface Props { records: IngresoRecord[] clientes?: ClienteRecord[] @@ -111,23 +130,39 @@ interface Props { const props = defineProps() const UButton = resolveComponent('UButton') +const UBadge = resolveComponent('UBadge') +const UIcon = resolveComponent('UIcon') +const UTooltip = resolveComponent('UTooltip') +const UAvatar = resolveComponent('UAvatar') const globalFilter = ref('') const table = useTemplateRef<{ tableApi?: any }>('table') const currentPage = ref(1) const recordsPerPage = 100 const dateFormat = ref<'short' | 'long'>('short') - -const UBadge = resolveComponent('UBadge') -const UIcon = resolveComponent('UIcon') -const UTooltip = resolveComponent('UTooltip') +const expanded = ref>({}) function toggleDateFormat() { dateFormat.value = dateFormat.value === 'short' ? 'long' : 'short' } +// Map records to include cliente as children +const recordsWithChildren = computed((): IngresoWithChildren[] => { + if (!props.clientes || props.clientes.length === 0) { + return props.records as IngresoWithChildren[] + } + + return props.records.map(ingreso => { + const cliente = props.clientes?.find(c => c.id === ingreso.cliente_id) + return { + ...ingreso, + children: cliente ? [cliente] : [] + } + }) +}) + // Paginación -const totalRecords = computed(() => props.records.length) +const totalRecords = computed(() => recordsWithChildren.value.length) const totalPages = computed(() => Math.ceil(totalRecords.value / recordsPerPage)) const startRecord = computed(() => { @@ -144,7 +179,7 @@ const endRecord = computed(() => { const limitedRecords = computed(() => { const start = (currentPage.value - 1) * recordsPerPage const end = start + recordsPerPage - return (props.records || []).slice(start, end) + return recordsWithChildren.value.slice(start, end) }) function nextPage() { @@ -159,7 +194,7 @@ function previousPage() { } } -// Seleccionar columnas importantes manualmente en lugar de todas las propiedades +// Seleccionar columnas importantes manualmente const selectedColumns = [ 'id', 'created_at', @@ -187,7 +222,7 @@ const selectedColumns = [ 'tara', ] -// Generate table columns only for selected fields +// Generate table columns const tableColumns = computed((): TableColumn>[] => { if (!limitedRecords.value.length) return [] @@ -197,22 +232,89 @@ const tableColumns = computed((): TableColumn>[] => { // Solo usar columnas que existen en el primer registro const availableColumns = selectedColumns.filter(col => col in firstRow) - return availableColumns.map((column: string) => ({ - accessorKey: column, - header: ({ column: tableColumn }) => { - const isSorted = tableColumn.getIsSorted() + return availableColumns.map((column: string) => { + // Columna ID especial con botón de expansión + if (column === 'id') { + return { + accessorKey: column, + header: ({ column: tableColumn }) => { + const isSorted = tableColumn.getIsSorted() + return h(UButton, { + color: 'neutral', + variant: 'ghost', + label: '#', + icon: isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down', + class: '-mx-2.5 text-white hover:text-cyan-100', + onClick: () => tableColumn.toggleSorting(tableColumn.getIsSorted() === 'asc') + }) + }, + cell: ({ row }) => { + const original = row.original as any + const isCliente = 'name' in original && !('tipo' in original) + const isIngreso = 'tipo' in original + const isParent = row.depth === 0 - return h(UButton, { - color: 'neutral', - variant: 'ghost', - label: upperFirst(column), - icon: isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down', - class: '-mx-2.5 text-white hover:text-cyan-100', - onClick: () => tableColumn.toggleSorting(tableColumn.getIsSorted() === 'asc') - }) - }, - cell: ({ row }) => formatCellValue(row.getValue(column), column) - })) + return h( + 'div', + { + style: { + paddingLeft: `${row.depth * 2}rem` + }, + class: 'flex items-center gap-2' + }, + [ + // Botón de expansión solo en parent rows + isParent && props.clientes && props.clientes.length > 0 && h(UButton, { + color: 'neutral', + variant: 'outline', + size: 'xs', + icon: row.getIsExpanded() ? 'i-lucide-minus' : 'i-lucide-plus', + class: !row.getCanExpand() && 'invisible', + ui: { + base: 'p-0 rounded-sm', + leadingIcon: 'size-4' + }, + onClick: row.getToggleExpandedHandler() + }), + // Badge de ID + isIngreso && h(UBadge, { + label: `I-${row.getValue('id')}`, + color: 'primary', + variant: 'subtle', + size: 'md', + class: 'rounded-full' + }), + isCliente && h(UBadge, { + label: `C-${row.getValue('id')}`, + color: 'warning', + variant: 'subtle', + size: 'md', + class: 'rounded-full' + }) + ] + ) + } + } + } + + // Otras columnas normales + return { + accessorKey: column, + header: ({ column: tableColumn }) => { + const isSorted = tableColumn.getIsSorted() + + return h(UButton, { + color: 'neutral', + variant: 'ghost', + label: upperFirst(column), + icon: isSorted ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow') : 'i-lucide-arrow-up-down', + class: '-mx-2.5 text-white hover:text-cyan-100', + onClick: () => tableColumn.toggleSorting(tableColumn.getIsSorted() === 'asc') + }) + }, + cell: ({ row }) => formatCellValue(row.getValue(column), column, row.original as any, row.depth) + } + }) }) // Column visibility dropdown items @@ -236,20 +338,49 @@ const columnVisibilityItems = computed((): any[] => { }) -function formatCellValue(value: unknown, column: string): any { - if (value === null || value === undefined) { +function formatCellValue(value: unknown, column: string, row: any, depth: number): any { + // Detectar si es cliente (child row) + const isCliente = 'name' in row && !('tipo' in row) + const isIngreso = 'tipo' in row + + // Si es una fila de cliente expandida, mostrar info del cliente + if (isCliente && depth > 0) { + if (column === 'created_at' || column === 'tipo' || column === 'estado') { + // Mostrar información del cliente en columnas relevantes + if (column === 'created_at') { + return h('div', { class: 'flex flex-col gap-1' }, [ + h('span', { class: 'font-semibold text-yellow-500' }, row.name), + row.ubicacion && h('span', { class: 'text-xs text-gray-400' }, `📍 ${row.ubicacion}`), + row.telefono && h('span', { class: 'text-xs text-gray-400' }, `📞 ${row.telefono}`) + ]) + } + if (column === 'tipo' && row.cedula) { + return h(UBadge, { + label: String(row.cedula), + color: 'neutral', + variant: 'soft', + size: 'sm', + class: 'font-mono' + }) + } + if (column === 'estado' && row.empleado !== undefined) { + return row.empleado + ? h(UIcon, { + name: 'i-lucide-briefcase', + class: 'text-purple-600 w-5 h-5' + }) + : h(UIcon, { + name: 'i-lucide-user', + class: 'text-gray-400 w-5 h-5' + }) + } + } return '—' } - // ID column - badge con formato I-#### - if (column === 'id' && typeof value === 'number') { - return h(UBadge, { - label: `I-${value}`, - color: 'primary', - variant: 'subtle', - size: 'md', - class: 'rounded-full' - }) + // Valores null/undefined + if (value === null || value === undefined) { + return '—' } // comercio_id - badge con formato C-#### en cyan diff --git a/nuxt4-app/app/pages/comparativa-cosechas.vue b/nuxt4-app/app/pages/comparativa-cosechas.vue index 0e3ca47..a937a77 100644 --- a/nuxt4-app/app/pages/comparativa-cosechas.vue +++ b/nuxt4-app/app/pages/comparativa-cosechas.vue @@ -58,6 +58,17 @@