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:
48
nuxt4/server/api/lotes/[id].delete.ts
Normal file
48
nuxt4/server/api/lotes/[id].delete.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { deleteLote } from '~/server/utils/queries'
|
||||
|
||||
/**
|
||||
* DELETE /api/lotes/:id
|
||||
* Elimina un lote
|
||||
*
|
||||
* CUIDADO: Esta operación es irreversible y eliminará también
|
||||
* todas las relaciones en operacion_lotes (CASCADE).
|
||||
* Usar solo en casos excepcionales.
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const id = getRouterParam(event, 'id')
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'ID de lote requerido',
|
||||
})
|
||||
}
|
||||
|
||||
const deleted = await deleteLote(id)
|
||||
|
||||
if (!deleted) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Lote no encontrado',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Lote eliminado correctamente',
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error eliminando lote:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Error eliminando lote',
|
||||
data: { message: error.message },
|
||||
})
|
||||
}
|
||||
})
|
||||
44
nuxt4/server/api/lotes/[id].get.ts
Normal file
44
nuxt4/server/api/lotes/[id].get.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { getLoteById } from '~/server/utils/queries'
|
||||
|
||||
/**
|
||||
* GET /api/lotes/:id
|
||||
* Obtiene un lote específico por su ID
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const id = getRouterParam(event, 'id')
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'ID de lote requerido',
|
||||
})
|
||||
}
|
||||
|
||||
const lote = await getLoteById(id)
|
||||
|
||||
if (!lote) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Lote no encontrado',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: lote,
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error obteniendo lote:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Error obteniendo lote',
|
||||
data: { message: error.message },
|
||||
})
|
||||
}
|
||||
})
|
||||
55
nuxt4/server/api/lotes/[id].patch.ts
Normal file
55
nuxt4/server/api/lotes/[id].patch.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { updateLote } from '~/server/utils/queries'
|
||||
|
||||
/**
|
||||
* PATCH /api/lotes/:id
|
||||
* Actualiza un lote existente
|
||||
*
|
||||
* Body (todos opcionales):
|
||||
* {
|
||||
* codigo?: string | null,
|
||||
* tipo?: string,
|
||||
* cantidad_kg?: number | null,
|
||||
* lugar_id?: number | null,
|
||||
* meta?: object | null
|
||||
* }
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const id = getRouterParam(event, 'id')
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'ID de lote requerido',
|
||||
})
|
||||
}
|
||||
|
||||
const body = await readBody(event)
|
||||
|
||||
const lote = await updateLote(id, body)
|
||||
|
||||
if (!lote) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Lote no encontrado',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: lote,
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error actualizando lote:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Error actualizando lote',
|
||||
data: { message: error.message },
|
||||
})
|
||||
}
|
||||
})
|
||||
54
nuxt4/server/api/lotes/[id]/trazabilidad.get.ts
Normal file
54
nuxt4/server/api/lotes/[id]/trazabilidad.get.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { getTrazabilidad, getEstadisticasLote } from '~/server/utils/queries'
|
||||
|
||||
/**
|
||||
* GET /api/lotes/:id/trazabilidad
|
||||
* Obtiene el historial completo de trazabilidad de un lote
|
||||
*
|
||||
* Retorna todos los lotes ancestros hasta llegar a los ingresos iniciales,
|
||||
* organizado por profundidad (0 = lote actual, n = ancestros más antiguos)
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const id = getRouterParam(event, 'id')
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'ID de lote requerido',
|
||||
})
|
||||
}
|
||||
|
||||
// Obtener trazabilidad completa
|
||||
const trazabilidad = await getTrazabilidad(id)
|
||||
|
||||
if (trazabilidad.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Lote no encontrado',
|
||||
})
|
||||
}
|
||||
|
||||
// Obtener estadísticas
|
||||
const estadisticas = await getEstadisticasLote(id)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
historial: trazabilidad,
|
||||
estadisticas,
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error obteniendo trazabilidad:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Error obteniendo trazabilidad',
|
||||
data: { message: error.message },
|
||||
})
|
||||
}
|
||||
})
|
||||
37
nuxt4/server/api/lotes/index.get.ts
Normal file
37
nuxt4/server/api/lotes/index.get.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { getLotes } from '~/server/utils/queries'
|
||||
|
||||
/**
|
||||
* GET /api/lotes
|
||||
* Lista todos los lotes con filtros opcionales
|
||||
*
|
||||
* Query params:
|
||||
* - tipo: filtrar por tipo de lote (uva, despulpado_primera, oreado, etc.)
|
||||
* - limit: límite de resultados
|
||||
* - offset: offset para paginación
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const query = getQuery(event)
|
||||
|
||||
const filtros = {
|
||||
tipo: query.tipo as string | undefined,
|
||||
limit: query.limit ? parseInt(query.limit as string) : undefined,
|
||||
offset: query.offset ? parseInt(query.offset as string) : undefined,
|
||||
}
|
||||
|
||||
const lotes = await getLotes(filtros)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: lotes,
|
||||
count: lotes.length,
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error obteniendo lotes:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Error obteniendo lotes',
|
||||
data: { message: error.message },
|
||||
})
|
||||
}
|
||||
})
|
||||
53
nuxt4/server/api/lotes/index.post.ts
Normal file
53
nuxt4/server/api/lotes/index.post.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { createLote } from '~/server/utils/queries'
|
||||
|
||||
/**
|
||||
* POST /api/lotes
|
||||
* Crea un nuevo lote
|
||||
*
|
||||
* Body:
|
||||
* {
|
||||
* codigo?: string,
|
||||
* tipo: string,
|
||||
* cantidad_kg?: number,
|
||||
* lugar_id?: number,
|
||||
* meta?: object
|
||||
* }
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validaciones básicas
|
||||
if (!body.tipo) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'El campo "tipo" es requerido',
|
||||
})
|
||||
}
|
||||
|
||||
const lote = await createLote({
|
||||
codigo: body.codigo,
|
||||
tipo: body.tipo,
|
||||
cantidad_kg: body.cantidad_kg,
|
||||
lugar_id: body.lugar_id,
|
||||
meta: body.meta,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: lote,
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error creando lote:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Error creando lote',
|
||||
data: { message: error.message },
|
||||
})
|
||||
}
|
||||
})
|
||||
44
nuxt4/server/api/operaciones/[id].get.ts
Normal file
44
nuxt4/server/api/operaciones/[id].get.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { getOperacionConLotes } from '~/server/utils/queries'
|
||||
|
||||
/**
|
||||
* GET /api/operaciones/:id
|
||||
* Obtiene una operación específica con sus lotes relacionados (inputs y outputs)
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const id = getRouterParam(event, 'id')
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'ID de operación requerido',
|
||||
})
|
||||
}
|
||||
|
||||
const operacion = await getOperacionConLotes(id)
|
||||
|
||||
if (!operacion) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Operación no encontrada',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: operacion,
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error obteniendo operación:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Error obteniendo operación',
|
||||
data: { message: error.message },
|
||||
})
|
||||
}
|
||||
})
|
||||
37
nuxt4/server/api/operaciones/index.get.ts
Normal file
37
nuxt4/server/api/operaciones/index.get.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { getOperaciones } from '~/server/utils/queries'
|
||||
|
||||
/**
|
||||
* GET /api/operaciones
|
||||
* Lista todas las operaciones con filtros opcionales
|
||||
*
|
||||
* Query params:
|
||||
* - tipo: filtrar por tipo de operación (ingreso, despulpado, oreado, etc.)
|
||||
* - limit: límite de resultados
|
||||
* - offset: offset para paginación
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const query = getQuery(event)
|
||||
|
||||
const filtros = {
|
||||
tipo: query.tipo as string | undefined,
|
||||
limit: query.limit ? parseInt(query.limit as string) : undefined,
|
||||
offset: query.offset ? parseInt(query.offset as string) : undefined,
|
||||
}
|
||||
|
||||
const operaciones = await getOperaciones(filtros)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: operaciones,
|
||||
count: operaciones.length,
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error obteniendo operaciones:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Error obteniendo operaciones',
|
||||
data: { message: error.message },
|
||||
})
|
||||
}
|
||||
})
|
||||
96
nuxt4/server/api/operaciones/index.post.ts
Normal file
96
nuxt4/server/api/operaciones/index.post.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { createOperacion } from '~/server/utils/queries'
|
||||
|
||||
/**
|
||||
* POST /api/operaciones
|
||||
* Crea una nueva operación con sus lotes de entrada y salida
|
||||
*
|
||||
* Body:
|
||||
* {
|
||||
* tipo: string, // ingreso, despulpado, oreado, etc.
|
||||
* fecha?: string, // ISO date (opcional, default: ahora)
|
||||
* lugar_id?: number,
|
||||
* meta?: object,
|
||||
* inputs: [ // Lotes de entrada
|
||||
* { lote_id: string, cantidad_kg?: number }
|
||||
* ],
|
||||
* outputs: [ // Lotes de salida (se crearán)
|
||||
* { codigo?: string, tipo: string, cantidad_kg?: number, meta?: object }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validaciones básicas
|
||||
if (!body.tipo) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'El campo "tipo" es requerido',
|
||||
})
|
||||
}
|
||||
|
||||
if (!body.inputs || !Array.isArray(body.inputs)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'El campo "inputs" es requerido y debe ser un array',
|
||||
})
|
||||
}
|
||||
|
||||
if (!body.outputs || !Array.isArray(body.outputs)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'El campo "outputs" es requerido y debe ser un array',
|
||||
})
|
||||
}
|
||||
|
||||
// Validar que cada input tenga lote_id
|
||||
for (const input of body.inputs) {
|
||||
if (!input.lote_id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Cada input debe tener un "lote_id"',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validar que cada output tenga tipo
|
||||
for (const output of body.outputs) {
|
||||
if (!output.tipo) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Cada output debe tener un "tipo"',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const result = await createOperacion({
|
||||
tipo: body.tipo,
|
||||
fecha: body.fecha ? new Date(body.fecha) : undefined,
|
||||
lugar_id: body.lugar_id,
|
||||
meta: body.meta,
|
||||
inputs: body.inputs,
|
||||
outputs: body.outputs,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
operacion: result.operacion,
|
||||
lotes_creados: result.lotes_creados,
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error creando operación:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Error creando operación',
|
||||
data: { message: error.message },
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user