diff --git a/backend/db.js b/backend/db.js index df7b2a7..039b7be 100644 --- a/backend/db.js +++ b/backend/db.js @@ -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);`); diff --git a/backend/routes/productionOrderRoutes.js b/backend/routes/productionOrderRoutes.js index b066a9a..5c5819a 100644 --- a/backend/routes/productionOrderRoutes.js +++ b/backend/routes/productionOrderRoutes.js @@ -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)); diff --git a/backend/services/databaseDiagnosticService.js b/backend/services/databaseDiagnosticService.js index dc03b55..87b1a04 100644 --- a/backend/services/databaseDiagnosticService.js +++ b/backend/services/databaseDiagnosticService.js @@ -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: { diff --git a/backend/services/productionOrderService.js b/backend/services/productionOrderService.js index 4089814..92e1567 100644 --- a/backend/services/productionOrderService.js +++ b/backend/services/productionOrderService.js @@ -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( - JSON_BUILD_OBJECT( - 'label', pom.label, - 'color', pom.color + ( + SELECT JSON_AGG( + JSON_BUILD_OBJECT( + 'label', marker.label, + 'color', marker.color + ) + ORDER BY marker.label ) - ORDER BY pom.label - ) FILTER (WHERE pom.id IS NOT NULL), + 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 }; diff --git a/src/pages/ProductionOrders.tsx b/src/pages/ProductionOrders.tsx index 368e5ba..0335953 100644 --- a/src/pages/ProductionOrders.tsx +++ b/src/pages/ProductionOrders.tsx @@ -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,63 +510,136 @@ const ProductionOrders = () => { {paginatedOrders.map((order: ProductionOrderItem) => { const statusStyle = getStatusStyle(order.status, order.statusLabel); + const showDetails = hasProductionOrderDetails(order); return ( - - {order.number || '-'} - {order.orderReference || '-'} - - - - {formatDate(order.issueDate)} - - - {formatDate(order.expectedDate)} - -
{order.productDescription}
-
{order.productSku || 'Sem SKU'}
- - - {formatQuantity(order.quantity)} - {order.unit} - - - {order.markers.length ? ( -
- {order.markers.map(marker => ( - - - {marker.label} - + + + {order.number || '-'} + {order.orderReference || '-'} + + + + {formatDate(order.issueDate)} + + + {formatDate(order.expectedDate)} + +
{order.productDescription}
+
{order.productSku || 'Sem SKU'}
+ + + {formatQuantity(order.quantity)} + {order.unit} + + + {order.markers.length ? ( +
+ {order.markers.map(marker => ( + + + {marker.label} + + ))} +
+ ) : ( + - + )} + + + + + {order.integrationStatus || 'Tiny'} + + + + + void handleStatusChange(order, event.target.value as ProductionOrderStatusTab)} - className={`h-8 rounded-lg border bg-dark-input px-2 text-[10px] font-bold uppercase tracking-wide outline-none transition-colors focus:border-brand-primary disabled:cursor-not-allowed disabled:opacity-50 ${statusStyle.className}`} - > - {editableStatusOptions.map(option => ( - - ))} - - - + + + + {showDetails && ( + + +
+
+

Composição

+ {order.components.length ? ( +
+ + + + + + + + + + + {order.components.map(component => ( + + + + + + + ))} + +
ProdutoSKUQtd.Total
{component.componentName}{component.componentSku || '-'}{formatQuantity(component.quantityPerUnit)} {component.unit}{formatQuantity(component.totalQuantity)} {component.unit}
+
+ ) : ( +

Sem composição sincronizada.

+ )} +
+ +
+

Etapas

+ {order.steps.length ? ( +
+ {order.steps.map(step => ( +
+ {step.stepNumber ?? '-'} +
+

{step.name}

+

{formatDate(step.startDate)} - {formatDate(step.endDate)}

+
+ +
+ ))} +
+ ) : ( +

Sem etapas sincronizadas.

+ )} +
+ +
+

Observações

+
+
Fornecedor
{order.supplier || '-'}
+
Lote
{order.lotCode || '-'}
+
Rolos
{formatOptionalQuantity(order.rollQuantity)}
+
Malha kg
{formatOptionalQuantity(order.fabricKg)}
+
Ribana kg
{formatOptionalQuantity(order.ribKg)}
+
Rendimento
{formatOptionalQuantity(order.yieldPiecesPerKg)}
+
+ {order.notes &&

{order.notes}

} +
+
+ + + )} +
); })} diff --git a/src/types.ts b/src/types.ts index 61a25dc..2730a19 100644 --- a/src/types.ts +++ b/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; }