Files
seguidorDeLotes/nuxt4/app/components/externos/TablaRegistros.vue
josedario87 ce8bad68d5
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 2m46s
Agregar sistema de vinculaciones con registros externos de Metabase
- Nuevo schema BD para vinculaciones_externas con constraint único por período
- Cliente Metabase para consultar Ingresos, Carretas, Salidas y Rechazos
- Endpoints API para registros externos (/api/externos/*) y vinculaciones (/api/vinculaciones/*)
- Composable useRegistrosExternos con lógica de vinculación individual y masiva
- Componentes: TablaRegistros, ModalAsignar, ProgressDashboard
- Tab "Externos" en app.vue con sub-tabs y dashboard de progreso
- LotesCard.vue ahora muestra registros vinculados al lote
2025-11-29 15:25:26 -06:00

234 lines
6.9 KiB
Vue

<template>
<div class="space-y-4">
<!-- Header con filtros -->
<div class="flex flex-wrap gap-4 items-center justify-between">
<div class="flex items-center gap-2">
<UIcon :name="tipoConfig.icon" class="w-6 h-6" :class="`text-${tipoConfig.color}-500`" />
<div>
<h3 class="text-lg font-semibold">{{ tipoConfig.label }}</h3>
<p v-if="meta" class="text-sm text-gray-500">
{{ meta.vinculados }} de {{ meta.total }} vinculados ({{ meta.sinVincular }} pendientes)
</p>
</div>
</div>
<div class="flex gap-2">
<UCheckbox v-model="soloSinVincular" label="Solo sin vincular" />
<UButton
icon="i-heroicons-arrow-path"
label="Refrescar"
variant="outline"
size="sm"
:loading="loading"
@click="$emit('refresh')"
/>
<UButton
v-if="seleccionados.length > 0"
icon="i-heroicons-link"
:label="`Vincular ${seleccionados.length}`"
color="primary"
size="sm"
@click="$emit('vincular-seleccionados', seleccionados)"
/>
</div>
</div>
<!-- Barra de progreso -->
<div v-if="meta" class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
class="h-2 rounded-full transition-all duration-300"
:class="`bg-${tipoConfig.color}-500`"
:style="{ width: `${(meta.vinculados / Math.max(meta.total, 1)) * 100}%` }"
/>
</div>
<!-- Tabla -->
<UCard>
<UTable
v-model:selected="seleccionados"
:data="registros"
:columns="columnsWithSelect"
:loading="loading"
:empty-state="{
icon: 'i-heroicons-inbox',
label: 'No hay registros'
}"
>
<!-- Columna de selección -->
<template #select-header>
<UCheckbox
:model-value="allSelected"
:indeterminate="someSelected && !allSelected"
@update:model-value="toggleAll"
/>
</template>
<template #select-cell="{ row }">
<UCheckbox
:model-value="isSelected(row.original)"
:disabled="row.original.vinculado"
@update:model-value="toggleSelection(row.original)"
/>
</template>
<!-- Estado de vinculación -->
<template #vinculado-cell="{ getValue }">
<UBadge v-if="getValue()" color="green" variant="subtle">
<UIcon name="i-heroicons-check" class="w-3 h-3 mr-1" />
Vinculado
</UBadge>
<UBadge v-else color="yellow" variant="subtle">
<UIcon name="i-heroicons-clock" class="w-3 h-3 mr-1" />
Pendiente
</UBadge>
</template>
<!-- Fecha -->
<template #created_at-cell="{ getValue }">
{{ formatFecha(getValue()) }}
</template>
<!-- Peso (ingresos) -->
<template #peso_seco-cell="{ getValue }">
<span v-if="getValue() !== null && getValue() !== undefined" class="font-medium">
{{ formatPeso(getValue()) }} qqSeco
</span>
<span v-else class="text-gray-400">-</span>
</template>
<!-- Peso (carretas) -->
<template #qq_seco_estimado-cell="{ getValue }">
<span v-if="getValue() !== null && getValue() !== undefined" class="font-medium">
{{ formatPeso(getValue()) }} qqSeco (est.)
</span>
<span v-else class="text-gray-400">-</span>
</template>
<!-- Peso (salidas) -->
<template #qq_seco-cell="{ getValue }">
<span v-if="getValue() !== null && getValue() !== undefined" class="font-medium">
{{ formatPeso(getValue()) }} qqSeco
</span>
<span v-else class="text-gray-400">-</span>
</template>
<!-- Cantidad (rechazos) -->
<template #cantidad-cell="{ row }">
<span class="font-medium">
{{ row.original.cantidad }} {{ row.original.unidad }}
</span>
</template>
<!-- Acciones -->
<template #actions-cell="{ row }">
<div class="flex gap-1">
<UButton
v-if="!row.original.vinculado"
icon="i-heroicons-link"
size="xs"
variant="ghost"
color="primary"
@click="$emit('vincular', row.original)"
/>
<UButton
icon="i-heroicons-eye"
size="xs"
variant="ghost"
@click="$emit('ver-detalle', row.original)"
/>
</div>
</template>
</UTable>
</UCard>
</div>
</template>
<script setup lang="ts">
import type { ColumnDef } from '@tanstack/vue-table'
import type { TipoRegistro } from '~/composables/useRegistrosExternos'
const props = defineProps<{
tipo: TipoRegistro
registros: any[]
loading: boolean
meta?: {
total: number
vinculados: number
sinVincular: number
} | null
columns: ColumnDef<any>[]
}>()
const emit = defineEmits<{
refresh: []
vincular: [registro: any]
'vincular-seleccionados': [registros: any[]]
'ver-detalle': [registro: any]
'update:solo-sin-vincular': [value: boolean]
}>()
const { TIPOS_REGISTRO, formatFecha, formatPeso } = useRegistrosExternos()
// Config del tipo
const tipoConfig = computed(() => {
const found = TIPOS_REGISTRO.find(t => t.value === props.tipo)
return found || { value: props.tipo, label: props.tipo, icon: 'i-heroicons-document', color: 'gray' }
})
// Filtro de solo sin vincular
const soloSinVincular = ref(false)
watch(soloSinVincular, (val) => {
emit('update:solo-sin-vincular', val)
})
// Selección múltiple
const seleccionados = ref<any[]>([])
const registrosSeleccionables = computed(() =>
props.registros.filter(r => !r.vinculado)
)
const allSelected = computed(() =>
registrosSeleccionables.value.length > 0 &&
registrosSeleccionables.value.every(r => isSelected(r))
)
const someSelected = computed(() =>
seleccionados.value.length > 0
)
const isSelected = (registro: any) => {
return seleccionados.value.some(s => s.id === registro.id)
}
const toggleSelection = (registro: any) => {
if (registro.vinculado) return
const index = seleccionados.value.findIndex(s => s.id === registro.id)
if (index >= 0) {
seleccionados.value.splice(index, 1)
} else {
seleccionados.value.push(registro)
}
}
const toggleAll = () => {
if (allSelected.value) {
seleccionados.value = []
} else {
seleccionados.value = [...registrosSeleccionables.value]
}
}
// Agregar columna de selección al inicio
const columnsWithSelect = computed<ColumnDef<any>[]>(() => [
{ id: 'select', header: '', size: 40 },
...props.columns,
{ id: 'vinculado', accessorKey: 'vinculado', header: 'Estado' },
{ id: 'actions', header: 'Acciones' },
])
// Limpiar selección cuando cambian los datos
watch(() => props.registros, () => {
seleccionados.value = []
})
</script>