Add production quality and workstation screen

This commit is contained in:
Cauê Faleiros
2026-08-26 16:34:25 -03:00
parent 2c15719b13
commit da6f3b2955
6 changed files with 195 additions and 5 deletions

View File

@@ -13,6 +13,7 @@ const Cutting = React.lazy(() => import('./pages/Cutting'));
const PlanningIssues = React.lazy(() => import('./pages/PlanningIssues'));
const ProductionOrders = React.lazy(() => import('./pages/ProductionOrders'));
const ProductionPlans = React.lazy(() => import('./pages/ProductionPlans'));
const ProductionQuality = React.lazy(() => import('./pages/ProductionQuality'));
const Supplies = React.lazy(() => import('./pages/Supplies'));
const Clients = React.lazy(() => import('./pages/Clients'));
const ClientDetails = React.lazy(() => import('./pages/ClientDetails'));
@@ -66,6 +67,7 @@ function App() {
<Route path="stock-alerts" element={<Navigate to="/products" replace />} />
<Route path="production-orders" element={<ProductionOrders />} />
<Route path="production-plans" element={<ProductionPlans />} />
<Route path="production-quality" element={<ProductionQuality />} />
<Route path="clients" element={<Clients />} />
<Route path="clients/:clientToken" element={<ClientDetails />} />
<Route path="sellers" element={<Sellers />} />

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { Outlet, Link, useLocation } from 'react-router-dom';
import { LayoutDashboard, Users, BarChart3, ChevronLeft, ChevronRight, Package, LogOut, Megaphone, Grid3X3, Shield, Moon, Sun, Tags, Boxes, ClipboardList, Menu, X, BriefcaseBusiness, Radio } from 'lucide-react';
import { LayoutDashboard, Users, BarChart3, ChevronLeft, ChevronRight, Package, LogOut, Megaphone, Grid3X3, Shield, ShieldCheck, Moon, Sun, Tags, Boxes, ClipboardList, Menu, X, BriefcaseBusiness, Radio } from 'lucide-react';
import type { DateRange, OrderData } from '../types';
import { isSuperAdmin, logout } from '../dataService';
import { rangeForLastDays } from '../dateRanges';
@@ -81,6 +81,9 @@ const Layout = () => {
{ name: 'Dados Pendentes', href: '/planning-issues', icon: ClipboardList },
{ name: 'Cadastros', href: '/registrations', icon: Tags },
{ name: 'Suprimentos', href: '/supplies', icon: Boxes },
{ name: 'Planos de Produção', href: '/production-plans', icon: ClipboardList },
{ name: 'Ordens de Produção', href: '/production-orders', icon: Package },
{ name: 'Qualidade', href: '/production-quality', icon: ShieldCheck },
],
},
{

View File

@@ -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, 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 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, ProductionWorkstationPayload, 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,12 +306,34 @@ export const updateProductionOrderExecution = async (id: number, payload: Produc
return data as ProductionOrderItem;
};
export const fetchProductionWorkstations = async (): Promise<ProductionWorkstation[]> => {
const response = await authFetch('/production-workstations', { cache: 'no-store' });
export const fetchProductionWorkstations = async (includeInactive = false): Promise<ProductionWorkstation[]> => {
const response = await authFetch(`/production-workstations${includeInactive ? '?includeInactive=true' : ''}`, { 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 createProductionWorkstation = async (payload: ProductionWorkstationPayload): Promise<ProductionWorkstation> => {
const response = await authFetch('/production-workstations', {
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 criar o posto de trabalho.');
return data as ProductionWorkstation;
};
export const updateProductionWorkstation = async (id: number, payload: ProductionWorkstationPayload): Promise<ProductionWorkstation> => {
const response = await authFetch(`/production-workstations/${id}`, {
method: 'PUT',
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 atualizar o posto de trabalho.');
return data as ProductionWorkstation;
};
export const addProductionOrderDefect = async (id: number, payload: ProductionOrderDefectPayload): Promise<ProductionOrderItem> => {
const response = await authFetch(`/production-orders/${id}/defects`, {
method: 'POST',

View File

@@ -477,7 +477,7 @@ const ProductionOrders = () => {
</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 locais geradas pelo plano de corte.</p>
<Link to="/production-plans" className="mt-3 inline-flex items-center gap-2 text-sm font-bold text-brand-primary transition-colors hover:text-brand-primary/80"><ClipboardList className="h-4 w-4" />Ver planos de produção</Link>
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-2"><Link to="/production-plans" className="inline-flex items-center gap-2 text-sm font-bold text-brand-primary transition-colors hover:text-brand-primary/80"><ClipboardList className="h-4 w-4" />Ver planos de produção</Link><Link to="/production-quality" className="inline-flex items-center gap-2 text-sm font-bold text-brand-primary transition-colors hover:text-brand-primary/80"><AlertTriangle className="h-4 w-4" />Ver qualidade</Link></div>
</div>
<DateRangePicker
dateRange={dateRange}

View File

@@ -0,0 +1,155 @@
import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useOutletContext } from 'react-router-dom';
import { AlertTriangle, ArrowLeft, ClipboardCheck, Factory, Loader2, Pencil, Plus, Save, Settings2, X } from 'lucide-react';
import DateRangePicker from '../components/DateRangePicker';
import { createProductionWorkstation, fetchProductionOrders, fetchProductionWorkstations, updateProductionWorkstation } from '../dataService';
import type { DateRange, ProductionOrderSummary, ProductionWorkstation } from '../types';
const emptySummary: ProductionOrderSummary = {
orders: [],
counts: { all: 0, open: 0, cutting: 0, sewing: 0, review: 0, finished: 0, canceled: 0 }
};
const inputClassName = 'mt-1 h-10 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';
const formatQuantity = (value: number) => new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 2 }).format(value);
const formatDateTime = (value: string | null) => value ? new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(value)) : '—';
const ProductionQuality = () => {
const { dateRange, setDateRange, refreshInterval, setRefreshInterval } = useOutletContext<{
dateRange: DateRange;
setDateRange: (range: DateRange) => void;
refreshInterval: number;
setRefreshInterval: (interval: number) => void;
}>();
const [summary, setSummary] = useState<ProductionOrderSummary>(emptySummary);
const [workstations, setWorkstations] = useState<ProductionWorkstation[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [message, setMessage] = useState('');
const [editingStation, setEditingStation] = useState<ProductionWorkstation | null>(null);
const [stationForm, setStationForm] = useState({ name: '', code: '', color: '#38bdf8', sortOrder: '50', isActive: true });
const load = useCallback(async () => {
setIsLoading(true);
try {
const [nextSummary, nextWorkstations] = await Promise.all([
fetchProductionOrders(dateRange, undefined, { force: true }),
fetchProductionWorkstations(true)
]);
setSummary(nextSummary);
setWorkstations(nextWorkstations);
} catch (error) {
setMessage(error instanceof Error ? error.message : 'Não foi possível carregar a qualidade da produção.');
} finally {
setIsLoading(false);
}
}, [dateRange]);
useEffect(() => {
const timer = window.setTimeout(() => void load(), 0);
return () => window.clearTimeout(timer);
}, [load]);
useEffect(() => {
if (!refreshInterval) return undefined;
const interval = window.setInterval(() => void load(), refreshInterval);
return () => window.clearInterval(interval);
}, [load, refreshInterval]);
const defects = useMemo(() => summary.orders.flatMap(order => order.defects.map(defect => ({
...defect,
orderId: order.id,
orderNumber: order.number || `OP #${order.id}`,
productDescription: order.productDescription,
unit: order.unit
}))), [summary.orders]);
const totalDefectiveQuantity = useMemo(() => defects.reduce((total, defect) => total + defect.quantity, 0), [defects]);
const affectedOrders = useMemo(() => new Set(defects.map(defect => defect.orderId)).size, [defects]);
const workstationSummary = useMemo(() => {
const grouped = new Map<string, { label: string; quantity: number; occurrences: number }>();
defects.forEach(defect => {
const label = defect.workstationName || 'Sem posto';
const current = grouped.get(label) || { label, quantity: 0, occurrences: 0 };
current.quantity += defect.quantity;
current.occurrences += 1;
grouped.set(label, current);
});
return [...grouped.values()].sort((a, b) => b.quantity - a.quantity);
}, [defects]);
const defectTypeSummary = useMemo(() => {
const grouped = new Map<string, { label: string; quantity: number; occurrences: number }>();
defects.forEach(defect => {
const label = defect.defectType || 'Sem tipo';
const current = grouped.get(label) || { label, quantity: 0, occurrences: 0 };
current.quantity += defect.quantity;
current.occurrences += 1;
grouped.set(label, current);
});
return [...grouped.values()].sort((a, b) => b.quantity - a.quantity);
}, [defects]);
const recentDefects = useMemo(() => [...defects].sort((a, b) => new Date(b.occurredAt || 0).getTime() - new Date(a.occurredAt || 0).getTime()).slice(0, 12), [defects]);
const resetStationForm = () => {
setEditingStation(null);
setStationForm({ name: '', code: '', color: '#38bdf8', sortOrder: String((workstations.at(-1)?.sortOrder || 0) + 10), isActive: true });
};
const editStation = (station: ProductionWorkstation) => {
setEditingStation(station);
setStationForm({ name: station.name, code: station.code, color: station.color, sortOrder: String(station.sortOrder), isActive: station.isActive });
};
const saveStation = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsSaving(true);
setMessage('');
try {
const payload = { ...stationForm, sortOrder: Number(stationForm.sortOrder || 0) };
if (editingStation) await updateProductionWorkstation(editingStation.id, payload);
else await createProductionWorkstation(payload);
await load();
setMessage(editingStation ? 'Posto de trabalho atualizado.' : 'Posto de trabalho criado.');
resetStationForm();
} catch (error) {
setMessage(error instanceof Error ? error.message : 'Não foi possível salvar o posto de trabalho.');
} finally {
setIsSaving(false);
}
};
return (
<div className="space-y-6">
<div className="flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
<div>
<Link to="/production-orders" className="mb-3 inline-flex items-center gap-2 text-sm font-bold text-dark-muted transition-colors hover:text-dark-text"><ArrowLeft className="h-4 w-4" />Ordens de Produção</Link>
<h1 className="text-2xl font-bold text-dark-text">Qualidade e postos</h1>
<p className="mt-2 font-medium text-dark-muted">Acompanhe defeitos por origem e mantenha as etapas reais da operação.</p>
</div>
<DateRangePicker dateRange={dateRange} onChange={setDateRange} refreshInterval={refreshInterval} setRefreshInterval={setRefreshInterval} onManualRefresh={() => void load()} />
</div>
{message && <div className="rounded-xl border border-dark-border bg-dark-card px-4 py-3 text-sm font-bold text-dark-muted">{message}</div>}
{isLoading ? <div className="flex min-h-72 items-center justify-center rounded-2xl border border-dark-border bg-dark-card text-brand-primary"><Loader2 className="h-8 w-8 animate-spin" /></div> : <>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm"><div className="flex items-center justify-between"><p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Defeitos registrados</p><AlertTriangle className="h-5 w-5 text-amber-300" /></div><p className="mt-3 text-3xl font-bold text-dark-text">{defects.length}</p><p className="mt-1 text-xs font-semibold text-dark-muted">Ocorrências no período</p></div>
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm"><div className="flex items-center justify-between"><p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Peças afetadas</p><ClipboardCheck className="h-5 w-5 text-amber-300" /></div><p className="mt-3 text-3xl font-bold text-amber-200">{formatQuantity(totalDefectiveQuantity)}</p><p className="mt-1 text-xs font-semibold text-dark-muted">Somadas pelos apontamentos</p></div>
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm"><div className="flex items-center justify-between"><p className="text-xs font-bold uppercase tracking-widest text-dark-muted">OPs com ocorrência</p><Factory className="h-5 w-5 text-violet-300" /></div><p className="mt-3 text-3xl font-bold text-dark-text">{affectedOrders}</p><p className="mt-1 text-xs font-semibold text-dark-muted">De {summary.orders.length} OPs no período</p></div>
</div>
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
<section className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm"><div className="border-b border-dark-border p-5"><h2 className="font-bold text-dark-text">Levantamento de defeitos</h2><p className="mt-1 text-xs font-semibold text-dark-muted">Priorize o maior impacto antes de atacar ocorrências isoladas.</p></div><div className="divide-y divide-dark-border">{workstationSummary.length ? workstationSummary.map(item => <div key={item.label} className="flex items-center justify-between gap-4 p-4"><div><p className="text-sm font-bold text-dark-text">{item.label}</p><p className="mt-1 text-xs font-semibold text-dark-muted">{item.occurrences} {item.occurrences === 1 ? 'ocorrência' : 'ocorrências'}</p></div><p className="text-lg font-bold text-amber-200">{formatQuantity(item.quantity)}</p></div>) : <p className="p-5 text-sm font-semibold text-dark-muted">Nenhum defeito registrado no período.</p>}</div></section>
<section className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm"><div className="border-b border-dark-border p-5"><h2 className="font-bold text-dark-text">Tipos mais recorrentes</h2><p className="mt-1 text-xs font-semibold text-dark-muted">Use estes dados para orientar revisão, fornecedor e correção de processo.</p></div><div className="divide-y divide-dark-border">{defectTypeSummary.length ? defectTypeSummary.map(item => <div key={item.label} className="flex items-center justify-between gap-4 p-4"><div><p className="text-sm font-bold text-dark-text">{item.label}</p><p className="mt-1 text-xs font-semibold text-dark-muted">{item.occurrences} {item.occurrences === 1 ? 'ocorrência' : 'ocorrências'}</p></div><p className="text-lg font-bold text-amber-200">{formatQuantity(item.quantity)}</p></div>) : <p className="p-5 text-sm font-semibold text-dark-muted">Nenhum tipo de defeito registrado no período.</p>}</div></section>
</div>
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1.2fr)_minmax(380px,.8fr)]">
<section className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm"><div className="border-b border-dark-border p-5"><h2 className="font-bold text-dark-text">Ocorrências recentes</h2><p className="mt-1 text-xs font-semibold text-dark-muted">Rastreabilidade por OP, produto, posto e lote.</p></div><div className="divide-y divide-dark-border">{recentDefects.length ? recentDefects.map(defect => <div key={defect.id} className="grid gap-3 p-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start"><div><div className="flex flex-wrap items-center gap-2"><span className="font-mono text-xs font-bold text-brand-primary">{defect.orderNumber}</span><span className="rounded-full border border-dark-border bg-dark-input px-2 py-0.5 text-[10px] font-bold text-dark-muted">{defect.workstationName || 'Sem posto'}</span></div><p className="mt-2 text-sm font-bold text-dark-text">{defect.defectType}</p><p className="mt-1 text-xs font-semibold text-dark-muted">{defect.productDescription} · {defect.origin || 'origem não informada'}{defect.lotCode ? ` · lote ${defect.lotCode}` : ''}</p>{defect.notes && <p className="mt-1 text-xs font-medium text-dark-muted">{defect.notes}</p>}</div><div className="text-right"><p className="text-sm font-bold text-amber-200">{formatQuantity(defect.quantity)} {defect.unit}</p><p className="mt-1 text-[11px] font-semibold text-dark-muted">{formatDateTime(defect.occurredAt)}</p></div></div>) : <p className="p-5 text-sm font-semibold text-dark-muted">Nenhuma ocorrência registrada no período.</p>}</div></section>
<section className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm"><div className="flex items-center justify-between gap-3"><div><h2 className="font-bold text-dark-text">Postos de trabalho</h2><p className="mt-1 text-xs font-semibold text-dark-muted">Etapas disponíveis para o apontamento da operação.</p></div><Settings2 className="h-5 w-5 text-brand-primary" /></div><form onSubmit={saveStation} className="mt-5 rounded-xl border border-dark-border bg-dark-input/40 p-3"><div className="flex items-center justify-between gap-3"><p className="text-xs font-bold text-dark-text">{editingStation ? 'Editar posto' : 'Novo posto'}</p>{editingStation && <button type="button" onClick={resetStationForm} className="inline-flex h-7 items-center gap-1 text-xs font-bold text-dark-muted hover:text-dark-text cursor-pointer"><X className="h-3.5 w-3.5" />Cancelar</button>}</div><div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-1"><label className="text-xs font-bold text-dark-muted">Nome<input required value={stationForm.name} onChange={(event) => setStationForm(current => ({ ...current, name: event.target.value }))} className={inputClassName} placeholder="Ex.: Bordado" /></label><label className="text-xs font-bold text-dark-muted">Código (opcional)<input value={stationForm.code} onChange={(event) => setStationForm(current => ({ ...current, code: event.target.value }))} className={inputClassName} placeholder="bordado" /></label><div className="grid grid-cols-2 gap-3"><label className="text-xs font-bold text-dark-muted">Ordem<input inputMode="numeric" value={stationForm.sortOrder} onChange={(event) => setStationForm(current => ({ ...current, sortOrder: event.target.value }))} className={inputClassName} /></label><label className="text-xs font-bold text-dark-muted">Cor<input type="color" value={stationForm.color} onChange={(event) => setStationForm(current => ({ ...current, color: event.target.value }))} className="mt-1 h-10 w-full cursor-pointer rounded-lg border border-dark-border bg-dark-input p-1" /></label></div><label className="flex items-center gap-2 text-xs font-bold text-dark-muted"><input type="checkbox" checked={stationForm.isActive} onChange={(event) => setStationForm(current => ({ ...current, isActive: event.target.checked }))} className="h-4 w-4 accent-brand-primary" />Disponível para novos apontamentos</label></div><button type="submit" disabled={isSaving} className="mt-4 inline-flex h-10 w-full items-center justify-center gap-2 rounded-lg border border-brand-primary/35 bg-brand-primary/10 px-3 text-sm font-bold text-brand-primary transition-colors hover:bg-brand-primary/20 disabled:opacity-50 cursor-pointer">{editingStation ? <Save className="h-4 w-4" /> : <Plus className="h-4 w-4" />}{isSaving ? 'Salvando...' : editingStation ? 'Salvar posto' : 'Adicionar posto'}</button></form><div className="mt-4 divide-y divide-dark-border rounded-xl border border-dark-border">{workstations.map(station => <div key={station.id} className="flex items-center gap-3 p-3"><span className="h-3 w-3 shrink-0 rounded-full" style={{ backgroundColor: station.color }} /><div className="min-w-0 flex-1"><p className="truncate text-sm font-bold text-dark-text">{station.name}</p><p className="text-xs font-semibold text-dark-muted">{station.code} · posição {station.sortOrder}{station.isActive ? '' : ' · inativo'}</p></div><button type="button" onClick={() => editStation(station)} className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-dark-border bg-dark-input text-dark-muted transition-colors hover:border-brand-primary hover:text-brand-primary cursor-pointer" title={`Editar ${station.name}`}><Pencil className="h-3.5 w-3.5" /></button></div>)}</div></section>
</div>
</>}
</div>
);
};
export default ProductionQuality;

View File

@@ -99,6 +99,14 @@ export interface ProductionWorkstation {
isActive: boolean;
}
export type ProductionWorkstationPayload = {
code?: string;
name: string;
color?: string;
sortOrder?: number;
isActive?: boolean;
};
export interface ProductionOrderDefect {
id: number;
workstationId: number | null;