Improve production order preview selection
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 42s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 42s
This commit is contained in:
@@ -19,6 +19,7 @@ type CorrectionIssueFilter = CutProductIssue | 'all';
|
||||
type SettingsSection = 'rules' | 'corrections';
|
||||
type CuttingView = 'plan' | 'families' | 'issues';
|
||||
type PreviewStep = 'selection' | 'confirmation';
|
||||
type PreviewReviewFilter = 'all' | 'needs_review' | 'complete';
|
||||
type MaterialReadiness = { status: 'ready' | 'material_ready' | 'blocked' | 'missing'; blockers: string[] };
|
||||
type CutMaterialRequirement = { key: string; label: string; unit: string; quantity: number };
|
||||
|
||||
@@ -167,6 +168,7 @@ const Cutting = () => {
|
||||
const [previewStep, setPreviewStep] = useState<PreviewStep>('selection');
|
||||
const [previewPage, setPreviewPage] = useState(1);
|
||||
const [previewItemsPerPage, setPreviewItemsPerPage] = useState(10);
|
||||
const [previewReviewFilter, setPreviewReviewFilter] = useState<PreviewReviewFilter>('all');
|
||||
const [selectedPreviewRowIds, setSelectedPreviewRowIds] = useState<string[]>([]);
|
||||
const [previewQuantityOverrides, setPreviewQuantityOverrides] = useState<Record<string, string>>({});
|
||||
const [lotSelections, setLotSelections] = useState<Record<string, { lotId: string; quantity: string }>>({});
|
||||
@@ -448,13 +450,17 @@ const Cutting = () => {
|
||||
const previewRowsWithIssues = useMemo(() => (
|
||||
selectedPreviewRows.filter(row => row.issues.length).length
|
||||
), [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 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 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 safeCurrentPage = Math.min(currentPage, totalPages || 1);
|
||||
@@ -680,6 +686,7 @@ const Cutting = () => {
|
||||
|
||||
setGenerationMessage('');
|
||||
setPreviewPage(1);
|
||||
setPreviewReviewFilter('all');
|
||||
setSelectedPreviewRowIds(rowsAvailableForOrder.map(row => row.id));
|
||||
setPreviewQuantityOverrides({});
|
||||
setLotSelections({});
|
||||
@@ -692,10 +699,6 @@ const Cutting = () => {
|
||||
setIsOrderPreviewOpen(true);
|
||||
};
|
||||
|
||||
const setPreviewSelection = (rows: CutPlanSkuRow[]) => {
|
||||
setSelectedPreviewRowIds(rows.map(row => row.id));
|
||||
};
|
||||
|
||||
const togglePreviewRow = (productId: string) => {
|
||||
setSelectedPreviewRowIds(current => (
|
||||
current.includes(productId)
|
||||
@@ -704,16 +707,11 @@ const Cutting = () => {
|
||||
));
|
||||
};
|
||||
|
||||
const togglePreviewPage = () => {
|
||||
const pageIds = paginatedPreviewRows.map(row => row.id);
|
||||
const toggleAllPreviewRows = () => {
|
||||
setSelectedPreviewRowIds(current => {
|
||||
const currentIds = new Set(current);
|
||||
if (pageIds.every(id => currentIds.has(id))) {
|
||||
pageIds.forEach(id => currentIds.delete(id));
|
||||
} else {
|
||||
pageIds.forEach(id => currentIds.add(id));
|
||||
}
|
||||
return Array.from(currentIds);
|
||||
const allRowsSelected = rowsAvailableForOrder.length > 0 && rowsAvailableForOrder.every(row => currentIds.has(row.id));
|
||||
return allRowsSelected ? [] : rowsAvailableForOrder.map(row => row.id);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1022,11 +1020,23 @@ const Cutting = () => {
|
||||
|
||||
{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([])} 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>
|
||||
<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>
|
||||
<label className="flex shrink-0 items-center gap-2 text-xs font-bold text-dark-muted">
|
||||
Revisão de dados
|
||||
<select
|
||||
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 className="overflow-auto">
|
||||
@@ -1041,7 +1051,7 @@ const Cutting = () => {
|
||||
</colgroup>
|
||||
<thead className="border-b border-dark-border bg-dark-header text-dark-muted">
|
||||
<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">Cortar agora</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-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">{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>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -1088,7 +1111,7 @@ const Cutting = () => {
|
||||
</section>}
|
||||
|
||||
{previewStep === 'selection' && <PaginationControls
|
||||
totalItems={rowsAvailableForOrder.length}
|
||||
totalItems={previewRows.length}
|
||||
currentPage={safePreviewPage}
|
||||
totalPages={previewTotalPages}
|
||||
pageSize={previewItemsPerPage}
|
||||
@@ -1096,7 +1119,7 @@ const Cutting = () => {
|
||||
itemLabel="OPs"
|
||||
pageSizeLabel="por página"
|
||||
startIndex={previewStartIndex}
|
||||
endIndex={Math.min(previewStartIndex + previewItemsPerPage, rowsAvailableForOrder.length)}
|
||||
endIndex={Math.min(previewStartIndex + previewItemsPerPage, previewRows.length)}
|
||||
onPageChange={setPreviewPage}
|
||||
onPageSizeChange={(pageSize) => { setPreviewItemsPerPage(pageSize); setPreviewPage(1); }}
|
||||
className="border-t border-dark-border px-4 py-3"
|
||||
|
||||
Reference in New Issue
Block a user