diff --git a/backend/package.json b/backend/package.json index 2ab5d4c..aef3b2e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,6 +5,7 @@ "main": "index.js", "scripts": { "start": "node index.js", + "seed:supplies-demo": "node scripts/seedSuppliesDemo.js", "test": "node --test" }, "keywords": [], diff --git a/backend/scripts/seedSuppliesDemo.js b/backend/scripts/seedSuppliesDemo.js new file mode 100644 index 0000000..3a3f6a4 --- /dev/null +++ b/backend/scripts/seedSuppliesDemo.js @@ -0,0 +1,543 @@ +const { pool, initDB } = require('../db'); + +// This seed is deliberately isolated from Tiny/Olist data. It only creates or +// refreshes records with LOCAL-DEMO / DEMONSTRAÇÃO identifiers, so it is safe +// to run again in a local environment when the operational screens need data. +const DEMO = 'DEMONSTRAÇÃO'; + +const normalizeMaterialKey = (value) => String(value || '') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .trim() + .replace(/\s+/g, ' '); + +const today = new Date().toISOString().slice(0, 10); +const offsetDate = (days) => { + const date = new Date(); + date.setDate(date.getDate() + days); + return date.toISOString().slice(0, 10); +}; + +const getCategoryId = async (client, name) => { + const result = await client.query('SELECT id FROM catalog_categories WHERE name = $1', [name]); + return result.rows[0]?.id || null; +}; + +const upsertCatalogProduct = async (client, product) => { + const categoryId = await getCategoryId(client, product.category); + const result = await client.query(` + INSERT INTO catalog_products (type, sku, name, category_id, color, sizes, notes, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, CURRENT_TIMESTAMP) + ON CONFLICT (sku) DO UPDATE + SET type = EXCLUDED.type, + name = EXCLUDED.name, + category_id = EXCLUDED.category_id, + color = EXCLUDED.color, + sizes = EXCLUDED.sizes, + notes = EXCLUDED.notes, + updated_at = CURRENT_TIMESTAMP + RETURNING id; + `, [ + product.type, + product.sku, + product.name, + categoryId, + product.color || null, + product.sizes || [], + `${DEMO} — registro local para visualizar o fluxo de Suprimentos.` + ]); + return Number(result.rows[0].id); +}; + +const findCatalogProduct = async (client, sku) => { + const result = await client.query('SELECT id FROM catalog_products WHERE sku = $1', [sku]); + return result.rows[0] ? Number(result.rows[0].id) : null; +}; + +const upsertConsumptionReference = async (client, reference) => { + const existing = await client.query(` + SELECT id + FROM consumption_references + WHERE product_id = $1 + AND COALESCE(material_product_id, 0) = COALESCE($2, 0) + AND COALESCE(color, '') = COALESCE($3, '') + AND COALESCE(source, 'manual') = 'demonstration' + LIMIT 1; + `, [reference.productId, reference.materialProductId, reference.color || null]); + + const values = [ + reference.productId, + reference.materialProductId, + reference.color || null, + reference.generalYield || null, + reference.consumptionQuantity || null, + reference.consumptionUnit || null + ]; + if (existing.rows[0]) { + await client.query(` + UPDATE consumption_references + SET general_yield = $4, + consumption_quantity = $5, + consumption_unit = $6, + updated_at = CURRENT_TIMESTAMP + WHERE id = $7; + `, [...values, existing.rows[0].id]); + return; + } + await client.query(` + INSERT INTO consumption_references ( + product_id, material_product_id, color, general_yield, + consumption_quantity, consumption_unit, source + ) VALUES ($1, $2, $3, $4, $5, $6, 'demonstration'); + `, values); +}; + +const upsertPurchaseOrder = async (client, order) => { + const result = await client.query(` + INSERT INTO supply_purchase_orders (number, supplier, status, expected_date, notes, updated_at) + VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP) + ON CONFLICT (number) DO UPDATE + SET supplier = EXCLUDED.supplier, + status = EXCLUDED.status, + expected_date = EXCLUDED.expected_date, + notes = EXCLUDED.notes, + updated_at = CURRENT_TIMESTAMP + RETURNING id; + `, [order.number, order.supplier, order.status, order.expectedDate, order.notes]); + const id = Number(result.rows[0].id); + for (const item of order.items) { + await client.query(` + INSERT INTO supply_purchase_order_items (purchase_order_id, material, quantity, unit, unit_cost) + SELECT $1, $2, $3, $4, $5 + WHERE NOT EXISTS ( + SELECT 1 FROM supply_purchase_order_items + WHERE purchase_order_id = $1 AND material = $2 + ); + `, [id, item.material, item.quantity, item.unit, item.unitCost]); + } + return id; +}; + +const upsertReceipt = async (client, receipt) => { + const existing = await client.query('SELECT id FROM supply_receipts WHERE invoice = $1 LIMIT 1', [receipt.invoice]); + if (existing.rows[0]) { + await client.query(` + UPDATE supply_receipts + SET category = $1, product = $2, quantity = $3, unit = $4, + supplier = $5, notes = $6, status = $7, purchase_order_id = $8, + approved_at = CASE WHEN $7 = 'approved' THEN COALESCE(approved_at, CURRENT_TIMESTAMP) ELSE NULL END, + updated_at = CURRENT_TIMESTAMP + WHERE id = $9; + `, [receipt.category, receipt.product, receipt.quantity, receipt.unit, receipt.supplier, receipt.notes, receipt.status, receipt.purchaseOrderId, existing.rows[0].id]); + return Number(existing.rows[0].id); + } + const result = await client.query(` + INSERT INTO supply_receipts ( + category, product, quantity, unit, supplier, invoice, notes, status, + purchase_order_id, approved_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, + CASE WHEN $8 = 'approved' THEN CURRENT_TIMESTAMP ELSE NULL END) + RETURNING id; + `, [receipt.category, receipt.product, receipt.quantity, receipt.unit, receipt.supplier, receipt.invoice, receipt.notes, receipt.status, receipt.purchaseOrderId]); + return Number(result.rows[0].id); +}; + +const ensureLotAndMovement = async (client, receiptId) => { + const receipt = await client.query(` + SELECT id, category, product, quantity, unit, supplier, invoice + FROM supply_receipts + WHERE id = $1 AND status = 'approved'; + `, [receiptId]); + if (!receipt.rows[0]) return null; + const row = receipt.rows[0]; + const lot = await client.query(` + INSERT INTO supply_stock_lots (receipt_id, category, product, quantity, unit, supplier, invoice, status) + SELECT $1, $2, $3, $4, $5, $6, $7, 'active' + WHERE NOT EXISTS (SELECT 1 FROM supply_stock_lots WHERE receipt_id = $1) + RETURNING id; + `, [row.id, row.category, row.product, row.quantity, row.unit, row.supplier, row.invoice]); + const lotId = lot.rows[0]?.id || (await client.query('SELECT id FROM supply_stock_lots WHERE receipt_id = $1', [row.id])).rows[0]?.id; + if (!lotId) return null; + await client.query(` + INSERT INTO supply_movements (receipt_id, lot_id, type, category, product, quantity, unit, reason) + SELECT $1, $2, 'receipt', $3, $4, $5, $6, $7 + WHERE NOT EXISTS ( + SELECT 1 FROM supply_movements + WHERE receipt_id = $1 AND type = 'receipt' + ); + `, [row.id, lotId, row.category, row.product, row.quantity, row.unit, `${DEMO} — entrada aprovada para visualização.`]); + return Number(lotId); +}; + +const upsertProductionPlan = async (client, plan, responsibleUserId) => { + const result = await client.query(` + INSERT INTO production_plans ( + number, name, status, contractor, responsible_user_id, responsible_name, + planned_start_date, expected_date, notes, source + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'cut_plan') + ON CONFLICT (number) DO UPDATE + SET name = EXCLUDED.name, + status = EXCLUDED.status, + contractor = EXCLUDED.contractor, + responsible_user_id = EXCLUDED.responsible_user_id, + responsible_name = EXCLUDED.responsible_name, + planned_start_date = EXCLUDED.planned_start_date, + expected_date = EXCLUDED.expected_date, + notes = EXCLUDED.notes, + updated_at = CURRENT_TIMESTAMP + RETURNING id; + `, [ + plan.number, plan.name, plan.status, plan.contractor, responsibleUserId, + plan.responsibleName, plan.plannedStartDate, plan.expectedDate, plan.notes + ]); + return Number(result.rows[0].id); +}; + +const upsertProductionOrder = async (client, order) => { + const existing = await client.query('SELECT id FROM production_orders WHERE number = $1 LIMIT 1', [order.number]); + const values = [ + order.status, order.planId, order.contractor, order.orderReference, order.issueDate, + order.expectedDate, order.productSku, order.productDescription, order.quantity, order.unit, + order.notes, order.supplier, order.lotCode, order.rollQuantity, order.fabricKg, + order.ribKg, order.yieldPiecesPerKg, order.actualCutQuantity, order.rejectedQuantity, + order.finishedQuantity, order.qualityNotes, order.cutLayers, order.markerReference + ]; + if (existing.rows[0]) { + await client.query(` + UPDATE production_orders + SET status = $1, production_plan_id = $2, contractor = $3, order_reference = $4, + issue_date = $5, expected_date = $6, product_sku = $7, product_description = $8, + quantity = $9, unit = $10, notes = $11, supplier = $12, lot_code = $13, + roll_quantity = $14, fabric_kg = $15, rib_kg = $16, yield_pieces_per_kg = $17, + actual_cut_quantity = $18, rejected_quantity = $19, finished_quantity = $20, + quality_notes = $21, cut_layers = $22, marker_reference = $23, + updated_at = CURRENT_TIMESTAMP + WHERE id = $24; + `, [...values, existing.rows[0].id]); + return Number(existing.rows[0].id); + } + const result = await client.query(` + INSERT INTO production_orders ( + number, status, production_plan_id, contractor, order_reference, issue_date, + expected_date, product_sku, product_description, quantity, unit, notes, supplier, + lot_code, roll_quantity, fabric_kg, rib_kg, yield_pieces_per_kg, + actual_cut_quantity, rejected_quantity, finished_quantity, quality_notes, + cut_layers, marker_reference + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, + $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24) + RETURNING id; + `, [order.number, ...values]); + return Number(result.rows[0].id); +}; + +const ensureOrderDetails = async (client, orderId, details) => { + for (const marker of details.markers) { + await client.query(` + INSERT INTO production_order_markers (production_order_id, label, color) + SELECT $1, $2, $3 + WHERE NOT EXISTS ( + SELECT 1 FROM production_order_markers WHERE production_order_id = $1 AND label = $2 + ); + `, [orderId, marker.label, marker.color]); + } + for (const component of details.components) { + await client.query(` + INSERT INTO production_order_components (production_order_id, component_sku, component_name, quantity_per_unit, total_quantity, unit) + SELECT $1, $2, $3, $4, $5, $6 + WHERE NOT EXISTS ( + SELECT 1 FROM production_order_components + WHERE production_order_id = $1 AND component_name = $3 + ); + `, [orderId, component.sku, component.name, component.quantityPerUnit, component.totalQuantity, component.unit]); + } + for (const step of details.steps) { + await client.query(` + INSERT INTO production_order_steps (production_order_id, step_number, name, start_date, end_date, status, color) + SELECT $1, $2, $3, $4, $5, $6, $7 + WHERE NOT EXISTS ( + SELECT 1 FROM production_order_steps + WHERE production_order_id = $1 AND step_number = $2 + ); + `, [orderId, step.number, step.name, step.startDate, step.endDate, step.status, step.color]); + } +}; + +async function main() { + await initDB(); + const client = await pool.connect(); + try { + await client.query('BEGIN'); + + const materials = { + fabric: await upsertCatalogProduct(client, { + type: 'raw_material', sku: 'LOCAL-DEMO-MAT-MALHA-PRETA', category: 'Malha', + name: 'DEMO-MALHA-300 MALHA 30/1 COR PRETO', color: 'Preto' + }), + rib: await upsertCatalogProduct(client, { + type: 'raw_material', sku: 'LOCAL-DEMO-MAT-RIBANA-PRETA', category: 'Aviamentos', + name: 'DEMO-0201 RIBANA 2X1 COR PRETO', color: 'Preto' + }), + hoodieFabric: await upsertCatalogProduct(client, { + type: 'raw_material', sku: 'LOCAL-DEMO-MAT-MOLETOM-GRAFITE', category: 'Malha', + name: 'DEMO-MOLETOM 3 CABOS COR GRAFITE', color: 'Grafite' + }), + labelM: await upsertCatalogProduct(client, { + type: 'raw_material', sku: 'LOCAL-DEMO-MAT-ETIQUETA-M', category: 'Embalagens', + name: 'DEMO-ETIQUETA TAMANHO M', sizes: ['M'] + }), + labelG: await upsertCatalogProduct(client, { + type: 'raw_material', sku: 'LOCAL-DEMO-MAT-ETIQUETA-G', category: 'Embalagens', + name: 'DEMO-ETIQUETA TAMANHO G', sizes: ['G'] + }) + }; + + const finishedProducts = { + teeM: await findCatalogProduct(client, 'LOCAL-DEMO-UX-TEE-M'), + teeG: await findCatalogProduct(client, 'LOCAL-DEMO-UX-TEE-G'), + hoodieM: await findCatalogProduct(client, 'LOCAL-DEMO-UX-HOODIE-M') + }; + if (!Object.values(finishedProducts).every(Boolean)) { + throw new Error('Os produtos locais de demonstração esperados não foram encontrados.'); + } + + await Promise.all([ + upsertConsumptionReference(client, { productId: finishedProducts.teeM, materialProductId: materials.fabric, color: 'Preto', consumptionQuantity: 0.185, consumptionUnit: 'kg' }), + upsertConsumptionReference(client, { productId: finishedProducts.teeM, materialProductId: materials.rib, color: 'Preto', consumptionQuantity: 0.01, consumptionUnit: 'kg' }), + upsertConsumptionReference(client, { productId: finishedProducts.teeM, materialProductId: materials.labelM, consumptionQuantity: 1, consumptionUnit: 'un.' }), + upsertConsumptionReference(client, { productId: finishedProducts.teeG, materialProductId: materials.fabric, color: 'Preto', consumptionQuantity: 0.2, consumptionUnit: 'kg' }), + upsertConsumptionReference(client, { productId: finishedProducts.teeG, materialProductId: materials.rib, color: 'Preto', consumptionQuantity: 0.011, consumptionUnit: 'kg' }), + upsertConsumptionReference(client, { productId: finishedProducts.teeG, materialProductId: materials.labelG, consumptionQuantity: 1, consumptionUnit: 'un.' }), + upsertConsumptionReference(client, { productId: finishedProducts.hoodieM, materialProductId: materials.hoodieFabric, color: 'Grafite', consumptionQuantity: 0.75, consumptionUnit: 'kg' }), + upsertConsumptionReference(client, { productId: finishedProducts.hoodieM, materialProductId: materials.rib, color: 'Grafite', consumptionQuantity: 0.045, consumptionUnit: 'kg' }), + upsertConsumptionReference(client, { productId: finishedProducts.hoodieM, materialProductId: materials.labelM, consumptionQuantity: 1, consumptionUnit: 'un.' }) + ]); + + const supplier = `${DEMO} Têxteis`; + const materialDetails = [ + ['DEMO-MALHA-300 MALHA 30/1 COR PRETO', supplier, 'Malha reservada para a semana de demonstração.'], + ['DEMO-0201 RIBANA 2X1 COR PRETO', supplier, 'Aviamento de gola e acabamento.'], + ['DEMO-MOLETOM 3 CABOS COR GRAFITE', `${DEMO} Malharia`, 'Material para o plano de moletom.'] + ]; + for (const [material, preferredSupplier, notes] of materialDetails) { + await client.query(` + INSERT INTO supply_material_details (material_key, display_name, preferred_supplier, notes, updated_at) + VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) + ON CONFLICT (material_key) DO UPDATE + SET display_name = EXCLUDED.display_name, + preferred_supplier = EXCLUDED.preferred_supplier, + notes = EXCLUDED.notes, + updated_at = CURRENT_TIMESTAMP; + `, [normalizeMaterialKey(material), material, preferredSupplier, notes]); + } + + const fabricPlans = [ + ['DEMO-MALHA-300 MALHA 30/1 COR PRETO', 'Preto', 120, supplier, 'Crítico'], + ['DEMO-0201 RIBANA 2X1 COR PRETO', 'Preto', 24, supplier, 'Atenção'], + ['DEMO-MOLETOM 3 CABOS COR GRAFITE', 'Grafite', 90, `${DEMO} Malharia`, 'Normal'] + ]; + for (const [material, color, quantity, planSupplier, priority] of fabricPlans) { + await client.query(` + INSERT INTO supply_fabric_plans (material, color, quantity_kg, supplier, priority, status, updated_at) + SELECT $1, $2, $3, $4, $5, 'active', CURRENT_TIMESTAMP + WHERE NOT EXISTS ( + SELECT 1 FROM supply_fabric_plans + WHERE material = $1 AND COALESCE(color, '') = COALESCE($2, '') AND status = 'active' + ); + `, [material, color, quantity, planSupplier, priority]); + } + + const poApproved = await upsertPurchaseOrder(client, { + number: 'OC-DEMO-0001', supplier, status: 'approved', expectedDate: offsetDate(-2), + notes: `${DEMO} — compra aprovada; o recebimento está disponível para conferência.`, + items: [{ material: 'DEMO-MALHA-300 MALHA 30/1 COR PRETO', quantity: 120, unit: 'kg', unitCost: 29.9 }] + }); + const poSent = await upsertPurchaseOrder(client, { + number: 'OC-DEMO-0002', supplier, status: 'sent', expectedDate: offsetDate(4), + notes: `${DEMO} — pedido enviado ao fornecedor e aguardando chegada.`, + items: [{ material: 'DEMO-0201 RIBANA 2X1 COR PRETO', quantity: 24, unit: 'kg', unitCost: 34.5 }] + }); + const poFinancial = await upsertPurchaseOrder(client, { + number: 'OC-DEMO-0003', supplier: `${DEMO} Etiquetas`, status: 'pending_financial', expectedDate: offsetDate(6), + notes: `${DEMO} — aguardando aprovação financeira; não libera saldo físico.`, + items: [{ material: 'DEMO-ETIQUETA TAMANHO M', quantity: 500, unit: 'un.', unitCost: 0.28 }] + }); + + const receiptIds = []; + receiptIds.push(await upsertReceipt(client, { + category: 'Malha', product: 'DEMO-MALHA-300 MALHA 30/1 COR PRETO', quantity: 120, unit: 'kg', + supplier, invoice: 'DEMO-NF-004', status: 'approved', purchaseOrderId: poApproved, + notes: `${DEMO} — recebimento conferido e saldo liberado.` + })); + receiptIds.push(await upsertReceipt(client, { + category: 'Aviamentos', product: 'DEMO-0201 RIBANA 2X1 COR PRETO', quantity: 24, unit: 'kg', + supplier, invoice: 'DEMO-NF-005', status: 'pending', purchaseOrderId: poSent, + notes: `${DEMO} — aguardando conferência física e aprovação da compra.` + })); + receiptIds.push(await upsertReceipt(client, { + category: 'Embalagens', product: 'DEMO-ETIQUETA TAMANHO M', quantity: 500, unit: 'un.', + supplier: `${DEMO} Etiquetas`, invoice: 'DEMO-NF-006', status: 'pending', purchaseOrderId: poFinancial, + notes: `${DEMO} — saldo bloqueado até a aprovação financeira da OC.` + })); + const lotIds = []; + for (const receiptId of receiptIds) { + const lotId = await ensureLotAndMovement(client, receiptId); + if (lotId) lotIds.push(lotId); + } + + const transformations = [ + ['DEMO-LT-2408-01', 'tingimento', 'DEMO-MALHA-300 CRUA', 260, 'DEMO-MALHA-300 MALHA 30/1', 'Preto', 2, 254.8, `${DEMO} Tinturaria`, 'Atenção', 'in_progress', `${DEMO} — lote em tingimento.`], + ['DEMO-LT-2408-02', 'acabamento', 'DEMO-MOLETOM 3 CABOS', 96, 'DEMO-MOLETOM 3 CABOS', 'Grafite', 1.5, 94.56, `${DEMO} Malharia`, 'Normal', 'planned', `${DEMO} — acabamento programado.`], + ['DEMO-LT-2408-00', 'malharia', 'DEMO-FIO 30/1 PRETO', 180, 'DEMO-MALHA-300 MALHA 30/1', 'Preto', 3, 174.6, `${DEMO} Malharia`, 'Normal', 'completed', `${DEMO} — lote concluído para rastreabilidade.`] + ]; + for (const transformation of transformations) { + await client.query(` + INSERT INTO supply_material_transformations ( + batch_code, stage, input_material, input_quantity_kg, output_material, + output_color, expected_loss_percent, expected_output_kg, supplier, + priority, status, notes, updated_at + ) SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, CURRENT_TIMESTAMP + WHERE NOT EXISTS ( + SELECT 1 FROM supply_material_transformations + WHERE batch_code = $1 AND stage = $2 + ); + `, transformation); + } + + const user = await client.query(` + SELECT id, name FROM app_users WHERE is_active = TRUE ORDER BY id LIMIT 1; + `); + const responsibleUserId = user.rows[0]?.id || null; + const responsibleName = user.rows[0]?.name || 'Operação Graphs'; + const planCut = await upsertProductionPlan(client, { + number: 'PP-DEMO-0001', name: `${DEMO} | Corte preto — semana operacional`, status: 'cutting', + contractor: `${DEMO} Confecção`, responsibleName, plannedStartDate: offsetDate(-1), expectedDate: offsetDate(3), + notes: `${DEMO} — plano liberado com lote e risco definidos.` + }, responsibleUserId); + const planHoodie = await upsertProductionPlan(client, { + number: 'PP-DEMO-0002', name: `${DEMO} | Moletom grafite — reposição`, status: 'open', + contractor: `${DEMO} Confecção`, responsibleName, plannedStartDate: offsetDate(3), expectedDate: offsetDate(8), + notes: `${DEMO} — aguardando liberação da etapa de malha.` + }, responsibleUserId); + await client.query(` + INSERT INTO production_plan_reviews (production_plan_id, decision, checklist, notes, reviewed_by, reviewed_at) + VALUES ($1, 'approved', $2::jsonb, $3, $4, CURRENT_TIMESTAMP) + ON CONFLICT (production_plan_id) DO UPDATE + SET decision = EXCLUDED.decision, + checklist = EXCLUDED.checklist, + notes = EXCLUDED.notes, + reviewed_by = EXCLUDED.reviewed_by, + reviewed_at = EXCLUDED.reviewed_at, + updated_at = CURRENT_TIMESTAMP; + `, [planCut, JSON.stringify({ material: true, yield: true, responsible: true, deadline: true }), `${DEMO} — checklist operacional concluído.`, responsibleName]); + + const orderM = await upsertProductionOrder(client, { + number: 'OP-DEMO-0001', status: 'cutting', planId: planCut, contractor: `${DEMO} Confecção`, + orderReference: 'DEMO-CORTE-35', issueDate: offsetDate(-1), expectedDate: offsetDate(2), + productSku: 'LOCAL-DEMO-UX-TEE-M', productDescription: 'DEMO UX CAMISETA PRETA TAMANHO M', + quantity: 180, unit: 'UN', notes: `${DEMO} — acompanhar aproveitamento de risco.`, supplier, + lotCode: 'DEMO-NF-004', rollQuantity: 3, fabricKg: 33.3, ribKg: 1.8, yieldPiecesPerKg: 5.4, + actualCutQuantity: 150, rejectedQuantity: 2, finishedQuantity: 0, + qualityNotes: `${DEMO} — dois recortes separados para revisão de tecido.`, cutLayers: 12, + markerReference: 'RISCO-DEMO-M-12' + }); + const orderG = await upsertProductionOrder(client, { + number: 'OP-DEMO-0002', status: 'sewing', planId: planCut, contractor: `${DEMO} Confecção`, + orderReference: 'DEMO-CORTE-35', issueDate: today, expectedDate: offsetDate(3), + productSku: 'LOCAL-DEMO-UX-TEE-G', productDescription: 'DEMO UX CAMISETA PRETA TAMANHO G', + quantity: 140, unit: 'UN', notes: `${DEMO} — lote encaminhado para costura.`, supplier, + lotCode: 'DEMO-NF-004', rollQuantity: 2, fabricKg: 28, ribKg: 1.54, yieldPiecesPerKg: 5, + actualCutQuantity: 140, rejectedQuantity: 1, finishedQuantity: 92, + qualityNotes: `${DEMO} — conferir gola nas primeiras peças.`, cutLayers: 9, + markerReference: 'RISCO-DEMO-G-09' + }); + const orderHoodie = await upsertProductionOrder(client, { + number: 'OP-DEMO-0003', status: 'review', planId: planHoodie, contractor: `${DEMO} Confecção`, + orderReference: 'DEMO-MOLETOM-35', issueDate: offsetDate(2), expectedDate: offsetDate(8), + productSku: 'LOCAL-DEMO-UX-HOODIE-M', productDescription: 'DEMO UX MOLETOM GRAFITE TAMANHO M', + quantity: 60, unit: 'UN', notes: `${DEMO} — amostra segue para revisão antes da liberação total.`, supplier: `${DEMO} Malharia`, + lotCode: 'DEMO-LT-2408-02', rollQuantity: 2, fabricKg: 45, ribKg: 2.7, yieldPiecesPerKg: 1.33, + actualCutQuantity: 60, rejectedQuantity: 2, finishedQuantity: 52, + qualityNotes: `${DEMO} — registrar avarias por posto de trabalho.`, cutLayers: 6, + markerReference: 'RISCO-DEMO-MOLETOM-06' + }); + + await ensureOrderDetails(client, orderM, { + markers: [{ label: 'Risco M — 12 camadas', color: '#fbbf24' }], + components: [ + { sku: 'LOCAL-DEMO-MAT-MALHA-PRETA', name: 'DEMO-MALHA-300 MALHA 30/1 COR PRETO', quantityPerUnit: 0.185, totalQuantity: 33.3, unit: 'kg' }, + { sku: 'LOCAL-DEMO-MAT-RIBANA-PRETA', name: 'DEMO-0201 RIBANA 2X1 COR PRETO', quantityPerUnit: 0.01, totalQuantity: 1.8, unit: 'kg' } + ], + steps: [ + { number: 1, name: 'Separação do lote', startDate: offsetDate(-1), endDate: offsetDate(-1), status: 'finished', color: '#22c55e' }, + { number: 2, name: 'Corte', startDate: today, endDate: offsetDate(1), status: 'in_progress', color: '#fbbf24' }, + { number: 3, name: 'Costura', startDate: offsetDate(1), endDate: offsetDate(2), status: 'pending', color: '#38bdf8' } + ] + }); + await ensureOrderDetails(client, orderG, { + markers: [{ label: 'Risco G — 9 camadas', color: '#38bdf8' }], + components: [{ sku: 'LOCAL-DEMO-MAT-MALHA-PRETA', name: 'DEMO-MALHA-300 MALHA 30/1 COR PRETO', quantityPerUnit: 0.2, totalQuantity: 28, unit: 'kg' }], + steps: [ + { number: 1, name: 'Corte', startDate: offsetDate(-1), endDate: today, status: 'finished', color: '#22c55e' }, + { number: 2, name: 'Costura', startDate: today, endDate: offsetDate(2), status: 'in_progress', color: '#38bdf8' }, + { number: 3, name: 'Revisão', startDate: offsetDate(2), endDate: offsetDate(3), status: 'pending', color: '#a78bfa' } + ] + }); + await ensureOrderDetails(client, orderHoodie, { + markers: [{ label: 'Risco moletom — 6 camadas', color: '#a78bfa' }], + components: [{ sku: 'LOCAL-DEMO-MAT-MOLETOM-GRAFITE', name: 'DEMO-MOLETOM 3 CABOS COR GRAFITE', quantityPerUnit: 0.75, totalQuantity: 45, unit: 'kg' }], + steps: [ + { number: 1, name: 'Corte', startDate: offsetDate(2), endDate: offsetDate(3), status: 'finished', color: '#22c55e' }, + { number: 2, name: 'Costura', startDate: offsetDate(3), endDate: offsetDate(6), status: 'finished', color: '#38bdf8' }, + { number: 3, name: 'Revisão', startDate: offsetDate(6), endDate: offsetDate(8), status: 'in_progress', color: '#a78bfa' } + ] + }); + + const lotForReservation = lotIds[0]; + if (lotForReservation) { + await client.query(` + INSERT INTO cut_plan_lot_reservations ( + cut_plan_reference, production_plan_id, supply_lot_id, material_key, + quantity, consumed_quantity, status + ) SELECT 'DEMO-CORTE-35', $1, $2, $3, 61.3, 0, 'active' + WHERE NOT EXISTS ( + SELECT 1 FROM cut_plan_lot_reservations + WHERE cut_plan_reference = 'DEMO-CORTE-35' AND supply_lot_id = $2 + ); + `, [planCut, lotForReservation, normalizeMaterialKey('DEMO-MALHA-300 MALHA 30/1 COR PRETO')]); + } + + const workstations = await client.query('SELECT id, code FROM production_workstations'); + const workstationByCode = new Map(workstations.rows.map(row => [row.code, row.id])); + const defects = [ + [orderM, workstationByCode.get('cutting'), 'Furo na malha', 'Tecido', 2, 'DEMO-NF-004', `${DEMO} — conferir rolo antes de seguir o corte.`], + [orderG, workstationByCode.get('sewing'), 'Costura desalinhada', 'Costura', 1, 'DEMO-NF-004', `${DEMO} — ajuste solicitado ao posto de costura.`], + [orderHoodie, workstationByCode.get('review'), 'Mancha de acabamento', 'Acabamento', 2, 'DEMO-LT-2408-02', `${DEMO} — lote separado para avaliação de qualidade.`] + ]; + for (const defect of defects) { + await client.query(` + INSERT INTO production_order_defects ( + production_order_id, workstation_id, defect_type, origin, quantity, lot_code, notes + ) SELECT $1, $2, $3, $4, $5, $6, $7 + WHERE NOT EXISTS ( + SELECT 1 FROM production_order_defects + WHERE production_order_id = $1 AND defect_type = $3 AND lot_code = $6 + ); + `, defect); + } + + await client.query('COMMIT'); + console.log('Demonstração de Suprimentos criada/atualizada com sucesso.'); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + await pool.end(); + } +} + +main().catch((error) => { + console.error('Não foi possível criar os dados de demonstração:', error.message); + process.exitCode = 1; +});