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