Files
seguidorDeLotes/nuxt4/app/app.vue
josedario87 c4be9649b8
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m5s
mejoras UI 5
2025-11-22 02:19:32 -06:00

512 lines
16 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">
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>
</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 desde el lote seleccionado (por defecto el más reciente).</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 :lote-id="selectedGraphLoteId" @close="selectedGraphLoteId = null" />
</div>
<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"
scrollable
:ui="{
content: 'w-[calc(100vw-2rem)] max-w-4xl rounded-lg shadow-lg ring ring-default max-h-[80vh]',
body: 'overflow-y-auto'
}"
>
<template #content>
<LotesTrazabilidad
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>
<OperacionesForm
@cancel="showCreateOperacionModal = false"
@success="handleOperacionFormSuccess"
/>
</template>
</UModal>
<!-- Modal: Ver Detalle de Operación -->
<UModal v-model:open="showOperacionDetailModal">
<template #content>
<UCard>
<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" v-if="selectedOperacion">
{{ getTipoLabel(selectedOperacion.tipo) }} · {{ formatDate(selectedOperacion.fecha) }}
</p>
</div>
<UButton icon="i-heroicons-x-mark" variant="ghost" @click="closeOperacionDetailModal" />
</div>
</template>
<div v-if="selectedOperacion" 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">{{ getTipoLabel(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>
<div v-else class="text-sm text-gray-500">No hay operación seleccionada.</div>
</UCard>
</template>
</UModal>
</UApp>
</template>
<script setup lang="ts">
import type { Lote, Operacion } from '~/composables/useLotes'
const { isAuthenticated } = useAuthentik()
const { fetchLotes: fetchLotesComposable } = useLotes()
// 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' },
]
// 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
}
// Datos para grafos
const graphSelectItems = computed(() =>
graphLotes.value.map((lote) => ({
label: `${lote.codigo || lote.id} · ${getTipoLabel(lote.tipo)}`,
value: lote.id,
}))
)
const loadGraphLotes = async () => {
graphLoading.value = true
graphError.value = null
try {
const lotes = await fetchLotesComposable()
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 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>