Persist cutting rules in database
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import type { DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
|
||||
import type { CutFamilyKey, CutProductOverride, DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
|
||||
import { normalizeProductText, parseProductName, sortProductSizes } from '../productParsing.ts';
|
||||
|
||||
export type CutFamilyKey = 'BLCS' | 'BLOS' | 'BLMC' | 'BLPM' | 'OUTROS';
|
||||
export type { CutFamilyKey, CutProductOverride };
|
||||
|
||||
export type CutIssue = 'missing_family_rule' | 'missing_color' | 'missing_size' | 'missing_yield_rule';
|
||||
|
||||
export interface CutFamilyRule {
|
||||
@@ -12,12 +13,6 @@ export interface CutFamilyRule {
|
||||
unitsPerRoll: number | null;
|
||||
}
|
||||
|
||||
export interface CutProductOverride {
|
||||
familyKey?: CutFamilyKey | '';
|
||||
color?: string;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export interface CutPlanOptions {
|
||||
familyYields?: Partial<Record<CutFamilyKey, number>>;
|
||||
productOverrides?: Record<string, CutProductOverride>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, ClientAnalyticsItem, ClientDetailsAnalytics, ClientFilterOptions, ClientMetadataFilters, ClientPurchasePatternAnalytics, CreateUserResult, DashboardAnalytics, DateRange, ManagedUser, OrderData, ProductAnalyticsItem, ProductDetailsAnalytics, ProductionOrderSummary, RfmAnalytics, StockData } from './types';
|
||||
import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, ClientAnalyticsItem, ClientDetailsAnalytics, ClientFilterOptions, ClientMetadataFilters, ClientPurchasePatternAnalytics, CreateUserResult, CuttingSettings, DashboardAnalytics, DateRange, ManagedUser, OrderData, ProductAnalyticsItem, ProductDetailsAnalytics, ProductionOrderSummary, RfmAnalytics, StockData } from './types';
|
||||
import { formatDateParam } from './dateRanges';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
@@ -183,6 +183,32 @@ export const fetchProductionOrders = async (
|
||||
}, options);
|
||||
};
|
||||
|
||||
export const fetchCuttingSettings = async (): Promise<CuttingSettings> => {
|
||||
try {
|
||||
const response = await authFetch('/cutting-settings');
|
||||
if (!response.ok) return { familyYields: {}, productOverrides: {} };
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch cutting settings failed', error);
|
||||
return { familyYields: {}, productOverrides: {} };
|
||||
}
|
||||
};
|
||||
|
||||
export const saveCuttingSettings = async (settings: CuttingSettings): Promise<CuttingSettings> => {
|
||||
const response = await authFetch('/cutting-settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
|
||||
const data = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.error || 'Não foi possível salvar as regras de corte.');
|
||||
}
|
||||
|
||||
return data as CuttingSettings;
|
||||
};
|
||||
|
||||
export const fetchDashboardAnalytics = async (dateRange: DateRange, options?: CacheOptions): Promise<DashboardAnalytics | null> => {
|
||||
const path = `/analytics/dashboard?${buildDateRangeParams(dateRange).toString()}`;
|
||||
return getCachedAnalytics(path, async () => {
|
||||
|
||||
@@ -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 => (
|
||||
|
||||
13
src/types.ts
13
src/types.ts
@@ -65,6 +65,19 @@ export interface ProductionOrderSummary {
|
||||
counts: ProductionOrderCounts;
|
||||
}
|
||||
|
||||
export type CutFamilyKey = 'BLCS' | 'BLOS' | 'BLMC' | 'BLPM' | 'OUTROS';
|
||||
|
||||
export interface CutProductOverride {
|
||||
familyKey?: CutFamilyKey | '';
|
||||
color?: string;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export interface CuttingSettings {
|
||||
familyYields: Partial<Record<CutFamilyKey, number>>;
|
||||
productOverrides: Record<string, CutProductOverride>;
|
||||
}
|
||||
|
||||
export interface DateRange {
|
||||
start: Date;
|
||||
end: Date;
|
||||
|
||||
Reference in New Issue
Block a user