All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 2m46s
- 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
216 lines
7.7 KiB
Vue
216 lines
7.7 KiB
Vue
<template>
|
|
<div class="space-y-6">
|
|
<!-- Header -->
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<h2 class="text-2xl font-bold">Dashboard de Vinculación</h2>
|
|
<p class="text-gray-500">Progreso de vinculación de registros a lotes</p>
|
|
</div>
|
|
<div class="flex gap-2 items-center">
|
|
<USelect
|
|
v-model="periodoSeleccionado"
|
|
:items="PERIODOS_COSECHA"
|
|
label-key="label"
|
|
value-key="value"
|
|
class="w-80"
|
|
/>
|
|
<UButton
|
|
icon="i-heroicons-arrow-path"
|
|
variant="outline"
|
|
:loading="loading"
|
|
@click="cargarEstadisticas"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cards de resumen -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
|
<!-- Card por tipo -->
|
|
<UCard
|
|
v-for="tipo in tiposConEstadisticas"
|
|
:key="tipo.value"
|
|
class="cursor-pointer hover:shadow-lg transition-shadow"
|
|
@click="$emit('seleccionar-tipo', tipo.value)"
|
|
>
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<UIcon :name="tipo.icon" class="w-5 h-5" :class="`text-${tipo.color}-500`" />
|
|
<span class="font-medium">{{ tipo.label }}</span>
|
|
</div>
|
|
<div class="text-3xl font-bold mb-1">
|
|
{{ tipo.stats?.vinculados || 0 }}
|
|
<span class="text-lg text-gray-400 font-normal">/ {{ tipo.stats?.total || 0 }}</span>
|
|
</div>
|
|
<p class="text-sm text-gray-500">
|
|
{{ tipo.stats?.sinVincular || 0 }} pendientes
|
|
</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<div
|
|
class="text-2xl font-bold"
|
|
:class="{
|
|
'text-green-500': (tipo.stats?.porcentaje || 0) >= 80,
|
|
'text-yellow-500': (tipo.stats?.porcentaje || 0) >= 50 && (tipo.stats?.porcentaje || 0) < 80,
|
|
'text-red-500': (tipo.stats?.porcentaje || 0) < 50
|
|
}"
|
|
>
|
|
{{ tipo.stats?.porcentaje || 0 }}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Barra de progreso -->
|
|
<div class="mt-3 w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
<div
|
|
class="h-2 rounded-full transition-all duration-500"
|
|
:class="`bg-${tipo.color}-500`"
|
|
:style="{ width: `${tipo.stats?.porcentaje || 0}%` }"
|
|
/>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- Card resumen total -->
|
|
<UCard class="bg-gray-50 dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-600">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<UIcon name="i-heroicons-chart-pie" class="w-5 h-5 text-gray-600" />
|
|
<span class="font-medium">Total General</span>
|
|
</div>
|
|
<div class="text-3xl font-bold mb-1">
|
|
{{ estadisticas?.resumen?.vinculados || 0 }}
|
|
<span class="text-lg text-gray-400 font-normal">/ {{ estadisticas?.resumen?.total || 0 }}</span>
|
|
</div>
|
|
<p class="text-sm text-gray-500">
|
|
{{ estadisticas?.resumen?.sinVincular || 0 }} pendientes
|
|
</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<div
|
|
class="text-3xl font-bold"
|
|
:class="{
|
|
'text-green-500': (estadisticas?.resumen?.porcentaje || 0) >= 80,
|
|
'text-yellow-500': (estadisticas?.resumen?.porcentaje || 0) >= 50 && (estadisticas?.resumen?.porcentaje || 0) < 80,
|
|
'text-red-500': (estadisticas?.resumen?.porcentaje || 0) < 50
|
|
}"
|
|
>
|
|
{{ estadisticas?.resumen?.porcentaje || 0 }}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Barra de progreso -->
|
|
<div class="mt-3 w-full bg-gray-300 dark:bg-gray-600 rounded-full h-3">
|
|
<div
|
|
class="h-3 rounded-full transition-all duration-500 bg-gradient-to-r from-purple-500 via-blue-500 to-green-500"
|
|
:style="{ width: `${estadisticas?.resumen?.porcentaje || 0}%` }"
|
|
/>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
|
|
<!-- Gráfico de barras horizontal -->
|
|
<UCard>
|
|
<template #header>
|
|
<h3 class="font-semibold">Progreso por tipo de registro</h3>
|
|
</template>
|
|
<div class="space-y-4">
|
|
<div
|
|
v-for="tipo in tiposConEstadisticas"
|
|
:key="tipo.value"
|
|
class="flex items-center gap-4"
|
|
>
|
|
<div class="w-24 flex items-center gap-2">
|
|
<UIcon :name="tipo.icon" class="w-4 h-4" :class="`text-${tipo.color}-500`" />
|
|
<span class="text-sm">{{ tipo.label }}</span>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-4 relative">
|
|
<div
|
|
class="h-4 rounded-full transition-all duration-500 flex items-center justify-end pr-2"
|
|
:class="`bg-${tipo.color}-500`"
|
|
:style="{ width: `${Math.max(tipo.stats?.porcentaje || 0, 5)}%` }"
|
|
>
|
|
<span v-if="(tipo.stats?.porcentaje || 0) > 10" class="text-xs text-white font-medium">
|
|
{{ tipo.stats?.vinculados || 0 }}
|
|
</span>
|
|
</div>
|
|
<span
|
|
v-if="(tipo.stats?.porcentaje || 0) <= 10"
|
|
class="absolute left-2 top-0 h-4 flex items-center text-xs font-medium text-gray-600"
|
|
>
|
|
{{ tipo.stats?.vinculados || 0 }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="w-20 text-right text-sm text-gray-500">
|
|
de {{ tipo.stats?.total || 0 }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- Estado de carga -->
|
|
<div v-if="loading" class="flex justify-center py-8">
|
|
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin text-gray-400" />
|
|
</div>
|
|
|
|
<!-- Error -->
|
|
<UCard v-if="error" class="bg-red-50 dark:bg-red-950 border-red-500">
|
|
<div class="flex items-center gap-3">
|
|
<UIcon name="i-heroicons-exclamation-triangle" class="w-6 h-6 text-red-600" />
|
|
<div>
|
|
<h3 class="font-semibold text-red-600">Error cargando estadísticas</h3>
|
|
<p class="text-sm text-red-700 dark:text-red-400">{{ error }}</p>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { EstadisticasVinculacion, TipoRegistro } from '~/composables/useRegistrosExternos'
|
|
|
|
const emit = defineEmits<{
|
|
'seleccionar-tipo': [tipo: TipoRegistro]
|
|
}>()
|
|
|
|
const { fetchEstadisticas, TIPOS_REGISTRO, PERIODOS_COSECHA } = useRegistrosExternos()
|
|
|
|
const periodoSeleccionado = ref('25-26')
|
|
const estadisticas = ref<EstadisticasVinculacion | null>(null)
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
const tiposConEstadisticas = computed(() =>
|
|
TIPOS_REGISTRO.map(tipo => ({
|
|
...tipo,
|
|
stats: estadisticas.value?.[tipo.value === 'carreta' ? 'carretas' : tipo.value === 'ingreso' ? 'ingresos' : tipo.value === 'salida' ? 'salidas' : 'rechazos'] as any,
|
|
}))
|
|
)
|
|
|
|
const cargarEstadisticas = async () => {
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const result = await fetchEstadisticas(periodoSeleccionado.value)
|
|
estadisticas.value = result
|
|
} catch (err: any) {
|
|
error.value = err.message || 'Error cargando estadísticas'
|
|
console.error('Error cargando estadísticas:', err)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// Cargar al montar
|
|
onMounted(() => {
|
|
cargarEstadisticas()
|
|
})
|
|
|
|
// Recargar cuando cambia el período
|
|
watch(periodoSeleccionado, () => {
|
|
cargarEstadisticas()
|
|
})
|
|
</script>
|