const { pool } = require('../db'); const STATUS_LABELS = { open: 'Em aberto', in_progress: 'Em andamento', finished: 'Finalizada', canceled: 'Cancelada' }; const normalizeStatus = (status) => { const normalizedStatus = String(status || 'open').trim().toLowerCase(); if (['open', 'em_aberto', 'em aberto', 'aberta'].includes(normalizedStatus)) return 'open'; if (['in_progress', 'andamento', 'em andamento'].includes(normalizedStatus)) return 'in_progress'; if (['finished', 'finalizada', 'finalizado'].includes(normalizedStatus)) return 'finished'; if (['canceled', 'cancelada', 'cancelado', 'cancelled'].includes(normalizedStatus)) return 'canceled'; return normalizedStatus || 'open'; }; const normalizeDateParam = (value) => { if (!value) return null; const date = new Date(`${value}T00:00:00`); if (Number.isNaN(date.getTime())) return null; return value; }; const formatDate = (value) => { if (!value) return null; if (value instanceof Date && !Number.isNaN(value.getTime())) { return value.toISOString().slice(0, 10); } return String(value).slice(0, 10); }; const normalizeText = (value) => String(value || '').trim(); const normalizeQuantity = (value) => { const quantity = Number(value); if (!Number.isFinite(quantity) || quantity <= 0) return 0; return quantity; }; const mapProductionOrderRow = (row) => { const status = normalizeStatus(row.status); return { id: row.id, tinyId: row.tiny_id || '', number: row.number || '', status, statusLabel: STATUS_LABELS[status] || row.status || 'Em aberto', orderReference: row.order_reference || '', issueDate: formatDate(row.issue_date), expectedDate: formatDate(row.expected_date), productSku: row.product_sku || '', productDescription: row.product_description || '', quantity: Number(row.quantity || 0), unit: row.unit || 'UN', integrationStatus: row.integration_status || '', markers: Array.isArray(row.markers) ? row.markers.filter(Boolean) : [], createdAt: row.created_at || null, updatedAt: row.updated_at || null }; }; const getOrderById = async (id, client = pool) => { const result = await client.query(` SELECT po.id, po.tiny_id, po.number, po.status, po.order_reference, po.issue_date, po.expected_date, po.product_sku, po.product_description, po.quantity, po.unit, po.integration_status, po.created_at, po.updated_at, COALESCE( JSON_AGG( JSON_BUILD_OBJECT( 'label', pom.label, 'color', pom.color ) ORDER BY pom.label ) FILTER (WHERE pom.id IS NOT NULL), '[]'::json ) as markers FROM production_orders po LEFT JOIN production_order_markers pom ON pom.production_order_id = po.id WHERE po.id = $1 GROUP BY po.id; `, [id]); return result.rows[0] ? mapProductionOrderRow(result.rows[0]) : null; }; const listProductionOrders = async (filters = {}) => { const params = []; const where = []; const normalizedStart = normalizeDateParam(filters.start); const normalizedEnd = normalizeDateParam(filters.end); const normalizedSearch = String(filters.search || '').trim(); if (normalizedStart) { params.push(normalizedStart); where.push(`COALESCE(po.issue_date, po.expected_date, po.created_at::date) >= $${params.length}::date`); } if (normalizedEnd) { params.push(normalizedEnd); where.push(`COALESCE(po.issue_date, po.expected_date, po.created_at::date) <= $${params.length}::date`); } if (normalizedSearch) { params.push(`%${normalizedSearch}%`); where.push(`( po.number ILIKE $${params.length} OR po.order_reference ILIKE $${params.length} OR po.product_sku ILIKE $${params.length} OR po.product_description ILIKE $${params.length} )`); } const result = await pool.query(` SELECT po.id, po.tiny_id, po.number, po.status, po.order_reference, po.issue_date, po.expected_date, po.product_sku, po.product_description, po.quantity, po.unit, po.integration_status, po.created_at, po.updated_at, COALESCE( JSON_AGG( JSON_BUILD_OBJECT( 'label', pom.label, 'color', pom.color ) ORDER BY pom.label ) FILTER (WHERE pom.id IS NOT NULL), '[]'::json ) as markers FROM production_orders po LEFT JOIN production_order_markers pom ON pom.production_order_id = po.id ${where.length ? `WHERE ${where.join(' AND ')}` : ''} GROUP BY po.id ORDER BY COALESCE(po.issue_date, po.created_at::date) DESC, CASE WHEN po.number ~ '^\\d+$' THEN po.number::bigint ELSE NULL END DESC NULLS LAST, po.id DESC; `, params); const orders = result.rows.map(mapProductionOrderRow); const counts = orders.reduce((nextCounts, order) => { nextCounts.all += 1; nextCounts[order.status] = (nextCounts[order.status] || 0) + 1; return nextCounts; }, { all: 0, open: 0, in_progress: 0, finished: 0, canceled: 0 }); return { orders, counts }; }; const createProductionOrders = async (orders = []) => { const client = await pool.connect(); try { await client.query('BEGIN'); const created = []; const skipped = []; for (const order of orders) { const productDescription = normalizeText(order.productDescription); const productSku = normalizeText(order.productSku); const orderReference = normalizeText(order.orderReference); const quantity = normalizeQuantity(order.quantity); if (!productDescription || !quantity) { skipped.push({ productSku, productDescription, reason: 'invalid_payload' }); continue; } const duplicateResult = await client.query(` SELECT id FROM production_orders WHERE COALESCE(order_reference, '') = $1 AND COALESCE(product_sku, '') = $2 AND status IN ('open', 'in_progress') LIMIT 1; `, [orderReference, productSku]); if (duplicateResult.rows.length) { skipped.push({ productSku, productDescription, reason: 'duplicate_open_order' }); continue; } const insertResult = await client.query(` INSERT INTO production_orders ( tiny_id, number, status, order_reference, issue_date, expected_date, product_sku, product_description, quantity, unit, integration_status, tiny_payload ) VALUES ( NULL, NULL, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb ) RETURNING id; `, [ normalizeStatus(order.status || 'open'), orderReference, normalizeDateParam(order.issueDate) || formatDate(new Date()), normalizeDateParam(order.expectedDate), productSku, productDescription, quantity, normalizeText(order.unit) || 'UN', normalizeText(order.integrationStatus) || 'Local', JSON.stringify(order.metadata || {}) ]); const orderId = insertResult.rows[0].id; await client.query(` UPDATE production_orders SET number = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2; `, [normalizeText(order.number) || `OP-${String(orderId).padStart(5, '0')}`, orderId]); const markers = Array.isArray(order.markers) ? order.markers : []; for (const marker of markers) { const label = normalizeText(marker.label); if (!label) continue; await client.query(` INSERT INTO production_order_markers (production_order_id, label, color) VALUES ($1, $2, $3) ON CONFLICT (production_order_id, label) DO UPDATE SET color = EXCLUDED.color; `, [orderId, label, normalizeText(marker.color) || null]); } const createdOrder = await getOrderById(orderId, client); if (createdOrder) created.push(createdOrder); } await client.query('COMMIT'); return { created, skipped }; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } }; const updateProductionOrderStatus = async (id, status) => { const normalizedStatus = normalizeStatus(status); if (!STATUS_LABELS[normalizedStatus]) { const error = new Error('Invalid production order status'); error.statusCode = 400; throw error; } const result = await pool.query(` UPDATE production_orders SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING id; `, [normalizedStatus, id]); if (!result.rows.length) { const error = new Error('Production order not found'); error.statusCode = 404; throw error; } return getOrderById(id); }; module.exports = { createProductionOrders, listProductionOrders, normalizeStatus, updateProductionOrderStatus };