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 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user