Add production order defect capture
This commit is contained in:
@@ -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';
|
import { formatDateParam } from './dateRanges';
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || '/api';
|
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;
|
return data as ProductionOrderItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchProductionWorkstations = async (): Promise<ProductionWorkstation[]> => {
|
||||||
|
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<ProductionWorkstation[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addProductionOrderDefect = async (id: number, payload: ProductionOrderDefectPayload): Promise<ProductionOrderItem> => {
|
||||||
|
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<CuttingSettings> => {
|
export const fetchCuttingSettings = async (): Promise<CuttingSettings> => {
|
||||||
try {
|
try {
|
||||||
const response = await authFetch('/cutting-settings');
|
const response = await authFetch('/cutting-settings');
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Fragment, type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
import { Fragment, type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useNavigate, useOutletContext } from 'react-router-dom';
|
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 DateRangePicker from '../components/DateRangePicker';
|
||||||
import PaginationControls from '../components/PaginationControls';
|
import PaginationControls from '../components/PaginationControls';
|
||||||
import RefreshStatus from '../components/RefreshStatus';
|
import RefreshStatus from '../components/RefreshStatus';
|
||||||
import { consumeSupplyLotForProduction, exportToCSV, fetchProductionOrders, fetchSupplySummary, updateProductionOrderExecution, updateProductionOrderStatus } from '../dataService';
|
import { addProductionOrderDefect, consumeSupplyLotForProduction, exportToCSV, fetchProductionOrders, fetchProductionWorkstations, fetchSupplySummary, updateProductionOrderExecution, updateProductionOrderStatus } from '../dataService';
|
||||||
import type { DateRange, ProductionOrderItem, ProductionOrderStatus, ProductionOrderSummary, SupplyLot } from '../types';
|
import type { DateRange, ProductionOrderItem, ProductionOrderStatus, ProductionOrderSummary, ProductionWorkstation, SupplyLot } from '../types';
|
||||||
|
|
||||||
type ProductionOrderStatusTab = 'all' | 'open' | 'cutting' | 'sewing' | 'review' | 'finished' | 'canceled';
|
type ProductionOrderStatusTab = 'all' | 'open' | 'cutting' | 'sewing' | 'review' | 'finished' | 'canceled';
|
||||||
|
|
||||||
@@ -140,11 +140,14 @@ const ProductionOrders = () => {
|
|||||||
const [statusFilter, setStatusFilter] = useState<ProductionOrderStatusTab>('all');
|
const [statusFilter, setStatusFilter] = useState<ProductionOrderStatusTab>('all');
|
||||||
const [summary, setSummary] = useState<ProductionOrderSummary>(emptySummary);
|
const [summary, setSummary] = useState<ProductionOrderSummary>(emptySummary);
|
||||||
const [supplyLots, setSupplyLots] = useState<SupplyLot[]>([]);
|
const [supplyLots, setSupplyLots] = useState<SupplyLot[]>([]);
|
||||||
|
const [workstations, setWorkstations] = useState<ProductionWorkstation[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSupplyBusy, setIsSupplyBusy] = useState(false);
|
const [isSupplyBusy, setIsSupplyBusy] = useState(false);
|
||||||
const [busyExecutionOrderId, setBusyExecutionOrderId] = useState<number | null>(null);
|
const [busyExecutionOrderId, setBusyExecutionOrderId] = useState<number | null>(null);
|
||||||
const [supplyMessage, setSupplyMessage] = useState('');
|
const [supplyMessage, setSupplyMessage] = useState('');
|
||||||
const [executionMessage, setExecutionMessage] = useState('');
|
const [executionMessage, setExecutionMessage] = useState('');
|
||||||
|
const [busyDefectOrderId, setBusyDefectOrderId] = useState<number | null>(null);
|
||||||
|
const [defectMessage, setDefectMessage] = useState('');
|
||||||
const [busyStatusOrderId, setBusyStatusOrderId] = useState<number | null>(null);
|
const [busyStatusOrderId, setBusyStatusOrderId] = useState<number | null>(null);
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null);
|
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null);
|
||||||
@@ -169,13 +172,15 @@ const ProductionOrders = () => {
|
|||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const [nextSummary, nextSupplySummary] = await Promise.all([
|
const [nextSummary, nextSupplySummary, nextWorkstations] = await Promise.all([
|
||||||
fetchProductionOrders(dateRange, { search: searchTerm }),
|
fetchProductionOrders(dateRange, { search: searchTerm }),
|
||||||
fetchSupplySummary()
|
fetchSupplySummary(),
|
||||||
|
fetchProductionWorkstations()
|
||||||
]);
|
]);
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
setSummary(nextSummary);
|
setSummary(nextSummary);
|
||||||
setSupplyLots(nextSupplySummary.lots);
|
setSupplyLots(nextSupplySummary.lots);
|
||||||
|
setWorkstations(nextWorkstations);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -313,6 +318,35 @@ const ProductionOrders = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDefectSave = async (event: FormEvent<HTMLFormElement>, 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 printProductionSheet = (order: ProductionOrderItem) => {
|
||||||
const printWindow = window.open('', '_blank', 'noopener,noreferrer');
|
const printWindow = window.open('', '_blank', 'noopener,noreferrer');
|
||||||
if (!printWindow) {
|
if (!printWindow) {
|
||||||
@@ -392,6 +426,27 @@ const ProductionOrders = () => {
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Defeitos e origem</h3>
|
||||||
|
<p className="mt-1 text-xs font-semibold text-dark-muted">Registre cada ocorrência para acompanhar perdas por posto, lote e tipo de defeito.</p>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={(event) => void handleDefectSave(event, order)} className="mt-3 rounded-xl border border-dark-border bg-dark-card p-4">
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<label className="text-xs font-bold text-dark-muted">Posto de trabalho<select required name="workstationId" defaultValue="" className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none focus:border-brand-primary"><option value="">Selecione...</option>{workstations.map(workstation => <option key={workstation.id} value={workstation.id}>{workstation.name}</option>)}</select></label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted">Tipo de defeito<input required name="defectType" className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Ex.: furo, mancha" /></label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted">Quantidade<input required name="quantity" inputMode="decimal" className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="0" /></label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted">Lote (opcional)<input name="lotCode" defaultValue={order.lotCode} className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Código do lote" /></label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted sm:col-span-2">Origem (opcional)<input name="origin" className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Ex.: recebimento, corte, costura" /></label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted sm:col-span-2">Observação (opcional)<input name="notes" className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Detalhe necessário para a revisão" /></label>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex flex-wrap items-center justify-between gap-3"><p className="text-xs font-semibold text-dark-muted">{workstations.length ? `${workstations.length} postos ativos disponíveis.` : 'Nenhum posto de trabalho ativo cadastrado.'}</p><button type="submit" disabled={busyDefectOrderId === order.id || !workstations.length} className="inline-flex h-10 items-center gap-2 rounded-lg border border-amber-400/35 bg-amber-400/10 px-4 text-sm font-bold text-amber-200 transition-colors hover:bg-amber-400/20 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"><AlertTriangle className="h-4 w-4" />{busyDefectOrderId === order.id ? 'Registrando...' : 'Registrar defeito'}</button></div>
|
||||||
|
{defectMessage && <p className="mt-3 rounded-lg border border-dark-border bg-dark-input px-3 py-2 text-sm font-bold text-dark-muted">{defectMessage}</p>}
|
||||||
|
</form>
|
||||||
|
{order.defects.length > 0 && <div className="mt-3 overflow-hidden rounded-xl border border-dark-border bg-dark-card"><div className="grid grid-cols-[minmax(0,1fr)_auto] gap-3 border-b border-dark-border bg-dark-input/40 px-4 py-2.5 text-[10px] font-bold uppercase tracking-widest text-dark-muted"><span>Ocorrência</span><span>Quantidade</span></div><div className="divide-y divide-dark-border">{order.defects.map(defect => <div key={defect.id} className="grid grid-cols-[minmax(0,1fr)_auto] gap-3 px-4 py-3"><div><p className="text-sm font-bold text-dark-text">{defect.defectType}</p><p className="mt-1 text-xs font-semibold text-dark-muted">{[defect.workstationName || 'Sem posto', defect.origin, defect.lotCode ? `Lote ${defect.lotCode}` : ''].filter(Boolean).join(' · ')}</p>{defect.notes && <p className="mt-1 text-xs font-medium text-dark-muted">{defect.notes}</p>}</div><p className="text-sm font-bold text-amber-200">{formatQuantity(defect.quantity)} {order.unit}</p></div>)}</div></div>}
|
||||||
|
{!order.defects.length && <p className="mt-3 rounded-xl border border-dark-border bg-dark-card px-4 py-3 text-sm font-semibold text-dark-muted">Nenhum defeito registrado nesta OP.</p>}
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Composição</h3>
|
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Composição</h3>
|
||||||
{order.components.length ? <div className="mt-3 overflow-hidden rounded-xl border border-dark-border"><table className="w-full text-xs"><thead className="bg-dark-header text-dark-muted"><tr><th className="px-3 py-2.5 text-left font-bold">Material</th><th className="px-3 py-2.5 text-right font-bold">Por unidade</th><th className="px-3 py-2.5 text-right font-bold">Total</th></tr></thead><tbody className="divide-y divide-dark-border">{order.components.map(component => <tr key={component.id}><td className="px-3 py-3"><p className="font-bold text-dark-text">{component.componentName}</p><p className="mt-0.5 font-mono text-[10px] font-semibold text-dark-muted">{component.componentSku || 'Sem SKU'}</p></td><td className="px-3 py-3 text-right font-semibold text-dark-muted">{formatQuantity(component.quantityPerUnit)} {component.unit}</td><td className="px-3 py-3 text-right font-bold text-dark-text">{formatQuantity(component.totalQuantity)} {component.unit}</td></tr>)}</tbody></table></div> : <p className="mt-3 rounded-xl border border-dark-border bg-dark-card px-4 py-3 text-sm font-semibold text-dark-muted">Sem composição sincronizada.</p>}
|
{order.components.length ? <div className="mt-3 overflow-hidden rounded-xl border border-dark-border"><table className="w-full text-xs"><thead className="bg-dark-header text-dark-muted"><tr><th className="px-3 py-2.5 text-left font-bold">Material</th><th className="px-3 py-2.5 text-right font-bold">Por unidade</th><th className="px-3 py-2.5 text-right font-bold">Total</th></tr></thead><tbody className="divide-y divide-dark-border">{order.components.map(component => <tr key={component.id}><td className="px-3 py-3"><p className="font-bold text-dark-text">{component.componentName}</p><p className="mt-0.5 font-mono text-[10px] font-semibold text-dark-muted">{component.componentSku || 'Sem SKU'}</p></td><td className="px-3 py-3 text-right font-semibold text-dark-muted">{formatQuantity(component.quantityPerUnit)} {component.unit}</td><td className="px-3 py-3 text-right font-bold text-dark-text">{formatQuantity(component.totalQuantity)} {component.unit}</td></tr>)}</tbody></table></div> : <p className="mt-3 rounded-xl border border-dark-border bg-dark-card px-4 py-3 text-sm font-semibold text-dark-muted">Sem composição sincronizada.</p>}
|
||||||
|
|||||||
31
src/types.ts
31
src/types.ts
@@ -90,6 +90,36 @@ export interface ProductionOrderLotReservation {
|
|||||||
lotSupplier: string;
|
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 {
|
export interface ProductionOrderItem {
|
||||||
id: number;
|
id: number;
|
||||||
tinyId: string;
|
tinyId: string;
|
||||||
@@ -121,6 +151,7 @@ export interface ProductionOrderItem {
|
|||||||
rejectedQuantity: number;
|
rejectedQuantity: number;
|
||||||
finishedQuantity: number | null;
|
finishedQuantity: number | null;
|
||||||
qualityNotes: string;
|
qualityNotes: string;
|
||||||
|
defects: ProductionOrderDefect[];
|
||||||
lotReservations: ProductionOrderLotReservation[];
|
lotReservations: ProductionOrderLotReservation[];
|
||||||
markers: ProductionOrderMarker[];
|
markers: ProductionOrderMarker[];
|
||||||
components: ProductionOrderComponent[];
|
components: ProductionOrderComponent[];
|
||||||
|
|||||||
Reference in New Issue
Block a user