Add focused cut item editor
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 49s

This commit is contained in:
Cauê Faleiros
2026-08-11 10:40:41 -03:00
parent 2b618a015e
commit 5f7f9d94d4

View File

@@ -5,7 +5,6 @@ import DateRangePicker from '../components/DateRangePicker';
import PaginationControls from '../components/PaginationControls';
import ProductColorBadge from '../components/ProductColorBadge';
import RefreshStatus from '../components/RefreshStatus';
import { buildCuttingSkuConfigPath } from '../catalogLinks';
import { CUT_FAMILY_RULES, buildCutPlan, buildOpenProductionByProductId, type CutFamilyKey, type CutIssue, type CutPlanSkuRow, type CutProductOverride } from '../analytics/cutting';
import { createProductionOrders, exportToCSV, fetchCuttingSettings, fetchProductAnalytics, fetchProductCompositions, fetchProductionOrders, fetchStock, saveCuttingSettings } from '../dataService';
import type { CuttingSettings, DateRange, ProductAnalyticsItem, ProductComposition, ProductionOrderItem, StockData } from '../types';
@@ -160,6 +159,9 @@ const Cutting = () => {
const [itemsPerPage, setItemsPerPage] = useState(10);
const [isGeneratingOrders, setIsGeneratingOrders] = useState(false);
const [isOrderPreviewOpen, setIsOrderPreviewOpen] = useState(false);
const [editingCutRow, setEditingCutRow] = useState<CutPlanSkuRow | null>(null);
const [quickCutEdit, setQuickCutEdit] = useState<CutProductOverride>({});
const [isQuickCutEditSaving, setIsQuickCutEditSaving] = useState(false);
const [activeCuttingView, setActiveCuttingView] = useState<CuttingView>('plan');
const [generationMessage, setGenerationMessage] = useState('');
const cutConfigSection = searchParams.get('config');
@@ -410,12 +412,14 @@ const Cutting = () => {
}, [correctionItemsPerPage, correctionRows, cutConfigSection, targetCorrectionSku]);
useEffect(() => {
if (!isSettingsOpen && !isOrderPreviewOpen) return undefined;
if (!isSettingsOpen && !isOrderPreviewOpen && !editingCutRow) return undefined;
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
if (isOrderPreviewOpen) {
setIsOrderPreviewOpen(false);
} else if (editingCutRow) {
setEditingCutRow(null);
} else {
setIsSettingsOpen(false);
}
@@ -429,7 +433,7 @@ const Cutting = () => {
document.body.style.overflow = previousOverflow;
document.removeEventListener('keydown', closeOnEscape);
};
}, [isOrderPreviewOpen, isSettingsOpen]);
}, [editingCutRow, isOrderPreviewOpen, isSettingsOpen]);
const updateFamilyYield = (familyKey: CutFamilyKey, value: string) => {
const parsedValue = Number(value);
@@ -503,6 +507,55 @@ const Cutting = () => {
setIsSettingsOpen(true);
};
const openQuickCutEdit = (row: CutPlanSkuRow) => {
const override = cuttingSettings.productOverrides[row.id] || {};
setEditingCutRow(row);
setQuickCutEdit({
familyKey: override.familyKey || '',
color: override.color || '',
size: override.size || ''
});
};
const saveQuickCutEdit = async () => {
if (!editingCutRow) return;
const normalizedOverride: CutProductOverride = {
familyKey: quickCutEdit.familyKey || '',
color: quickCutEdit.color || '',
size: quickCutEdit.size || ''
};
const productOverrides = { ...cuttingSettings.productOverrides };
if (!normalizedOverride.familyKey && !normalizedOverride.color && !normalizedOverride.size) {
delete productOverrides[editingCutRow.id];
} else {
productOverrides[editingCutRow.id] = normalizedOverride;
}
const nextSettings = { ...cuttingSettings, productOverrides };
setIsQuickCutEditSaving(true);
try {
const savedSettings = await saveCuttingSettings(nextSettings);
setCuttingSettings(savedSettings);
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(savedSettings));
setHasUnsavedSettings(false);
setSaveStatus('saved');
setEditingCutRow(null);
} catch (error) {
console.error('Save quick cut edit failed', error);
setSaveStatus('error');
} finally {
setIsQuickCutEditSaving(false);
}
};
const openBulkCorrectionForRow = (row: CutPlanSkuRow) => {
const correctionIndex = correctionRows.findIndex(item => item.id === row.id);
setEditingCutRow(null);
setSettingsSection('corrections');
setCorrectionIssueFilter('all');
if (correctionIndex >= 0) setCorrectionPage(Math.floor(correctionIndex / correctionItemsPerPage) + 1);
setIsSettingsOpen(true);
};
const exportRows = () => {
exportToCSV(filteredRows.map(row => ({
'ID Produto': row.id,
@@ -813,6 +866,39 @@ const Cutting = () => {
</div>
)}
{editingCutRow && (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm" role="dialog" aria-modal="true" aria-label="Editar dados de corte" onClick={() => !isQuickCutEditSaving && setEditingCutRow(null)}>
<div className="w-full max-w-md rounded-2xl border border-dark-border bg-dark-card shadow-2xl" onClick={(event) => event.stopPropagation()}>
<div className="flex items-start justify-between gap-4 border-b border-dark-border p-5">
<div className="min-w-0">
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Editar dados de corte</p>
<h2 className="mt-1 truncate text-base font-bold text-dark-text" title={editingCutRow.name}>{editingCutRow.name}</h2>
<p className="mt-1 font-mono text-xs font-semibold text-brand-primary">#{editingCutRow.id}</p>
</div>
<button type="button" onClick={() => setEditingCutRow(null)} disabled={isQuickCutEditSaving} title="Fechar" aria-label="Fechar" className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-dark-border bg-dark-input text-dark-muted transition-colors hover:border-brand-primary hover:text-dark-text disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"><X className="h-4 w-4" /></button>
</div>
<div className="space-y-4 p-5">
<div className="flex flex-wrap gap-2 text-xs font-bold text-dark-muted">
<span className={`rounded-full border px-2.5 py-1 ${familyStyles[editingCutRow.family.key]}`}>{editingCutRow.family.materialLabel}</span>
<ProductColorBadge label={editingCutRow.color} className="px-2.5 py-1 text-xs" />
<span className="rounded-full border border-emerald-400/25 bg-emerald-400/10 px-2.5 py-1 text-emerald-300">{editingCutRow.size || 'Sem tamanho'}</span>
</div>
<p className="text-xs font-semibold leading-relaxed text-dark-muted">Altere apenas quando a leitura automática do SKU estiver incorreta. Salvar atualiza este SKU imediatamente.</p>
<label className="block text-xs font-bold text-dark-muted">Família<select value={quickCutEdit.familyKey || ''} onChange={(event) => setQuickCutEdit(current => ({ ...current, familyKey: event.target.value as CutFamilyKey | '' }))} className="mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text outline-none focus:border-brand-primary cursor-pointer"><option value="">Automática ({editingCutRow.family.materialLabel})</option>{familyOptions.filter(option => option.value !== 'all').map(option => <option key={option.value} value={option.value}>{option.label}</option>)}</select></label>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<label className="text-xs font-bold text-dark-muted">Cor<input value={quickCutEdit.color || ''} onChange={(event) => setQuickCutEdit(current => ({ ...current, color: event.target.value }))} placeholder={editingCutRow.color || 'Informar cor'} className="mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text outline-none focus:border-brand-primary" /></label>
<label className="text-xs font-bold text-dark-muted">Tamanho<input value={quickCutEdit.size || ''} onChange={(event) => setQuickCutEdit(current => ({ ...current, size: event.target.value }))} placeholder={editingCutRow.size || 'Informar tamanho'} className="mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text outline-none focus:border-brand-primary" /></label>
</div>
<button type="button" onClick={() => setQuickCutEdit({})} className="inline-flex items-center gap-2 text-xs font-bold text-dark-muted transition-colors hover:text-dark-text cursor-pointer"><RotateCcw className="h-3.5 w-3.5" /> Usar leitura automática</button>
</div>
<div className="flex flex-col gap-2 border-t border-dark-border p-4 sm:flex-row sm:items-center sm:justify-between">
<button type="button" onClick={() => openBulkCorrectionForRow(editingCutRow)} disabled={isQuickCutEditSaving} className="text-left text-xs font-bold text-brand-primary transition-colors hover:text-brand-primary/80 disabled:opacity-50 cursor-pointer">Abrir correções em lote</button>
<div className="flex gap-2 sm:justify-end"><button type="button" onClick={() => setEditingCutRow(null)} disabled={isQuickCutEditSaving} className="h-10 rounded-xl border border-dark-border bg-dark-input px-3 text-xs font-bold text-dark-text transition-colors hover:border-brand-primary disabled:opacity-50 cursor-pointer">Cancelar</button><button type="button" onClick={() => void saveQuickCutEdit()} disabled={isQuickCutEditSaving} className="inline-flex h-10 items-center gap-2 rounded-xl bg-brand-primary px-3 text-xs font-bold text-brand-contrast transition-colors hover:bg-brand-primary/90 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"><SaveIcon className="h-4 w-4" />{isQuickCutEditSaving ? 'Salvando' : 'Salvar'}</button></div>
</div>
</div>
</div>
)}
{isSettingsOpen && (
<div
className="fixed inset-0 z-50 flex h-dvh justify-end overflow-hidden bg-zinc-950/80 backdrop-blur-md dark:bg-black/75"
@@ -1316,14 +1402,15 @@ const Cutting = () => {
<td className="px-4 py-2.5 align-middle">{renderCutStatus(row)}</td>
<td className="px-4 py-2.5 text-right">
<div className="flex justify-end gap-2">
<Link
to={buildCuttingSkuConfigPath({ sku: row.id })}
className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-dark-input text-dark-text transition-colors hover:bg-dark-border"
title={`Configurar corte do SKU ${row.id}`}
aria-label={`Configurar corte do SKU ${row.id}`}
<button
type="button"
onClick={() => openQuickCutEdit(row)}
className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-dark-input text-dark-text transition-colors hover:bg-dark-border cursor-pointer"
title={`Editar dados de corte do SKU ${row.id}`}
aria-label={`Editar dados de corte do SKU ${row.id}`}
>
<Pencil className="h-3.5 w-3.5" />
</Link>
</button>
<Link
to={`/products/${row.id}`}
className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-brand-primary/10 text-brand-primary transition-opacity hover:opacity-80"