Implementar sistema completo de trazabilidad de lotes
Some checks failed
build-and-deploy / build-and-deploy (push) Failing after 1m47s

- Agregar PostgreSQL 16 con esquema completo
- Crear API endpoints para lotes y operaciones
- Implementar UI con Nuxt UI (tablas, formularios, trazabilidad)
- Agregar datos de ejemplo del flujo completo
- Documentar sistema en PLAN_TRAZABILIDAD.md
This commit is contained in:
2025-11-21 18:39:04 -06:00
parent e5456bf522
commit ee3dffa38e
26 changed files with 4223 additions and 74 deletions

View File

@@ -0,0 +1,393 @@
/**
* Composable para gestión de lotes y operaciones de trazabilidad
*/
export interface Lote {
id: string
codigo: string | null
tipo: string
fecha_creado: string
lugar_id: number | null
cantidad_kg: number | null
meta: Record<string, any> | null
}
export interface Operacion {
id: string
tipo: string
fecha: string
lugar_id: number | null
meta: Record<string, any> | null
}
export interface TrazabilidadRow {
lote_id: string
codigo: string | null
tipo: string
cantidad_kg: number | null
operacion_id: string | null
operacion_tipo: string | null
profundidad: number
}
export const useLotes = () => {
const toast = useToast()
// =====================================================
// FUNCIONES PARA LOTES
// =====================================================
/**
* Obtiene todos los lotes con filtros opcionales
*/
const fetchLotes = async (filtros?: {
tipo?: string
limit?: number
offset?: number
}) => {
try {
const query = new URLSearchParams()
if (filtros?.tipo) query.append('tipo', filtros.tipo)
if (filtros?.limit) query.append('limit', filtros.limit.toString())
if (filtros?.offset) query.append('offset', filtros.offset.toString())
const queryString = query.toString()
const url = `/api/lotes${queryString ? `?${queryString}` : ''}`
const { data, error } = await useFetch<{
success: boolean
data: Lote[]
count: number
}>(url)
if (error.value) {
throw new Error(error.value.message || 'Error obteniendo lotes')
}
return data.value?.data || []
} catch (err: any) {
console.error('Error fetching lotes:', err)
toast.add({
title: 'Error',
description: err.message || 'Error obteniendo lotes',
color: 'red',
})
return []
}
}
/**
* Obtiene un lote por su ID
*/
const fetchLoteById = async (id: string) => {
try {
const { data, error } = await useFetch<{
success: boolean
data: Lote
}>(`/api/lotes/${id}`)
if (error.value) {
throw new Error(error.value.message || 'Error obteniendo lote')
}
return data.value?.data || null
} catch (err: any) {
console.error('Error fetching lote:', err)
toast.add({
title: 'Error',
description: err.message || 'Error obteniendo lote',
color: 'red',
})
return null
}
}
/**
* Crea un nuevo lote
*/
const createLote = async (loteData: {
codigo?: string
tipo: string
cantidad_kg?: number
lugar_id?: number
meta?: Record<string, any>
}) => {
try {
const { data, error } = await useFetch<{
success: boolean
data: Lote
}>('/api/lotes', {
method: 'POST',
body: loteData,
})
if (error.value) {
throw new Error(error.value.message || 'Error creando lote')
}
toast.add({
title: 'Éxito',
description: 'Lote creado correctamente',
color: 'green',
})
return data.value?.data || null
} catch (err: any) {
console.error('Error creating lote:', err)
toast.add({
title: 'Error',
description: err.message || 'Error creando lote',
color: 'red',
})
return null
}
}
/**
* Actualiza un lote existente
*/
const updateLote = async (
id: string,
updates: Partial<{
codigo: string | null
tipo: string
cantidad_kg: number | null
lugar_id: number | null
meta: Record<string, any> | null
}>
) => {
try {
const { data, error } = await useFetch<{
success: boolean
data: Lote
}>(`/api/lotes/${id}`, {
method: 'PATCH',
body: updates,
})
if (error.value) {
throw new Error(error.value.message || 'Error actualizando lote')
}
toast.add({
title: 'Éxito',
description: 'Lote actualizado correctamente',
color: 'green',
})
return data.value?.data || null
} catch (err: any) {
console.error('Error updating lote:', err)
toast.add({
title: 'Error',
description: err.message || 'Error actualizando lote',
color: 'red',
})
return null
}
}
/**
* Elimina un lote
*/
const deleteLote = async (id: string) => {
try {
const { error } = await useFetch(`/api/lotes/${id}`, {
method: 'DELETE',
})
if (error.value) {
throw new Error(error.value.message || 'Error eliminando lote')
}
toast.add({
title: 'Éxito',
description: 'Lote eliminado correctamente',
color: 'green',
})
return true
} catch (err: any) {
console.error('Error deleting lote:', err)
toast.add({
title: 'Error',
description: err.message || 'Error eliminando lote',
color: 'red',
})
return false
}
}
/**
* Obtiene el historial completo de trazabilidad de un lote
*/
const fetchTrazabilidad = async (id: string) => {
try {
const { data, error } = await useFetch<{
success: boolean
data: {
historial: TrazabilidadRow[]
estadisticas: {
total_ancestros: number
profundidad_maxima: number
kg_iniciales: number
}
}
}>(`/api/lotes/${id}/trazabilidad`)
if (error.value) {
throw new Error(error.value.message || 'Error obteniendo trazabilidad')
}
return data.value?.data || null
} catch (err: any) {
console.error('Error fetching trazabilidad:', err)
toast.add({
title: 'Error',
description: err.message || 'Error obteniendo trazabilidad',
color: 'red',
})
return null
}
}
// =====================================================
// FUNCIONES PARA OPERACIONES
// =====================================================
/**
* Obtiene todas las operaciones con filtros opcionales
*/
const fetchOperaciones = async (filtros?: {
tipo?: string
limit?: number
offset?: number
}) => {
try {
const query = new URLSearchParams()
if (filtros?.tipo) query.append('tipo', filtros.tipo)
if (filtros?.limit) query.append('limit', filtros.limit.toString())
if (filtros?.offset) query.append('offset', filtros.offset.toString())
const queryString = query.toString()
const url = `/api/operaciones${queryString ? `?${queryString}` : ''}`
const { data, error } = await useFetch<{
success: boolean
data: Operacion[]
count: number
}>(url)
if (error.value) {
throw new Error(error.value.message || 'Error obteniendo operaciones')
}
return data.value?.data || []
} catch (err: any) {
console.error('Error fetching operaciones:', err)
toast.add({
title: 'Error',
description: err.message || 'Error obteniendo operaciones',
color: 'red',
})
return []
}
}
/**
* Crea una nueva operación con sus lotes inputs/outputs
*/
const createOperacion = async (operacionData: {
tipo: string
fecha?: string
lugar_id?: number
meta?: Record<string, any>
inputs: Array<{ lote_id: string; cantidad_kg?: number }>
outputs: Array<{
codigo?: string
tipo: string
cantidad_kg?: number
meta?: Record<string, any>
}>
}) => {
try {
const { data, error } = await useFetch<{
success: boolean
data: {
operacion: Operacion
lotes_creados: Lote[]
}
}>('/api/operaciones', {
method: 'POST',
body: operacionData,
})
if (error.value) {
throw new Error(error.value.message || 'Error creando operación')
}
toast.add({
title: 'Éxito',
description: 'Operación creada correctamente',
color: 'green',
})
return data.value?.data || null
} catch (err: any) {
console.error('Error creating operacion:', err)
toast.add({
title: 'Error',
description: err.message || 'Error creando operación',
color: 'red',
})
return null
}
}
// =====================================================
// CONSTANTES ÚTILES
// =====================================================
const TIPOS_LOTE = [
{ value: 'uva', label: 'Uva' },
{ value: 'despulpado_primera', label: 'Despulpado Primera' },
{ value: 'despulpado_segunda', label: 'Despulpado Segunda' },
{ value: 'despulpado_rechazos', label: 'Despulpado Rechazos' },
{ value: 'oreado', label: 'Oreado' },
{ value: 'presecado', label: 'Presecado' },
{ value: 'reposo', label: 'Reposo' },
{ value: 'secado', label: 'Secado' },
]
const TIPOS_OPERACION = [
{ value: 'ingreso', label: 'Ingreso', icon: 'i-heroicons-arrow-down-tray' },
{ value: 'despulpado', label: 'Despulpado', icon: 'i-heroicons-beaker' },
{ value: 'oreado', label: 'Oreado', icon: 'i-heroicons-sun' },
{ value: 'presecado', label: 'Presecado', icon: 'i-heroicons-fire' },
{ value: 'reposo', label: 'Reposo', icon: 'i-heroicons-pause' },
{ value: 'secado', label: 'Secado', icon: 'i-heroicons-check-circle' },
{ value: 'traslado', label: 'Traslado', icon: 'i-heroicons-arrow-right' },
{ value: 'mezcla', label: 'Mezcla', icon: 'i-heroicons-beaker' },
{ value: 'ajuste_merma', label: 'Ajuste Merma', icon: 'i-heroicons-adjustments-horizontal' },
{ value: 'ajuste_cantidad', label: 'Ajuste Cantidad', icon: 'i-heroicons-calculator' },
{ value: 'ajuste_tipo', label: 'Ajuste Tipo', icon: 'i-heroicons-pencil' },
]
return {
// Lotes
fetchLotes,
fetchLoteById,
createLote,
updateLote,
deleteLote,
fetchTrazabilidad,
// Operaciones
fetchOperaciones,
createOperacion,
// Constantes
TIPOS_LOTE,
TIPOS_OPERACION,
}
}