Add production orders page
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m20s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m20s
This commit is contained in:
@@ -91,6 +91,45 @@ const initDB = async () => {
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS production_orders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tiny_id VARCHAR(100) UNIQUE,
|
||||
number VARCHAR(100),
|
||||
status VARCHAR(40) DEFAULT 'open',
|
||||
order_reference TEXT,
|
||||
issue_date DATE,
|
||||
expected_date DATE,
|
||||
product_sku VARCHAR(255),
|
||||
product_description TEXT NOT NULL,
|
||||
quantity NUMERIC(14, 4) DEFAULT 0,
|
||||
unit VARCHAR(20) DEFAULT 'UN',
|
||||
integration_status VARCHAR(100),
|
||||
tiny_payload JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS production_order_markers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
production_order_id INTEGER NOT NULL REFERENCES production_orders(id) ON DELETE CASCADE,
|
||||
label VARCHAR(100) NOT NULL,
|
||||
color VARCHAR(40),
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (production_order_id, label)
|
||||
);
|
||||
`);
|
||||
|
||||
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(`
|
||||
CREATE TABLE IF NOT EXISTS app_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@@ -154,6 +193,10 @@ const initDB = async () => {
|
||||
});
|
||||
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_stock_campaign_queue_status ON stock_campaign_queue (status);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_orders_status ON production_orders (status);`);
|
||||
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_orders_cliente_fone ON orders (cliente_fone);`);
|
||||
await pool.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_normalized_cliente_nome
|
||||
|
||||
16
backend/routes/productionOrderRoutes.js
Normal file
16
backend/routes/productionOrderRoutes.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const express = require('express');
|
||||
const { verifyToken } = require('../auth');
|
||||
const { listProductionOrders } = require('../services/productionOrderService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/production-orders', verifyToken, async (req, res) => {
|
||||
try {
|
||||
res.json(await listProductionOrders(req.query || {}));
|
||||
} catch (error) {
|
||||
console.error('Error fetching production orders:', error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -8,6 +8,7 @@ const campaignRoutes = require('./routes/campaignRoutes');
|
||||
const internalRoutes = require('./routes/internalRoutes');
|
||||
const analyticsRoutes = require('./routes/analyticsRoutes');
|
||||
const userRoutes = require('./routes/userRoutes');
|
||||
const productionOrderRoutes = require('./routes/productionOrderRoutes');
|
||||
|
||||
const createApp = () => {
|
||||
const app = express();
|
||||
@@ -19,6 +20,7 @@ const createApp = () => {
|
||||
app.use('/api', dataRoutes);
|
||||
app.use('/api', stockRoutes);
|
||||
app.use('/api', campaignRoutes);
|
||||
app.use('/api', productionOrderRoutes);
|
||||
app.use('/api', analyticsRoutes);
|
||||
app.use('/api', userRoutes);
|
||||
app.use('/api/internal', internalRoutes);
|
||||
|
||||
139
backend/services/productionOrderService.js
Normal file
139
backend/services/productionOrderService.js
Normal file
@@ -0,0 +1,139 @@
|
||||
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 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 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 };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
listProductionOrders,
|
||||
normalizeStatus
|
||||
};
|
||||
Reference in New Issue
Block a user