Implementar sistema completo de trazabilidad de lotes
Some checks failed
build-and-deploy / build-and-deploy (push) Failing after 1m47s
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:
393
nuxt4/app/composables/useLotes.ts
Normal file
393
nuxt4/app/composables/useLotes.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user