Split production order preview into steps
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useOutletContext, useSearchParams } from 'react-router-dom';
|
||||
import { AlertTriangle, ArrowLeft, ClipboardList, Download, Eye, Layers3, Pencil, RotateCcw, Ruler, Save as SaveIcon, Scissors, Search, Settings2, X } from 'lucide-react';
|
||||
import { AlertTriangle, ArrowLeft, ArrowRight, ClipboardList, Download, Eye, Layers3, Pencil, RotateCcw, Ruler, Save as SaveIcon, Scissors, Search, Settings2, X } from 'lucide-react';
|
||||
import DateRangePicker from '../components/DateRangePicker';
|
||||
import PaginationControls from '../components/PaginationControls';
|
||||
import ProductColorBadge from '../components/ProductColorBadge';
|
||||
@@ -18,6 +18,7 @@ type CutProductIssue = Exclude<CutIssue, 'missing_yield_rule'>;
|
||||
type CorrectionIssueFilter = CutProductIssue | 'all';
|
||||
type SettingsSection = 'rules' | 'corrections';
|
||||
type CuttingView = 'plan' | 'families' | 'issues';
|
||||
type PreviewStep = 'selection' | 'confirmation';
|
||||
type MaterialReadiness = { status: 'ready' | 'material_ready' | 'blocked' | 'missing'; blockers: string[] };
|
||||
type CutMaterialRequirement = { key: string; label: string; unit: string; quantity: number };
|
||||
|
||||
@@ -163,6 +164,7 @@ const Cutting = () => {
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
const [isGeneratingOrders, setIsGeneratingOrders] = useState(false);
|
||||
const [isOrderPreviewOpen, setIsOrderPreviewOpen] = useState(false);
|
||||
const [previewStep, setPreviewStep] = useState<PreviewStep>('selection');
|
||||
const [previewPage, setPreviewPage] = useState(1);
|
||||
const [previewItemsPerPage, setPreviewItemsPerPage] = useState(10);
|
||||
const [selectedPreviewRowIds, setSelectedPreviewRowIds] = useState<string[]>([]);
|
||||
@@ -670,6 +672,7 @@ const Cutting = () => {
|
||||
setLotSelections({});
|
||||
setProductionPlanForm({ name: cutPlanReference, contractor: '', expectedDate: '', notes: '' });
|
||||
setIsPlanDetailsOpen(false);
|
||||
setPreviewStep('selection');
|
||||
setIsOrderPreviewOpen(true);
|
||||
};
|
||||
|
||||
@@ -885,9 +888,11 @@ const Cutting = () => {
|
||||
>
|
||||
<div className="flex flex-col gap-4 border-b border-dark-border p-5 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-dark-text">Prévia das OPs</h2>
|
||||
<h2 className="text-lg font-bold text-dark-text">{previewStep === 'selection' ? 'Selecionar OPs' : 'Confirmar plano'}</h2>
|
||||
<p className="mt-1 text-sm font-semibold text-dark-muted">
|
||||
Escolha os SKUs para criar ordens locais das necessidades do filtro atual.
|
||||
{previewStep === 'selection'
|
||||
? 'Escolha os SKUs que entrarão neste plano de produção.'
|
||||
: 'Revise o plano e as reservas antes de criar as ordens locais.'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -902,7 +907,19 @@ const Cutting = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 border-b border-dark-border bg-dark-input/25 px-5 py-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-2 border-b border-dark-border bg-dark-input/30 px-5 py-3 text-xs font-bold">
|
||||
<span className={`inline-flex items-center gap-2 rounded-full px-3 py-1.5 ${previewStep === 'selection' ? 'bg-brand-primary/15 text-brand-primary' : 'text-dark-muted'}`}>
|
||||
<span className={`inline-flex h-5 w-5 items-center justify-center rounded-full ${previewStep === 'selection' ? 'bg-brand-primary text-brand-contrast' : 'border border-dark-border'}`}>1</span>
|
||||
Selecionar OPs
|
||||
</span>
|
||||
<ArrowRight className="h-3.5 w-3.5 text-dark-muted" />
|
||||
<span className={`inline-flex items-center gap-2 rounded-full px-3 py-1.5 ${previewStep === 'confirmation' ? 'bg-brand-primary/15 text-brand-primary' : 'text-dark-muted'}`}>
|
||||
<span className={`inline-flex h-5 w-5 items-center justify-center rounded-full ${previewStep === 'confirmation' ? 'bg-brand-primary text-brand-contrast' : 'border border-dark-border'}`}>2</span>
|
||||
Confirmar plano
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{previewStep === 'selection' && <div className="flex flex-col gap-3 border-b border-dark-border bg-dark-input/25 px-5 py-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm font-bold text-dark-text">
|
||||
<span>{formatNumber(selectedPreviewRows.length)} selecionada{selectedPreviewRows.length === 1 ? '' : 's'}</span>
|
||||
<span className="text-dark-muted">·</span>
|
||||
@@ -910,9 +927,9 @@ const Cutting = () => {
|
||||
{previewRowsWithIssues > 0 && <><span className="text-dark-muted">·</span><span className="text-amber-300">{formatNumber(previewRowsWithIssues)} para revisar</span></>}
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-dark-muted">Cobertura de {formatNumber(targetCoverageDays)} dias</span>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<div className="border-b border-dark-border px-5 py-4">
|
||||
{previewStep === 'confirmation' && <div className="border-b border-dark-border px-5 py-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-end">
|
||||
<label className="block min-w-0 flex-1 text-xs font-bold text-dark-muted">Plano de produção
|
||||
<input value={productionPlanForm.name} onChange={(event) => setProductionPlanForm(current => ({ ...current, name: event.target.value }))} className="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 focus:border-brand-primary" />
|
||||
@@ -926,13 +943,41 @@ const Cutting = () => {
|
||||
<label className="text-xs font-bold text-dark-muted">Previsão de conclusão<input type="date" value={productionPlanForm.expectedDate} onChange={(event) => setProductionPlanForm(current => ({ ...current, expectedDate: event.target.value }))} className="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 focus:border-brand-primary" /></label>
|
||||
<label className="text-xs font-bold text-dark-muted">Observação<input value={productionPlanForm.notes} onChange={(event) => setProductionPlanForm(current => ({ ...current, notes: event.target.value }))} placeholder="Opcional" className="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" /></label>
|
||||
</div>}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{previewStep === 'confirmation' && <div className="min-h-0 flex-1 overflow-y-auto p-5">
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/30 p-4">
|
||||
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-dark-text">OPs incluídas</h3>
|
||||
<p className="mt-1 text-xs font-semibold text-dark-muted">{formatNumber(selectedPreviewRows.length)} SKU{selectedPreviewRows.length === 1 ? '' : 's'} · {formatNumber(orderPreviewTotalUnits)} un. para cortar</p>
|
||||
</div>
|
||||
{previewRowsWithIssues > 0 && <span className="text-xs font-bold text-amber-300">{formatNumber(previewRowsWithIssues)} para revisar</span>}
|
||||
</div>
|
||||
<div className="mt-3 divide-y divide-dark-border overflow-hidden rounded-lg border border-dark-border bg-dark-card">
|
||||
{selectedPreviewRows.map(row => (
|
||||
<div key={row.id} className="flex items-center justify-between gap-4 px-3 py-2.5">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-bold text-dark-text" title={row.name}>{row.name}</p>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
||||
<span className="font-mono text-[10px] text-dark-muted">#{row.id}</span>
|
||||
<span className={`rounded-full border px-2 py-0.5 text-[10px] font-bold ${familyStyles[row.family.key]}`}>{row.family.materialLabel}</span>
|
||||
<ProductColorBadge label={row.color} className="max-w-[100px] px-2" />
|
||||
<span className="text-[10px] font-semibold text-dark-muted">{row.size || 'Sem tamanho'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="shrink-0 text-sm font-bold text-brand-primary">{formatNumber(row.suggestedCutQuantity)} un.</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{previewStep === 'selection' && <>
|
||||
<div className="flex flex-col gap-3 border-b border-dark-border bg-dark-input/40 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-xs font-semibold text-dark-muted">Todos os SKUs abaixo estão aptos para gerar OP. A seleção permanece ao trocar de página.</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button type="button" onClick={() => setPreviewSelection(rowsAvailableForOrder)} className="rounded-lg border border-dark-border bg-dark-card px-3 py-1.5 text-xs font-bold text-dark-text transition-colors hover:border-brand-primary cursor-pointer">{areAllPreviewRowsSelected ? 'Todos selecionados' : 'Selecionar todos'}</button>
|
||||
<button type="button" onClick={() => setPreviewSelection(rowsAvailableForOrder.filter(row => !row.issues.length))} className="rounded-lg border border-emerald-400/30 bg-emerald-400/10 px-3 py-1.5 text-xs font-bold text-emerald-300 transition-colors hover:border-emerald-400/60 cursor-pointer">Somente dados completos</button>
|
||||
<button type="button" onClick={() => setPreviewSelection([])} disabled={!selectedPreviewRows.length} className="rounded-lg border border-dark-border bg-dark-card px-3 py-1.5 text-xs font-bold text-dark-muted transition-colors hover:border-red-400/40 hover:text-red-300 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer">Limpar</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -982,8 +1027,9 @@ const Cutting = () => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>}
|
||||
|
||||
{selectedMaterialRequirements.length > 0 && <section className="border-t border-dark-border bg-dark-input/30 p-4">
|
||||
{previewStep === 'confirmation' && selectedMaterialRequirements.length > 0 && <section className="border-t border-dark-border bg-dark-input/30 p-4">
|
||||
<div className="flex flex-col gap-1 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-dark-text">Reservar lotes para este corte</h3>
|
||||
@@ -994,7 +1040,7 @@ const Cutting = () => {
|
||||
{selectedMaterialRequirements.length ? <div className="mt-4 overflow-x-auto rounded-xl border border-dark-border bg-dark-card"><table className="w-full min-w-[760px] text-left text-xs"><thead className="bg-dark-header text-dark-muted"><tr><th className="px-3 py-2.5 font-bold">Material necessário</th><th className="px-3 py-2.5 text-right font-bold">Necessário</th><th className="px-3 py-2.5 font-bold">Lote reservado</th><th className="px-3 py-2.5 text-right font-bold">Quantidade reservada</th></tr></thead><tbody className="divide-y divide-dark-border">{selectedMaterialRequirements.map(requirement => { const selection = lotSelections[requirement.key] || { lotId: '', quantity: '' }; const lots = supplyLots.filter(lot => { const lotKey = normalizeMaterialKey(lot.product); const materialMatch = lotKey === requirement.key || lotKey === normalizeMaterialKey(requirement.label); return lot.availableQuantity > 0 && materialMatch && lot.unit.toLowerCase() === requirement.unit.toLowerCase(); }); return <tr key={requirement.key}><td className="px-3 py-3"><p className="font-bold text-dark-text">{requirement.label}</p><p className="mt-0.5 font-mono text-[10px] text-dark-muted">{requirement.key}</p></td><td className="px-3 py-3 text-right font-bold text-dark-text">{formatNumber(requirement.quantity, 4)} {requirement.unit}</td><td className="px-3 py-3"><select value={selection.lotId} onChange={(event) => setLotSelections(current => ({ ...current, [requirement.key]: { lotId: event.target.value, quantity: current[requirement.key]?.quantity || '' } }))} className="h-9 w-full min-w-[230px] rounded-lg border border-dark-border bg-dark-input px-2 text-xs font-semibold text-dark-text outline-none focus:border-brand-primary"><option value="">{lots.length ? 'Não reservar agora' : 'Sem lote compatível'}</option>{lots.map(lot => <option key={lot.id} value={lot.id}>#{lot.id} · {lot.product} · disp. {formatNumber(lot.availableQuantity, 4)} {lot.unit}</option>)}</select></td><td className="px-3 py-3"><input inputMode="decimal" value={selection.quantity} onChange={(event) => setLotSelections(current => ({ ...current, [requirement.key]: { lotId: current[requirement.key]?.lotId || '', quantity: event.target.value } }))} disabled={!selection.lotId} className="h-9 w-32 rounded-lg border border-dark-border bg-dark-input px-2 text-right text-xs font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary disabled:cursor-not-allowed disabled:opacity-50" placeholder={formatNumber(requirement.quantity, 4)} /></td></tr>; })}</tbody></table></div> : <p className="mt-3 rounded-xl border border-dark-border bg-dark-card px-3 py-3 text-xs font-semibold text-dark-muted">Os SKUs selecionados não possuem componentes de material sincronizados. Eles podem seguir para OP, mas não geram reserva.</p>}
|
||||
</section>}
|
||||
|
||||
<PaginationControls
|
||||
{previewStep === 'selection' && <PaginationControls
|
||||
totalItems={rowsAvailableForOrder.length}
|
||||
currentPage={safePreviewPage}
|
||||
totalPages={previewTotalPages}
|
||||
@@ -1007,29 +1053,32 @@ const Cutting = () => {
|
||||
onPageChange={setPreviewPage}
|
||||
onPageSizeChange={(pageSize) => { setPreviewItemsPerPage(pageSize); setPreviewPage(1); }}
|
||||
className="border-t border-dark-border px-4 py-3"
|
||||
/>
|
||||
/>}
|
||||
|
||||
<div className="flex flex-col gap-3 border-t border-dark-border p-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-xs font-semibold text-dark-muted">
|
||||
OPs abertas já existentes para o mesmo plano e SKU serão ignoradas.
|
||||
<p className="text-xs font-semibold text-dark-muted">{previewStep === 'selection'
|
||||
? 'Você poderá revisar o plano e reservar lotes na próxima etapa.'
|
||||
: 'OPs abertas já existentes para o mesmo plano e SKU serão ignoradas.'}
|
||||
</p>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOrderPreviewOpen(false)}
|
||||
onClick={() => previewStep === 'selection' ? setIsOrderPreviewOpen(false) : setPreviewStep('selection')}
|
||||
disabled={isGeneratingOrders}
|
||||
className="inline-flex items-center justify-center rounded-xl border border-dark-border bg-dark-input px-4 py-2 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
Cancelar
|
||||
{previewStep === 'selection' ? 'Cancelar' : 'Voltar'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void generateProductionOrders()}
|
||||
onClick={() => previewStep === 'selection' ? setPreviewStep('confirmation') : void generateProductionOrders()}
|
||||
disabled={isGeneratingOrders || !selectedPreviewRows.length}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-xl border border-brand-primary/30 bg-brand-primary/15 px-4 py-2 text-sm font-bold text-brand-primary transition-colors hover:border-brand-primary disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
<ClipboardList className="h-4 w-4" />
|
||||
{isGeneratingOrders ? 'Gerando OPs' : `Confirmar ${formatNumber(selectedPreviewRows.length)} ${selectedPreviewRows.length === 1 ? 'OP' : 'OPs'}`}
|
||||
{previewStep === 'selection' ? <ArrowRight className="h-4 w-4" /> : <ClipboardList className="h-4 w-4" />}
|
||||
{previewStep === 'selection'
|
||||
? `Continuar com ${formatNumber(selectedPreviewRows.length)} ${selectedPreviewRows.length === 1 ? 'OP' : 'OPs'}`
|
||||
: isGeneratingOrders ? 'Gerando OPs' : `Confirmar ${formatNumber(selectedPreviewRows.length)} ${selectedPreviewRows.length === 1 ? 'OP' : 'OPs'}`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user