mejoras de ui UX demasido intensas

This commit is contained in:
2025-10-01 07:44:14 -06:00
parent 32b10e1ad6
commit 053501a16d
5 changed files with 545 additions and 276 deletions

View File

@@ -8,13 +8,15 @@
{{ props.records.length }} registros filtrados
</p>
</div>
<UButton
:label="dateFormat === 'short' ? 'Fecha Larga' : 'Fecha Corta'"
color="neutral"
variant="outline"
icon="i-lucide-calendar"
@click="toggleDateFormat"
/>
<div class="flex items-center gap-2">
<UButton
:label="dateFormat === 'short' ? 'Fecha Larga' : 'Fecha Corta'"
color="neutral"
variant="outline"
icon="i-lucide-calendar"
@click="toggleDateFormat"
/>
</div>
</div>
</template>
@@ -46,13 +48,19 @@
<!-- Table Component -->
<UTable
ref="table"
v-model:expanded="expanded"
:data="limitedRecords"
:columns="tableColumns"
:global-filter="globalFilter"
:get-sub-rows="(row: any) => row.children"
sticky
class="h-96"
:ui="{
thead: 'bg-cyan-400/20 [&>tr>th]:text-white [&>tr>th]:font-semibold'
thead: 'bg-cyan-400/20 [&>tr>th]:text-white [&>tr>th]:font-semibold',
base: 'border-separate border-spacing-0',
tbody: '[&>tr]:last:[&>td]:border-b-0',
tr: 'group',
td: 'empty:p-0 group-has-[td:not(:empty)]:border-b border-default'
}"
/>
@@ -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<Props>()
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<Record<string, boolean>>({})
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<Record<string, unknown>>[] => {
if (!limitedRecords.value.length) return []
@@ -197,22 +232,89 @@ const tableColumns = computed((): TableColumn<Record<string, unknown>>[] => {
// 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