All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m5s
532 lines
17 KiB
Vue
532 lines
17 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">
|
||
☕ 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">
|
||
{{ getOperacionTipoLabel(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">{{ 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>
|
||
<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, TIPOS_LOTE, TIPOS_OPERACION } = 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
|
||
}
|
||
|
||
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,
|
||
}))
|
||
)
|
||
|
||
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>
|