Streamline production order workflow
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 44s

This commit is contained in:
Cauê Faleiros
2026-08-27 20:30:15 -03:00
parent 9592b2eb7d
commit 5e742feb60

View File

@@ -1,6 +1,6 @@
import { Fragment, type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useNavigate, useOutletContext } from 'react-router-dom';
import { AlertTriangle, 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, X, XCircle } from 'lucide-react';
import DateRangePicker from '../components/DateRangePicker';
import PaginationControls from '../components/PaginationControls';
import RefreshStatus from '../components/RefreshStatus';
@@ -88,6 +88,16 @@ const getStatusStyle = (status: ProductionOrderStatus, fallbackLabel: string) =>
}
);
const getNextStage = (status: ProductionOrderStatus): { status: Exclude<ProductionOrderStatusTab, 'all'>; label: string; description: string } | null => {
switch (status) {
case 'open': return { status: 'cutting', label: 'Iniciar corte', description: 'Libera o apontamento de corte e a baixa física de materiais.' };
case 'cutting': return { status: 'sewing', label: 'Avançar para costura', description: 'Use quando o corte desta OP estiver concluído.' };
case 'sewing': return { status: 'review', label: 'Enviar para revisão', description: 'Use quando as peças estiverem prontas para a conferência final.' };
case 'review': return { status: 'finished', label: 'Finalizar OP', description: 'Confirme a quantidade aprovada antes de encerrar a ordem.' };
default: return null;
}
};
const ProductionOrdersSkeleton = () => (
<div className="space-y-6" aria-label="Carregando ordens de produção">
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
@@ -216,8 +226,10 @@ const ProductionOrders = () => {
const openCount = summary.counts.open || 0;
const progressCount = (summary.counts.cutting || 0) + (summary.counts.sewing || 0) + (summary.counts.review || 0);
const finishedCount = summary.counts.finished || 0;
const totalQuantity = summary.orders.reduce((total, order) => total + order.quantity, 0);
const finishedQuantity = summary.orders.filter(order => order.status === 'finished').reduce((total, order) => total + (order.finishedQuantity ?? order.quantity), 0);
const selectedOrder = summary.orders.find(order => order.id === selectedOrderId) || null;
const selectedOrderNextStage = selectedOrder ? getNextStage(selectedOrder.status) : null;
const selectedOrderCanReturnToCutPlan = Boolean(selectedOrder && selectedOrder.status === 'open' && (selectedOrder.integrationStatus || '').toLowerCase() === 'local');
const handleManualRefresh = () => {
void loadProductionOrders({ force: true });
@@ -379,18 +391,10 @@ const ProductionOrders = () => {
};
const renderOrderDetails = (order: ProductionOrderItem) => {
const canReturnToCutPlan = order.status === 'open' && (order.integrationStatus || '').toLowerCase() === 'local';
const canRecordOperationalData = ['cutting', 'sewing', 'review'].includes(order.status);
return (
<div className="space-y-6 rounded-xl border border-dark-border bg-dark-input/25 p-5">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Detalhes da OP</p>
<p className="mt-1 text-sm font-bold text-dark-text">{order.number || `OP #${order.id}`} · {order.productDescription}</p>
</div>
{canReturnToCutPlan && <button type="button" onClick={() => void returnOrderToCutPlan(order)} disabled={busyStatusOrderId === order.id} className="inline-flex h-9 shrink-0 items-center justify-center gap-2 rounded-lg border border-amber-400/30 bg-amber-400/10 px-3 text-xs font-bold text-amber-200 transition-colors hover:border-amber-400/60 hover:bg-amber-400/15 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"><Undo2 className="h-4 w-4" />Devolver ao Plano de Corte</button>}
</div>
<div className="space-y-6">
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
<div className="rounded-xl border border-dark-border bg-dark-card p-3"><p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Quantidade</p><p className="mt-1 text-lg font-bold text-dark-text">{formatQuantity(order.quantity)} <span className="text-xs text-dark-muted">{order.unit}</span></p></div>
<div className="relative rounded-xl border border-dark-border bg-dark-card p-3"><p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Status</p><button type="button" onClick={() => setStatusEditorOrderId(current => current === order.id ? null : order.id)} aria-expanded={statusEditorOrderId === order.id} className={`mt-2 inline-flex cursor-pointer items-center gap-2 rounded-lg border px-2.5 py-1 text-xs font-bold ${getStatusStyle(order.status, order.statusLabel).className}`}><span className={`h-2 w-2 rounded-full ${getStatusStyle(order.status, order.statusLabel).dotClass}`} />{getStatusStyle(order.status, order.statusLabel).label}{statusEditorOrderId === order.id ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}</button>{statusEditorOrderId === order.id && <div className="absolute left-0 top-full z-20 mt-2 w-64 overflow-hidden rounded-xl border border-dark-border bg-dark-card shadow-xl"><p className="border-b border-dark-border px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-dark-muted">Alterar status</p>{editableStatusOptions.map(option => { const style = getStatusStyle(option.value, option.label); const isCurrent = order.status === option.value; const Icon = option.value === 'finished' ? CheckCircle2 : option.value === 'canceled' ? XCircle : option.value === 'cutting' ? Scissors : option.value === 'sewing' ? PlayCircle : option.value === 'review' ? ClipboardList : Clock3; return <button key={option.value} type="button" disabled={isCurrent || busyStatusOrderId === order.id} onClick={() => void handleStatusChange(order, option.value)} className={`flex w-full cursor-pointer items-center gap-3 px-3 py-2.5 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60 ${isCurrent ? style.className : 'hover:bg-dark-input/70'}`}><Icon className="h-4 w-4 shrink-0" /><span className="text-xs font-bold text-dark-text">{option.label}{isCurrent && ' · Atual'}</span></button>; })}</div>}</div>
@@ -398,6 +402,8 @@ const ProductionOrders = () => {
<div className="rounded-xl border border-dark-border bg-dark-card p-3"><p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Previsão</p><p className="mt-1 text-sm font-bold text-dark-text">{formatDate(order.expectedDate)}</p></div>
</div>
{order.status === 'finished' && <section><h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Resultado registrado</h3><div className="mt-3 grid gap-3 sm:grid-cols-3"><div className="rounded-xl border border-dark-border bg-dark-card p-3"><p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Cortado</p><p className="mt-1 text-lg font-bold text-dark-text">{formatQuantity(order.actualCutQuantity ?? 0)} <span className="text-xs text-dark-muted">{order.unit}</span></p></div><div className="rounded-xl border border-dark-border bg-dark-card p-3"><p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Perdas</p><p className="mt-1 text-lg font-bold text-amber-200">{formatQuantity(order.rejectedQuantity || 0)} <span className="text-xs text-dark-muted">{order.unit}</span></p></div><div className="rounded-xl border border-dark-border bg-dark-card p-3"><p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Aprovado</p><p className="mt-1 text-lg font-bold text-emerald-300">{formatQuantity(order.finishedQuantity ?? 0)} <span className="text-xs text-dark-muted">{order.unit}</span></p></div></div>{(order.markerReference || order.qualityNotes) && <div className="mt-3 rounded-xl border border-dark-border bg-dark-card p-3 text-xs font-semibold text-dark-muted">{order.markerReference && <p>Referência do risco: <span className="font-bold text-dark-text">{order.markerReference}</span></p>}{order.qualityNotes && <p className={order.markerReference ? 'mt-2' : ''}>{order.qualityNotes}</p>}</div>}</section>}
<section>
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Produto e marcadores</h3>
<div className="mt-3 rounded-xl border border-dark-border bg-dark-card p-4">
@@ -407,14 +413,8 @@ const ProductionOrders = () => {
</div>
</section>
<section>
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Execução e qualidade</h3>
<p className="mt-1 text-xs font-semibold text-dark-muted">Registre o corte real, perdas e a quantidade aprovada antes de finalizar.</p>
</div>
<button type="button" onClick={() => printProductionSheet(order)} className="inline-flex h-9 items-center gap-2 rounded-lg border border-dark-border bg-dark-card px-3 text-xs font-bold text-dark-text transition-colors hover:border-brand-primary hover:text-brand-primary cursor-pointer"><Printer className="h-4 w-4" />Imprimir ficha</button>
</div>
{canRecordOperationalData && <section>
<div><h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Execução e qualidade</h3><p className="mt-1 text-xs font-semibold text-dark-muted">Registre o corte real, perdas e a quantidade aprovada antes de avançar a OP.</p></div>
<form onSubmit={(event) => void handleExecutionSave(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-3">
<label className="text-xs font-bold text-dark-muted">Cortado real<input name="actualCutQuantity" inputMode="decimal" defaultValue={order.actualCutQuantity ?? ''} 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={formatQuantity(order.quantity)} /></label>
@@ -426,14 +426,14 @@ const ProductionOrders = () => {
<div className="mt-4 flex flex-wrap items-center justify-between gap-3"><p className="text-xs font-semibold text-dark-muted">Planejado: {formatQuantity(order.quantity)} {order.unit}</p><button type="submit" disabled={busyExecutionOrderId === order.id} className="inline-flex h-10 items-center gap-2 rounded-lg border border-brand-primary/35 bg-brand-primary/10 px-4 text-sm font-bold text-brand-primary transition-colors hover:bg-brand-primary/20 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"><CheckCircle2 className="h-4 w-4" />{busyExecutionOrderId === order.id ? 'Salvando...' : 'Salvar execução'}</button></div>
{executionMessage && <p className="mt-3 rounded-lg border border-dark-border bg-dark-input px-3 py-2 text-sm font-bold text-dark-muted">{executionMessage}</p>}
</form>
</section>
</section>}
<section>
{(canRecordOperationalData || order.defects.length > 0) && <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">
{canRecordOperationalData && <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>
@@ -444,10 +444,10 @@ const ProductionOrders = () => {
</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>
</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>
{!order.defects.length && canRecordOperationalData && <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>
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Composição</h3>
@@ -459,10 +459,10 @@ const ProductionOrders = () => {
<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">Lote</th><th className="px-3 py-2.5 text-left font-bold">Material</th><th className="px-3 py-2.5 text-right font-bold">Reservado</th><th className="px-3 py-2.5 text-right font-bold">Consumido</th></tr></thead><tbody className="divide-y divide-dark-border">{order.lotReservations.map(reservation => <tr key={reservation.id}><td className="px-3 py-3"><p className="font-bold text-dark-text">#{reservation.lotId}</p><p className="mt-0.5 text-[10px] font-semibold text-dark-muted">{reservation.lotSupplier || 'Sem fornecedor'}</p></td><td className="px-3 py-3"><p className="font-bold text-dark-text">{reservation.lotProduct}</p><p className="mt-0.5 font-mono text-[10px] font-semibold text-dark-muted">{reservation.materialKey}</p></td><td className="px-3 py-3 text-right font-bold text-brand-primary">{formatQuantity(reservation.quantity)} {reservation.lotUnit}</td><td className="px-3 py-3 text-right font-semibold text-dark-muted">{formatQuantity(reservation.consumedQuantity)} {reservation.lotUnit}</td></tr>)}</tbody></table></div>
</section>}
<section>
{order.status === 'cutting' && <section>
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Baixa de material</h3>
{order.status === 'cutting' ? <form onSubmit={handleProductionExit} className="mt-3 rounded-xl border border-dark-border bg-dark-card p-4"><p className="text-sm font-semibold text-dark-muted">Registre uma saída somente quando o material for realmente consumido pela OP.</p><div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2"><label className="text-xs font-bold text-dark-muted">Lote de material<select value={exitForm.lotId} onChange={(event) => setExitForm(current => ({ ...current, lotId: event.target.value }))} 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>{supplyLots.map(lot => <option key={lot.id} value={lot.id}>#{lot.id} · {lot.product} · {formatQuantity(lot.quantity)} {lot.unit}</option>)}</select></label><label className="text-xs font-bold text-dark-muted">Quantidade<input inputMode="decimal" value={exitForm.quantity} onChange={(event) => setExitForm(current => ({ ...current, quantity: event.target.value }))} 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="kg" /></label><label className="sm:col-span-2 text-xs font-bold text-dark-muted">Motivo (opcional)<input value={exitForm.reason} onChange={(event) => setExitForm(current => ({ ...current, reason: event.target.value }))} 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.: corte do pedido" /></label></div>{selectedLot && <p className="mt-3 text-xs font-bold text-dark-muted">Saldo do lote #{selectedLot.id}: {formatQuantity(selectedLot.quantity)} {selectedLot.unit}</p>}<button type="submit" disabled={isSupplyBusy || !supplyLots.length || !exitForm.lotId || !exitForm.quantity} className="mt-4 inline-flex h-11 items-center gap-2 rounded-lg border border-brand-primary/35 bg-brand-primary/10 px-4 text-sm font-bold text-brand-primary transition-colors hover:bg-brand-primary/20 disabled:cursor-not-allowed disabled:opacity-50"><PackageCheck className="h-4 w-4" />{isSupplyBusy ? 'Registrando...' : 'Registrar baixa'}</button>{supplyMessage && <p className="mt-3 rounded-lg border border-dark-border bg-dark-input px-3 py-2 text-sm font-bold text-dark-muted">{supplyMessage}</p>}</form> : <div className="mt-3 rounded-xl border border-dark-border bg-dark-card px-4 py-3 text-sm font-semibold text-dark-muted">Altere a OP para <span className="font-bold text-amber-200">Em corte</span> antes de registrar a baixa de materiais.</div>}
</section>
</section>}
{(order.steps.length > 0 || order.notes || order.supplier || order.lotCode || order.rollQuantity || order.fabricKg || order.ribKg || order.yieldPiecesPerKg) && <section><h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Informações adicionais</h3><div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">{(order.supplier || order.lotCode || order.rollQuantity || order.fabricKg || order.ribKg || order.yieldPiecesPerKg) && <dl className="grid grid-cols-2 gap-3 rounded-xl border border-dark-border bg-dark-card p-4 text-xs sm:col-span-2">{order.supplier && <div><dt className="font-bold text-dark-muted">Fornecedor</dt><dd className="mt-1 font-bold text-dark-text">{order.supplier}</dd></div>}{order.lotCode && <div><dt className="font-bold text-dark-muted">Lote</dt><dd className="mt-1 font-bold text-dark-text">{order.lotCode}</dd></div>}{order.rollQuantity !== null && <div><dt className="font-bold text-dark-muted">Rolos</dt><dd className="mt-1 font-bold text-dark-text">{formatQuantity(order.rollQuantity)}</dd></div>}{order.fabricKg !== null && <div><dt className="font-bold text-dark-muted">Malha</dt><dd className="mt-1 font-bold text-dark-text">{formatQuantity(order.fabricKg)} kg</dd></div>}{order.ribKg !== null && <div><dt className="font-bold text-dark-muted">Ribana</dt><dd className="mt-1 font-bold text-dark-text">{formatQuantity(order.ribKg)} kg</dd></div>}{order.yieldPiecesPerKg !== null && <div><dt className="font-bold text-dark-muted">Rendimento</dt><dd className="mt-1 font-bold text-dark-text">{formatQuantity(order.yieldPiecesPerKg)} un./kg</dd></div>}</dl>}{order.steps.length > 0 && <div className="rounded-xl border border-dark-border bg-dark-card p-4 sm:col-span-2"><p className="text-xs font-bold text-dark-text">Etapas</p><div className="mt-3 divide-y divide-dark-border">{order.steps.map(step => <div key={step.id} className="flex items-center gap-3 py-2 text-xs"><span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: step.color || 'var(--color-brand-primary)' }} /><span className="font-bold text-dark-text">{step.name}</span><span className="ml-auto font-semibold text-dark-muted">{formatDate(step.startDate)} - {formatDate(step.endDate)}</span></div>)}</div></div>}{order.notes && <p className="whitespace-pre-wrap rounded-xl border border-dark-border bg-dark-card p-4 text-sm font-semibold text-dark-muted sm:col-span-2">{order.notes}</p>}</div></section>}
</div>
@@ -478,7 +478,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 locais geradas pelo plano de corte.</p>
<p className="mt-2 text-dark-muted font-medium">Acompanhe a fila de produção e o histórico das ordens.</p>
<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
@@ -541,7 +541,7 @@ const ProductionOrders = () => {
<div>
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Finalizadas</p>
<p className="mt-2 text-3xl font-bold text-emerald-300">{finishedCount}</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">{formatQuantity(totalQuantity)} un. no período</p>
<p className="mt-1 text-xs font-semibold text-dark-muted">{formatQuantity(finishedQuantity)} un. aprovadas</p>
</div>
<div className="rounded-xl border border-emerald-400/25 bg-emerald-400/10 p-3 text-emerald-300">
<CheckCircle2 className="h-5 w-5" />
@@ -658,8 +658,7 @@ const ProductionOrders = () => {
{paginatedOrders.map((order: ProductionOrderItem) => {
const statusStyle = getStatusStyle(order.status, order.statusLabel);
return (
<Fragment key={order.id}>
<tr className="transition-colors hover:bg-dark-input/50">
<tr key={order.id} className="transition-colors hover:bg-dark-input/50">
<td className="px-6 py-3 font-mono text-xs font-bold text-dark-text">{order.number || '-'}</td>
<td className="px-6 py-3">
<div className="font-bold text-dark-text">{order.productDescription}</div>
@@ -684,21 +683,19 @@ const ProductionOrders = () => {
<button
type="button"
onClick={() => {
setSelectedOrderId(current => current === order.id ? null : order.id);
setSelectedOrderId(order.id);
setSupplyMessage('');
setExitForm({ lotId: '', quantity: '', reason: '' });
}}
title={selectedOrderId === order.id ? 'Fechar detalhes da OP' : 'Abrir detalhes da OP'}
aria-label={`${selectedOrderId === order.id ? 'Fechar' : 'Abrir'} detalhes da OP ${order.number || order.id}`}
aria-expanded={selectedOrderId === order.id}
className={`inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-lg border transition-colors hover:border-brand-primary hover:text-brand-primary ${selectedOrderId === order.id ? 'border-brand-primary bg-brand-primary/10 text-brand-primary' : 'border-dark-border bg-dark-input text-dark-muted'}`}
title="Abrir ficha da OP"
aria-label={`Abrir ficha da OP ${order.number || order.id}`}
className="inline-flex h-9 items-center justify-center gap-2 rounded-lg border border-dark-border bg-dark-input px-3 text-xs font-bold text-dark-muted transition-colors hover:border-brand-primary hover:text-brand-primary cursor-pointer"
>
{selectedOrderId === order.id ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
Abrir OP
<ChevronDown className="h-4 w-4" />
</button>
</td>
</tr>
{selectedOrderId === order.id && <tr className="bg-dark-input/20"><td colSpan={5} className="px-6 pb-5 pt-3">{renderOrderDetails(order)}</td></tr>}
</Fragment>
);
})}
</tbody>
@@ -717,7 +714,7 @@ const ProductionOrders = () => {
</div>
)}
<PaginationControls
{totalPages > 1 && <PaginationControls
totalItems={filteredOrders.length}
currentPage={safeCurrentPage}
totalPages={totalPages}
@@ -732,13 +729,28 @@ const ProductionOrders = () => {
setItemsPerPage(pageSize);
setCurrentPage(1);
}}
/>
/>}
</div>
</>
)}
</div>
)}
{selectedOrder && <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm" role="dialog" aria-modal="true" aria-label={`Ficha da OP ${selectedOrder.number || selectedOrder.id}`} onClick={() => busyStatusOrderId !== selectedOrder.id && setSelectedOrderId(null)}>
<section className="flex max-h-[calc(100vh-2rem)] w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-2xl" onClick={(event) => event.stopPropagation()}>
<header className="flex items-start justify-between gap-4 border-b border-dark-border px-5 py-4">
<div className="min-w-0"><div className="flex flex-wrap items-center gap-2"><span className="font-mono text-xs font-bold text-brand-primary">{selectedOrder.number || `OP #${selectedOrder.id}`}</span><span className={`inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-[10px] font-bold ${getStatusStyle(selectedOrder.status, selectedOrder.statusLabel).className}`}><span className={`h-1.5 w-1.5 rounded-full ${getStatusStyle(selectedOrder.status, selectedOrder.statusLabel).dotClass}`} />{getStatusStyle(selectedOrder.status, selectedOrder.statusLabel).label}</span></div><h2 className="mt-2 truncate text-lg font-bold text-dark-text" title={selectedOrder.productDescription}>{selectedOrder.productDescription}</h2><div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs font-semibold text-dark-muted"><span className="font-mono">{selectedOrder.productSku || 'Sem SKU'}</span>{selectedOrder.productionPlanNumber && <Link to="/production-plans" className="text-brand-primary hover:underline">{selectedOrder.productionPlanNumber}</Link>}<span>Previsão: {formatDate(selectedOrder.expectedDate)}</span></div></div>
<div className="flex shrink-0 items-center gap-2"><button type="button" onClick={() => printProductionSheet(selectedOrder)} title="Imprimir ficha" aria-label="Imprimir ficha" className="inline-flex h-9 w-9 items-center justify-center rounded-xl border border-dark-border bg-dark-input text-dark-muted transition-colors hover:border-brand-primary hover:text-brand-primary cursor-pointer"><Printer className="h-4 w-4" /></button><button type="button" onClick={() => setSelectedOrderId(null)} disabled={busyStatusOrderId === selectedOrder.id} title="Fechar" aria-label="Fechar ficha da OP" className="inline-flex h-9 w-9 items-center justify-center rounded-xl border border-dark-border bg-dark-input text-dark-muted transition-colors hover:border-brand-primary hover:text-dark-text disabled:cursor-not-allowed disabled:opacity-60 cursor-pointer"><X className="h-4 w-4" /></button></div>
</header>
<div className="overflow-y-auto 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>}
{selectedOrderNextStage ? <section className="flex flex-col gap-3 rounded-xl border border-brand-primary/25 bg-brand-primary/[0.055] p-4 sm:flex-row sm:items-center sm:justify-between"><div><p className="text-[10px] font-bold uppercase tracking-wider text-brand-primary">Próxima ação</p><p className="mt-1 text-sm font-bold text-dark-text">{selectedOrderNextStage.label}</p><p className="mt-1 text-xs font-semibold text-dark-muted">{selectedOrderNextStage.description}</p></div><button type="button" onClick={() => void handleStatusChange(selectedOrder, selectedOrderNextStage.status)} disabled={busyStatusOrderId === selectedOrder.id} className="inline-flex h-10 shrink-0 items-center justify-center gap-2 rounded-xl bg-brand-primary px-4 text-sm font-bold text-brand-contrast transition-colors hover:bg-brand-primary/90 disabled:cursor-not-allowed disabled:opacity-60 cursor-pointer">{busyStatusOrderId === selectedOrder.id ? 'Atualizando...' : selectedOrderNextStage.label}<ChevronDown className="h-4 w-4 -rotate-90" /></button></section> : <section className="rounded-xl border border-dark-border bg-dark-input/45 px-4 py-3 text-sm font-semibold text-dark-muted">{selectedOrder.status === 'finished' ? 'Esta OP foi finalizada. Os registros abaixo permanecem disponíveis para consulta.' : 'Esta OP foi cancelada e não possui próxima etapa operacional.'}</section>}
<div className="mt-5">{renderOrderDetails(selectedOrder)}</div>
</div>
<footer className="flex flex-col-reverse gap-2 border-t border-dark-border px-5 py-4 sm:flex-row sm:items-center sm:justify-between"><div>{selectedOrderCanReturnToCutPlan && <button type="button" onClick={() => void returnOrderToCutPlan(selectedOrder)} disabled={busyStatusOrderId === selectedOrder.id} className="inline-flex h-10 items-center gap-2 rounded-xl border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-muted transition-colors hover:border-amber-400/45 hover:text-amber-200 disabled:cursor-not-allowed disabled:opacity-60 cursor-pointer"><Undo2 className="h-4 w-4" />Devolver ao Plano de Corte</button>}</div><button type="button" onClick={() => setSelectedOrderId(null)} disabled={busyStatusOrderId === selectedOrder.id} className="h-10 rounded-xl border border-dark-border bg-dark-input px-4 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary disabled:cursor-not-allowed disabled:opacity-60 cursor-pointer">Fechar</button></footer>
</section>
</div>}
</div>
);
};