Add local production order workflow
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m40s

This commit is contained in:
Cauê Faleiros
2026-07-20 12:57:28 -03:00
parent 9af5f296a7
commit 88089343a8
6 changed files with 384 additions and 13 deletions

View File

@@ -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 && (