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
982 lines
32 KiB
Vue
982 lines
32 KiB
Vue
<template>
|
||
<UApp>
|
||
<NuxtRouteAnnouncer />
|
||
<UNotifications />
|
||
|
||
<UContainer class="py-8">
|
||
<div class="space-y-6">
|
||
<!-- Header -->
|
||
<div class="flex justify-between items-center mb-8">
|
||
<div>
|
||
<h1 class="text-3xl font-bold text-primary-600 dark:text-primary-400 flex items-center gap-2">
|
||
<NuxtImg src="/icon-64x64.png" alt="Logo Seguidor de Lotes" width="32" height="32" />
|
||
Seguidor de Lotes
|
||
</h1>
|
||
<p class="text-gray-600 dark:text-gray-400 text-lg">
|
||
Sistema de trazabilidad y gestión de lotes de café
|
||
</p>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<AuthUserAvatar v-if="isAuthenticated" />
|
||
<AuthLogoutButton v-if="isAuthenticated" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Botones de Prueba API -->
|
||
<UCard class="mb-4">
|
||
<div class="flex gap-2 flex-wrap">
|
||
<UButton @click="testGetLotes" color="primary">
|
||
Probar GET /api/lotes
|
||
</UButton>
|
||
<UButton @click="testGetOperaciones" color="primary">
|
||
Probar GET /api/operaciones
|
||
</UButton>
|
||
<UButton @click="testGetTrazabilidad" color="primary">
|
||
Probar Trazabilidad
|
||
</UButton>
|
||
</div>
|
||
<p class="text-sm text-gray-500 mt-2">
|
||
Los resultados se mostrarán en la consola del navegador (F12)
|
||
</p>
|
||
</UCard>
|
||
|
||
<!-- ⚠️⚠️⚠️ BOTONES DE DEBUG - TEMPORALES ⚠️⚠️⚠️ -->
|
||
<!-- NO ELIMINAR SIN CONSULTAR A DARIO/DRAGANEL/NUCLEO000 -->
|
||
<UCard class="mb-4 border-2 border-red-500 bg-red-50 dark:bg-red-950">
|
||
<div class="flex items-center gap-2 mb-3">
|
||
<UIcon name="i-heroicons-exclamation-triangle" class="w-6 h-6 text-red-600" />
|
||
<h3 class="text-lg font-bold text-red-600">
|
||
⚠️ DEBUG - BOTONES TEMPORALES ⚠️
|
||
</h3>
|
||
</div>
|
||
<p class="text-sm text-red-700 dark:text-red-400 mb-3">
|
||
<strong>ADVERTENCIA:</strong> Estos botones modifican la base de datos directamente.
|
||
<br />
|
||
<strong>NO ELIMINAR</strong> este código sin consultar a Dario/Draganel/nucleo000.
|
||
</p>
|
||
<div class="flex gap-2 flex-wrap">
|
||
<UButton
|
||
@click="resetDatabase"
|
||
color="red"
|
||
variant="solid"
|
||
:loading="resettingDB"
|
||
>
|
||
🗑️ BORRAR TODA LA BD
|
||
</UButton>
|
||
<UButton
|
||
@click="seedDatabase"
|
||
color="orange"
|
||
variant="solid"
|
||
:loading="seedingDB"
|
||
>
|
||
🌱 CARGAR DATOS DE EJEMPLO
|
||
</UButton>
|
||
<UButton
|
||
@click="clearData"
|
||
color="yellow"
|
||
variant="solid"
|
||
:loading="clearingData"
|
||
>
|
||
🧹 LIMPIAR DATOS (solo datos)
|
||
</UButton>
|
||
<UButton
|
||
@click="exportDatabase"
|
||
color="blue"
|
||
variant="solid"
|
||
:loading="exportingDB"
|
||
>
|
||
💾 EXPORTAR BACKUP
|
||
</UButton>
|
||
<UButton
|
||
@click="triggerFileInput"
|
||
color="green"
|
||
variant="solid"
|
||
:loading="importingDB"
|
||
>
|
||
📥 IMPORTAR BACKUP
|
||
</UButton>
|
||
<input
|
||
ref="fileInputRef"
|
||
type="file"
|
||
accept=".sql"
|
||
@change="importDatabase"
|
||
style="display: none"
|
||
/>
|
||
</div>
|
||
<p class="text-xs text-red-600 dark:text-red-400 mt-2">
|
||
Resultados en consola (F12). Recarga la página después de usar estos botones.
|
||
</p>
|
||
</UCard>
|
||
<!-- ⚠️⚠️⚠️ FIN BOTONES DE DEBUG ⚠️⚠️⚠️ -->
|
||
|
||
<!-- Contenido principal -->
|
||
<div v-if="isAuthenticated">
|
||
<!-- Navegación por Tabs -->
|
||
<UTabs v-model="selectedTab" :items="tabs" class="mb-6">
|
||
<!-- Tab: Lotes -->
|
||
<template #lotes>
|
||
<div class="py-4">
|
||
<h3>Contenido del Tab Lotes</h3>
|
||
<ClientOnly>
|
||
<LotesTable
|
||
@create="showCreateLoteModal = true"
|
||
@view="handleViewLote"
|
||
@edit="handleEditLote"
|
||
@trazabilidad="handleViewTrazabilidad"
|
||
/>
|
||
</ClientOnly>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Tab: Operaciones -->
|
||
<template #operaciones>
|
||
<div class="py-4">
|
||
<h3>Contenido del Tab Operaciones</h3>
|
||
<ClientOnly>
|
||
<OperacionesTable
|
||
@create="showCreateOperacionModal = true"
|
||
@view="handleViewOperacion"
|
||
/>
|
||
</ClientOnly>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Tab: Grafos -->
|
||
<template #grafos>
|
||
<div class="py-4 space-y-4">
|
||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||
<div>
|
||
<h3 class="text-xl font-semibold">Grafo de trazabilidad</h3>
|
||
<p class="text-sm text-gray-500 dark:text-gray-400">Visualiza el grafo completo desde un lote final (sin hijos). Solo se muestran lotes que no han sido procesados.</p>
|
||
</div>
|
||
<div class="flex gap-2 items-center">
|
||
<USelect
|
||
v-model="selectedGraphLoteId"
|
||
:items="graphSelectItems"
|
||
label-key="label"
|
||
value-key="value"
|
||
searchable
|
||
placeholder="Selecciona lote"
|
||
class="w-72"
|
||
:loading="graphLoading"
|
||
:disabled="graphLoading || graphSelectItems.length === 0"
|
||
/>
|
||
<UButton icon="i-heroicons-arrow-path" variant="outline" @click="loadGraphLotes">
|
||
Recargar
|
||
</UButton>
|
||
</div>
|
||
</div>
|
||
|
||
<UAlert v-if="graphError" color="red" variant="soft" title="Error cargando lotes">
|
||
{{ graphError }}
|
||
</UAlert>
|
||
|
||
<ClientOnly>
|
||
<div v-if="selectedGraphLoteId" class="mt-2">
|
||
<LotesTrazabilidad
|
||
:key="selectedGraphLoteId"
|
||
:lote-id="selectedGraphLoteId"
|
||
@close="selectedGraphLoteId = null"
|
||
/>
|
||
</div>
|
||
<template #fallback>
|
||
<USkeleton class="h-64" />
|
||
</template>
|
||
</ClientOnly>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Tab: Externos - Vinculación de registros externos con lotes -->
|
||
<template #externos>
|
||
<div class="py-4">
|
||
<ClientOnly>
|
||
<!-- Sub-navegación por tipo de registro -->
|
||
<UTabs v-model="externosSubTab" :items="externosSubTabs" class="mb-4">
|
||
<!-- Dashboard de progreso -->
|
||
<template #dashboard>
|
||
<VinculacionesProgressDashboard
|
||
@seleccionar-tipo="handleSeleccionarTipoExterno"
|
||
/>
|
||
</template>
|
||
|
||
<!-- Ingresos -->
|
||
<template #ingresos>
|
||
<ExternosTablaRegistros
|
||
tipo="ingreso"
|
||
:registros="ingresosData"
|
||
:loading="ingresosLoading"
|
||
:meta="ingresosMeta"
|
||
:columns="ingresosColumns"
|
||
@refresh="cargarIngresos"
|
||
@vincular="handleVincularRegistro"
|
||
@vincular-seleccionados="handleVincularSeleccionados"
|
||
@ver-detalle="handleVerDetalleRegistro"
|
||
@update:solo-sin-vincular="(val) => { soloSinVincularIngresos = val; cargarIngresos() }"
|
||
/>
|
||
</template>
|
||
|
||
<!-- Carretas -->
|
||
<template #carretas>
|
||
<ExternosTablaRegistros
|
||
tipo="carreta"
|
||
:registros="carretasData"
|
||
:loading="carretasLoading"
|
||
:meta="carretasMeta"
|
||
:columns="carretasColumns"
|
||
@refresh="cargarCarretas"
|
||
@vincular="handleVincularRegistro"
|
||
@vincular-seleccionados="handleVincularSeleccionados"
|
||
@ver-detalle="handleVerDetalleRegistro"
|
||
@update:solo-sin-vincular="(val) => { soloSinVincularCarretas = val; cargarCarretas() }"
|
||
/>
|
||
</template>
|
||
|
||
<!-- Salidas -->
|
||
<template #salidas>
|
||
<ExternosTablaRegistros
|
||
tipo="salida"
|
||
:registros="salidasData"
|
||
:loading="salidasLoading"
|
||
:meta="salidasMeta"
|
||
:columns="salidasColumns"
|
||
@refresh="cargarSalidas"
|
||
@vincular="handleVincularRegistro"
|
||
@vincular-seleccionados="handleVincularSeleccionados"
|
||
@ver-detalle="handleVerDetalleRegistro"
|
||
@update:solo-sin-vincular="(val) => { soloSinVincularSalidas = val; cargarSalidas() }"
|
||
/>
|
||
</template>
|
||
|
||
<!-- Rechazos -->
|
||
<template #rechazos>
|
||
<ExternosTablaRegistros
|
||
tipo="rechazo"
|
||
:registros="rechazosData"
|
||
:loading="rechazosLoading"
|
||
:meta="rechazosMeta"
|
||
:columns="rechazosColumns"
|
||
@refresh="cargarRechazos"
|
||
@vincular="handleVincularRegistro"
|
||
@vincular-seleccionados="handleVincularSeleccionados"
|
||
@ver-detalle="handleVerDetalleRegistro"
|
||
@update:solo-sin-vincular="(val) => { soloSinVincularRechazos = val; cargarRechazos() }"
|
||
/>
|
||
</template>
|
||
</UTabs>
|
||
<template #fallback>
|
||
<USkeleton class="h-64" />
|
||
</template>
|
||
</ClientOnly>
|
||
</div>
|
||
</template>
|
||
</UTabs>
|
||
</div>
|
||
|
||
<!-- Mensaje si no está autenticado -->
|
||
<UCard v-else class="text-center">
|
||
<div class="py-8">
|
||
<UIcon name="i-heroicons-shield-exclamation" class="w-16 h-16 mx-auto mb-4 text-gray-400" />
|
||
<h2 class="text-2xl font-semibold mb-2">No autenticado</h2>
|
||
<p class="text-gray-600 dark:text-gray-400">
|
||
Authentik Proxy Outpost debería redirigirte automáticamente.
|
||
</p>
|
||
</div>
|
||
</UCard>
|
||
</div>
|
||
</UContainer>
|
||
|
||
<!-- Modal: Crear/Editar Lote -->
|
||
<UModal v-model:open="showLoteFormModal">
|
||
<template #content>
|
||
<LotesForm
|
||
:lote="selectedLote"
|
||
@cancel="closeLoteFormModal"
|
||
@success="handleLoteFormSuccess"
|
||
/>
|
||
</template>
|
||
</UModal>
|
||
|
||
<!-- Modal: Ver Detalle de Lote -->
|
||
<UModal v-model:open="showLoteDetailModal">
|
||
<template #content>
|
||
<LotesCard
|
||
v-if="selectedLote"
|
||
:lote="selectedLote"
|
||
@edit="handleEditLoteFromDetail"
|
||
@trazabilidad="handleViewTrazabilidadFromDetail"
|
||
/>
|
||
</template>
|
||
</UModal>
|
||
|
||
<!-- Modal: Ver Trazabilidad -->
|
||
<UModal
|
||
v-model:open="showTrazabilidadModal"
|
||
:ui="{
|
||
content: 'w-[calc(100vw-2rem)] max-w-4xl rounded-lg shadow-lg ring ring-default max-h-[80vh]'
|
||
}"
|
||
>
|
||
<template #header>
|
||
<div class="flex justify-between items-center">
|
||
<div>
|
||
<h3 class="text-xl font-bold">Trazabilidad de Lote</h3>
|
||
<p class="text-sm text-gray-500">Historial completo desde los ingresos iniciales</p>
|
||
</div>
|
||
<UButton
|
||
icon="i-heroicons-x-mark"
|
||
variant="ghost"
|
||
size="sm"
|
||
@click="showTrazabilidadModal = false"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<template #body>
|
||
<LotesTrazabilidad
|
||
v-if="trazabilidadLoteId"
|
||
:lote-id="trazabilidadLoteId"
|
||
/>
|
||
</template>
|
||
</UModal>
|
||
|
||
<!-- Modal: Crear Operación -->
|
||
<UModal
|
||
v-model:open="showCreateOperacionModal"
|
||
:ui="{ content: 'w-[calc(100vw-2rem)] max-w-3xl rounded-lg shadow-lg ring ring-default max-h-[80vh]' }"
|
||
>
|
||
<template #header>
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">Flujo guiado</p>
|
||
<h3 class="text-xl font-semibold">Nueva Operación</h3>
|
||
<p class="text-sm text-gray-500 dark:text-gray-400">Define el tipo, selecciona inputs y crea los outputs.</p>
|
||
</div>
|
||
<UButton
|
||
icon="i-heroicons-x-mark"
|
||
variant="ghost"
|
||
size="sm"
|
||
@click="showCreateOperacionModal = false"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<template #body>
|
||
<OperacionesForm
|
||
@cancel="showCreateOperacionModal = false"
|
||
@success="handleOperacionFormSuccess"
|
||
/>
|
||
</template>
|
||
</UModal>
|
||
|
||
<!-- Modal: Ver Detalle de Operación -->
|
||
<UModal
|
||
v-model:open="showOperacionDetailModal"
|
||
:ui="{ content: 'w-[calc(100vw-2rem)] max-w-2xl rounded-lg shadow-lg ring ring-default' }"
|
||
>
|
||
<template #content>
|
||
<UCard v-if="selectedOperacion">
|
||
<template #header>
|
||
<div class="flex justify-between items-center">
|
||
<div>
|
||
<h3 class="text-lg font-semibold">Detalle de Operación</h3>
|
||
<p class="text-sm text-gray-500">
|
||
{{ getOperacionTipoLabel(selectedOperacion.tipo) }} · {{ formatDate(selectedOperacion.fecha) }}
|
||
</p>
|
||
</div>
|
||
<UButton icon="i-heroicons-x-mark" variant="ghost" @click="closeOperacionDetailModal" />
|
||
</div>
|
||
</template>
|
||
<div class="space-y-3">
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<p class="text-xs uppercase text-gray-500">ID</p>
|
||
<p class="font-mono text-sm">{{ selectedOperacion.id }}</p>
|
||
</div>
|
||
<div>
|
||
<p class="text-xs uppercase text-gray-500">Fecha</p>
|
||
<p class="text-sm">{{ formatDate(selectedOperacion.fecha) }}</p>
|
||
</div>
|
||
<div class="col-span-2">
|
||
<p class="text-xs uppercase text-gray-500">Tipo</p>
|
||
<UBadge color="blue" variant="subtle">{{ getOperacionTipoLabel(selectedOperacion.tipo) }}</UBadge>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<p class="text-xs uppercase text-gray-500 mb-1">Meta</p>
|
||
<div class="rounded-lg border border-gray-200 dark:border-slate-800 bg-gray-50 dark:bg-slate-900/60 p-2">
|
||
<pre class="text-xs text-gray-800 dark:text-slate-200 whitespace-pre-wrap overflow-x-auto">
|
||
{{ JSON.stringify(selectedOperacion.meta || {}, null, 2) }}
|
||
</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</UCard>
|
||
</template>
|
||
</UModal>
|
||
|
||
<!-- Modal: Asignar Vinculación -->
|
||
<VinculacionesModalAsignar
|
||
v-model:open="showVinculacionModal"
|
||
:tipo="tipoRegistroParaVincular"
|
||
:registros="registrosParaVincular"
|
||
@vinculado="handleVinculacionSuccess"
|
||
/>
|
||
</UApp>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { Lote, Operacion } from '~/composables/useLotes'
|
||
|
||
const { isAuthenticated } = useAuthentik()
|
||
const { fetchLotes: fetchLotesComposable, TIPOS_LOTE, TIPOS_OPERACION } = useLotes()
|
||
const { fetchIngresos, fetchCarretas, fetchSalidas, fetchRechazos, TIPOS_REGISTRO } = useRegistrosExternos()
|
||
import type { TipoRegistro } from '~/composables/useRegistrosExternos'
|
||
import type { ColumnDef } from '@tanstack/vue-table'
|
||
|
||
// Navegación
|
||
const selectedTab = ref('lotes')
|
||
const tabs = [
|
||
{ label: 'Lotes', icon: 'i-heroicons-cube', slot: 'lotes', value: 'lotes' },
|
||
{ label: 'Operaciones', icon: 'i-heroicons-beaker', slot: 'operaciones', value: 'operaciones' },
|
||
{ label: 'Grafos', icon: 'i-heroicons-share', slot: 'grafos', value: 'grafos' },
|
||
{ label: 'Externos', icon: 'i-heroicons-link', slot: 'externos', value: 'externos' },
|
||
]
|
||
|
||
// Estados de modales
|
||
const showLoteFormModal = ref(false)
|
||
const showLoteDetailModal = ref(false)
|
||
const showTrazabilidadModal = ref(false)
|
||
const showCreateLoteModal = ref(false)
|
||
const showCreateOperacionModal = ref(false)
|
||
const showOperacionDetailModal = ref(false)
|
||
|
||
// Estados de datos
|
||
const selectedLote = ref<Lote | null>(null)
|
||
const selectedOperacion = ref<Operacion | null>(null)
|
||
const trazabilidadLoteId = ref<string | null>(null)
|
||
const graphLotes = ref<Lote[]>([])
|
||
const graphLoading = ref(false)
|
||
const graphError = ref<string | null>(null)
|
||
const selectedGraphLoteId = ref<string | null>(null)
|
||
|
||
// Handlers para Lotes
|
||
const handleViewLote = (lote: Lote) => {
|
||
selectedLote.value = lote
|
||
showLoteDetailModal.value = true
|
||
}
|
||
|
||
const handleEditLote = (lote: Lote) => {
|
||
selectedLote.value = lote
|
||
showLoteFormModal.value = true
|
||
}
|
||
|
||
const handleEditLoteFromDetail = () => {
|
||
showLoteDetailModal.value = false
|
||
showLoteFormModal.value = true
|
||
}
|
||
|
||
const handleViewTrazabilidad = (lote: Lote) => {
|
||
trazabilidadLoteId.value = lote.id
|
||
showTrazabilidadModal.value = true
|
||
}
|
||
|
||
const handleViewTrazabilidadFromDetail = () => {
|
||
if (selectedLote.value) {
|
||
showLoteDetailModal.value = false
|
||
trazabilidadLoteId.value = selectedLote.value.id
|
||
showTrazabilidadModal.value = true
|
||
}
|
||
}
|
||
|
||
const closeLoteFormModal = () => {
|
||
showLoteFormModal.value = false
|
||
selectedLote.value = null
|
||
}
|
||
|
||
const handleLoteFormSuccess = () => {
|
||
closeLoteFormModal()
|
||
// La tabla se recargará automáticamente
|
||
}
|
||
|
||
// Handlers para Operaciones
|
||
const handleViewOperacion = (operacion: Operacion) => {
|
||
selectedOperacion.value = operacion
|
||
showOperacionDetailModal.value = true
|
||
}
|
||
|
||
const handleOperacionFormSuccess = () => {
|
||
showCreateOperacionModal.value = false
|
||
// Las tablas se recargarán automáticamente
|
||
}
|
||
|
||
const closeOperacionDetailModal = () => {
|
||
showOperacionDetailModal.value = false
|
||
selectedOperacion.value = null
|
||
}
|
||
|
||
const getTipoLabel = (tipo: string) => {
|
||
const found = TIPOS_LOTE.find((t) => t.value === tipo)
|
||
return found?.label || tipo
|
||
}
|
||
|
||
const getOperacionTipoLabel = (tipo: string) => {
|
||
const found = TIPOS_OPERACION.find((t) => t.value === tipo)
|
||
return found?.label || tipo
|
||
}
|
||
|
||
const formatDate = (dateString: string) => {
|
||
return new Date(dateString).toLocaleDateString('es-AR', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
})
|
||
}
|
||
|
||
// Datos para grafos
|
||
const graphSelectItems = computed(() =>
|
||
graphLotes.value.map((lote) => ({
|
||
label: `${lote.codigo || lote.id} · ${getTipoLabel(lote.tipo)}`,
|
||
value: lote.id,
|
||
}))
|
||
)
|
||
|
||
// =====================================================
|
||
// SECCIÓN: REGISTROS EXTERNOS Y VINCULACIONES
|
||
// =====================================================
|
||
|
||
const externosSubTab = ref('dashboard')
|
||
const externosSubTabs = [
|
||
{ label: 'Dashboard', icon: 'i-heroicons-chart-pie', slot: 'dashboard', value: 'dashboard' },
|
||
{ label: 'Ingresos', icon: 'i-heroicons-inbox-arrow-down', slot: 'ingresos', value: 'ingresos' },
|
||
{ label: 'Carretas', icon: 'i-heroicons-truck', slot: 'carretas', value: 'carretas' },
|
||
{ label: 'Salidas', icon: 'i-heroicons-arrow-up-tray', slot: 'salidas', value: 'salidas' },
|
||
{ label: 'Rechazos', icon: 'i-heroicons-x-circle', slot: 'rechazos', value: 'rechazos' },
|
||
]
|
||
|
||
// Estados de datos externos
|
||
const ingresosData = ref<any[]>([])
|
||
const ingresosLoading = ref(false)
|
||
const ingresosMeta = ref<any>(null)
|
||
const soloSinVincularIngresos = ref(false)
|
||
|
||
const carretasData = ref<any[]>([])
|
||
const carretasLoading = ref(false)
|
||
const carretasMeta = ref<any>(null)
|
||
const soloSinVincularCarretas = ref(false)
|
||
|
||
const salidasData = ref<any[]>([])
|
||
const salidasLoading = ref(false)
|
||
const salidasMeta = ref<any>(null)
|
||
const soloSinVincularSalidas = ref(false)
|
||
|
||
const rechazosData = ref<any[]>([])
|
||
const rechazosLoading = ref(false)
|
||
const rechazosMeta = ref<any>(null)
|
||
const soloSinVincularRechazos = ref(false)
|
||
|
||
// Modal de vinculación
|
||
const showVinculacionModal = ref(false)
|
||
const registrosParaVincular = ref<any[]>([])
|
||
const tipoRegistroParaVincular = ref<TipoRegistro>('ingreso')
|
||
|
||
// Columnas para tablas
|
||
const ingresosColumns: ColumnDef<any>[] = [
|
||
{ accessorKey: 'id', header: 'ID' },
|
||
{ accessorKey: 'created_at', header: 'Fecha' },
|
||
{ accessorKey: 'tipo', header: 'Tipo' },
|
||
{ accessorKey: 'cliente_nombre', header: 'Cliente' },
|
||
{ accessorKey: 'peso_seco', header: 'Peso Seco' },
|
||
{ accessorKey: 'calidad', header: 'Calidad' },
|
||
]
|
||
|
||
const carretasColumns: ColumnDef<any>[] = [
|
||
{ accessorKey: 'id', header: 'ID' },
|
||
{ accessorKey: 'created_at', header: 'Fecha' },
|
||
{ accessorKey: 'titulo', header: 'Título' },
|
||
{ accessorKey: 'qq_seco_estimado', header: 'qqSeco Est.' },
|
||
{ accessorKey: 'estado', header: 'Estado' },
|
||
]
|
||
|
||
const salidasColumns: ColumnDef<any>[] = [
|
||
{ accessorKey: 'id', header: 'ID' },
|
||
{ accessorKey: 'created_at', header: 'Fecha' },
|
||
{ accessorKey: 'comprador', header: 'Comprador' },
|
||
{ accessorKey: 'qq_seco', header: 'qqSeco' },
|
||
]
|
||
|
||
const rechazosColumns: ColumnDef<any>[] = [
|
||
{ accessorKey: 'id', header: 'ID' },
|
||
{ accessorKey: 'created_at', header: 'Fecha' },
|
||
{ accessorKey: 'tipo', header: 'Tipo' },
|
||
{ accessorKey: 'cantidad', header: 'Cantidad' },
|
||
{ accessorKey: 'comprador_nombre', header: 'Comprador' },
|
||
]
|
||
|
||
// Funciones para cargar datos externos
|
||
const cargarIngresos = async () => {
|
||
ingresosLoading.value = true
|
||
try {
|
||
const result = await fetchIngresos({ sinVincular: soloSinVincularIngresos.value })
|
||
ingresosData.value = result.data
|
||
ingresosMeta.value = result.meta
|
||
} finally {
|
||
ingresosLoading.value = false
|
||
}
|
||
}
|
||
|
||
const cargarCarretas = async () => {
|
||
carretasLoading.value = true
|
||
try {
|
||
const result = await fetchCarretas({ sinVincular: soloSinVincularCarretas.value })
|
||
carretasData.value = result.data
|
||
carretasMeta.value = result.meta
|
||
} finally {
|
||
carretasLoading.value = false
|
||
}
|
||
}
|
||
|
||
const cargarSalidas = async () => {
|
||
salidasLoading.value = true
|
||
try {
|
||
const result = await fetchSalidas({ sinVincular: soloSinVincularSalidas.value })
|
||
salidasData.value = result.data
|
||
salidasMeta.value = result.meta
|
||
} finally {
|
||
salidasLoading.value = false
|
||
}
|
||
}
|
||
|
||
const cargarRechazos = async () => {
|
||
rechazosLoading.value = true
|
||
try {
|
||
const result = await fetchRechazos({ sinVincular: soloSinVincularRechazos.value })
|
||
rechazosData.value = result.data
|
||
rechazosMeta.value = result.meta
|
||
} finally {
|
||
rechazosLoading.value = false
|
||
}
|
||
}
|
||
|
||
// Handlers para vinculación
|
||
const handleVincularRegistro = (registro: any) => {
|
||
registrosParaVincular.value = [registro]
|
||
tipoRegistroParaVincular.value = externosSubTab.value as TipoRegistro
|
||
showVinculacionModal.value = true
|
||
}
|
||
|
||
const handleVincularSeleccionados = (registros: any[]) => {
|
||
registrosParaVincular.value = registros
|
||
tipoRegistroParaVincular.value = externosSubTab.value as TipoRegistro
|
||
showVinculacionModal.value = true
|
||
}
|
||
|
||
const handleVerDetalleRegistro = (registro: any) => {
|
||
// TODO: Implementar modal de detalle
|
||
console.log('Ver detalle:', registro)
|
||
}
|
||
|
||
const handleSeleccionarTipoExterno = (tipo: TipoRegistro) => {
|
||
const tabMap: Record<TipoRegistro, string> = {
|
||
ingreso: 'ingresos',
|
||
carreta: 'carretas',
|
||
salida: 'salidas',
|
||
rechazo: 'rechazos',
|
||
}
|
||
externosSubTab.value = tabMap[tipo]
|
||
}
|
||
|
||
const handleVinculacionSuccess = () => {
|
||
showVinculacionModal.value = false
|
||
// Recargar el tab actual
|
||
if (externosSubTab.value === 'ingresos') cargarIngresos()
|
||
else if (externosSubTab.value === 'carretas') cargarCarretas()
|
||
else if (externosSubTab.value === 'salidas') cargarSalidas()
|
||
else if (externosSubTab.value === 'rechazos') cargarRechazos()
|
||
}
|
||
|
||
// Cargar datos cuando cambia el sub-tab
|
||
watch(externosSubTab, (newTab) => {
|
||
if (newTab === 'ingresos' && ingresosData.value.length === 0) cargarIngresos()
|
||
else if (newTab === 'carretas' && carretasData.value.length === 0) cargarCarretas()
|
||
else if (newTab === 'salidas' && salidasData.value.length === 0) cargarSalidas()
|
||
else if (newTab === 'rechazos' && rechazosData.value.length === 0) cargarRechazos()
|
||
})
|
||
|
||
const loadGraphLotes = async () => {
|
||
graphLoading.value = true
|
||
graphError.value = null
|
||
try {
|
||
// Filtrar solo lotes finales (sin hijos) para el grafo
|
||
const lotes = await fetchLotesComposable({ soloFinales: true })
|
||
graphLotes.value = lotes.sort((a, b) =>
|
||
new Date(b.fecha_creado).getTime() - new Date(a.fecha_creado).getTime()
|
||
)
|
||
if (!selectedGraphLoteId.value && graphLotes.value.length > 0) {
|
||
selectedGraphLoteId.value = graphLotes.value[0].id
|
||
}
|
||
} catch (err: any) {
|
||
graphError.value = err?.message || 'Error cargando lotes'
|
||
} finally {
|
||
graphLoading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadGraphLotes()
|
||
})
|
||
|
||
// ⚠️⚠️⚠️ FUNCIONES DE DEBUG - TEMPORALES ⚠️⚠️⚠️
|
||
// NO ELIMINAR SIN CONSULTAR A DARIO/DRAGANEL/NUCLEO000
|
||
const resettingDB = ref(false)
|
||
const seedingDB = ref(false)
|
||
const clearingData = ref(false)
|
||
const exportingDB = ref(false)
|
||
const importingDB = ref(false)
|
||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||
|
||
const resetDatabase = async () => {
|
||
if (!confirm('⚠️ ADVERTENCIA: Esto BORRARÁ TODOS LOS DATOS de la base de datos.\n\n¿Estás seguro de continuar?')) {
|
||
return
|
||
}
|
||
|
||
console.log('🗑️ === RESETEANDO BASE DE DATOS ===')
|
||
resettingDB.value = true
|
||
|
||
try {
|
||
const response = await fetch('/api/debug/reset-database', {
|
||
method: 'POST',
|
||
})
|
||
const data = await response.json()
|
||
console.log('Status:', response.status)
|
||
console.log('Respuesta:', data)
|
||
|
||
if (data.success) {
|
||
alert('✅ Base de datos reseteada exitosamente.\n\nRecarga la página para ver los cambios.')
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Error:', error)
|
||
alert('❌ Error reseteando la base de datos. Ver consola.')
|
||
} finally {
|
||
resettingDB.value = false
|
||
}
|
||
}
|
||
|
||
const seedDatabase = async () => {
|
||
console.log('🌱 === CARGANDO DATOS DE EJEMPLO ===')
|
||
seedingDB.value = true
|
||
|
||
try {
|
||
const response = await fetch('/api/debug/seed-database', {
|
||
method: 'POST',
|
||
})
|
||
const data = await response.json()
|
||
console.log('Status:', response.status)
|
||
console.log('Respuesta:', data)
|
||
|
||
if (data.success) {
|
||
alert('✅ Datos de ejemplo cargados exitosamente.\n\nRecarga la página para ver los cambios.')
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Error:', error)
|
||
alert('❌ Error cargando datos. Ver consola.')
|
||
} finally {
|
||
seedingDB.value = false
|
||
}
|
||
}
|
||
|
||
const clearData = async () => {
|
||
if (!confirm('⚠️ ADVERTENCIA: Esto ELIMINARÁ TODOS LOS DATOS de las tablas (TRUNCATE) pero mantendrá la estructura.\n\n¿Estás seguro de continuar?')) {
|
||
return
|
||
}
|
||
|
||
console.log('🧹 === LIMPIANDO DATOS DE TABLAS ===')
|
||
clearingData.value = true
|
||
|
||
try {
|
||
const response = await fetch('/api/debug/clear-data', {
|
||
method: 'POST',
|
||
})
|
||
const data = await response.json()
|
||
console.log('Status:', response.status)
|
||
console.log('Respuesta:', data)
|
||
|
||
if (data.success) {
|
||
alert('✅ Datos eliminados exitosamente. Las tablas están vacías.\n\nRecarga la página para ver los cambios.')
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Error:', error)
|
||
alert('❌ Error limpiando datos. Ver consola.')
|
||
} finally {
|
||
clearingData.value = false
|
||
}
|
||
}
|
||
|
||
const exportDatabase = async () => {
|
||
console.log('💾 === EXPORTANDO BACKUP DE BASE DE DATOS ===')
|
||
exportingDB.value = true
|
||
|
||
try {
|
||
const response = await fetch('/api/debug/export-database', {
|
||
method: 'POST',
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Error en la exportación')
|
||
}
|
||
|
||
// Descargar el archivo SQL
|
||
const blob = await response.blob()
|
||
const url = window.URL.createObjectURL(blob)
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
|
||
// Nombre del archivo con timestamp
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)
|
||
a.download = `backup-seguidordelotes-${timestamp}.sql`
|
||
|
||
document.body.appendChild(a)
|
||
a.click()
|
||
window.URL.revokeObjectURL(url)
|
||
document.body.removeChild(a)
|
||
|
||
console.log('✅ Backup descargado exitosamente')
|
||
alert('✅ Backup de base de datos descargado exitosamente.')
|
||
} catch (error) {
|
||
console.error('❌ Error:', error)
|
||
alert('❌ Error exportando backup. Ver consola.')
|
||
} finally {
|
||
exportingDB.value = false
|
||
}
|
||
}
|
||
|
||
const triggerFileInput = () => {
|
||
fileInputRef.value?.click()
|
||
}
|
||
|
||
const importDatabase = async (event: Event) => {
|
||
const input = event.target as HTMLInputElement
|
||
const file = input.files?.[0]
|
||
|
||
if (!file) {
|
||
return
|
||
}
|
||
|
||
if (!file.name.endsWith('.sql')) {
|
||
alert('⚠️ Por favor selecciona un archivo .sql')
|
||
input.value = ''
|
||
return
|
||
}
|
||
|
||
if (!confirm('⚠️ ADVERTENCIA: Esto REEMPLAZARÁ TODA LA BASE DE DATOS con el contenido del backup.\n\nSe eliminará todo lo existente y se cargará el backup.\n\n¿Estás seguro de continuar?')) {
|
||
input.value = ''
|
||
return
|
||
}
|
||
|
||
console.log('📥 === IMPORTANDO BACKUP DE BASE DE DATOS ===')
|
||
console.log(` - Archivo: ${file.name}`)
|
||
console.log(` - Tamaño: ${(file.size / 1024).toFixed(2)} KB`)
|
||
importingDB.value = true
|
||
|
||
try {
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
|
||
const response = await fetch('/api/debug/import-database', {
|
||
method: 'POST',
|
||
body: formData,
|
||
})
|
||
|
||
const data = await response.json()
|
||
console.log('Status:', response.status)
|
||
console.log('Respuesta:', data)
|
||
|
||
if (data.success) {
|
||
alert('✅ Backup importado exitosamente.\n\nLa base de datos ha sido restaurada.\n\nRecarga la página para ver los cambios.')
|
||
} else {
|
||
throw new Error(data.message || 'Error desconocido')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('❌ Error:', error)
|
||
alert(`❌ Error importando backup: ${error.message || 'Ver consola para más detalles'}`)
|
||
} finally {
|
||
importingDB.value = false
|
||
input.value = '' // Limpiar el input
|
||
}
|
||
}
|
||
// ⚠️⚠️⚠️ FIN FUNCIONES DE DEBUG ⚠️⚠️⚠️
|
||
|
||
// Funciones de prueba de API
|
||
const testGetLotes = async () => {
|
||
console.log('=== Probando GET /api/lotes ===')
|
||
try {
|
||
const response = await fetch('/api/lotes')
|
||
const data = await response.json()
|
||
console.log('Status:', response.status)
|
||
console.log('Datos recibidos:', data)
|
||
} catch (error) {
|
||
console.error('Error:', error)
|
||
}
|
||
}
|
||
|
||
const testGetOperaciones = async () => {
|
||
console.log('=== Probando GET /api/operaciones ===')
|
||
try {
|
||
const response = await fetch('/api/operaciones')
|
||
const data = await response.json()
|
||
console.log('Status:', response.status)
|
||
console.log('Datos recibidos:', data)
|
||
} catch (error) {
|
||
console.error('Error:', error)
|
||
}
|
||
}
|
||
|
||
const testGetTrazabilidad = async () => {
|
||
console.log('=== Probando Trazabilidad ===')
|
||
try {
|
||
// Primero obtener lotes para tener un ID
|
||
const lotesResponse = await fetch('/api/lotes')
|
||
const lotesData = await lotesResponse.json()
|
||
console.log('Lotes disponibles:', lotesData)
|
||
|
||
if (lotesData.data && lotesData.data.length > 0) {
|
||
const primerLoteId = lotesData.data[0].id
|
||
console.log('Obteniendo trazabilidad para lote:', primerLoteId)
|
||
|
||
const trazResponse = await fetch(`/api/lotes/${primerLoteId}/trazabilidad`)
|
||
const trazData = await trazResponse.json()
|
||
console.log('Status:', trazResponse.status)
|
||
console.log('Trazabilidad:', trazData)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error:', error)
|
||
}
|
||
}
|
||
|
||
// Watch para crear lote
|
||
watch(showCreateLoteModal, (value) => {
|
||
if (value) {
|
||
selectedLote.value = null
|
||
showLoteFormModal.value = true
|
||
showCreateLoteModal.value = false
|
||
}
|
||
})
|
||
|
||
// Configurar meta tags para PWA
|
||
useHead({
|
||
title: 'Seguidor de Lotes - Trazabilidad de Café',
|
||
link: [
|
||
{ rel: 'manifest', href: '/manifest.webmanifest' },
|
||
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: '/icon-32x32.png' },
|
||
{ rel: 'icon', type: 'image/png', sizes: '16x16', href: '/icon-16x16.png' },
|
||
{ rel: 'apple-touch-icon', href: '/apple-touch-icon.png' }
|
||
],
|
||
meta: [
|
||
{ name: 'theme-color', content: '#16a34a' },
|
||
{ name: 'mobile-web-app-capable', content: 'yes' },
|
||
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
|
||
{ name: 'apple-mobile-web-app-status-bar-style', content: 'default' }
|
||
]
|
||
})
|
||
</script>
|