Improve production order preview selection
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 42s

This commit is contained in:
Cauê Faleiros
2026-08-27 20:02:51 -03:00
parent 4b66e91442
commit c77bc752ae

View File

@@ -19,6 +19,7 @@ type CorrectionIssueFilter = CutProductIssue | 'all';
type SettingsSection = 'rules' | 'corrections'; type SettingsSection = 'rules' | 'corrections';
type CuttingView = 'plan' | 'families' | 'issues'; type CuttingView = 'plan' | 'families' | 'issues';
type PreviewStep = 'selection' | 'confirmation'; type PreviewStep = 'selection' | 'confirmation';
type PreviewReviewFilter = 'all' | 'needs_review' | 'complete';
type MaterialReadiness = { status: 'ready' | 'material_ready' | 'blocked' | 'missing'; blockers: string[] }; type MaterialReadiness = { status: 'ready' | 'material_ready' | 'blocked' | 'missing'; blockers: string[] };
type CutMaterialRequirement = { key: string; label: string; unit: string; quantity: number }; type CutMaterialRequirement = { key: string; label: string; unit: string; quantity: number };
@@ -167,6 +168,7 @@ const Cutting = () => {
const [previewStep, setPreviewStep] = useState<PreviewStep>('selection'); const [previewStep, setPreviewStep] = useState<PreviewStep>('selection');
const [previewPage, setPreviewPage] = useState(1); const [previewPage, setPreviewPage] = useState(1);
const [previewItemsPerPage, setPreviewItemsPerPage] = useState(10); const [previewItemsPerPage, setPreviewItemsPerPage] = useState(10);
const [previewReviewFilter, setPreviewReviewFilter] = useState<PreviewReviewFilter>('all');
const [selectedPreviewRowIds, setSelectedPreviewRowIds] = useState<string[]>([]); const [selectedPreviewRowIds, setSelectedPreviewRowIds] = useState<string[]>([]);
const [previewQuantityOverrides, setPreviewQuantityOverrides] = useState<Record<string, string>>({}); const [previewQuantityOverrides, setPreviewQuantityOverrides] = useState<Record<string, string>>({});
const [lotSelections, setLotSelections] = useState<Record<string, { lotId: string; quantity: string }>>({}); const [lotSelections, setLotSelections] = useState<Record<string, { lotId: string; quantity: string }>>({});
@@ -448,13 +450,17 @@ const Cutting = () => {
const previewRowsWithIssues = useMemo(() => ( const previewRowsWithIssues = useMemo(() => (
selectedPreviewRows.filter(row => row.issues.length).length selectedPreviewRows.filter(row => row.issues.length).length
), [selectedPreviewRows]); ), [selectedPreviewRows]);
const previewTotalPages = Math.ceil(rowsAvailableForOrder.length / previewItemsPerPage); const previewRows = useMemo(() => rowsAvailableForOrder.filter(row => {
if (previewReviewFilter === 'needs_review') return row.issues.length > 0;
if (previewReviewFilter === 'complete') return row.issues.length === 0;
return true;
}), [previewReviewFilter, rowsAvailableForOrder]);
const previewTotalPages = Math.ceil(previewRows.length / previewItemsPerPage);
const safePreviewPage = Math.min(previewPage, previewTotalPages || 1); const safePreviewPage = Math.min(previewPage, previewTotalPages || 1);
const previewStartIndex = (safePreviewPage - 1) * previewItemsPerPage; const previewStartIndex = (safePreviewPage - 1) * previewItemsPerPage;
const paginatedPreviewRows = rowsAvailableForOrder.slice(previewStartIndex, previewStartIndex + previewItemsPerPage); const paginatedPreviewRows = previewRows.slice(previewStartIndex, previewStartIndex + previewItemsPerPage);
const selectedPreviewRowIdSet = useMemo(() => new Set(selectedPreviewRowIds), [selectedPreviewRowIds]); const selectedPreviewRowIdSet = useMemo(() => new Set(selectedPreviewRowIds), [selectedPreviewRowIds]);
const areAllPreviewRowsSelected = rowsAvailableForOrder.length > 0 && selectedPreviewRows.length === rowsAvailableForOrder.length; const areAllPreviewRowsSelected = rowsAvailableForOrder.length > 0 && selectedPreviewRows.length === rowsAvailableForOrder.length;
const areAllPagePreviewRowsSelected = paginatedPreviewRows.length > 0 && paginatedPreviewRows.every(row => selectedPreviewRowIdSet.has(row.id));
const totalPages = Math.ceil(filteredRows.length / itemsPerPage); const totalPages = Math.ceil(filteredRows.length / itemsPerPage);
const safeCurrentPage = Math.min(currentPage, totalPages || 1); const safeCurrentPage = Math.min(currentPage, totalPages || 1);
@@ -680,6 +686,7 @@ const Cutting = () => {
setGenerationMessage(''); setGenerationMessage('');
setPreviewPage(1); setPreviewPage(1);
setPreviewReviewFilter('all');
setSelectedPreviewRowIds(rowsAvailableForOrder.map(row => row.id)); setSelectedPreviewRowIds(rowsAvailableForOrder.map(row => row.id));
setPreviewQuantityOverrides({}); setPreviewQuantityOverrides({});
setLotSelections({}); setLotSelections({});
@@ -692,10 +699,6 @@ const Cutting = () => {
setIsOrderPreviewOpen(true); setIsOrderPreviewOpen(true);
}; };
const setPreviewSelection = (rows: CutPlanSkuRow[]) => {
setSelectedPreviewRowIds(rows.map(row => row.id));
};
const togglePreviewRow = (productId: string) => { const togglePreviewRow = (productId: string) => {
setSelectedPreviewRowIds(current => ( setSelectedPreviewRowIds(current => (
current.includes(productId) current.includes(productId)
@@ -704,16 +707,11 @@ const Cutting = () => {
)); ));
}; };
const togglePreviewPage = () => { const toggleAllPreviewRows = () => {
const pageIds = paginatedPreviewRows.map(row => row.id);
setSelectedPreviewRowIds(current => { setSelectedPreviewRowIds(current => {
const currentIds = new Set(current); const currentIds = new Set(current);
if (pageIds.every(id => currentIds.has(id))) { const allRowsSelected = rowsAvailableForOrder.length > 0 && rowsAvailableForOrder.every(row => currentIds.has(row.id));
pageIds.forEach(id => currentIds.delete(id)); return allRowsSelected ? [] : rowsAvailableForOrder.map(row => row.id);
} else {
pageIds.forEach(id => currentIds.add(id));
}
return Array.from(currentIds);
}); });
}; };
@@ -1022,11 +1020,23 @@ const Cutting = () => {
{previewStep === 'selection' && <> {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"> <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> <p className="text-xs font-semibold text-dark-muted">A seleção é mantida em todas as páginas. Use a caixa no cabeçalho para marcar ou desmarcar todos os SKUs aptos.</p>
<div className="flex flex-wrap gap-2"> <label className="flex shrink-0 items-center gap-2 text-xs font-bold text-dark-muted">
<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> Revisão de dados
<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> <select
</div> value={previewReviewFilter}
onChange={(event) => {
setPreviewReviewFilter(event.target.value as PreviewReviewFilter);
setPreviewPage(1);
}}
className="h-9 rounded-lg border border-dark-border bg-dark-card px-2.5 text-xs font-bold text-dark-text outline-none transition-colors focus:border-brand-primary cursor-pointer"
aria-label="Filtrar produtos por revisão de dados"
>
<option value="all">Todos</option>
<option value="needs_review">Precisam de revisão</option>
<option value="complete">Sem revisão pendente</option>
</select>
</label>
</div> </div>
<div className="overflow-auto"> <div className="overflow-auto">
@@ -1041,7 +1051,7 @@ const Cutting = () => {
</colgroup> </colgroup>
<thead className="border-b border-dark-border bg-dark-header text-dark-muted"> <thead className="border-b border-dark-border bg-dark-header text-dark-muted">
<tr> <tr>
<th className="px-4 py-3 text-center"><input type="checkbox" checked={areAllPagePreviewRowsSelected} onChange={togglePreviewPage} aria-label="Selecionar OPs desta página" className="h-4 w-4 accent-brand-primary cursor-pointer" /></th> <th className="px-4 py-3 text-center"><input type="checkbox" checked={areAllPreviewRowsSelected} onChange={toggleAllPreviewRows} aria-label="Selecionar ou desmarcar todas as OPs aptas" className="h-4 w-4 accent-brand-primary cursor-pointer" /></th>
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Produto</th> <th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Produto</th>
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Cortar agora</th> <th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Cortar agora</th>
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Demanda</th> <th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Demanda</th>
@@ -1068,7 +1078,20 @@ const Cutting = () => {
<td className="px-4 py-3 font-bold whitespace-nowrap text-brand-primary">{formatNumber(row.suggestedCutQuantity)} un.</td> <td className="px-4 py-3 font-bold whitespace-nowrap text-brand-primary">{formatNumber(row.suggestedCutQuantity)} un.</td>
<td className="px-4 py-3 font-bold whitespace-nowrap text-dark-text">{formatNumber(row.projectedDemand, 1)} un.</td> <td className="px-4 py-3 font-bold whitespace-nowrap text-dark-text">{formatNumber(row.projectedDemand, 1)} un.</td>
<td className="px-4 py-3 whitespace-nowrap"><div className="font-bold text-dark-text">{formatNumber(row.availableQuantity)} un.</div>{row.openProductionQuantity ? <div className="mt-1 text-[10px] font-semibold text-dark-muted">{formatNumber(row.openProductionQuantity)} em OP</div> : null}</td> <td className="px-4 py-3 whitespace-nowrap"><div className="font-bold text-dark-text">{formatNumber(row.availableQuantity)} un.</div>{row.openProductionQuantity ? <div className="mt-1 text-[10px] font-semibold text-dark-muted">{formatNumber(row.openProductionQuantity)} em OP</div> : null}</td>
<td className="px-4 py-3">{renderIssueBadge(row)}</td> <td className="px-4 py-3">
<div className="flex items-center gap-2">
{renderIssueBadge(row)}
<button
type="button"
onClick={() => openQuickCutEdit(row)}
className="inline-flex h-7 w-7 shrink-0 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={`Revisar dados do SKU ${row.id}`}
aria-label={`Revisar dados do SKU ${row.id}`}
>
<Pencil className="h-3.5 w-3.5" />
</button>
</div>
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -1088,7 +1111,7 @@ const Cutting = () => {
</section>} </section>}
{previewStep === 'selection' && <PaginationControls {previewStep === 'selection' && <PaginationControls
totalItems={rowsAvailableForOrder.length} totalItems={previewRows.length}
currentPage={safePreviewPage} currentPage={safePreviewPage}
totalPages={previewTotalPages} totalPages={previewTotalPages}
pageSize={previewItemsPerPage} pageSize={previewItemsPerPage}
@@ -1096,7 +1119,7 @@ const Cutting = () => {
itemLabel="OPs" itemLabel="OPs"
pageSizeLabel="por página" pageSizeLabel="por página"
startIndex={previewStartIndex} startIndex={previewStartIndex}
endIndex={Math.min(previewStartIndex + previewItemsPerPage, rowsAvailableForOrder.length)} endIndex={Math.min(previewStartIndex + previewItemsPerPage, previewRows.length)}
onPageChange={setPreviewPage} onPageChange={setPreviewPage}
onPageSizeChange={(pageSize) => { setPreviewItemsPerPage(pageSize); setPreviewPage(1); }} onPageSizeChange={(pageSize) => { setPreviewItemsPerPage(pageSize); setPreviewPage(1); }}
className="border-t border-dark-border px-4 py-3" className="border-t border-dark-border px-4 py-3"