|
|
|
|
@@ -1,30 +1,34 @@
|
|
|
|
|
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, Search, Undo2, XCircle } from 'lucide-react';
|
|
|
|
|
import { 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, updateProductionOrderStatus } from '../dataService';
|
|
|
|
|
import { consumeSupplyLotForProduction, exportToCSV, fetchProductionOrders, fetchSupplySummary, updateProductionOrderExecution, updateProductionOrderStatus } from '../dataService';
|
|
|
|
|
import type { DateRange, ProductionOrderItem, ProductionOrderStatus, ProductionOrderSummary, SupplyLot } from '../types';
|
|
|
|
|
|
|
|
|
|
type ProductionOrderStatusTab = 'all' | 'open' | 'in_progress' | 'finished' | 'canceled';
|
|
|
|
|
type ProductionOrderStatusTab = 'all' | 'open' | 'cutting' | 'sewing' | 'review' | 'finished' | 'canceled';
|
|
|
|
|
|
|
|
|
|
const emptySummary: ProductionOrderSummary = {
|
|
|
|
|
orders: [],
|
|
|
|
|
counts: { all: 0, open: 0, in_progress: 0, finished: 0, canceled: 0 }
|
|
|
|
|
counts: { all: 0, open: 0, cutting: 0, sewing: 0, review: 0, finished: 0, canceled: 0 }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const statusTabs: Array<{ key: ProductionOrderStatusTab; label: string; dotClass: string }> = [
|
|
|
|
|
{ key: 'all', label: 'Todas', dotClass: 'bg-dark-muted' },
|
|
|
|
|
{ key: 'open', label: 'Em aberto', dotClass: 'bg-amber-400' },
|
|
|
|
|
{ key: 'in_progress', label: 'Em andamento', dotClass: 'bg-sky-400' },
|
|
|
|
|
{ key: 'cutting', label: 'Em corte', dotClass: 'bg-amber-300' },
|
|
|
|
|
{ key: 'sewing', label: 'Em costura', dotClass: 'bg-sky-400' },
|
|
|
|
|
{ key: 'review', label: 'Em revisão', dotClass: 'bg-violet-400' },
|
|
|
|
|
{ key: 'finished', label: 'Finalizada', dotClass: 'bg-emerald-400' },
|
|
|
|
|
{ key: 'canceled', label: 'Cancelada', dotClass: 'bg-zinc-500' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const editableStatusOptions: Array<{ value: Exclude<ProductionOrderStatusTab, 'all'>; label: string; description: string }> = [
|
|
|
|
|
{ value: 'open', label: 'Em aberto', description: 'Aguardando o início da produção.' },
|
|
|
|
|
{ value: 'in_progress', label: 'Em andamento', description: 'A produção já foi iniciada.' },
|
|
|
|
|
{ value: 'cutting', label: 'Em corte', description: 'Os materiais estão sendo transformados em peças cortadas.' },
|
|
|
|
|
{ value: 'sewing', label: 'Em costura', description: 'As peças cortadas estão em montagem.' },
|
|
|
|
|
{ value: 'review', label: 'Em revisão', description: 'A qualidade e as perdas estão sendo conferidas.' },
|
|
|
|
|
{ value: 'finished', label: 'Finalizada', description: 'A produção desta OP foi concluída.' },
|
|
|
|
|
{ value: 'canceled', label: 'Cancelada', description: 'Esta OP não seguirá para produção.' }
|
|
|
|
|
];
|
|
|
|
|
@@ -35,11 +39,21 @@ const statusStyles: Record<string, { label: string; className: string; dotClass:
|
|
|
|
|
className: 'border-amber-400/35 bg-amber-400/10 text-amber-300',
|
|
|
|
|
dotClass: 'bg-amber-400'
|
|
|
|
|
},
|
|
|
|
|
in_progress: {
|
|
|
|
|
label: 'Em andamento',
|
|
|
|
|
cutting: {
|
|
|
|
|
label: 'Em corte',
|
|
|
|
|
className: 'border-amber-300/35 bg-amber-300/10 text-amber-200',
|
|
|
|
|
dotClass: 'bg-amber-300'
|
|
|
|
|
},
|
|
|
|
|
sewing: {
|
|
|
|
|
label: 'Em costura',
|
|
|
|
|
className: 'border-sky-400/35 bg-sky-400/10 text-sky-300',
|
|
|
|
|
dotClass: 'bg-sky-400'
|
|
|
|
|
},
|
|
|
|
|
review: {
|
|
|
|
|
label: 'Em revisão',
|
|
|
|
|
className: 'border-violet-400/35 bg-violet-400/10 text-violet-300',
|
|
|
|
|
dotClass: 'bg-violet-400'
|
|
|
|
|
},
|
|
|
|
|
finished: {
|
|
|
|
|
label: 'Finalizada',
|
|
|
|
|
className: 'border-emerald-400/35 bg-emerald-400/10 text-emerald-300',
|
|
|
|
|
@@ -128,7 +142,9 @@ const ProductionOrders = () => {
|
|
|
|
|
const [supplyLots, setSupplyLots] = useState<SupplyLot[]>([]);
|
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
const [isSupplyBusy, setIsSupplyBusy] = useState(false);
|
|
|
|
|
const [busyExecutionOrderId, setBusyExecutionOrderId] = useState<number | null>(null);
|
|
|
|
|
const [supplyMessage, setSupplyMessage] = useState('');
|
|
|
|
|
const [executionMessage, setExecutionMessage] = useState('');
|
|
|
|
|
const [busyStatusOrderId, setBusyStatusOrderId] = useState<number | null>(null);
|
|
|
|
|
const [statusMessage, setStatusMessage] = useState('');
|
|
|
|
|
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null);
|
|
|
|
|
@@ -193,7 +209,7 @@ const ProductionOrders = () => {
|
|
|
|
|
const isRefreshing = isLoading && summary.orders.length > 0;
|
|
|
|
|
|
|
|
|
|
const openCount = summary.counts.open || 0;
|
|
|
|
|
const progressCount = summary.counts.in_progress || 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 selectedOrder = summary.orders.find(order => order.id === selectedOrderId) || null;
|
|
|
|
|
@@ -270,6 +286,46 @@ const ProductionOrders = () => {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleExecutionSave = async (event: FormEvent<HTMLFormElement>, order: ProductionOrderItem) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const form = new FormData(event.currentTarget);
|
|
|
|
|
const parseOptionalQuantity = (name: string) => {
|
|
|
|
|
const raw = String(form.get(name) || '').trim();
|
|
|
|
|
return raw ? Number(raw.replace(',', '.')) : null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setBusyExecutionOrderId(order.id);
|
|
|
|
|
setExecutionMessage('');
|
|
|
|
|
try {
|
|
|
|
|
await updateProductionOrderExecution(order.id, {
|
|
|
|
|
actualCutQuantity: parseOptionalQuantity('actualCutQuantity'),
|
|
|
|
|
rejectedQuantity: parseOptionalQuantity('rejectedQuantity'),
|
|
|
|
|
finishedQuantity: parseOptionalQuantity('finishedQuantity'),
|
|
|
|
|
qualityNotes: String(form.get('qualityNotes') || '').trim(),
|
|
|
|
|
});
|
|
|
|
|
const nextSummary = await fetchProductionOrders(dateRange, { search: searchTerm }, { force: true });
|
|
|
|
|
setSummary(nextSummary);
|
|
|
|
|
setExecutionMessage(`Execução da OP ${order.number || `#${order.id}`} registrada.`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setExecutionMessage(error instanceof Error ? error.message : 'Não foi possível registrar a execução.');
|
|
|
|
|
} finally {
|
|
|
|
|
setBusyExecutionOrderId(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const printProductionSheet = (order: ProductionOrderItem) => {
|
|
|
|
|
const printWindow = window.open('', '_blank', 'noopener,noreferrer');
|
|
|
|
|
if (!printWindow) {
|
|
|
|
|
setStatusMessage('Permita pop-ups para imprimir a ficha de produção.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const componentRows = order.components.map(component => `<tr><td>${component.componentName}</td><td>${formatQuantity(component.totalQuantity)} ${component.unit}</td></tr>`).join('') || '<tr><td colspan="2">Composição não sincronizada.</td></tr>';
|
|
|
|
|
printWindow.document.write(`<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><title>Ficha ${order.number || order.id}</title><style>body{font-family:Arial,sans-serif;color:#18181b;padding:32px}h1{font-size:24px;margin:0}h2{font-size:14px;text-transform:uppercase;letter-spacing:.08em;margin:28px 0 10px}.meta{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:20px}.box{border:1px solid #d4d4d8;border-radius:8px;padding:10px;font-size:13px}.label{display:block;color:#71717a;font-size:10px;font-weight:bold;text-transform:uppercase;margin-bottom:4px}table{border-collapse:collapse;width:100%;font-size:13px}th,td{border:1px solid #d4d4d8;padding:9px;text-align:left}th{background:#f4f4f5}.line{border-bottom:1px solid #a1a1aa;height:28px;margin-top:8px}</style></head><body><h1>Ficha de Produção · ${order.number || `OP #${order.id}`}</h1><p>${order.productDescription}</p><div class="meta"><div class="box"><span class="label">SKU</span>${order.productSku || '-'}</div><div class="box"><span class="label">Quantidade planejada</span>${formatQuantity(order.quantity)} ${order.unit}</div><div class="box"><span class="label">Status</span>${order.statusLabel}</div><div class="box"><span class="label">Referência</span>${order.orderReference || '-'}</div></div><h2>Composição prevista</h2><table><thead><tr><th>Material</th><th>Quantidade</th></tr></thead><tbody>${componentRows}</tbody></table><h2>Apontamento de execução</h2><div class="meta"><div class="box"><span class="label">Quantidade cortada</span><div class="line"></div></div><div class="box"><span class="label">Perdas / defeitos</span><div class="line"></div></div><div class="box"><span class="label">Quantidade aprovada</span><div class="line"></div></div><div class="box"><span class="label">Responsável</span><div class="line"></div></div></div><h2>Observações</h2><div class="line"></div><div class="line"></div></body></html>`);
|
|
|
|
|
printWindow.document.close();
|
|
|
|
|
printWindow.focus();
|
|
|
|
|
printWindow.print();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleExport = () => {
|
|
|
|
|
const exportData = filteredOrders.map(order => ({
|
|
|
|
|
'Numero': order.number,
|
|
|
|
|
@@ -302,7 +358,7 @@ const ProductionOrders = () => {
|
|
|
|
|
|
|
|
|
|
<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 === 'in_progress' ? PlayCircle : 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>
|
|
|
|
|
<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>
|
|
|
|
|
<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">Emissão</p><p className="mt-1 text-sm font-bold text-dark-text">{formatDate(order.issueDate)}</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">Previsão</p><p className="mt-1 text-sm font-bold text-dark-text">{formatDate(order.expectedDate)}</p></div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -316,6 +372,26 @@ 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>
|
|
|
|
|
<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>
|
|
|
|
|
<label className="text-xs font-bold text-dark-muted">Perdas / defeitos<input name="rejectedQuantity" inputMode="decimal" defaultValue={order.rejectedQuantity || ''} 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">Aprovado final<input name="finishedQuantity" inputMode="decimal" defaultValue={order.finishedQuantity ?? ''} 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="Após revisão" /></label>
|
|
|
|
|
<label className="text-xs font-bold text-dark-muted sm:col-span-3">Motivo de perda / observação<textarea name="qualityNotes" defaultValue={order.qualityNotes} className="mt-1.5 min-h-20 w-full rounded-lg border border-dark-border bg-dark-input px-3 py-2.5 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Ex.: 4 peças com mancha de tinta; 2 recuperadas na 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">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>
|
|
|
|
|
<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>}
|
|
|
|
|
@@ -323,7 +399,7 @@ const ProductionOrders = () => {
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Baixa de material</h3>
|
|
|
|
|
{order.status === 'in_progress' ? <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-sky-300">Em andamento</span> antes de registrar a baixa de materiais.</div>}
|
|
|
|
|
{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>
|
|
|
|
|
|
|
|
|
|
{(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>}
|
|
|
|
|
|