Compare commits

...

4 Commits

Author SHA1 Message Date
Cauê Faleiros
ef29365932 Fix cut plan confirmation cleanup
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m25s
2026-08-27 19:30:04 -03:00
Cauê Faleiros
64e5fefcb4 Move marker reference to production execution 2026-08-27 19:25:30 -03:00
Cauê Faleiros
d623688263 Add supplies workflow demonstration seed 2026-08-27 19:01:32 -03:00
Cauê Faleiros
e62793273b Add supplies operations guide 2026-08-27 15:39:41 -03:00
5 changed files with 722 additions and 10 deletions

View File

@@ -5,6 +5,7 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "node index.js", "start": "node index.js",
"seed:supplies-demo": "node scripts/seedSuppliesDemo.js",
"test": "node --test" "test": "node --test"
}, },
"keywords": [], "keywords": [],

View File

@@ -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;
});

View File

@@ -0,0 +1,175 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 13 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/PageMode /UseNone /Pages 13 0 R /Type /Catalog
>>
endobj
12 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260827153832-03'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260827153832-03'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
13 0 obj
<<
/Count 6 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R ] /Type /Pages
>>
endobj
14 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1327
>>
stream
Gatm:9iL(3&A@7.oJ&E26B^F@eXkDr?tYB9?ma[=.(0a^fp7ce=ap@Y'ZY2+W1@@r&7Whb<qgsE]?PlcLP>bR%IhC>c7Ttg%Z;&U5m/aAiF4NgT!?+080Xgn1.Z[Y,\I_WS)$dI1/9eD.>KlW8BbW0%A%3c9$1ZGKQ/Hf5@I-Ln.b58f(aY;-%`E*6psV4,RdGg-*(JIBh!`)1uH;D6]Cpk6;@PWc8(^-c-a%PNHn"'TnYBJJkMQl_P.%%Gpd6k*4Q8L_oJogIWb\P*,]XI^7BcQEqAu%qDVM=!A$FNBq1;Ro$.10?i:TE%H%'6QF1Tkj.J,GAQAWk_A9lgiCjioiCj[UD@\Z7Rmi!AN"(;K0pNVa+p_*U7'\6&nGp?W+!5Jc,q\15s(lEP\S5;UFijd*ASbMJ6G!nE/(Ni2>g88qr.8*%fBHOEW)]a4WChB?%=>6bFP_?d`YQ.*50qfKU47teMVlL*#*k[h<Qual4nkr`#Ec-D1"3aeN(5>90dDp[nk_q?"3sC=7##t50pL#hP-(MiTnt_Ln:8UfGSH7M=[))Qr*bW&4$$APaZE7/)2Yhe8:Tk3.@bd:+N(cD($M!m.WYca.?jr4lZD5AEp_XL'ZWF[l(HRlJR`8^J:ZGL%/b`_'ajfk].@5h=sP?ee-CIuO"@">FPa"Nla!ifGkHmi9-fUm1]X)6<a.GEHHb5CI1&M9]Rr_K>,acX-8a!FY6atFl9"N4ltSW;raK0nP5%Hu"Q<Po8.l*@Of-.@!*3,"Ji/.Q>#i8(/p^5V8cW5+-TG(\k72:7[0jduB:%"snH4i9iR+>@\A\e5)])Vms3&=IIP&=Pe]l__p3ho5RVrg-j]Vdp3U4+0U&,u1NE[9)&*cesm81f4,6?D+iA=lRLn<Jmp6n2F`pXSh`*&U$+L."BkDVY`7L[":bt&3Am)3#pj;@;8p`:L)knCY;9W+0iS(CoYg$BoNN+MUl6'=t.l6fRJO^5TZ"$t%H'_Dl^Zh=iAB^p!kIM)qbO%9G&8#5.Hnp"PV$J-3n@q^%^25!cH0"9q;T\H($4!q<AlNa8[Rf\A3lP1nn[5dj/Z5gdrg0.gO7TFC$Zs?0!j"pO1[g(Tt/(`EaLTXQbb\O6dj88AH`8D,G;.882aHk_`f=giRN32ti;P>2)auu'JCGTZ\EtJ+!&/8;_Tmk$2KR,Ean%b-E[2lrF33:X%jT2hD/+_Q^'!G9dA&cRRO_F?tLi7s\T(,mX1mX?9&SA[$aY5Y#=c$gBKLq=5*f$.;^NUV8/2D0*cnZ1@KZ:@AnF3Pm'&hn"I,?5a_NXqnDZ,r.,Kmmr&qelPC'!0q~>endstream
endobj
15 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2210
>>
stream
Gb!;e?$G$]&q0MXQq)"Sbn/fgV!M%FPBKa2HgK>+>@&EZ;NR%E"-BZ]If8+(-'E:Zf?%nD-T\LX&d2f!B'u4@"N>VBn@,?PIU(9YT*,HlX"B/b8nmrK]j@7X3"u1OkqZEho2s@%:Em>?1-C?HObgt3(DI1<b[lNQ\P+`;gRVbJ0eAO9WkoC3`.F>;#qmu:ku)f\=Sc+.VN%IBY&U>eF_gq?B_@rt5Hka7iH<9S3WB.J`$3pi,&#(?g#$2]V:m>4@1fJ>f<@_A="2S"C@Kh6\p0]Tg"$ieU8q[ArAI4\9>4X-f!M!a'=Q:`iUT+dH:b]t<*A*>gnG=MVT.l;GXYq#G[-VqJc#<%mB"gH4L4Q!-4nI)_L((-<ESEl:%T-5i[>b>RF@cFo?1:D8/%X`d3?UqIU=jDo_ZsIFdSo?'Np[?,0-%5DB><TLgg;IJ;&qmSB`GBMem-YO<Vl7'P6MaQ5$0:2MAPoQ^'SQ7?jTnRaVGS3)/-L0I=L1h$]noLTQ`3"Qk=,BYVjbR;p92+nYU[6KgkK8>KMKp1ct$=<'$.Ll^uj+caHNR!;joanI.FT,3!P2.@2F:Pf&.mVTsoFg_iY4CC<[^sIru"ZC'-o8X3ikeHil%mKq,^ppXO*[(I3(\Zn@i).S^.*3:96u8\SmkWbKH(6Ik&C#[5Xa1j)is!?i)85NFe!*<dSFJ9UbG4/*"%7meCkI\0_$YiWg[X3Nd-N5mi`:Jt(t1],6b:)W71H0PcI@_FD$m&\5-kW,CK3jrT0B)+cFlsCjN`9(`j@t.$5DKY17fEG3-2.?.6"s.FYtGX=!a%1?!Vg*($RecFMhW"V`Sj[.3/B&iGrW_VB4hoi::Ul5ACQcQj&$Ce+Ue3$ZF7L.B?:JmJ67RPf`SI>]:o6gCXK,1"]S5K#KPfgS%e`JX,tqojC$s>BY3ud/g[s-G``T;cB\76T&#UdC"H<V"@XM7emi#F:FEoFA)kuO(.nbe*C5eW\=XGnY%auVHVg@?.\u.#/N+PQ4Oun?bPndI2j-jbX^daR:X5/:o^rk\=IU,(]8d_(nnj^5:7FIN@`F`@!3V_s)9JP\f;e$SI@P>fmS;f`$1\3r,eCCdVM6XcX/6a2\q>;&1\^-d@Nee[;RVumcf=c!^ZX4@#-pK9EcYgs*VY5IsCDM<f@h`LPi$J0:32`;stc;JUA?WY9P2lDPq7116U@/\i.p"WI7k$5jqnZ%X8:VR1KiqcM-3rTtP+(`#dsuiNZ6H#&88V(3Lm[8@8aSkWTE,i+sH9%>86jF]@C\#K>51+/irA9VD=!i=WbTPSsrYlrq$0F6Xa?.TLEVi?3sd.WG`SXJ*IW1$u#R2en#G=HZ8b#kFW+NE0J^GP:W^i<qsHCLpU.<Vrn@:rIh+YbTneTbu,bcX?nf,2@B!6^Ag\*j:0ON3u$^dn]q,\3\dA,6amK&$/'B8Kl0JU1dlC][ZtVU]a_cW^+)`1eF)DI6^\b&J%K+.RjV(8TDjM*qb96>uHi(/Laq"E=cGsC-Goqj;dRHpFIUZf='!p3%f&r$RXrgZ.:H/IJPa&N)F\(#MYmV@5dK:1mF=cs*bV0E$W4Bd:_(b<Mg3?-_=hV!\oH6n-B6mor'[\YV1*(.!'Tq-,+qdguqWlqgf#9],54qhO;fAD2`CLD:YT>K;B+76@%Gbb&EBdL6%N.UBgX7fE)'Fc?O5&-CF#!FpAW=[O6S^l5K7GPhU9Zp3cLEhoJQ]M2f6I$=&6<H7UdZ2U?iL,"*,%MU]oQA:PT$6B63p5@k/7mol"F7MIiu2%.uZ/hAb"q<Trs--HVR4b$m3T7aA$/@dqO]r+M^O>=tnB8HpT=0s*Sd&0!PcLpk;k1.b"N=&0N%^ffF/d'q7^@]GrTTH^c9jsH.X$'MeWk?k)f-YS^qZAL`#'\kjO*^AUC[clpMKcZ,"-n>Y@8NEcoS!.18BdndRSTg13ZQZDm]2']0b3'BWR89/TlFe;=a0Vcl$mlVH07CXWE6),Z_,#8d_pPSKUNc\h(#SckIaRg^8p#/dJ9eWd[t<+pUPAopm7/ECQ8bFd=SZug)k(_5qV"^X7'6H2f0C3>!BG`1@WbJZ=O">i1n'c(fVuT&+33ld.&,jM%CnOnUQC_j1nNC0p[2B))uB:ZY=HIF:$i9-@(efD`jZ46?j*D(DP.eVnF9Al,R-Fke?sji12Ea@>T7>\N3pZH8lG%8%V*#rrmnQo`P~>endstream
endobj
16 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1991
>>
stream
Gb!SmbAu>q']&X:mS_0Z$t#a)A<hK;S@3TM[4?):$<#e*#oXpdf3dEV61LXN[c=0+S;p()"FpKt\\L3AE'QTnJbtr!7,Q8ReIMZa+jJUQ`*Q./?g7I`dFD*uCDkI2=Z#XNRO""3:89P86a[a*_(5o6$\E%)Nq""4@WZ*<8*DbmL)JH&`t0Bt;][-AG_"1HB#;tgdnfY"<g#*#d\pCdQ+"%:V7NU);2[jW:[jD:il):o?deiu:[K0/rUct\(]*YIUe*Q#RpHqjP8SZ+GijeB`KP!U"['BuO+dmo;_O#l32%#`0Tt!?d3ZOG!0.cNOV6NFA4h]f)e\q>%m&u@2dJDi?4oY73(<Lf73d<;69=Z2C3Bl2f,6X;LQ.JQP`s.ATV"s*7+m:PbcG>`RJ_-$=*;RV/Z.F]`HW$8hP=Yo@tG!sr"oj]Omc!Xbj_G(h9aq#H\c959P5s)[h1?`?QCc.ZFhTR-K,*$p,U_!N$nULL4O%1?<?-o"3(GSjs,1P1@DMI\iA([_q7$e).kX!]\.k("Ci/;A3qs7VW1D+K^K6ZaJZ'WA;H_kXI?%V["s6+OM$ln*u0f7.TV/Hc<ld;6rhuOoHCG9<am@,\.o?>fK-Id?1U`kP"S3'kO?G=Rqe*<aO*V_V8@uYmXpK18,jXs;iRd,<gWunheklF8n*HQb"U8FX04DF;AtZA7Nqp;\P]fP1=?V]=qO#!C+ECJ,^0\l@A_2I7PbU^<P"+S8Cu@c-ACPgWO2f.%r`(%>(L#N)>MOcE>)E!WQj:_g^ChK<Xc7W6m43(1eh1cDQO3iQje?Q[PBoIi>9-[O$&`B#Wi-80hM.]``&]-599jE?\8fWpKP'TaQUK@qnjn5YLq`J3bWW>Z]E)$F-e09`T3]GN2P-Ug,sk5:VO-K^T0fc["4kg^PV<0piL).-\44g6eR+1Rgt_>rI2Db!umhY!boDY*id!DgY`AnLSrPn;ol<-Yk0u`dkk)K.Zb@8SO9GXO_2"$>P(i5m9D^8&@0+7UlF*LS=h$#jH&iU<dGnTK`79NcWYR:I]p7id3VR5n;P'+T[KQ>6qLqZM*+T#Z9DTm%4.AtCe_JbmYV@b70@18%fQ_&`B>''ZI*g)&bKW]F5bNm`E(\bD%Hps;f0`&)M^b]`8cRN1Pp?Yl&g*gEl0r!$%`mB2mk+pRW?U.K\97]aL6QCglu"4nk#)M$"Ip`p!WI`XL0Kf.T`!fj:5(%Xo:Y.E-lu2kH<alb[Jlj%Wgi;gh@7<dd,:h_2?57_0Z+C0_rlRn@RamZ*O[lXdPO<&cN=F1GANhrP@&rR42iWQ*j1J#V/[X.9H6b!1qO_7$cDlJDsYY.&?OSl<q&X1I!928lM^J\nO:EKp%WbK2e10dH69#UiSbWo]SV^IA[hY#ckW^i$d%7$@9$Z)8*n,J\A&ub:uEKg528ldu-G_M7L9)!d8?143XSP<faq,bm?3LiQSn>_l-`/L;ZP#JV*[L'jnaXXiOP3MZD:mnSdp0&NFL7MDlQ.#%DHF,gqRQjCn5!6N9JXIF]"j5B@m-Z'B,cc0B8'`H(DWJA=LB$d.D7K(SW^S[#[FdL'3pHe`p.joZB+eqdecV]Up_Em?2%mA3Q,$U/LNK/@sC:)*N;nf<t7E#K1]nbG8oo\!J:4k>:<";CT\RF&k':f$7@ms!q--_cEUc4ps`iY9p"Rc0.Cl[egO\&t$H0;ic&W@b/>l."-1b]/+[ZXi+Lh=\@,<phTMSr'kF)C:pmSt3G;<tCYr1m<;I`=6LJH!c1cG9s)$:kkFo2;t=S)fUQP^;#p7<ZFtT=H=EZI#HT=,1q&qNWK;sL<UULm7X%3Z+Sm=,J1Tdh6cou,-/X$EX]Cs>uuSN"\D.>0@n^bW]s[&K<q+T@U0TBm`b>s0>,$-PJ@tqq;MU&lUS!T:,cd4n`Z3P&CWUR4.QD!3"@s]UH\t;p#PghWlt2Bs*+8u#oM#N(i#!Q-4.n2Dl@a&5<#SR'^\>CNrB&UVh0B~>endstream
endobj
17 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2404
>>
stream
Gb!SmbAuXO'&EF8HE!]N%c8J:9:XVIQ.1%@R[lCA"Nd#T#n8",aaALHRL`9$*k&7i-:u=F6iocO%iXUf=onGD"oLP7M8#J-Ri__>6CKMTLqT1Q^X*M?X%ue_gG95Nd#TOt9iBjVZREY2B_I$38CF$@,gQ@M]QfUY;*\`=<T12:V(hM83/pkJ`eBP^p_lN2(V4]hIC:%X[7R@Lg)3'foTP>L>%3;l)^9;iG\I_C@><q/S47u,'hpp@oP;&3G?SD=*F:o?L4QgGUU6S4gA0q;GoDb\"OSJgn/:@CVL*\K5m*mOc0,DEet6\KP4%C)5,_@[CMg[T[BoYHn*^bFS]6d.pNZEl3-^&uoeMCLhoepAn%-&I5jnS3#;R4U+.hN_T/u&'q1[YIg^%5NAna6@9)EM*nJP.#,_mZah<D?BL-9Sn\bh3>fQsqTPdG@.[;ipM5Fsi!QqNlpiIRs2M3mgM68.Wej%Xbf8!f!#Dok;[&>'3VC7Z?k%45RQ6$#Uu'Q[!,q^>%;RcuOAH7eNO\Aqk4>;Q$N-k;3a=NaT\GU3N2:%"c5Ok-:3D/HCG4:"U;$iB^lKpdu`DnAOWdsG_F]FtfcqTkae?Op6u"^DlHRA[i/s+RWVmREP3C+IL/i'(8RFcTS?#Vp_..P[eQf_OZ7fp/_iK.>AUbWES/2Y"&\J=(d@@eJCBL68FtMD<aSATna'8r<t:CF[;,)MOHG4j'VR7ui@sB`PW,j^B.>:N$npD-cFji5UOqLR5^VfNW2P-<7V)AnS_tnD6[fEe5'G6'>PG,A'PL9q^o?MS7m%midh]ap>UK9A^,C32-qopg<rK]^d%X8(c?7Ou.<R\T<>iI_(,t^CAhmh""0\US.A:ljoT`S_O-=f0Q<eKC(I'S9oN<nH2CY/JB2ni5Yr"HnK7`4`)ubKtNkLW@Y93&T9nc%h@(_ou_B=Gjf)G!FN;j'hA-"X/"Y&1O1bGGGb*n=3eV]_TI..Le<+1%(]Yp?UaK5ShHUR$I(OSS-Y?i.%G\`EoG75%d!_gDD&k[;7#R'i2BV_I"cm\NiGj2k?G8,Q23$O;5lmlj]TcpRU^Oq)aR*4`kl&o>pCn]ZngiR7EGUfhU2tHY7*r^qH]B]4p*+&OZA(sH6PR=W;:rY^tmm#/U8lua9O3,d&%M3i)U"7XC[]/Z=!nC\l4rI2Tdd63,mffHbS[$L9qQ=,XCt7!;^TiUU_&BEWpNqG,&5T2Te1_!OA+bAUs^Vjbi`R,jN$Q]pF_W_lHr'[+,]>:s`^Ik;>0%Qsn\,%%MYO`^&$6;PYbl=rF7&]h\/aT@J((d.%q9(iT,Ao^Ik'l.DZp&KU1a*7_;Rfb2bBjpL\\F8c&k,5B#A1$pp]+NK[f.h-Onk6hJ<`s_k]-`q8B?#StBN?%L<(16.\e`o3;[9*`*qV^0uQann$Na$CkR9Hu1X/^;Z"J!?G16lf^0Qp7d%,l6^2`#EX'!B'0O[L-Yd;puYZO4cD>*i.5`W)Yf(K?\^\Pt1cB>l8u"g;4CcIf@c66=oV0Vl&mB'.T`Y*Q.3cJ\sL=ZO5?8krFqGP'R[4-*e4dC8FF'^E#7BtG/Hg9!g*U4r&P77V/^jh@;*ln;A/>?2$Ol8_dn!1U6^X#;B[Q,[gCf3eh563;%ilt)15L/u2+Eu7Mr'nLl0aXQeib]^3X3(X>(pM;b][V)MbI0]/9bfJZdIZ)6q!O52Qcje8o7MgG=D<h>fcV2Ear<uTO:+T2VM4h^J"W+'2`:W'%BB^u:JcSO)./-&ccJUsH0cFgt$2B<)0b62o55P!3ku":(,`b&P!q7!^p;-$&V;O>7d-nU\T]`M!g=Yc$?alLpI21r`Y$TWISTH.sQV1MiY4pC[(-bkbi\DRc1<HtbFI^7!%f#>.Hf4*5cNX+[Ru<`KEV6DBm3JH8Z)23qW,*]:<`@fLXR(:5Eesp-URNK$e$$mW>s1pbbu:S&6'jT:@.mhM4:g+6D71?6=G-M!@7U<K.c&Z)=ZuOf@qS8Kg]=iPE2-ZT&q<h[rG`1G1kd,[DW3$m;0[[q&pl9*[laCL#oZlRYnMJsp<kRr4OT"*\-nGPW<Ve"2h^fkRVH&g(!@M[_StAcW^PqY]I+3$)<@nMI:!!'SSI_@:bQQ&M.1gn4qNER'pn`t`Ep$-FE6hbJSqC.!niKm>K<XfTWES`XNR%#8T+D=N>2pQHj)cP9u##:!@4!BHD,Rh6C2d#*@DcC*/neu0;0>,4d+1Z$l1,ASh7\(2b28%DTOj6Aj>0+G3"9b8!\Zp#H`C029$O'+#NP2f>p08%3#&RBuD.=E]^K$Mlg&Ne'uKMS*[gMRhPHBhqUlZ6uEQ!ERYI:+Rb-(KRX-b7&ck!:"Qa#lJM:6:FpZcka-Wn1C0G#W<=OpBpe=W\oe^m"Fj:]'5M^_-i-cHV>~>endstream
endobj
18 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2101
>>
stream
Gb!kua`?/p&A@B[qP^pD0M-5cS.nD3.S3d$jdRk6M&ATJ&emS\Nk[d6i&^F"YnC,P8SOS9!HpDPpNiSF*rbs&IpN0RF:Rbgn\GaoHjtPG69+uINm@B:pAA?2)R.!bP5l4K?qJKcI*N2-/G>5`"I:BCjFc.PebDDP=TfoB=;o4D]QKQ1_YVNKk60eO;5`T!#%))_>;>TS0]2IXPf%m@-UEEX5o/I)0?'P!'/C8#U"B?#`gp@4N0<q]jMhHR,a@Rl)KEU2"TQ!/>LKtVVNX6[q)+\W+Y-=XB?QJ!EJEt5e5gI<)1A>eF`nbb2WXF,<O-L"X=nf4=d<d$HY[rnTG"@`8sCI0<o\nV"[*tG*0h-p1o75U*$9dL];?>dX3pkDM]gp_Rnf>;^fo9_cD<97mtO&YeP6U1k(:Y/iht25Mqu`f79Q9lW,XKqY8T\6X_.('^39B"+kLjPF=Ck1*07in7t]1XhMC=:Y/Z`h]em<'DSB<rIe@E/.'c/Ac>ddTNjJKis/u)ubd5*GJakQ+&;A=93M$DE^#*<+r&KD5Rc3G49DI]T^#b'ZFk^-G>g1kIlLYQU_Mi%gnU-/!R3/?=#S[N6"oh,d385X=]6l_l_X..5W!>jF2[IT2r>eJiVtZ>9;ftS\Ju&ARF>n`oJPjsg5WfG6=$;.9FG4He;JCiTZ>_=U5$t*'7ORNrRo7c,g5ue@IUK:X<^[.G:e0FR(q(G_O.^D2KsW%EaN@r':q"cZ\l>LU"E'F'j_&n.=Sn`;m#HMq6$N9%BO8p<NTOrl]kEG3DunO.4iq@?Oe:&N#2Gd/TrsLI(0Xl#J*U?qbZds#MkSg_./arog^g>nppI^:D)S+h?c\SN]'MUr\3P<s\mkpdn5rnS*`TS(.F<>%Gk1f-D#o--@C>K2L#]&G0l\WV8hmX8^;*p:/WLRPLE+#($>6^M-^Rm?+(upFC4qNX[P^sA&D@;W]DE5dqm)_WV\])2!b!"PGd27>+2o,%.*T=(=GVE]cRpSQ0a'[(R@u]g)oRBHnVPNJic%d$8]9X5jTH<JEBaAmV,'9n<Yf4uTo[eC:--(BcCZBe94dTH)l(bkck5%f@j*.P^dGd`BGfW8fd"c4,F4=.9p,EmP?H5&UC'aCWsD[De0cgu!ai:!dAR,;NcG^OUO.P8(1E*q`TJKhh*cZ[nC;U"J2EMSnsnT(!1"+RZI)]I;BPC:C%&CLV8P*:B!bn-_kLgd@WY0)H+]LjM4aFl>>AuSq9"Q=1Db+UM8H@%=2s9;Aa]C\2hsegBX4*tIa#Hka.qQ!G]_Ah;M3rW"j1JqTar'_2OEtVP%?.aiu`dN7)>gd\^Il.hgVVbk[Q[%#>*9'kBt_bA/.rEab1ohd'q?(h'ecYGI#LK!'Cta?NXBBCnedAY4+,WiQL6gIfcbml?]R+,q/%0SM:&'ahkBXcN3/r>8C[3f3f5^M_abDWFf-EAZR_t/p:pO0Q?\N#k(e.G.-DQOBPFJ\h#;1.3tR`0NXB!7o;8niDfbVDQ*!PVK&M?2siTN-<BN0?')uUI<i1-T7hB@>XJZ?k2E^RCZ=8.@[a><QIpp](Ob>aN1D%IT"amj#nG62RE.g2+4a[Ko!T5O=fj$>@C=m6-qjr1,B>>)qV([DF""hU2gpcT?F!RD^(:cC:V`Y[C1Bj<@n5'7WEX)/%>dh7?Rr(K3Dh3>OrLY-9_H3o.ZN43/@^HVcIq!=`L(de:OdKOVi$='6O.c<NNcGk+IAHMR\:fkrS:[\p,o,[l5NO%0)M9j<e9EsgnVXg^;M']quifW*]d>$>W$"CZONdE6bq`mg,dom&B)S1(pgGs3^07UY+X'u`$KPlB)+oUD\)>ZM+*`u&rdf4cDdP2>Z0'QIH9\*+KgNIjdDdf]n`'lECpC<(r%ak,u6e)UX(^aaBeOJHoC']5k^R3f(NnY:s2YkFK1HW;#i<4[%6OSe[DLulWcPY/K,En-`j6./9'f_XMD4s*#%_A_YEl*q%NW]4_OoWE>f(lQB9t@EVp4Y0?P'gGAf2k3dq>5`@6Oh,UI&g@1^!$BD\=&T&,.7*F:<Kglm)Cehq'\2W+TP^/0O5Ak@pah,8A^Y&T(s$#Cu?YkL(FItF3~>endstream
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2061
>>
stream
Gatm<9lo&I&A@sBm*X';/-12Ah)%^PG/fB@jHL$?64,LoToc#6k1B>ZpaJ^2UlHZ]'F>>_f\Q;q_1Gp>K`++eM1/[UYAXOM8W+;ihskFN2n`O&ICg-jbf"^9+HhO)krE2="aCQqNfYFj'64.=E@!Vo'`4JpH@8fP[88n-8V&gafVb&kUfu74"W$21-4P-(CKLLC2("r^kY8T3#mE=UG``DRQ&1QG\n/gi,EETd<g=pu[i8ZH3rXgK?YT%^fDgA^CZja6ad>bJ7k_@9O#Gg^c.bspX%_-0RZbCmWlQu]Bu6e$RW#<WimMY^R%t*3Y\67M^rm*k'S2Hi?=J1-l2pBuf$WCJ9'9F-^pR0Hj4\nSI*[nSi,bYK*\$?kYQK[K)J$-fMle#S)13lIUM<5T2YC]9oH*U%;5VgT?Rd"=Hen/iL;U=5.iMd[X`g:hK=Cb+I_T2L>Z4rs:)r78?PD9Ph7pXDJq3igC98c=%@nm[;0QBoK,o's,OXDuG<U(+kKm,B7;'gPpPmjQT(-62]o0Lp3/JH^Jg(eeYW4ZdN2&O@SX7l7/A.?+._C)blZK<J1)jN"1cL]B)q:LWWlrB$A4U?c_Sj?'XYSEHLj`WedZL[S8b0R9E@:dL+H/[KS0[_O2oHG;qT/_bBd)Pa/uMACq*C)>&U$B/^6JU&6P%0^'(<HV0=\l4O;s%i%0kjB#fb9j$!<><%A(i"mA@s'Kg8.6YU`(ZCUNtY,/bai(eD;ujZ7]IK]Z?dQ"fg2"k>NBg-2@&"#1AW1QomN0LBW^G&'ohV$6b1=5XIW1AFM,,`)#t(W\)NeI@clhS*$W"`m*r-Lf`r>@sgh^HOOc/QB:!CSDfGY>Ogh@G.OBa;.":g?L_#"dufsS3p73js!@#Olsm+K.8Y+J6o9n^,'D4^lL`9c^'2bVV:04X(]O9*!A?/oJGB9+T.[\]eOXG_A&26Q]COG#%cuB``0ike`cu)VGNQWW_*tj3>kGrIdtXUr!A[d5e=365@5OO]BH=na=E9A",p/bJO<:HW%c0"i-=Gln&/'ll#XKnmW)<3V,JBnAr.:lep=5##dKl:cQ]Rl4V^obnV&RS6qHdnh7#V0AIp"=(YP%X,EIVcafDd,<W7D9PL^KFjAE`M0JH",`PIKm_E^&e;Tg!K8)&(JFeK$+gGd1JhS$tQO'%XQJr+mT"h-$Xph_RGZb6+ZlOYDn/7EX/.#5L)PtauV*^5Pkk%L2&A!]9B+AZ5n=H=in*_a#d/^RUh_;QloH$omZ>Y&;$&p?2gBZ&5M+"7,:G#0,!J`NJMk4/KeFrm0+V!b+EO+A=\`JB,>8i_!/^S5^D'iJg,^e/p(e\US,1+^2t^41>/6K?;647At(/Ttp;AJIh*Q)lK7UJ'\Ge`JtGnmdM>6\G*4YZ!^HUjKZXHQmPKH7D8E;eM;S/ga$Z`0Pg*%#ZO%&+]*IjL]([o0N7]*-MLAF$=C^%)[Y^m&b*m]0QISCMT!e*cEC7O(r/HIDS.?NCWC#6$ielmHGt&la-:qI>4dEi5qWT@h/m6Uo1o>:jh8"GbH2M#Q.+b(cPQ`%9jA_oO,YR0RuU3blC.M`Aop?"I(PCZ,aPO#B@Ufa-.ns+\OCkl@606L[K:pMObZINUm0d[&sf$?6oUjFUjB2b4Gia1#)9(@GR[@5rVm\JEA]jboYp?=BDWq]&^<>F"HT-T9%@3!Aj$ULa<nC7#gE36-dR#ehANBPNc\GgZrs)*q=OSY%\==h/0&qNZC3JWmS9PQG]DcO5Fc$O'(u8"S**'M1^@$ot\$o2?SZH57@1kR>4X(:,n+P:F%/E%Si+BR%rj2Xu)\$ARA-)/Z(IPaT[VY3.@rtN!Ic10J;?pQ2DO0*S/]JlS.!;Tr_!ShsL]u(,\IS@!(TQqEoH6L+6ROpHsS\O1+l>F;"A-DIkoX/m#J+ft+Y%],[7Q$P+js`Q&QX[T(_UCPS=t6F5-;G+o545*+E`bn3R<Y\17E/:F]t"KEa@ARJ[+qBOe[HEEm-Zmsd"1](rR_+lp@cu3F9nD)(QbeojJ!%+F:LARk)1OH3k!5lL0dFE`][2Y)~>endstream
endobj
xref
0 20
0000000000 65535 f
0000000061 00000 n
0000000112 00000 n
0000000219 00000 n
0000000331 00000 n
0000000408 00000 n
0000000613 00000 n
0000000818 00000 n
0000001023 00000 n
0000001228 00000 n
0000001433 00000 n
0000001639 00000 n
0000001709 00000 n
0000001990 00000 n
0000002081 00000 n
0000003500 00000 n
0000005802 00000 n
0000007885 00000 n
0000010381 00000 n
0000012574 00000 n
trailer
<<
/ID
[<3d879f6902a83f70d587d7d3563c8f7d><3d879f6902a83f70d587d7d3563c8f7d>]
% ReportLab generated PDF document -- digest (opensource)
/Info 12 0 R
/Root 11 0 R
/Size 20
>>
startxref
14727
%%EOF

