Add Tiny production order detail sync
This commit is contained in:
@@ -128,6 +128,13 @@ const initDB = async () => {
|
||||
quantity NUMERIC(14, 4) DEFAULT 0,
|
||||
unit VARCHAR(20) DEFAULT 'UN',
|
||||
integration_status VARCHAR(100),
|
||||
notes TEXT,
|
||||
supplier TEXT,
|
||||
lot_code VARCHAR(120),
|
||||
roll_quantity NUMERIC(14, 4),
|
||||
fabric_kg NUMERIC(14, 4),
|
||||
rib_kg NUMERIC(14, 4),
|
||||
yield_pieces_per_kg NUMERIC(14, 4),
|
||||
tiny_payload JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
@@ -145,6 +152,36 @@ const initDB = async () => {
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS production_order_components (
|
||||
id SERIAL PRIMARY KEY,
|
||||
production_order_id INTEGER NOT NULL REFERENCES production_orders(id) ON DELETE CASCADE,
|
||||
component_tiny_id VARCHAR(100),
|
||||
component_sku VARCHAR(255),
|
||||
component_name TEXT NOT NULL,
|
||||
quantity_per_unit NUMERIC(14, 4) DEFAULT 0,
|
||||
total_quantity NUMERIC(14, 4) DEFAULT 0,
|
||||
unit VARCHAR(30),
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS production_order_steps (
|
||||
id SERIAL PRIMARY KEY,
|
||||
production_order_id INTEGER NOT NULL REFERENCES production_orders(id) ON DELETE CASCADE,
|
||||
step_number INTEGER,
|
||||
name VARCHAR(160) NOT NULL,
|
||||
start_date DATE,
|
||||
end_date DATE,
|
||||
status VARCHAR(80),
|
||||
color VARCHAR(40),
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS cutting_family_rules (
|
||||
family_key VARCHAR(20) PRIMARY KEY,
|
||||
@@ -277,6 +314,33 @@ const initDB = async () => {
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE production_orders
|
||||
ADD COLUMN IF NOT EXISTS notes TEXT,
|
||||
ADD COLUMN IF NOT EXISTS supplier TEXT,
|
||||
ADD COLUMN IF NOT EXISTS lot_code VARCHAR(120),
|
||||
ADD COLUMN IF NOT EXISTS roll_quantity NUMERIC(14, 4),
|
||||
ADD COLUMN IF NOT EXISTS fabric_kg NUMERIC(14, 4),
|
||||
ADD COLUMN IF NOT EXISTS rib_kg NUMERIC(14, 4),
|
||||
ADD COLUMN IF NOT EXISTS yield_pieces_per_kg NUMERIC(14, 4);
|
||||
`).catch(() => {});
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE production_orders
|
||||
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
|
||||
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
|
||||
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
|
||||
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
|
||||
`).catch(() => {});
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE production_order_components
|
||||
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
|
||||
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
|
||||
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
|
||||
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
|
||||
`).catch(() => {});
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE production_order_steps
|
||||
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
|
||||
ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
|
||||
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
|
||||
@@ -425,6 +489,9 @@ const initDB = async () => {
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_orders_issue_date ON production_orders (issue_date DESC);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_orders_expected_date ON production_orders (expected_date DESC);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_order_markers_order_id ON production_order_markers (production_order_id);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_order_components_order_id ON production_order_components (production_order_id);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_order_components_sku ON production_order_components (component_sku);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_order_steps_order_id ON production_order_steps (production_order_id);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_cutting_product_overrides_family_key ON cutting_product_overrides (family_key);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_catalog_products_type ON catalog_products (type);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_catalog_products_category_id ON catalog_products (category_id);`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const express = require('express');
|
||||
const { verifyToken } = require('../auth');
|
||||
const { createProductionOrders, listProductionOrders, updateProductionOrderStatus } = require('../services/productionOrderService');
|
||||
const { authenticateAPIKey, verifyToken } = require('../auth');
|
||||
const { createProductionOrders, listProductionOrders, updateProductionOrderStatus, upsertTinyProductionOrderDetail } = require('../services/productionOrderService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -23,6 +23,15 @@ router.post('/production-orders', verifyToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/production-orders/tiny-sync', authenticateAPIKey, async (req, res) => {
|
||||
try {
|
||||
res.status(201).json(await upsertTinyProductionOrderDetail(req.body || {}));
|
||||
} catch (error) {
|
||||
console.error('Error syncing Tiny production order:', error);
|
||||
res.status(error.statusCode || 500).json({ error: error.message || 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
router.patch('/production-orders/:id/status', verifyToken, async (req, res) => {
|
||||
try {
|
||||
res.json(await updateProductionOrderStatus(req.params.id, req.body?.status));
|
||||
|
||||
@@ -31,8 +31,16 @@ const sampleSpecs = {
|
||||
columns: ['id', 'production_order_id', 'label', 'color', 'created_at'],
|
||||
orderBy: ['created_at', 'id']
|
||||
},
|
||||
production_order_components: {
|
||||
columns: ['id', 'production_order_id', 'component_tiny_id', 'component_sku', 'component_name', 'quantity_per_unit', 'total_quantity', 'unit', 'created_at', 'updated_at'],
|
||||
orderBy: ['updated_at', 'created_at', 'id']
|
||||
},
|
||||
production_order_steps: {
|
||||
columns: ['id', 'production_order_id', 'step_number', 'name', 'start_date', 'end_date', 'status', 'color', 'created_at', 'updated_at'],
|
||||
orderBy: ['updated_at', 'created_at', 'id']
|
||||
},
|
||||
production_orders: {
|
||||
columns: ['id', 'tiny_id', 'number', 'status', 'order_reference', 'issue_date', 'expected_date', 'product_sku', 'product_description', 'quantity', 'unit', 'integration_status', 'created_at', 'updated_at'],
|
||||
columns: ['id', 'tiny_id', 'number', 'status', 'order_reference', 'issue_date', 'expected_date', 'product_sku', 'product_description', 'quantity', 'unit', 'integration_status', 'notes', 'supplier', 'lot_code', 'roll_quantity', 'fabric_kg', 'rib_kg', 'yield_pieces_per_kg', 'created_at', 'updated_at'],
|
||||
orderBy: ['updated_at', 'created_at', 'id']
|
||||
},
|
||||
stock: {
|
||||
|
||||
@@ -18,9 +18,22 @@ const normalizeStatus = (status) => {
|
||||
|
||||
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 normalizedValue = String(value).trim();
|
||||
const isoMatch = normalizedValue.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/);
|
||||
if (isoMatch) {
|
||||
const [, year, month, day] = isoMatch;
|
||||
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}T00:00:00`);
|
||||
return Number.isNaN(date.getTime()) ? null : `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
const brMatch = normalizedValue.match(/^(\d{1,2})[-/](\d{1,2})[-/](\d{4})/);
|
||||
if (brMatch) {
|
||||
const [, day, month, year] = brMatch;
|
||||
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}T00:00:00`);
|
||||
return Number.isNaN(date.getTime()) ? null : `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const formatDate = (value) => {
|
||||
@@ -34,11 +47,44 @@ const formatDate = (value) => {
|
||||
const normalizeText = (value) => String(value || '').trim();
|
||||
|
||||
const normalizeQuantity = (value) => {
|
||||
const quantity = Number(value);
|
||||
const normalizedValue = typeof value === 'string' && value.includes(',')
|
||||
? value.replace(/\./g, '').replace(',', '.')
|
||||
: value;
|
||||
const quantity = Number(normalizedValue);
|
||||
if (!Number.isFinite(quantity) || quantity <= 0) return 0;
|
||||
return quantity;
|
||||
};
|
||||
|
||||
const normalizeNullableQuantity = (value) => {
|
||||
const quantity = normalizeQuantity(value);
|
||||
return quantity || null;
|
||||
};
|
||||
|
||||
const normalizeInteger = (value) => {
|
||||
const number = Number(value);
|
||||
return Number.isInteger(number) ? number : null;
|
||||
};
|
||||
|
||||
const mapComponent = (component) => ({
|
||||
id: component.id,
|
||||
componentTinyId: component.component_tiny_id || '',
|
||||
componentSku: component.component_sku || '',
|
||||
componentName: component.component_name || '',
|
||||
quantityPerUnit: Number(component.quantity_per_unit || 0),
|
||||
totalQuantity: Number(component.total_quantity || 0),
|
||||
unit: component.unit || ''
|
||||
});
|
||||
|
||||
const mapStep = (step) => ({
|
||||
id: step.id,
|
||||
stepNumber: step.step_number === null ? null : Number(step.step_number),
|
||||
name: step.name || '',
|
||||
startDate: formatDate(step.start_date),
|
||||
endDate: formatDate(step.end_date),
|
||||
status: step.status || '',
|
||||
color: step.color || ''
|
||||
});
|
||||
|
||||
const mapProductionOrderRow = (row) => {
|
||||
const status = normalizeStatus(row.status);
|
||||
|
||||
@@ -56,7 +102,16 @@ const mapProductionOrderRow = (row) => {
|
||||
quantity: Number(row.quantity || 0),
|
||||
unit: row.unit || 'UN',
|
||||
integrationStatus: row.integration_status || '',
|
||||
notes: row.notes || '',
|
||||
supplier: row.supplier || '',
|
||||
lotCode: row.lot_code || '',
|
||||
rollQuantity: row.roll_quantity === null ? null : Number(row.roll_quantity),
|
||||
fabricKg: row.fabric_kg === null ? null : Number(row.fabric_kg),
|
||||
ribKg: row.rib_kg === null ? null : Number(row.rib_kg),
|
||||
yieldPiecesPerKg: row.yield_pieces_per_kg === null ? null : Number(row.yield_pieces_per_kg),
|
||||
markers: Array.isArray(row.markers) ? row.markers.filter(Boolean) : [],
|
||||
components: Array.isArray(row.components) ? row.components.map(mapComponent) : [],
|
||||
steps: Array.isArray(row.steps) ? row.steps.map(mapStep) : [],
|
||||
createdAt: row.created_at || null,
|
||||
updatedAt: row.updated_at || null
|
||||
};
|
||||
@@ -77,27 +132,152 @@ const getOrderById = async (id, client = pool) => {
|
||||
po.quantity,
|
||||
po.unit,
|
||||
po.integration_status,
|
||||
po.notes,
|
||||
po.supplier,
|
||||
po.lot_code,
|
||||
po.roll_quantity,
|
||||
po.fabric_kg,
|
||||
po.rib_kg,
|
||||
po.yield_pieces_per_kg,
|
||||
po.created_at,
|
||||
po.updated_at,
|
||||
COALESCE(
|
||||
JSON_AGG(
|
||||
(
|
||||
SELECT JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'label', pom.label,
|
||||
'color', pom.color
|
||||
'label', marker.label,
|
||||
'color', marker.color
|
||||
)
|
||||
ORDER BY pom.label
|
||||
) FILTER (WHERE pom.id IS NOT NULL),
|
||||
ORDER BY marker.label
|
||||
)
|
||||
FROM production_order_markers marker
|
||||
WHERE marker.production_order_id = po.id
|
||||
),
|
||||
'[]'::json
|
||||
) as markers
|
||||
) as markers,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', component.id,
|
||||
'component_tiny_id', component.component_tiny_id,
|
||||
'component_sku', component.component_sku,
|
||||
'component_name', component.component_name,
|
||||
'quantity_per_unit', component.quantity_per_unit,
|
||||
'total_quantity', component.total_quantity,
|
||||
'unit', component.unit
|
||||
)
|
||||
ORDER BY component.id
|
||||
)
|
||||
FROM production_order_components component
|
||||
WHERE component.production_order_id = po.id
|
||||
),
|
||||
'[]'::json
|
||||
) as components,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', step.id,
|
||||
'step_number', step.step_number,
|
||||
'name', step.name,
|
||||
'start_date', step.start_date,
|
||||
'end_date', step.end_date,
|
||||
'status', step.status,
|
||||
'color', step.color
|
||||
)
|
||||
ORDER BY step.step_number NULLS LAST, step.id
|
||||
)
|
||||
FROM production_order_steps step
|
||||
WHERE step.production_order_id = po.id
|
||||
),
|
||||
'[]'::json
|
||||
) as steps
|
||||
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;
|
||||
WHERE po.id = $1;
|
||||
`, [id]);
|
||||
|
||||
return result.rows[0] ? mapProductionOrderRow(result.rows[0]) : null;
|
||||
};
|
||||
|
||||
const baseProductionOrderSelect = `
|
||||
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.notes,
|
||||
po.supplier,
|
||||
po.lot_code,
|
||||
po.roll_quantity,
|
||||
po.fabric_kg,
|
||||
po.rib_kg,
|
||||
po.yield_pieces_per_kg,
|
||||
po.created_at,
|
||||
po.updated_at,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'label', marker.label,
|
||||
'color', marker.color
|
||||
)
|
||||
ORDER BY marker.label
|
||||
)
|
||||
FROM production_order_markers marker
|
||||
WHERE marker.production_order_id = po.id
|
||||
),
|
||||
'[]'::json
|
||||
) as markers,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', component.id,
|
||||
'component_tiny_id', component.component_tiny_id,
|
||||
'component_sku', component.component_sku,
|
||||
'component_name', component.component_name,
|
||||
'quantity_per_unit', component.quantity_per_unit,
|
||||
'total_quantity', component.total_quantity,
|
||||
'unit', component.unit
|
||||
)
|
||||
ORDER BY component.id
|
||||
)
|
||||
FROM production_order_components component
|
||||
WHERE component.production_order_id = po.id
|
||||
),
|
||||
'[]'::json
|
||||
) as components,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', step.id,
|
||||
'step_number', step.step_number,
|
||||
'name', step.name,
|
||||
'start_date', step.start_date,
|
||||
'end_date', step.end_date,
|
||||
'status', step.status,
|
||||
'color', step.color
|
||||
)
|
||||
ORDER BY step.step_number NULLS LAST, step.id
|
||||
)
|
||||
FROM production_order_steps step
|
||||
WHERE step.production_order_id = po.id
|
||||
),
|
||||
'[]'::json
|
||||
) as steps
|
||||
FROM production_orders po
|
||||
`;
|
||||
|
||||
const listProductionOrders = async (filters = {}) => {
|
||||
const params = [];
|
||||
const where = [];
|
||||
@@ -126,35 +306,8 @@ const listProductionOrders = async (filters = {}) => {
|
||||
}
|
||||
|
||||
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
|
||||
${baseProductionOrderSelect}
|
||||
${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,
|
||||
@@ -285,6 +438,222 @@ const createProductionOrders = async (orders = []) => {
|
||||
}
|
||||
};
|
||||
|
||||
const resolveTinyOrderPayload = (payload = {}) => {
|
||||
const order = payload.order && typeof payload.order === 'object' ? payload.order : payload;
|
||||
const quantity = normalizeQuantity(order.quantity);
|
||||
const productDescription = normalizeText(order.productDescription || order.productName || order.produto || order.descricao);
|
||||
|
||||
if (!productDescription || !quantity) {
|
||||
const error = new Error('Produto e quantidade da OP são obrigatórios.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
tinyId: normalizeText(order.tinyId || order.tiny_id || order.idTiny || order.id),
|
||||
number: normalizeText(order.number || order.numero || order.numeroOp || order.productionOrderNumber),
|
||||
status: normalizeStatus(order.status || order.situacao || 'in_progress'),
|
||||
orderReference: normalizeText(order.orderReference || order.reference || order.plano || order.pedidos),
|
||||
issueDate: normalizeDateParam(order.issueDate || order.date || order.data),
|
||||
expectedDate: normalizeDateParam(order.expectedDate || order.dataPrevista),
|
||||
productSku: normalizeText(order.productSku || order.sku || order.codigo || order.codigoSku),
|
||||
productDescription,
|
||||
quantity,
|
||||
unit: normalizeText(order.unit || order.unidade) || 'UN',
|
||||
integrationStatus: normalizeText(order.integrationStatus) || 'Tiny',
|
||||
notes: normalizeText(order.notes || order.observations || order.observacoes),
|
||||
supplier: normalizeText(order.supplier || order.fornecedor),
|
||||
lotCode: normalizeText(order.lotCode || order.lote),
|
||||
rollQuantity: normalizeNullableQuantity(order.rollQuantity || order.quantidadeRolos),
|
||||
fabricKg: normalizeNullableQuantity(order.fabricKg || order.quilosMalha),
|
||||
ribKg: normalizeNullableQuantity(order.ribKg || order.quilosRibana),
|
||||
yieldPiecesPerKg: normalizeNullableQuantity(order.yieldPiecesPerKg || order.rendimento),
|
||||
rawPayload: payload
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeComponents = (components = [], orderQuantity = 0) => (
|
||||
Array.isArray(components) ? components : []
|
||||
).map(component => {
|
||||
const quantityPerUnit = normalizeQuantity(component.quantityPerUnit || component.quantity || component.quantidade);
|
||||
const totalQuantity = normalizeQuantity(component.totalQuantity || component.quantidadeTotal) || quantityPerUnit * orderQuantity;
|
||||
|
||||
return {
|
||||
componentTinyId: normalizeText(component.componentTinyId || component.idComponente || component.id_componente || component.id),
|
||||
componentSku: normalizeText(component.componentSku || component.sku || component.codigo || component.code),
|
||||
componentName: normalizeText(component.componentName || component.name || component.nome || component.produto),
|
||||
quantityPerUnit,
|
||||
totalQuantity,
|
||||
unit: normalizeText(component.unit || component.unidade)
|
||||
};
|
||||
}).filter(component => component.componentName);
|
||||
|
||||
const normalizeSteps = (steps = []) => (
|
||||
Array.isArray(steps) ? steps : []
|
||||
).map(step => ({
|
||||
stepNumber: normalizeInteger(step.stepNumber || step.number || step.nro || step.numero),
|
||||
name: normalizeText(step.name || step.etapa || step.posto || step.description),
|
||||
startDate: normalizeDateParam(step.startDate || step.dataInicio),
|
||||
endDate: normalizeDateParam(step.endDate || step.dataFim),
|
||||
status: normalizeText(step.status || step.situacao),
|
||||
color: normalizeText(step.color || step.cor)
|
||||
})).filter(step => step.name);
|
||||
|
||||
const upsertTinyProductionOrderDetail = async (payload = {}) => {
|
||||
const order = resolveTinyOrderPayload(payload);
|
||||
const components = normalizeComponents(payload.components || payload.composition || payload.composicao, order.quantity);
|
||||
const steps = normalizeSteps(payload.steps || payload.etapas);
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
const existingResult = await client.query(`
|
||||
SELECT id
|
||||
FROM production_orders
|
||||
WHERE ($1 <> '' AND tiny_id = $1)
|
||||
OR ($2 <> '' AND number = $2)
|
||||
ORDER BY CASE WHEN tiny_id = $1 THEN 0 ELSE 1 END
|
||||
LIMIT 1;
|
||||
`, [order.tinyId, order.number]);
|
||||
|
||||
let orderId = existingResult.rows[0]?.id;
|
||||
|
||||
if (orderId) {
|
||||
await client.query(`
|
||||
UPDATE production_orders
|
||||
SET tiny_id = COALESCE(NULLIF($1, ''), tiny_id),
|
||||
number = COALESCE(NULLIF($2, ''), number),
|
||||
status = $3,
|
||||
order_reference = NULLIF($4, ''),
|
||||
issue_date = $5,
|
||||
expected_date = $6,
|
||||
product_sku = NULLIF($7, ''),
|
||||
product_description = $8,
|
||||
quantity = $9,
|
||||
unit = $10,
|
||||
integration_status = $11,
|
||||
notes = NULLIF($12, ''),
|
||||
supplier = NULLIF($13, ''),
|
||||
lot_code = NULLIF($14, ''),
|
||||
roll_quantity = $15,
|
||||
fabric_kg = $16,
|
||||
rib_kg = $17,
|
||||
yield_pieces_per_kg = $18,
|
||||
tiny_payload = $19::jsonb,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $20;
|
||||
`, [
|
||||
order.tinyId,
|
||||
order.number,
|
||||
order.status,
|
||||
order.orderReference,
|
||||
order.issueDate,
|
||||
order.expectedDate,
|
||||
order.productSku,
|
||||
order.productDescription,
|
||||
order.quantity,
|
||||
order.unit,
|
||||
order.integrationStatus,
|
||||
order.notes,
|
||||
order.supplier,
|
||||
order.lotCode,
|
||||
order.rollQuantity,
|
||||
order.fabricKg,
|
||||
order.ribKg,
|
||||
order.yieldPiecesPerKg,
|
||||
JSON.stringify(order.rawPayload),
|
||||
orderId
|
||||
]);
|
||||
} else {
|
||||
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,
|
||||
notes, supplier, lot_code, roll_quantity, fabric_kg, rib_kg,
|
||||
yield_pieces_per_kg, tiny_payload
|
||||
)
|
||||
VALUES (
|
||||
NULLIF($1, ''), NULLIF($2, ''), $3, NULLIF($4, ''), $5, $6,
|
||||
NULLIF($7, ''), $8, $9, $10, $11, NULLIF($12, ''),
|
||||
NULLIF($13, ''), NULLIF($14, ''), $15, $16, $17, $18, $19::jsonb
|
||||
)
|
||||
RETURNING id;
|
||||
`, [
|
||||
order.tinyId,
|
||||
order.number,
|
||||
order.status,
|
||||
order.orderReference,
|
||||
order.issueDate,
|
||||
order.expectedDate,
|
||||
order.productSku,
|
||||
order.productDescription,
|
||||
order.quantity,
|
||||
order.unit,
|
||||
order.integrationStatus,
|
||||
order.notes,
|
||||
order.supplier,
|
||||
order.lotCode,
|
||||
order.rollQuantity,
|
||||
order.fabricKg,
|
||||
order.ribKg,
|
||||
order.yieldPiecesPerKg,
|
||||
JSON.stringify(order.rawPayload)
|
||||
]);
|
||||
orderId = insertResult.rows[0].id;
|
||||
}
|
||||
|
||||
await client.query('DELETE FROM production_order_components WHERE production_order_id = $1;', [orderId]);
|
||||
for (const component of components) {
|
||||
await client.query(`
|
||||
INSERT INTO production_order_components (
|
||||
production_order_id, component_tiny_id, component_sku, component_name,
|
||||
quantity_per_unit, total_quantity, unit
|
||||
)
|
||||
VALUES ($1, NULLIF($2, ''), NULLIF($3, ''), $4, $5, $6, NULLIF($7, ''));
|
||||
`, [
|
||||
orderId,
|
||||
component.componentTinyId,
|
||||
component.componentSku,
|
||||
component.componentName,
|
||||
component.quantityPerUnit,
|
||||
component.totalQuantity,
|
||||
component.unit
|
||||
]);
|
||||
}
|
||||
|
||||
await client.query('DELETE FROM production_order_steps WHERE production_order_id = $1;', [orderId]);
|
||||
for (const step of steps) {
|
||||
await client.query(`
|
||||
INSERT INTO production_order_steps (
|
||||
production_order_id, step_number, name, start_date, end_date, status, color
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, NULLIF($6, ''), NULLIF($7, ''));
|
||||
`, [
|
||||
orderId,
|
||||
step.stepNumber,
|
||||
step.name,
|
||||
step.startDate,
|
||||
step.endDate,
|
||||
step.status,
|
||||
step.color
|
||||
]);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
return {
|
||||
order: await getOrderById(orderId),
|
||||
componentCount: components.length,
|
||||
stepCount: steps.length
|
||||
};
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
const updateProductionOrderStatus = async (id, status) => {
|
||||
const normalizedStatus = normalizeStatus(status);
|
||||
|
||||
@@ -314,5 +683,6 @@ module.exports = {
|
||||
createProductionOrders,
|
||||
listProductionOrders,
|
||||
normalizeStatus,
|
||||
upsertTinyProductionOrderDetail,
|
||||
updateProductionOrderStatus
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Fragment, type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useOutletContext } from 'react-router-dom';
|
||||
import { ArrowLeft, CalendarDays, CheckCircle2, ClipboardList, Clock3, Download, PackageCheck, Search } from 'lucide-react';
|
||||
import DateRangePicker from '../components/DateRangePicker';
|
||||
@@ -63,6 +63,16 @@ const formatQuantity = (value: number) => (
|
||||
}).format(value)
|
||||
);
|
||||
|
||||
const formatOptionalQuantity = (value: number | null) => (
|
||||
value === null ? '-' : formatQuantity(value)
|
||||
);
|
||||
|
||||
const hasProductionOrderDetails = (order: ProductionOrderItem) => (
|
||||
order.components.length > 0 ||
|
||||
order.steps.length > 0 ||
|
||||
Boolean(order.notes || order.supplier || order.lotCode || order.rollQuantity || order.fabricKg || order.ribKg || order.yieldPiecesPerKg)
|
||||
);
|
||||
|
||||
const getStatusStyle = (status: ProductionOrderStatus, fallbackLabel: string) => (
|
||||
statusStyles[String(status)] || {
|
||||
label: fallbackLabel || 'Em aberto',
|
||||
@@ -500,8 +510,10 @@ const ProductionOrders = () => {
|
||||
<tbody className="divide-y divide-dark-border">
|
||||
{paginatedOrders.map((order: ProductionOrderItem) => {
|
||||
const statusStyle = getStatusStyle(order.status, order.statusLabel);
|
||||
const showDetails = hasProductionOrderDetails(order);
|
||||
return (
|
||||
<tr key={order.id} className="transition-colors hover:bg-dark-input/50">
|
||||
<Fragment key={order.id}>
|
||||
<tr className="transition-colors hover:bg-dark-input/50">
|
||||
<td className="px-6 py-3 font-mono text-xs font-bold text-dark-text">{order.number || '-'}</td>
|
||||
<td className="px-6 py-3 text-xs font-semibold text-dark-muted">{order.orderReference || '-'}</td>
|
||||
<td className="px-6 py-3 text-xs font-semibold text-dark-muted">
|
||||
@@ -557,6 +569,77 @@ const ProductionOrders = () => {
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
{showDetails && (
|
||||
<tr className="bg-dark-input/20">
|
||||
<td colSpan={9} className="px-6 pb-5 pt-1">
|
||||
<div className="grid grid-cols-1 gap-4 rounded-xl border border-dark-border bg-dark-card/80 p-4 xl:grid-cols-[1.4fr_1fr_1fr]">
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Composição</h3>
|
||||
{order.components.length ? (
|
||||
<div className="mt-3 overflow-hidden rounded-lg border border-dark-border">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-dark-header text-dark-muted">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-bold">Produto</th>
|
||||
<th className="px-3 py-2 text-left font-bold">SKU</th>
|
||||
<th className="px-3 py-2 text-right font-bold">Qtd.</th>
|
||||
<th className="px-3 py-2 text-right font-bold">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-dark-border">
|
||||
{order.components.map(component => (
|
||||
<tr key={component.id}>
|
||||
<td className="px-3 py-2 font-semibold text-dark-text">{component.componentName}</td>
|
||||
<td className="px-3 py-2 font-mono text-dark-muted">{component.componentSku || '-'}</td>
|
||||
<td className="px-3 py-2 text-right font-semibold text-dark-muted">{formatQuantity(component.quantityPerUnit)} {component.unit}</td>
|
||||
<td className="px-3 py-2 text-right font-bold text-dark-text">{formatQuantity(component.totalQuantity)} {component.unit}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-3 text-xs font-semibold text-dark-muted">Sem composição sincronizada.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Etapas</h3>
|
||||
{order.steps.length ? (
|
||||
<div className="mt-3 divide-y divide-dark-border rounded-lg border border-dark-border">
|
||||
{order.steps.map(step => (
|
||||
<div key={step.id} className="grid grid-cols-[32px_1fr_auto] items-center gap-3 px-3 py-2 text-xs">
|
||||
<span className="font-mono font-bold text-dark-muted">{step.stepNumber ?? '-'}</span>
|
||||
<div>
|
||||
<p className="font-bold text-dark-text">{step.name}</p>
|
||||
<p className="mt-0.5 font-semibold text-dark-muted">{formatDate(step.startDate)} - {formatDate(step.endDate)}</p>
|
||||
</div>
|
||||
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: step.color || 'var(--color-brand-primary)' }} title={step.status || 'Sem status'} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-3 text-xs font-semibold text-dark-muted">Sem etapas sincronizadas.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Observações</h3>
|
||||
<dl className="mt-3 grid grid-cols-2 gap-2 text-xs">
|
||||
<div><dt className="font-bold text-dark-muted">Fornecedor</dt><dd className="mt-0.5 font-semibold text-dark-text">{order.supplier || '-'}</dd></div>
|
||||
<div><dt className="font-bold text-dark-muted">Lote</dt><dd className="mt-0.5 font-semibold text-dark-text">{order.lotCode || '-'}</dd></div>
|
||||
<div><dt className="font-bold text-dark-muted">Rolos</dt><dd className="mt-0.5 font-semibold text-dark-text">{formatOptionalQuantity(order.rollQuantity)}</dd></div>
|
||||
<div><dt className="font-bold text-dark-muted">Malha kg</dt><dd className="mt-0.5 font-semibold text-dark-text">{formatOptionalQuantity(order.fabricKg)}</dd></div>
|
||||
<div><dt className="font-bold text-dark-muted">Ribana kg</dt><dd className="mt-0.5 font-semibold text-dark-text">{formatOptionalQuantity(order.ribKg)}</dd></div>
|
||||
<div><dt className="font-bold text-dark-muted">Rendimento</dt><dd className="mt-0.5 font-semibold text-dark-text">{formatOptionalQuantity(order.yieldPiecesPerKg)}</dd></div>
|
||||
</dl>
|
||||
{order.notes && <p className="mt-3 whitespace-pre-wrap rounded-lg border border-dark-border bg-dark-input p-3 text-xs font-semibold text-dark-muted">{order.notes}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
|
||||
29
src/types.ts
29
src/types.ts
@@ -34,6 +34,26 @@ export interface ProductionOrderMarker {
|
||||
color?: string | null;
|
||||
}
|
||||
|
||||
export interface ProductionOrderComponent {
|
||||
id: number;
|
||||
componentTinyId: string;
|
||||
componentSku: string;
|
||||
componentName: string;
|
||||
quantityPerUnit: number;
|
||||
totalQuantity: number;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface ProductionOrderStep {
|
||||
id: number;
|
||||
stepNumber: number | null;
|
||||
name: string;
|
||||
startDate: string | null;
|
||||
endDate: string | null;
|
||||
status: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface ProductionOrderItem {
|
||||
id: number;
|
||||
tinyId: string;
|
||||
@@ -48,7 +68,16 @@ export interface ProductionOrderItem {
|
||||
quantity: number;
|
||||
unit: string;
|
||||
integrationStatus: string;
|
||||
notes: string;
|
||||
supplier: string;
|
||||
lotCode: string;
|
||||
rollQuantity: number | null;
|
||||
fabricKg: number | null;
|
||||
ribKg: number | null;
|
||||
yieldPiecesPerKg: number | null;
|
||||
markers: ProductionOrderMarker[];
|
||||
components: ProductionOrderComponent[];
|
||||
steps: ProductionOrderStep[];
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user