Select production orders in cut preview
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 50s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 50s
This commit is contained in:
@@ -163,6 +163,7 @@ const Cutting = () => {
|
|||||||
const [isOrderPreviewOpen, setIsOrderPreviewOpen] = useState(false);
|
const [isOrderPreviewOpen, setIsOrderPreviewOpen] = useState(false);
|
||||||
const [previewPage, setPreviewPage] = useState(1);
|
const [previewPage, setPreviewPage] = useState(1);
|
||||||
const [previewItemsPerPage, setPreviewItemsPerPage] = useState(10);
|
const [previewItemsPerPage, setPreviewItemsPerPage] = useState(10);
|
||||||
|
const [selectedPreviewRowIds, setSelectedPreviewRowIds] = useState<string[]>([]);
|
||||||
const [editingCutRow, setEditingCutRow] = useState<CutPlanSkuRow | null>(null);
|
const [editingCutRow, setEditingCutRow] = useState<CutPlanSkuRow | null>(null);
|
||||||
const [quickCutEdit, setQuickCutEdit] = useState<CutProductOverride>({});
|
const [quickCutEdit, setQuickCutEdit] = useState<CutProductOverride>({});
|
||||||
const [isQuickCutEditSaving, setIsQuickCutEditSaving] = useState(false);
|
const [isQuickCutEditSaving, setIsQuickCutEditSaving] = useState(false);
|
||||||
@@ -374,17 +375,24 @@ const Cutting = () => {
|
|||||||
};
|
};
|
||||||
}, [cutPlan.needRows, materialReadinessByProductId]);
|
}, [cutPlan.needRows, materialReadinessByProductId]);
|
||||||
|
|
||||||
|
const selectedPreviewRows = useMemo(() => {
|
||||||
|
const selectedIds = new Set(selectedPreviewRowIds);
|
||||||
|
return rowsAvailableForOrder.filter(row => selectedIds.has(row.id));
|
||||||
|
}, [rowsAvailableForOrder, selectedPreviewRowIds]);
|
||||||
const orderPreviewTotalUnits = useMemo(() => (
|
const orderPreviewTotalUnits = useMemo(() => (
|
||||||
rowsAvailableForOrder.reduce((total, row) => total + row.suggestedCutQuantity, 0)
|
selectedPreviewRows.reduce((total, row) => total + row.suggestedCutQuantity, 0)
|
||||||
), [rowsAvailableForOrder]);
|
), [selectedPreviewRows]);
|
||||||
|
|
||||||
const previewRowsWithIssues = useMemo(() => (
|
const previewRowsWithIssues = useMemo(() => (
|
||||||
rowsAvailableForOrder.filter(row => row.issues.length).length
|
selectedPreviewRows.filter(row => row.issues.length).length
|
||||||
), [rowsAvailableForOrder]);
|
), [selectedPreviewRows]);
|
||||||
const previewTotalPages = Math.ceil(rowsAvailableForOrder.length / previewItemsPerPage);
|
const previewTotalPages = Math.ceil(rowsAvailableForOrder.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 = rowsAvailableForOrder.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 totalPages = Math.ceil(filteredRows.length / itemsPerPage);
|
||||||
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
|
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
|
||||||
@@ -610,12 +618,38 @@ const Cutting = () => {
|
|||||||
|
|
||||||
setGenerationMessage('');
|
setGenerationMessage('');
|
||||||
setPreviewPage(1);
|
setPreviewPage(1);
|
||||||
|
setSelectedPreviewRowIds(rowsAvailableForOrder.map(row => row.id));
|
||||||
setIsOrderPreviewOpen(true);
|
setIsOrderPreviewOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setPreviewSelection = (rows: CutPlanSkuRow[]) => {
|
||||||
|
setSelectedPreviewRowIds(rows.map(row => row.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePreviewRow = (productId: string) => {
|
||||||
|
setSelectedPreviewRowIds(current => (
|
||||||
|
current.includes(productId)
|
||||||
|
? current.filter(id => id !== productId)
|
||||||
|
: [...current, productId]
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePreviewPage = () => {
|
||||||
|
const pageIds = paginatedPreviewRows.map(row => row.id);
|
||||||
|
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 generateProductionOrders = async () => {
|
const generateProductionOrders = async () => {
|
||||||
if (!rowsAvailableForOrder.length) {
|
if (!selectedPreviewRows.length) {
|
||||||
setGenerationMessage('Não há SKU com necessidade e materiais disponíveis no filtro atual.');
|
setGenerationMessage('Selecione pelo menos um SKU para gerar as ordens de produção.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,7 +660,7 @@ const Cutting = () => {
|
|||||||
const orderReference = `Plano de corte ${formatDateKey(dateRange.start)} a ${formatDateKey(dateRange.end)} · ${targetCoverageDays} dias`;
|
const orderReference = `Plano de corte ${formatDateKey(dateRange.start)} a ${formatDateKey(dateRange.end)} · ${targetCoverageDays} dias`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await createProductionOrders(rowsAvailableForOrder.map(row => ({
|
const result = await createProductionOrders(selectedPreviewRows.map(row => ({
|
||||||
status: 'open',
|
status: 'open',
|
||||||
orderReference,
|
orderReference,
|
||||||
issueDate,
|
issueDate,
|
||||||
@@ -785,7 +819,7 @@ const Cutting = () => {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-bold text-dark-text">Prévia das OPs</h2>
|
<h2 className="text-lg font-bold text-dark-text">Prévia das OPs</h2>
|
||||||
<p className="mt-1 text-sm font-semibold text-dark-muted">
|
<p className="mt-1 text-sm font-semibold text-dark-muted">
|
||||||
Serão criadas ordens locais para as necessidades do filtro atual.
|
Escolha os SKUs para criar ordens locais das necessidades do filtro atual.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -802,24 +836,35 @@ const Cutting = () => {
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 divide-x divide-y divide-dark-border border-b border-dark-border md:grid-cols-4 md:divide-y-0">
|
<div className="grid grid-cols-2 divide-x divide-y divide-dark-border border-b border-dark-border md:grid-cols-4 md:divide-y-0">
|
||||||
{[
|
{[
|
||||||
['OPs a criar', formatNumber(rowsAvailableForOrder.length), 'text-dark-text'],
|
['Selecionadas', `${formatNumber(selectedPreviewRows.length)} de ${formatNumber(rowsAvailableForOrder.length)}`, 'text-dark-text'],
|
||||||
['Unidades para cortar', `${formatNumber(orderPreviewTotalUnits)} un.`, 'text-brand-primary'],
|
['Unidades selecionadas', `${formatNumber(orderPreviewTotalUnits)} un.`, 'text-brand-primary'],
|
||||||
['Cobertura alvo', `${formatNumber(targetCoverageDays)} dias`, 'text-dark-text'],
|
['Cobertura alvo', `${formatNumber(targetCoverageDays)} dias`, 'text-dark-text'],
|
||||||
['Revisar dados', formatNumber(previewRowsWithIssues), previewRowsWithIssues ? 'text-amber-300' : 'text-dark-text']
|
['Revisar dados', formatNumber(previewRowsWithIssues), previewRowsWithIssues ? 'text-amber-300' : 'text-emerald-300']
|
||||||
].map(([label, value, tone]) => <div key={label} className="p-4"><p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">{label}</p><p className={`mt-1 text-xl font-bold ${tone}`}>{value}</p></div>)}
|
].map(([label, value, tone]) => <div key={label} className="p-4"><p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">{label}</p><p className={`mt-1 text-xl font-bold ${tone}`}>{value}</p></div>)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<div className="overflow-auto">
|
<div className="overflow-auto">
|
||||||
<table className="w-full min-w-[900px] table-fixed text-left text-sm">
|
<table className="w-full min-w-[950px] table-fixed text-left text-sm">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col className="w-[44%]" />
|
<col className="w-[5%]" />
|
||||||
<col className="w-[16%]" />
|
<col className="w-[41%]" />
|
||||||
<col className="w-[14%]" />
|
<col className="w-[15%]" />
|
||||||
<col className="w-[14%]" />
|
<col className="w-[13%]" />
|
||||||
<col className="w-[12%]" />
|
<col className="w-[13%]" />
|
||||||
|
<col className="w-[13%]" />
|
||||||
</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-[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>
|
||||||
@@ -829,7 +874,8 @@ const Cutting = () => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-dark-border">
|
<tbody className="divide-y divide-dark-border">
|
||||||
{paginatedPreviewRows.map(row => (
|
{paginatedPreviewRows.map(row => (
|
||||||
<tr key={row.id} className="hover:bg-dark-input/50">
|
<tr key={row.id} className={`hover:bg-dark-input/50 ${selectedPreviewRowIdSet.has(row.id) ? 'bg-brand-primary/[0.035]' : ''}`}>
|
||||||
|
<td className="px-4 py-3 text-center"><input type="checkbox" checked={selectedPreviewRowIdSet.has(row.id)} onChange={() => togglePreviewRow(row.id)} aria-label={`Selecionar ${row.name}`} className="h-4 w-4 accent-brand-primary cursor-pointer" /></td>
|
||||||
<td className="max-w-0 px-4 py-3">
|
<td className="max-w-0 px-4 py-3">
|
||||||
<div className="truncate font-bold text-dark-text" title={row.name}>{row.name}</div>
|
<div className="truncate font-bold text-dark-text" title={row.name}>{row.name}</div>
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
||||||
@@ -883,11 +929,11 @@ const Cutting = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void generateProductionOrders()}
|
onClick={() => void generateProductionOrders()}
|
||||||
disabled={isGeneratingOrders}
|
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"
|
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" />
|
<ClipboardList className="h-4 w-4" />
|
||||||
{isGeneratingOrders ? 'Gerando OPs' : 'Confirmar geração'}
|
{isGeneratingOrders ? 'Gerando OPs' : `Confirmar ${formatNumber(selectedPreviewRows.length)} ${selectedPreviewRows.length === 1 ? 'OP' : 'OPs'}`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user