Files
seguidorDeLotes/nuxt4/app/app.vue
josedario87 6f713a5ed1
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s
Fix: Actualizar implementación de UTabs a Nuxt UI v4
- Cambiar selectedTab de ref(0) a ref('lotes') para usar string
- Agregar propiedad value explícita a cada tab item
- Crear documento nuxt-ui-tips.md con documentación de cambios
- Los valores deben ser strings según API de v4
2025-11-21 20:44:04 -06:00

372 lines
11 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">Seguidor de Lotes</h1>
<p class="text-gray-600 dark:text-gray-400">
Sistema de trazabilidad 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>
</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">
<LotesLotesTable
@create="showCreateLoteModal = true"
@view="handleViewLote"
@edit="handleEditLote"
@trazabilidad="handleViewTrazabilidad"
/>
</div>
</template>
<!-- Tab: Operaciones -->
<template #operaciones>
<div class="py-4">
<OperacionesOperacionesTable
@create="showCreateOperacionModal = true"
@view="handleViewOperacion"
/>
</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>
<LotesLoteForm
:lote="selectedLote"
@cancel="closeLoteFormModal"
@success="handleLoteFormSuccess"
/>
</template>
</UModal>
<!-- Modal: Ver Detalle de Lote -->
<UModal v-model:open="showLoteDetailModal">
<template #content>
<LotesLoteCard
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' }"
>
<template #content>
<LotesTrazabilidadTree
v-if="trazabilidadLoteId"
:lote-id="trazabilidadLoteId"
@close="showTrazabilidadModal = false"
/>
</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' }"
>
<template #content>
<OperacionesOperacionForm
@cancel="showCreateOperacionModal = false"
@success="handleOperacionFormSuccess"
/>
</template>
</UModal>
</UApp>
</template>
<script setup lang="ts">
import type { Lote, Operacion } from '~/composables/useLotes'
const { isAuthenticated } = useAuthentik()
// 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' },
]
// Estados de modales
const showLoteFormModal = ref(false)
const showLoteDetailModal = ref(false)
const showTrazabilidadModal = ref(false)
const showCreateLoteModal = ref(false)
const showCreateOperacionModal = ref(false)
// Estados de datos
const selectedLote = ref<Lote | null>(null)
const trazabilidadLoteId = 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) => {
// TODO: Implementar vista de detalle de operación si es necesario
console.log('Ver operación:', operacion)
}
const handleOperacionFormSuccess = () => {
showCreateOperacionModal.value = false
// Las tablas se recargarán automáticamente
}
// ⚠️⚠️⚠️ FUNCIONES DE DEBUG - TEMPORALES ⚠️⚠️⚠️
// NO ELIMINAR SIN CONSULTAR A DARIO/DRAGANEL/NUCLEO000
const resettingDB = ref(false)
const seedingDB = ref(false)
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
}
}
// ⚠️⚠️⚠️ 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/svg+xml', href: '/icon.svg' },
{ rel: 'apple-touch-icon', href: '/apple-touch-icon.png' }
],
meta: [
{ name: 'theme-color', content: '#00DC82' },
{ 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>