Files
seguidorDeLotes/nuxt4/app/app.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

982 lines
32 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>