View File

@@ -170,7 +170,6 @@ const Cutting = () => {
const [selectedPreviewRowIds, setSelectedPreviewRowIds] = useState<string[]>([]); const [selectedPreviewRowIds, setSelectedPreviewRowIds] = useState<string[]>([]);
const [previewQuantityOverrides, setPreviewQuantityOverrides] = useState<Record<string, string>>({}); const [previewQuantityOverrides, setPreviewQuantityOverrides] = useState<Record<string, string>>({});
const [previewLayers, setPreviewLayers] = useState<Record<string, string>>({}); const [previewLayers, setPreviewLayers] = useState<Record<string, string>>({});
const [previewMarkerReferences, setPreviewMarkerReferences] = useState<Record<string, string>>({});
const [lotSelections, setLotSelections] = useState<Record<string, { lotId: string; quantity: string }>>({}); const [lotSelections, setLotSelections] = useState<Record<string, { lotId: string; quantity: string }>>({});
const [productionPlanForm, setProductionPlanForm] = useState({ name: '', responsibleUserId: null as number | null, responsibleName: '', expectedDate: '', notes: '' }); const [productionPlanForm, setProductionPlanForm] = useState({ name: '', responsibleUserId: null as number | null, responsibleName: '', expectedDate: '', notes: '' });
const [operationalUsers, setOperationalUsers] = useState<OperationalUser[]>([]); const [operationalUsers, setOperationalUsers] = useState<OperationalUser[]>([]);
@@ -685,7 +684,6 @@ const Cutting = () => {
setSelectedPreviewRowIds(rowsAvailableForOrder.map(row => row.id)); setSelectedPreviewRowIds(rowsAvailableForOrder.map(row => row.id));
setPreviewQuantityOverrides({}); setPreviewQuantityOverrides({});
setPreviewLayers({}); setPreviewLayers({});
setPreviewMarkerReferences({});
setLotSelections({}); setLotSelections({});
setProductionPlanForm({ name: cutPlanReference, responsibleUserId: null, responsibleName: '', expectedDate: '', notes: '' }); setProductionPlanForm({ name: cutPlanReference, responsibleUserId: null, responsibleName: '', expectedDate: '', notes: '' });
setIsPlanDetailsOpen(false); setIsPlanDetailsOpen(false);
@@ -757,7 +755,6 @@ const Cutting = () => {
{ label: row.family.materialLabel, color: '#38bdf8' }, { label: row.family.materialLabel, color: '#38bdf8' },
...(row.color ? [{ label: row.color, color: '#52DFA0' }] : []), ...(row.color ? [{ label: row.color, color: '#52DFA0' }] : []),
...(Number(previewLayers[row.id]) > 0 ? [{ label: `${previewLayers[row.id]} camadas`, color: '#c084fc' }] : []), ...(Number(previewLayers[row.id]) > 0 ? [{ label: `${previewLayers[row.id]} camadas`, color: '#c084fc' }] : []),
...(previewMarkerReferences[row.id]?.trim() ? [{ label: `Risco ${previewMarkerReferences[row.id].trim()}`, color: '#38bdf8' }] : []),
{ label: 'Material confirmado', color: '#52DFA0' }, { label: 'Material confirmado', color: '#52DFA0' },
...(row.issues.length ? [{ label: 'Dados pendentes', color: '#f59e0b' }] : []) ...(row.issues.length ? [{ label: 'Dados pendentes', color: '#f59e0b' }] : [])
], ],
@@ -774,7 +771,6 @@ const Cutting = () => {
openProductionQuantity: row.openProductionQuantity, openProductionQuantity: row.openProductionQuantity,
availableQuantity: row.availableQuantity, availableQuantity: row.availableQuantity,
plannedLayers: Number(previewLayers[row.id]) || null, plannedLayers: Number(previewLayers[row.id]) || null,
markerReference: previewMarkerReferences[row.id]?.trim() || '',
manuallyAdjustedQuantity: getPreviewQuantity(row) !== row.suggestedCutQuantity, manuallyAdjustedQuantity: getPreviewQuantity(row) !== row.suggestedCutQuantity,
issues: row.issues issues: row.issues
} }
@@ -1008,7 +1004,7 @@ const Cutting = () => {
</div> </div>
<div className="mt-3 divide-y divide-dark-border overflow-hidden rounded-lg border border-dark-border bg-dark-card"> <div className="mt-3 divide-y divide-dark-border overflow-hidden rounded-lg border border-dark-border bg-dark-card">
{selectedPreviewRows.map(row => ( {selectedPreviewRows.map(row => (
<div key={row.id} className="grid gap-3 px-3 py-3 lg:grid-cols-[minmax(0,1fr)_100px_100px_190px] lg:items-center"> <div key={row.id} className="grid gap-3 px-3 py-3 lg:grid-cols-[minmax(0,1fr)_100px_100px] lg:items-center">
<div className="min-w-0"> <div className="min-w-0">
<p className="truncate text-sm font-bold text-dark-text" title={row.name}>{row.name}</p> <p className="truncate text-sm font-bold text-dark-text" title={row.name}>{row.name}</p>
<div className="mt-1 flex flex-wrap items-center gap-1.5"> <div className="mt-1 flex flex-wrap items-center gap-1.5">
@@ -1024,13 +1020,10 @@ const Cutting = () => {
<label className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Camadas <label className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Camadas
<input inputMode="numeric" value={previewLayers[row.id] ?? ''} onChange={(event) => setPreviewLayers(current => ({ ...current, [row.id]: event.target.value }))} className="mt-1 h-9 w-full rounded-lg border border-dark-border bg-dark-input px-2 text-right text-sm font-bold text-dark-text outline-none focus:border-brand-primary" placeholder="Opcional" aria-label={`Camadas planejadas para ${row.name}`} /> <input inputMode="numeric" value={previewLayers[row.id] ?? ''} onChange={(event) => setPreviewLayers(current => ({ ...current, [row.id]: event.target.value }))} className="mt-1 h-9 w-full rounded-lg border border-dark-border bg-dark-input px-2 text-right text-sm font-bold text-dark-text outline-none focus:border-brand-primary" placeholder="Opcional" aria-label={`Camadas planejadas para ${row.name}`} />
</label> </label>
<label className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Risco / encaixe
<input value={previewMarkerReferences[row.id] ?? ''} onChange={(event) => setPreviewMarkerReferences(current => ({ ...current, [row.id]: event.target.value }))} className="mt-1 h-9 w-full rounded-lg border border-dark-border bg-dark-input px-2 text-sm font-bold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Código ou arquivo" aria-label={`Risco ou encaixe para ${row.name}`} />
</label>
</div> </div>
))} ))}
</div> </div>
<p className="mt-3 text-xs font-semibold text-dark-muted">A quantidade sugerida vem da cobertura. Ajustes manuais ficam marcados na OP; camadas e risco são referências de preparação, e o resultado real é confirmado durante a execução.</p> <p className="mt-3 text-xs font-semibold text-dark-muted">A quantidade sugerida vem da cobertura. Ajustes manuais ficam marcados na OP; as camadas orientam a preparação, e o resultado real é confirmado durante a execução.</p>
</div> </div>
</div>} </div>}

