From 7e65feac37f8411f115c81ed5cae779efa047994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Wed, 26 Aug 2026 16:28:59 -0300 Subject: [PATCH] Add production order defect capture --- src/dataService.ts | 20 ++++++++++- src/pages/ProductionOrders.tsx | 65 +++++++++++++++++++++++++++++++--- src/types.ts | 31 ++++++++++++++++ 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/dataService.ts b/src/dataService.ts index 44aae44..9f31211 100644 --- a/src/dataService.ts +++ b/src/dataService.ts @@ -1,4 +1,4 @@ -import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, CatalogCategory, CatalogCategoryPayload, CatalogProduct, CatalogProductPayload, CatalogSummary, ClientAnalyticsItem, ClientDetailsAnalytics, ClientFilterOptions, ClientMetadataFilters, ClientPurchasePatternAnalytics, ConsumptionReference, ConsumptionReferencePayload, CreateProductionOrdersResult, CreateUserResult, CutPlanLotReservationPayload, CuttingSettings, DashboardAnalytics, DateRange, GraphsStockBalanceHistoryItem, GraphsStockBalancePayload, ManagedUser, OperationalUser, OrderData, OlistRunDetails, OlistStatus, ProductAnalyticsItem, ProductComposition, ProductCompositionImportSummary, ProductCompositionPayload, ProductDetailsAnalytics, ProductionOrderExecutionPayload, ProductionOrderImportData, ProductionOrderItem, ProductionOrderPayload, ProductionOrderStatus, ProductionOrderSummary, ProductionPlan, ProductionPlanPayload, RfmAnalytics, SellerAnalyticsItem, SellerDetailsAnalytics, StockData, SupplyFabricPlan, SupplyFabricPlanPayload, SupplyInventoryAdjustmentPayload, SupplyLot, SupplyMaterialDetailsPayload, SupplyProductionExitPayload, SupplyPurchaseNeed, SupplyPurchaseOrder, SupplyPurchaseOrderPayload, SupplyPurchaseOrderStatus, 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, CutPlanLotReservationPayload, CuttingSettings, DashboardAnalytics, DateRange, GraphsStockBalanceHistoryItem, GraphsStockBalancePayload, ManagedUser, OperationalUser, OrderData, OlistRunDetails, OlistStatus, ProductAnalyticsItem, ProductComposition, ProductCompositionImportSummary, ProductCompositionPayload, ProductDetailsAnalytics, ProductionOrderDefectPayload, ProductionOrderExecutionPayload, ProductionOrderImportData, ProductionOrderItem, ProductionOrderPayload, ProductionOrderStatus, ProductionOrderSummary, ProductionPlan, ProductionPlanPayload, ProductionWorkstation, RfmAnalytics, SellerAnalyticsItem, SellerDetailsAnalytics, StockData, SupplyFabricPlan, SupplyFabricPlanPayload, SupplyInventoryAdjustmentPayload, SupplyLot, SupplyMaterialDetailsPayload, SupplyProductionExitPayload, SupplyPurchaseNeed, SupplyPurchaseOrder, SupplyPurchaseOrderPayload, SupplyPurchaseOrderStatus, SupplyReceipt, SupplyReceiptPayload, SupplySummary } from './types'; import { formatDateParam } from './dateRanges'; const API_URL = import.meta.env.VITE_API_URL || '/api'; @@ -306,6 +306,24 @@ export const updateProductionOrderExecution = async (id: number, payload: Produc return data as ProductionOrderItem; }; +export const fetchProductionWorkstations = async (): Promise => { + const response = await authFetch('/production-workstations', { cache: 'no-store' }); + if (!response.ok) throw new Error('Não foi possível carregar os postos de trabalho.'); + return response.json() as Promise; +}; + +export const addProductionOrderDefect = async (id: number, payload: ProductionOrderDefectPayload): Promise => { + const response = await authFetch(`/production-orders/${id}/defects`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + const data = await response.json().catch(() => null); + if (!response.ok) throw new Error(data?.error || 'Não foi possível registrar o defeito.'); + analyticsCache.clear(); + return data as ProductionOrderItem; +}; + export const fetchCuttingSettings = async (): Promise => { try { const response = await authFetch('/cutting-settings'); diff --git a/src/pages/ProductionOrders.tsx b/src/pages/ProductionOrders.tsx index d1a407b..7b83328 100644 --- a/src/pages/ProductionOrders.tsx +++ b/src/pages/ProductionOrders.tsx @@ -1,11 +1,11 @@ import { Fragment, type FormEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { Link, useNavigate, useOutletContext } from 'react-router-dom'; -import { ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Clock3, Download, PackageCheck, PlayCircle, Printer, Scissors, Search, Undo2, XCircle } from 'lucide-react'; +import { AlertTriangle, ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Clock3, Download, PackageCheck, PlayCircle, Printer, Scissors, Search, Undo2, XCircle } from 'lucide-react'; import DateRangePicker from '../components/DateRangePicker'; import PaginationControls from '../components/PaginationControls'; import RefreshStatus from '../components/RefreshStatus'; -import { consumeSupplyLotForProduction, exportToCSV, fetchProductionOrders, fetchSupplySummary, updateProductionOrderExecution, updateProductionOrderStatus } from '../dataService'; -import type { DateRange, ProductionOrderItem, ProductionOrderStatus, ProductionOrderSummary, SupplyLot } from '../types'; +import { addProductionOrderDefect, consumeSupplyLotForProduction, exportToCSV, fetchProductionOrders, fetchProductionWorkstations, fetchSupplySummary, updateProductionOrderExecution, updateProductionOrderStatus } from '../dataService'; +import type { DateRange, ProductionOrderItem, ProductionOrderStatus, ProductionOrderSummary, ProductionWorkstation, SupplyLot } from '../types'; type ProductionOrderStatusTab = 'all' | 'open' | 'cutting' | 'sewing' | 'review' | 'finished' | 'canceled'; @@ -140,11 +140,14 @@ const ProductionOrders = () => { const [statusFilter, setStatusFilter] = useState('all'); const [summary, setSummary] = useState(emptySummary); const [supplyLots, setSupplyLots] = useState([]); + const [workstations, setWorkstations] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isSupplyBusy, setIsSupplyBusy] = useState(false); const [busyExecutionOrderId, setBusyExecutionOrderId] = useState(null); const [supplyMessage, setSupplyMessage] = useState(''); const [executionMessage, setExecutionMessage] = useState(''); + const [busyDefectOrderId, setBusyDefectOrderId] = useState(null); + const [defectMessage, setDefectMessage] = useState(''); const [busyStatusOrderId, setBusyStatusOrderId] = useState(null); const [statusMessage, setStatusMessage] = useState(''); const [selectedOrderId, setSelectedOrderId] = useState(null); @@ -169,13 +172,15 @@ const ProductionOrders = () => { const load = async () => { setIsLoading(true); - const [nextSummary, nextSupplySummary] = await Promise.all([ + const [nextSummary, nextSupplySummary, nextWorkstations] = await Promise.all([ fetchProductionOrders(dateRange, { search: searchTerm }), - fetchSupplySummary() + fetchSupplySummary(), + fetchProductionWorkstations() ]); if (isMounted) { setSummary(nextSummary); setSupplyLots(nextSupplySummary.lots); + setWorkstations(nextWorkstations); setIsLoading(false); } }; @@ -313,6 +318,35 @@ const ProductionOrders = () => { } }; + const handleDefectSave = async (event: FormEvent, order: ProductionOrderItem) => { + event.preventDefault(); + const form = new FormData(event.currentTarget); + const quantity = Number(String(form.get('quantity') || '').replace(',', '.')); + const workstationId = Number(form.get('workstationId')); + if (!workstationId || !Number.isFinite(quantity) || quantity <= 0) return; + + setBusyDefectOrderId(order.id); + setDefectMessage(''); + try { + await addProductionOrderDefect(order.id, { + workstationId, + defectType: String(form.get('defectType') || '').trim(), + origin: String(form.get('origin') || '').trim(), + quantity, + lotCode: String(form.get('lotCode') || '').trim(), + notes: String(form.get('notes') || '').trim(), + }); + event.currentTarget.reset(); + const nextSummary = await fetchProductionOrders(dateRange, { search: searchTerm }, { force: true }); + setSummary(nextSummary); + setDefectMessage(`Defeito registrado na OP ${order.number || `#${order.id}`}.`); + } catch (error) { + setDefectMessage(error instanceof Error ? error.message : 'Não foi possível registrar o defeito.'); + } finally { + setBusyDefectOrderId(null); + } + }; + const printProductionSheet = (order: ProductionOrderItem) => { const printWindow = window.open('', '_blank', 'noopener,noreferrer'); if (!printWindow) { @@ -392,6 +426,27 @@ const ProductionOrders = () => { +
+
+

Defeitos e origem

+

Registre cada ocorrência para acompanhar perdas por posto, lote e tipo de defeito.

+
+
void handleDefectSave(event, order)} className="mt-3 rounded-xl border border-dark-border bg-dark-card p-4"> +
+ + + + + + +
+

{workstations.length ? `${workstations.length} postos ativos disponíveis.` : 'Nenhum posto de trabalho ativo cadastrado.'}

+ {defectMessage &&

{defectMessage}

} +
+ {order.defects.length > 0 &&
OcorrênciaQuantidade
{order.defects.map(defect =>

{defect.defectType}

{[defect.workstationName || 'Sem posto', defect.origin, defect.lotCode ? `Lote ${defect.lotCode}` : ''].filter(Boolean).join(' · ')}

{defect.notes &&

{defect.notes}

}

{formatQuantity(defect.quantity)} {order.unit}

)}
} + {!order.defects.length &&

Nenhum defeito registrado nesta OP.

} +
+

Composição

{order.components.length ?
{order.components.map(component => )}
MaterialPor unidadeTotal

{component.componentName}

{component.componentSku || 'Sem SKU'}

{formatQuantity(component.quantityPerUnit)} {component.unit}{formatQuantity(component.totalQuantity)} {component.unit}
:

Sem composição sincronizada.

} diff --git a/src/types.ts b/src/types.ts index b5773b3..362fed2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -90,6 +90,36 @@ export interface ProductionOrderLotReservation { lotSupplier: string; } +export interface ProductionWorkstation { + id: number; + code: string; + name: string; + color: string; + sortOrder: number; + isActive: boolean; +} + +export interface ProductionOrderDefect { + id: number; + workstationId: number | null; + workstationName: string; + defectType: string; + origin: string; + quantity: number; + lotCode: string; + notes: string; + occurredAt: string | null; +} + +export type ProductionOrderDefectPayload = { + workstationId?: number | null; + defectType: string; + origin?: string; + quantity: number; + lotCode?: string; + notes?: string; +}; + export interface ProductionOrderItem { id: number; tinyId: string; @@ -121,6 +151,7 @@ export interface ProductionOrderItem { rejectedQuantity: number; finishedQuantity: number | null; qualityNotes: string; + defects: ProductionOrderDefect[]; lotReservations: ProductionOrderLotReservation[]; markers: ProductionOrderMarker[]; components: ProductionOrderComponent[];