Persist cutting rules in database

This commit is contained in:
Cauê Faleiros
2026-07-08 09:32:28 -03:00
parent d512f8b00d
commit 2dcef87229
9 changed files with 348 additions and 19 deletions

View File

@@ -1,19 +1,16 @@
import { useEffect, useMemo, useState } from 'react';
import { Link, useOutletContext } from 'react-router-dom';
import { AlertTriangle, ClipboardList, Download, Layers3, Package, Palette, RotateCcw, Ruler, Scissors, Search, Settings2 } from 'lucide-react';
import { AlertTriangle, ClipboardList, Download, Layers3, Package, Palette, RotateCcw, Ruler, Save as SaveIcon, Scissors, Search, Settings2 } from 'lucide-react';
import DateRangePicker from '../components/DateRangePicker';
import PaginationControls from '../components/PaginationControls';
import RefreshStatus from '../components/RefreshStatus';
import { CUT_FAMILY_RULES, buildCutPlan, buildOpenProductionByProductId, type CutFamilyKey, type CutIssue, type CutPlanSkuRow, type CutProductOverride } from '../analytics/cutting';
import { exportToCSV, fetchProductAnalytics, fetchProductionOrders } from '../dataService';
import type { DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
import { exportToCSV, fetchCuttingSettings, fetchProductAnalytics, fetchProductionOrders, saveCuttingSettings } from '../dataService';
import type { CuttingSettings, DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
type CutFilter = 'need' | 'all' | 'issues' | 'covered';
type CutSort = 'need_desc' | 'need_asc' | 'demand_desc' | 'stock_asc' | 'sold_desc' | 'name_asc';
type CuttingSettings = {
familyYields: Partial<Record<CutFamilyKey, number>>;
productOverrides: Record<string, CutProductOverride>;
};
type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
const SETTINGS_STORAGE_KEY = 'nexstar_cutting_settings';
const coverageTargetOptions = [7, 15, 30, 60];
@@ -81,6 +78,11 @@ const loadCuttingSettings = (): CuttingSettings => {
}
};
const hasCuttingSettings = (settings: CuttingSettings) => (
Object.keys(settings.familyYields).length > 0 ||
Object.keys(settings.productOverrides).length > 0
);
const CuttingSkeleton = () => (
<div className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm" aria-label="Carregando plano de corte">
<div className="border-b border-dark-border p-4">
@@ -113,12 +115,36 @@ const Cutting = () => {
const [sortBy, setSortBy] = useState<CutSort>('need_desc');
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [cuttingSettings, setCuttingSettings] = useState<CuttingSettings>(loadCuttingSettings);
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle');
const [hasUnsavedSettings, setHasUnsavedSettings] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
useEffect(() => {
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(cuttingSettings));
}, [cuttingSettings]);
let isMounted = true;
const loadSettings = async () => {
const settings = await fetchCuttingSettings();
if (!isMounted) return;
if (hasCuttingSettings(settings)) {
setCuttingSettings(settings);
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
setHasUnsavedSettings(false);
} else {
const localSettings = loadCuttingSettings();
setCuttingSettings(localSettings);
setHasUnsavedSettings(hasCuttingSettings(localSettings));
}
setSaveStatus('idle');
};
void loadSettings();
return () => {
isMounted = false;
};
}, []);
useEffect(() => {
let isMounted = true;
@@ -219,6 +245,8 @@ const Cutting = () => {
}
return { ...current, familyYields };
});
setHasUnsavedSettings(true);
setSaveStatus('idle');
};
const updateProductOverride = (productId: string, patch: CutProductOverride) => {
@@ -238,6 +266,8 @@ const Cutting = () => {
}
return { ...current, productOverrides };
});
setHasUnsavedSettings(true);
setSaveStatus('idle');
};
const clearProductOverride = (productId: string) => {
@@ -246,6 +276,29 @@ const Cutting = () => {
delete productOverrides[productId];
return { ...current, productOverrides };
});
setHasUnsavedSettings(true);
setSaveStatus('idle');
};
const persistSettings = async (settings = cuttingSettings) => {
setSaveStatus('saving');
try {
const savedSettings = await saveCuttingSettings(settings);
setCuttingSettings(savedSettings);
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(savedSettings));
setHasUnsavedSettings(false);
setSaveStatus('saved');
} catch (error) {
console.error('Save cutting settings failed', error);
setSaveStatus('error');
}
};
const clearSettings = () => {
const emptySettings = { familyYields: {}, productOverrides: {} };
setCuttingSettings(emptySettings);
setHasUnsavedSettings(true);
setSaveStatus('idle');
};
const exportRows = () => {
@@ -337,13 +390,31 @@ const Cutting = () => {
</div>
<button
type="button"
onClick={() => setCuttingSettings({ familyYields: {}, productOverrides: {} })}
onClick={clearSettings}
className="inline-flex items-center justify-center gap-2 rounded-xl border border-dark-border bg-dark-input px-3 py-2 text-xs font-bold text-dark-muted transition-colors hover:border-red-400/40 hover:text-red-300 cursor-pointer"
>
<RotateCcw className="h-4 w-4" />
Limpar regras
</button>
<button
type="button"
onClick={() => void persistSettings()}
disabled={saveStatus === 'saving' || !hasUnsavedSettings}
className="inline-flex items-center justify-center gap-2 rounded-xl border border-brand-primary/30 bg-brand-primary/15 px-3 py-2 text-xs font-bold text-brand-primary transition-colors hover:border-brand-primary disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"
>
<SaveIcon className="h-4 w-4" />
{saveStatus === 'saving' ? 'Salvando' : 'Salvar regras'}
</button>
</div>
{saveStatus === 'saved' && (
<p className="text-xs font-semibold text-emerald-300">Regras salvas no banco de dados.</p>
)}
{saveStatus === 'error' && (
<p className="text-xs font-semibold text-red-300">Não foi possível salvar as regras. Tente novamente.</p>
)}
{hasUnsavedSettings && saveStatus !== 'saving' && (
<p className="text-xs font-semibold text-amber-300">Existem alterações ainda não salvas.</p>
)}
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
{CUT_FAMILY_RULES.map(rule => (