View File

@@ -422,7 +422,7 @@ const ProductionOrders = () => {
<label className="text-xs font-bold text-dark-muted">Perdas / defeitos<input name="rejectedQuantity" inputMode="decimal" defaultValue={order.rejectedQuantity || ''} className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="0" /></label> <label className="text-xs font-bold text-dark-muted">Perdas / defeitos<input name="rejectedQuantity" inputMode="decimal" defaultValue={order.rejectedQuantity || ''} className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="0" /></label>
<label className="text-xs font-bold text-dark-muted">Aprovado final<input name="finishedQuantity" inputMode="decimal" defaultValue={order.finishedQuantity ?? ''} className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Após revisão" /></label> <label className="text-xs font-bold text-dark-muted">Aprovado final<input name="finishedQuantity" inputMode="decimal" defaultValue={order.finishedQuantity ?? ''} className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Após revisão" /></label>
<label className="text-xs font-bold text-dark-muted">Camadas cortadas<input name="cutLayers" inputMode="numeric" defaultValue={order.cutLayers ?? ''} className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Ex.: 12" /></label> <label className="text-xs font-bold text-dark-muted">Camadas cortadas<input name="cutLayers" inputMode="numeric" defaultValue={order.cutLayers ?? ''} className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Ex.: 12" /></label>
<label className="text-xs font-bold text-dark-muted sm:col-span-2">Risco / encaixe<input name="markerReference" defaultValue={order.markerReference} className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Código do risco, encaixe ou arquivo" /></label> <label className="text-xs font-bold text-dark-muted sm:col-span-2">Referência do risco<input name="markerReference" defaultValue={order.markerReference} className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Código ou arquivo do risco" /></label>
<label className="text-xs font-bold text-dark-muted sm:col-span-3">Motivo de perda / observação<textarea name="qualityNotes" defaultValue={order.qualityNotes} className="mt-1.5 min-h-20 w-full rounded-lg border border-dark-border bg-dark-input px-3 py-2.5 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Ex.: 4 peças com mancha de tinta; 2 recuperadas na revisão." /></label> <label className="text-xs font-bold text-dark-muted sm:col-span-3">Motivo de perda / observação<textarea name="qualityNotes" defaultValue={order.qualityNotes} className="mt-1.5 min-h-20 w-full rounded-lg border border-dark-border bg-dark-input px-3 py-2.5 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Ex.: 4 peças com mancha de tinta; 2 recuperadas na revisão." /></label>
</div> </div>
<div className="mt-4 flex flex-wrap items-center justify-between gap-3"><p className="text-xs font-semibold text-dark-muted">Planejado: {formatQuantity(order.quantity)} {order.unit}</p><button type="submit" disabled={busyExecutionOrderId === order.id} className="inline-flex h-10 items-center gap-2 rounded-lg border border-brand-primary/35 bg-brand-primary/10 px-4 text-sm font-bold text-brand-primary transition-colors hover:bg-brand-primary/20 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"><CheckCircle2 className="h-4 w-4" />{busyExecutionOrderId === order.id ? 'Salvando...' : 'Salvar execução'}</button></div> <div className="mt-4 flex flex-wrap items-center justify-between gap-3"><p className="text-xs font-semibold text-dark-muted">Planejado: {formatQuantity(order.quantity)} {order.unit}</p><button type="submit" disabled={busyExecutionOrderId === order.id} className="inline-flex h-10 items-center gap-2 rounded-lg border border-brand-primary/35 bg-brand-primary/10 px-4 text-sm font-bold text-brand-primary transition-colors hover:bg-brand-primary/20 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"><CheckCircle2 className="h-4 w-4" />{busyExecutionOrderId === order.id ? 'Salvando...' : 'Salvar execução'}</button></div>