Add local production order workflow
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m40s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m40s
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
const express = require('express');
|
||||
const { verifyToken } = require('../auth');
|
||||
const { listProductionOrders } = require('../services/productionOrderService');
|
||||
const { createProductionOrders, listProductionOrders, updateProductionOrderStatus } = require('../services/productionOrderService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -13,4 +13,23 @@ router.get('/production-orders', verifyToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/production-orders', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const orders = Array.isArray(req.body?.orders) ? req.body.orders : [];
|
||||
res.status(201).json(await createProductionOrders(orders));
|
||||
} catch (error) {
|
||||
console.error('Error creating production orders:', 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));
|
||||
} catch (error) {
|
||||
console.error('Error updating production order status:', error);
|
||||
res.status(error.statusCode || 500).json({ error: error.message || 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -31,6 +31,14 @@ const formatDate = (value) => {
|
||||
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);
|
||||
|
||||
@@ -54,6 +62,42 @@ const mapProductionOrderRow = (row) => {
|
||||
};
|
||||
};
|
||||
|
||||
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 = [];
|
||||
@@ -133,7 +177,142 @@ const listProductionOrders = async (filters = {}) => {
|
||||
return { orders, counts };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
listProductionOrders,
|
||||
normalizeStatus
|
||||
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
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, CatalogCategory, CatalogCategoryPayload, CatalogProduct, CatalogProductPayload, CatalogSummary, ClientAnalyticsItem, ClientDetailsAnalytics, ClientFilterOptions, ClientMetadataFilters, ClientPurchasePatternAnalytics, ConsumptionReference, ConsumptionReferencePayload, CreateUserResult, CuttingSettings, DashboardAnalytics, DateRange, ManagedUser, OrderData, ProductAnalyticsItem, ProductDetailsAnalytics, ProductionOrderSummary, RfmAnalytics, StockData, SupplyFabricPlan, SupplyFabricPlanPayload, SupplyInventoryAdjustmentPayload, SupplyLot, SupplyProductionExitPayload, SupplyPurchaseNeed, SupplyReceipt, SupplyReceiptPayload, SupplySummary } from './types';
|
||||
import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, CatalogCategory, CatalogCategoryPayload, CatalogProduct, CatalogProductPayload, CatalogSummary, ClientAnalyticsItem, ClientDetailsAnalytics, ClientFilterOptions, ClientMetadataFilters, ClientPurchasePatternAnalytics, ConsumptionReference, ConsumptionReferencePayload, CreateProductionOrdersResult, CreateUserResult, CuttingSettings, DashboardAnalytics, DateRange, ManagedUser, OrderData, ProductAnalyticsItem, ProductDetailsAnalytics, ProductionOrderItem, ProductionOrderPayload, ProductionOrderStatus, ProductionOrderSummary, RfmAnalytics, StockData, SupplyFabricPlan, SupplyFabricPlanPayload, SupplyInventoryAdjustmentPayload, SupplyLot, SupplyProductionExitPayload, SupplyPurchaseNeed, SupplyReceipt, SupplyReceiptPayload, SupplySummary } from './types';
|
||||
import { formatDateParam } from './dateRanges';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
@@ -183,6 +183,30 @@ export const fetchProductionOrders = async (
|
||||
}, options);
|
||||
};
|
||||
|
||||
export const createProductionOrders = async (orders: ProductionOrderPayload[]): Promise<CreateProductionOrdersResult> => {
|
||||
const response = await authFetch('/production-orders', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ orders })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Não foi possível criar as ordens de produção.');
|
||||
analyticsCache.clear();
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export const updateProductionOrderStatus = async (id: number, status: ProductionOrderStatus): Promise<ProductionOrderItem> => {
|
||||
const response = await authFetch(`/production-orders/${id}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Não foi possível atualizar o status da ordem.');
|
||||
analyticsCache.clear();
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export const fetchCuttingSettings = async (): Promise<CuttingSettings> => {
|
||||
try {
|
||||
const response = await authFetch('/cutting-settings');
|
||||
|
||||
@@ -7,7 +7,7 @@ import ProductColorBadge from '../components/ProductColorBadge';
|
||||
import RefreshStatus from '../components/RefreshStatus';
|
||||
import { buildCuttingSkuConfigPath } from '../catalogLinks';
|
||||
import { CUT_FAMILY_RULES, buildCutPlan, buildOpenProductionByProductId, type CutFamilyKey, type CutIssue, type CutPlanSkuRow, type CutProductOverride } from '../analytics/cutting';
|
||||
import { exportToCSV, fetchCuttingSettings, fetchProductAnalytics, fetchProductionOrders, saveCuttingSettings } from '../dataService';
|
||||
import { createProductionOrders, exportToCSV, fetchCuttingSettings, fetchProductAnalytics, fetchProductionOrders, saveCuttingSettings } from '../dataService';
|
||||
import type { CuttingSettings, DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
|
||||
|
||||
type CutFilter = 'need' | 'all' | 'issues' | 'covered';
|
||||
@@ -65,6 +65,13 @@ const formatNumber = (value: number, maximumFractionDigits = 0) => (
|
||||
new Intl.NumberFormat('pt-BR', { maximumFractionDigits }).format(value)
|
||||
);
|
||||
|
||||
const formatDateKey = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const formatDays = (value: number | null) => {
|
||||
if (value === null) return '-';
|
||||
if (value > 999) return '999+ dias';
|
||||
@@ -135,6 +142,8 @@ const Cutting = () => {
|
||||
const [correctionPage, setCorrectionPage] = useState(1);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
const [isGeneratingOrders, setIsGeneratingOrders] = useState(false);
|
||||
const [generationMessage, setGenerationMessage] = useState('');
|
||||
const cutConfigSection = searchParams.get('config');
|
||||
const targetCorrectionSku = (searchParams.get('sku') || '').trim();
|
||||
|
||||
@@ -241,6 +250,10 @@ const Cutting = () => {
|
||||
});
|
||||
}, [cutFilter, cutPlan.rows, familyFilter, searchTerm, sortBy]);
|
||||
|
||||
const rowsAvailableForOrder = useMemo(() => (
|
||||
filteredRows.filter(row => row.suggestedCutQuantity > 0)
|
||||
), [filteredRows]);
|
||||
|
||||
const totalPages = Math.ceil(filteredRows.length / itemsPerPage);
|
||||
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
|
||||
const startIndex = (safeCurrentPage - 1) * itemsPerPage;
|
||||
@@ -400,6 +413,60 @@ const Cutting = () => {
|
||||
})), `plano_corte_${new Date().toISOString().split('T')[0]}.csv`);
|
||||
};
|
||||
|
||||
const generateProductionOrders = async () => {
|
||||
if (!rowsAvailableForOrder.length) {
|
||||
setGenerationMessage('Não há necessidade de corte no filtro atual.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGeneratingOrders(true);
|
||||
setGenerationMessage('');
|
||||
|
||||
const issueDate = formatDateKey(new Date());
|
||||
const orderReference = `Plano de corte ${formatDateKey(dateRange.start)} a ${formatDateKey(dateRange.end)} · ${targetCoverageDays} dias`;
|
||||
|
||||
try {
|
||||
const result = await createProductionOrders(rowsAvailableForOrder.map(row => ({
|
||||
status: 'open',
|
||||
orderReference,
|
||||
issueDate,
|
||||
productSku: row.id,
|
||||
productDescription: row.name,
|
||||
quantity: row.suggestedCutQuantity,
|
||||
unit: 'UN',
|
||||
integrationStatus: 'Local',
|
||||
markers: [
|
||||
{ label: row.family.materialLabel, color: '#38bdf8' },
|
||||
...(row.color ? [{ label: row.color, color: '#52DFA0' }] : []),
|
||||
{ label: 'Material pendente', color: '#facc15' },
|
||||
...(row.issues.length ? [{ label: 'Dados pendentes', color: '#f59e0b' }] : [])
|
||||
],
|
||||
metadata: {
|
||||
source: 'cut_plan',
|
||||
familyKey: row.family.key,
|
||||
familyLabel: row.family.label,
|
||||
materialLabel: row.family.materialLabel,
|
||||
color: row.color,
|
||||
size: row.size,
|
||||
targetCoverageDays,
|
||||
projectedDemand: row.projectedDemand,
|
||||
stock: row.stock,
|
||||
openProductionQuantity: row.openProductionQuantity,
|
||||
availableQuantity: row.availableQuantity,
|
||||
issues: row.issues
|
||||
}
|
||||
})));
|
||||
|
||||
const refreshedOrders = await fetchProductionOrders(allProductionOrdersRange, undefined, { force: true });
|
||||
setProductionOrders(refreshedOrders.orders);
|
||||
setGenerationMessage(`${result.created.length} OPs criadas${result.skipped.length ? ` · ${result.skipped.length} já existiam abertas` : ''}.`);
|
||||
} catch (error) {
|
||||
setGenerationMessage(error instanceof Error ? error.message : 'Não foi possível gerar as ordens de produção.');
|
||||
} finally {
|
||||
setIsGeneratingOrders(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderIssueBadge = (row: CutPlanSkuRow) => {
|
||||
if (!row.issues.length) {
|
||||
return <span className="text-xs font-bold text-emerald-300">OK</span>;
|
||||
@@ -444,6 +511,17 @@ const Cutting = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void generateProductionOrders()}
|
||||
disabled={isGeneratingOrders || !rowsAvailableForOrder.length}
|
||||
className="flex items-center justify-center gap-2 rounded-xl border border-brand-primary/30 bg-brand-primary/15 px-4 py-2.5 text-sm font-medium text-brand-primary shadow-sm transition-colors hover:border-brand-primary disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"
|
||||
title="Gerar ordens locais para as necessidades do filtro atual"
|
||||
>
|
||||
<ClipboardList size={16} />
|
||||
<span className="hidden sm:inline">{isGeneratingOrders ? 'Gerando' : 'Gerar OPs'}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={exportRows}
|
||||
@@ -466,6 +544,12 @@ const Cutting = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{generationMessage && (
|
||||
<div className="rounded-xl border border-dark-border bg-dark-card px-4 py-3 text-sm font-bold text-dark-muted">
|
||||
{generationMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RefreshStatus isRefreshing={isRefreshing} />
|
||||
|
||||
{isSettingsOpen && (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ArrowLeft, CalendarDays, CheckCircle2, ClipboardList, Clock3, Download,
|
||||
import DateRangePicker from '../components/DateRangePicker';
|
||||
import PaginationControls from '../components/PaginationControls';
|
||||
import RefreshStatus from '../components/RefreshStatus';
|
||||
import { consumeSupplyLotForProduction, exportToCSV, fetchProductionOrders, fetchSupplySummary } from '../dataService';
|
||||
import { consumeSupplyLotForProduction, exportToCSV, fetchProductionOrders, fetchSupplySummary, updateProductionOrderStatus } from '../dataService';
|
||||
import type { DateRange, ProductionOrderItem, ProductionOrderStatus, ProductionOrderSummary, SupplyLot } from '../types';
|
||||
|
||||
type ProductionOrderStatusTab = 'all' | 'open' | 'in_progress' | 'finished' | 'canceled';
|
||||
@@ -22,6 +22,10 @@ const statusTabs: Array<{ key: ProductionOrderStatusTab; label: string; dotClass
|
||||
{ key: 'canceled', label: 'Cancelada', dotClass: 'bg-zinc-500' }
|
||||
];
|
||||
|
||||
const editableStatusOptions: Array<{ value: ProductionOrderStatusTab; label: string }> = statusTabs
|
||||
.filter(tab => tab.key !== 'all')
|
||||
.map(tab => ({ value: tab.key, label: tab.label }));
|
||||
|
||||
const statusStyles: Record<string, { label: string; className: string; dotClass: string }> = {
|
||||
open: {
|
||||
label: 'Em aberto',
|
||||
@@ -121,6 +125,8 @@ const ProductionOrders = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSupplyBusy, setIsSupplyBusy] = useState(false);
|
||||
const [supplyMessage, setSupplyMessage] = useState('');
|
||||
const [busyStatusOrderId, setBusyStatusOrderId] = useState<number | null>(null);
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(20);
|
||||
const [exitForm, setExitForm] = useState({
|
||||
@@ -190,6 +196,23 @@ const ProductionOrders = () => {
|
||||
void loadProductionOrders({ force: true });
|
||||
};
|
||||
|
||||
const handleStatusChange = async (order: ProductionOrderItem, status: ProductionOrderStatusTab) => {
|
||||
if (status === 'all' || status === order.status) return;
|
||||
|
||||
setBusyStatusOrderId(order.id);
|
||||
setStatusMessage('');
|
||||
try {
|
||||
await updateProductionOrderStatus(order.id, status);
|
||||
const nextSummary = await fetchProductionOrders(dateRange, { search: searchTerm }, { force: true });
|
||||
setSummary(nextSummary);
|
||||
setStatusMessage(`OP ${order.number || `#${order.id}`} atualizada para ${statusStyles[status]?.label || status}.`);
|
||||
} catch (error) {
|
||||
setStatusMessage(error instanceof Error ? error.message : 'Não foi possível atualizar o status da OP.');
|
||||
} finally {
|
||||
setBusyStatusOrderId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshSupplyLots = async () => {
|
||||
const supplySummary = await fetchSupplySummary();
|
||||
setSupplyLots(supplySummary.lots);
|
||||
@@ -248,7 +271,7 @@ const ProductionOrders = () => {
|
||||
Suprimentos
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold text-dark-text">Ordens de Produção</h1>
|
||||
<p className="mt-2 text-dark-muted font-medium">Acompanhe as ordens de produção sincronizadas do Tiny.</p>
|
||||
<p className="mt-2 text-dark-muted font-medium">Acompanhe as ordens locais geradas pelo plano de corte.</p>
|
||||
</div>
|
||||
<DateRangePicker
|
||||
dateRange={dateRange}
|
||||
@@ -399,6 +422,11 @@ const ProductionOrders = () => {
|
||||
|
||||
<div className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm">
|
||||
<div className="border-b border-dark-border p-5">
|
||||
{statusMessage && (
|
||||
<div className="mb-4 rounded-xl border border-dark-border bg-dark-input px-3 py-2 text-sm font-bold text-dark-muted">
|
||||
{statusMessage}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div className="relative w-full xl:max-w-xl">
|
||||
<Search className="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-dark-muted" />
|
||||
@@ -515,10 +543,18 @@ const ProductionOrders = () => {
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-3">
|
||||
<span className={`inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-[10px] font-bold uppercase tracking-wide ${statusStyle.className}`}>
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${statusStyle.dotClass}`} />
|
||||
{statusStyle.label}
|
||||
</span>
|
||||
<label className="sr-only" htmlFor={`production-order-status-${order.id}`}>Status da OP</label>
|
||||
<select
|
||||
id={`production-order-status-${order.id}`}
|
||||
value={order.status}
|
||||
disabled={busyStatusOrderId === order.id}
|
||||
onChange={(event) => 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 => (
|
||||
<option key={option.value} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -534,7 +570,7 @@ const ProductionOrders = () => {
|
||||
</div>
|
||||
<p className="mt-4 text-sm font-bold text-dark-text">Nenhuma ordem de produção encontrada.</p>
|
||||
<p className="mt-1 max-w-md text-sm font-medium text-dark-muted">
|
||||
Quando a sincronização com o Tiny estiver ativa, as ordens aparecerão aqui com status, produto, quantidade e marcadores.
|
||||
Gere ordens pelo Plano de Corte para acompanhar status, produto, quantidade e baixa de material dentro do Graphs.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
29
src/types.ts
29
src/types.ts
@@ -67,6 +67,35 @@ export interface ProductionOrderSummary {
|
||||
counts: ProductionOrderCounts;
|
||||
}
|
||||
|
||||
export type ProductionOrderMarkerPayload = {
|
||||
label: string;
|
||||
color?: string | null;
|
||||
};
|
||||
|
||||
export type ProductionOrderPayload = {
|
||||
number?: string;
|
||||
status?: ProductionOrderStatus;
|
||||
orderReference?: string;
|
||||
issueDate?: string | null;
|
||||
expectedDate?: string | null;
|
||||
productSku?: string;
|
||||
productDescription: string;
|
||||
quantity: number;
|
||||
unit?: string;
|
||||
integrationStatus?: string;
|
||||
markers?: ProductionOrderMarkerPayload[];
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export interface CreateProductionOrdersResult {
|
||||
created: ProductionOrderItem[];
|
||||
skipped: Array<{
|
||||
productSku?: string;
|
||||
productDescription?: string;
|
||||
reason: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type CutFamilyKey = 'BLCS' | 'BLOS' | 'BLMC' | 'BLPM' | 'OUTROS';
|
||||
|
||||
export interface CutProductOverride {
|
||||
|
||||
Reference in New Issue
Block a user