Add editable cutting rules
This commit is contained in:
@@ -70,6 +70,38 @@ test('buildCutPlan subtracts open production quantity from suggested cut need',
|
|||||||
assert.equal(row.suggestedCutQuantity, 20);
|
assert.equal(row.suggestedCutQuantity, 20);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildCutPlan estimates rolls when a family yield is configured', () => {
|
||||||
|
const plan = buildCutPlan([product({})], range, 14, {}, { familyYields: { BLCS: 50 } });
|
||||||
|
const [row] = plan.needRows;
|
||||||
|
|
||||||
|
assert.equal(row.suggestedCutQuantity, 120);
|
||||||
|
assert.equal(row.estimatedRolls, 3);
|
||||||
|
assert.deepEqual(row.issues, []);
|
||||||
|
assert.equal(plan.summary.estimatedRolls, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildCutPlan applies per-product planning overrides', () => {
|
||||||
|
const plan = buildCutPlan([
|
||||||
|
product({
|
||||||
|
id: 'SKU-2',
|
||||||
|
name: 'BONÉ PRETO',
|
||||||
|
quantitySold: 70,
|
||||||
|
stock: 0
|
||||||
|
})
|
||||||
|
], range, 7, {}, {
|
||||||
|
familyYields: { BLCS: 35 },
|
||||||
|
productOverrides: {
|
||||||
|
'SKU-2': { familyKey: 'BLCS', color: 'Preto', size: 'M' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(plan.needRows[0].family.key, 'BLCS');
|
||||||
|
assert.equal(plan.needRows[0].color, 'Preto');
|
||||||
|
assert.equal(plan.needRows[0].size, 'M');
|
||||||
|
assert.equal(plan.needRows[0].estimatedRolls, 2);
|
||||||
|
assert.deepEqual(plan.needRows[0].issues, []);
|
||||||
|
});
|
||||||
|
|
||||||
test('buildOpenProductionByProductId matches open OPs by SKU and normalized product variant', () => {
|
test('buildOpenProductionByProductId matches open OPs by SKU and normalized product variant', () => {
|
||||||
const products = [
|
const products = [
|
||||||
product({ id: 'SKU-1' }),
|
product({ id: 'SKU-1' }),
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ export interface CutFamilyRule {
|
|||||||
unitsPerRoll: number | null;
|
unitsPerRoll: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CutProductOverride {
|
||||||
|
familyKey?: CutFamilyKey | '';
|
||||||
|
color?: string;
|
||||||
|
size?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CutPlanOptions {
|
||||||
|
familyYields?: Partial<Record<CutFamilyKey, number>>;
|
||||||
|
productOverrides?: Record<string, CutProductOverride>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CutPlanSkuRow extends ProductAnalyticsItem {
|
export interface CutPlanSkuRow extends ProductAnalyticsItem {
|
||||||
family: CutFamilyRule;
|
family: CutFamilyRule;
|
||||||
baseName: string;
|
baseName: string;
|
||||||
@@ -99,6 +110,11 @@ export const OUTROS_RULE: CutFamilyRule = {
|
|||||||
unitsPerRoll: null
|
unitsPerRoll: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FAMILY_RULES_BY_KEY = new Map<CutFamilyKey, CutFamilyRule>([
|
||||||
|
...CUT_FAMILY_RULES.map(rule => [rule.key, rule] as const),
|
||||||
|
[OUTROS_RULE.key, OUTROS_RULE]
|
||||||
|
]);
|
||||||
|
|
||||||
export const getRangeDays = (range: DateRange) => {
|
export const getRangeDays = (range: DateRange) => {
|
||||||
const start = new Date(range.start);
|
const start = new Date(range.start);
|
||||||
const end = new Date(range.end);
|
const end = new Date(range.end);
|
||||||
@@ -121,6 +137,14 @@ export const classifyCutFamily = (baseName: string): CutFamilyRule => {
|
|||||||
)) || OUTROS_RULE;
|
)) || OUTROS_RULE;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ruleWithConfiguredYield = (rule: CutFamilyRule, familyYields?: Partial<Record<CutFamilyKey, number>>): CutFamilyRule => {
|
||||||
|
const configuredYield = familyYields?.[rule.key];
|
||||||
|
return {
|
||||||
|
...rule,
|
||||||
|
unitsPerRoll: configuredYield && configuredYield > 0 ? configuredYield : rule.unitsPerRoll
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeMatchText = (value: string) => normalizeProductText(value)
|
const normalizeMatchText = (value: string) => normalizeProductText(value)
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
.replace(/\p{Diacritic}/gu, '')
|
.replace(/\p{Diacritic}/gu, '')
|
||||||
@@ -178,13 +202,18 @@ export const buildCutPlan = (
|
|||||||
products: ProductAnalyticsItem[],
|
products: ProductAnalyticsItem[],
|
||||||
dateRange: DateRange,
|
dateRange: DateRange,
|
||||||
targetCoverageDays: number,
|
targetCoverageDays: number,
|
||||||
openProductionByProductId: Record<string, number> = {}
|
openProductionByProductId: Record<string, number> = {},
|
||||||
|
options: CutPlanOptions = {}
|
||||||
): CutPlan => {
|
): CutPlan => {
|
||||||
const rangeDays = getRangeDays(dateRange);
|
const rangeDays = getRangeDays(dateRange);
|
||||||
|
|
||||||
const rows = products.map<CutPlanSkuRow>(product => {
|
const rows = products.map<CutPlanSkuRow>(product => {
|
||||||
const metadata = parseProductName(product.name);
|
const metadata = parseProductName(product.name);
|
||||||
const family = classifyCutFamily(metadata.baseName);
|
const override = options.productOverrides?.[product.id];
|
||||||
|
const overrideRule = override?.familyKey ? FAMILY_RULES_BY_KEY.get(override.familyKey) : undefined;
|
||||||
|
const family = ruleWithConfiguredYield(overrideRule || classifyCutFamily(metadata.baseName), options.familyYields);
|
||||||
|
const color = normalizeProductText(override?.color || metadata.color);
|
||||||
|
const size = normalizeProductText(override?.size || metadata.size).toUpperCase();
|
||||||
const dailySales = product.quantitySold / rangeDays;
|
const dailySales = product.quantitySold / rangeDays;
|
||||||
const projectedDemand = dailySales * targetCoverageDays;
|
const projectedDemand = dailySales * targetCoverageDays;
|
||||||
const openProductionQuantity = openProductionByProductId[product.id] || 0;
|
const openProductionQuantity = openProductionByProductId[product.id] || 0;
|
||||||
@@ -197,16 +226,16 @@ export const buildCutPlan = (
|
|||||||
const issues: CutIssue[] = [];
|
const issues: CutIssue[] = [];
|
||||||
|
|
||||||
if (family.key === 'OUTROS') issues.push('missing_family_rule');
|
if (family.key === 'OUTROS') issues.push('missing_family_rule');
|
||||||
if (!metadata.color) issues.push('missing_color');
|
if (!color) issues.push('missing_color');
|
||||||
if (!metadata.size) issues.push('missing_size');
|
if (!size) issues.push('missing_size');
|
||||||
if (suggestedCutQuantity > 0 && family.key !== 'OUTROS' && !family.unitsPerRoll) issues.push('missing_yield_rule');
|
if (suggestedCutQuantity > 0 && family.key !== 'OUTROS' && !family.unitsPerRoll) issues.push('missing_yield_rule');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...product,
|
...product,
|
||||||
family,
|
family,
|
||||||
baseName: metadata.baseName,
|
baseName: metadata.baseName,
|
||||||
color: metadata.color,
|
color,
|
||||||
size: metadata.size,
|
size,
|
||||||
dailySales,
|
dailySales,
|
||||||
projectedDemand,
|
projectedDemand,
|
||||||
openProductionQuantity,
|
openProductionQuantity,
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useOutletContext } from 'react-router-dom';
|
import { Link, useOutletContext } from 'react-router-dom';
|
||||||
import { AlertTriangle, ClipboardList, Download, Layers3, Package, Palette, Ruler, Scissors, Search } from 'lucide-react';
|
import { AlertTriangle, ClipboardList, Download, Layers3, Package, Palette, RotateCcw, Ruler, Scissors, Search, Settings2 } from 'lucide-react';
|
||||||
import DateRangePicker from '../components/DateRangePicker';
|
import DateRangePicker from '../components/DateRangePicker';
|
||||||
import PaginationControls from '../components/PaginationControls';
|
import PaginationControls from '../components/PaginationControls';
|
||||||
import RefreshStatus from '../components/RefreshStatus';
|
import RefreshStatus from '../components/RefreshStatus';
|
||||||
import { CUT_FAMILY_RULES, buildCutPlan, buildOpenProductionByProductId, type CutFamilyKey, type CutIssue, type CutPlanSkuRow } from '../analytics/cutting';
|
import { CUT_FAMILY_RULES, buildCutPlan, buildOpenProductionByProductId, type CutFamilyKey, type CutIssue, type CutPlanSkuRow, type CutProductOverride } from '../analytics/cutting';
|
||||||
import { exportToCSV, fetchProductAnalytics, fetchProductionOrders } from '../dataService';
|
import { exportToCSV, fetchProductAnalytics, fetchProductionOrders } from '../dataService';
|
||||||
import type { DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
|
import type { DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
|
||||||
|
|
||||||
type CutFilter = 'need' | 'all' | 'issues' | 'covered';
|
type CutFilter = 'need' | 'all' | 'issues' | 'covered';
|
||||||
type CutSort = 'need_desc' | 'need_asc' | 'demand_desc' | 'stock_asc' | 'sold_desc' | 'name_asc';
|
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>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SETTINGS_STORAGE_KEY = 'nexstar_cutting_settings';
|
||||||
const coverageTargetOptions = [7, 15, 30, 60];
|
const coverageTargetOptions = [7, 15, 30, 60];
|
||||||
const familyOptions: Array<{ value: CutFamilyKey | 'all'; label: string }> = [
|
const familyOptions: Array<{ value: CutFamilyKey | 'all'; label: string }> = [
|
||||||
{ value: 'all', label: 'Todas famílias' },
|
{ value: 'all', label: 'Todas famílias' },
|
||||||
@@ -62,6 +67,20 @@ const allProductionOrdersRange = {
|
|||||||
end: new Date(2100, 11, 31)
|
end: new Date(2100, 11, 31)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadCuttingSettings = (): CuttingSettings => {
|
||||||
|
try {
|
||||||
|
const rawSettings = localStorage.getItem(SETTINGS_STORAGE_KEY);
|
||||||
|
if (!rawSettings) return { familyYields: {}, productOverrides: {} };
|
||||||
|
const parsed = JSON.parse(rawSettings) as Partial<CuttingSettings>;
|
||||||
|
return {
|
||||||
|
familyYields: parsed.familyYields || {},
|
||||||
|
productOverrides: parsed.productOverrides || {}
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { familyYields: {}, productOverrides: {} };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const CuttingSkeleton = () => (
|
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="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">
|
<div className="border-b border-dark-border p-4">
|
||||||
@@ -92,9 +111,15 @@ const Cutting = () => {
|
|||||||
const [familyFilter, setFamilyFilter] = useState<CutFamilyKey | 'all'>('all');
|
const [familyFilter, setFamilyFilter] = useState<CutFamilyKey | 'all'>('all');
|
||||||
const [cutFilter, setCutFilter] = useState<CutFilter>('need');
|
const [cutFilter, setCutFilter] = useState<CutFilter>('need');
|
||||||
const [sortBy, setSortBy] = useState<CutSort>('need_desc');
|
const [sortBy, setSortBy] = useState<CutSort>('need_desc');
|
||||||
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
const [cuttingSettings, setCuttingSettings] = useState<CuttingSettings>(loadCuttingSettings);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(cuttingSettings));
|
||||||
|
}, [cuttingSettings]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
|
|
||||||
@@ -124,8 +149,8 @@ const Cutting = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const cutPlan = useMemo(
|
const cutPlan = useMemo(
|
||||||
() => buildCutPlan(products, dateRange, targetCoverageDays, openProductionByProductId),
|
() => buildCutPlan(products, dateRange, targetCoverageDays, openProductionByProductId, cuttingSettings),
|
||||||
[dateRange, openProductionByProductId, products, targetCoverageDays]
|
[cuttingSettings, dateRange, openProductionByProductId, products, targetCoverageDays]
|
||||||
);
|
);
|
||||||
|
|
||||||
const issueSummaries = useMemo(() => {
|
const issueSummaries = useMemo(() => {
|
||||||
@@ -177,6 +202,51 @@ const Cutting = () => {
|
|||||||
const startIndex = (safeCurrentPage - 1) * itemsPerPage;
|
const startIndex = (safeCurrentPage - 1) * itemsPerPage;
|
||||||
const paginatedRows = filteredRows.slice(startIndex, startIndex + itemsPerPage);
|
const paginatedRows = filteredRows.slice(startIndex, startIndex + itemsPerPage);
|
||||||
const isRefreshing = isLoading && products.length > 0;
|
const isRefreshing = isLoading && products.length > 0;
|
||||||
|
const correctionRows = cutPlan.rows.filter(row => (
|
||||||
|
row.issues.some(issue => issue !== 'missing_yield_rule')
|
||||||
|
)).slice(0, 12);
|
||||||
|
const configuredYieldCount = CUT_FAMILY_RULES.filter(rule => cuttingSettings.familyYields[rule.key]).length;
|
||||||
|
const productOverrideCount = Object.keys(cuttingSettings.productOverrides).length;
|
||||||
|
|
||||||
|
const updateFamilyYield = (familyKey: CutFamilyKey, value: string) => {
|
||||||
|
const parsedValue = Number(value);
|
||||||
|
setCuttingSettings(current => {
|
||||||
|
const familyYields = { ...current.familyYields };
|
||||||
|
if (Number.isFinite(parsedValue) && parsedValue > 0) {
|
||||||
|
familyYields[familyKey] = parsedValue;
|
||||||
|
} else {
|
||||||
|
delete familyYields[familyKey];
|
||||||
|
}
|
||||||
|
return { ...current, familyYields };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProductOverride = (productId: string, patch: CutProductOverride) => {
|
||||||
|
setCuttingSettings(current => {
|
||||||
|
const currentOverride = current.productOverrides[productId] || {};
|
||||||
|
const nextOverride = { ...currentOverride, ...patch };
|
||||||
|
const normalizedOverride: CutProductOverride = {
|
||||||
|
familyKey: nextOverride.familyKey || '',
|
||||||
|
color: nextOverride.color || '',
|
||||||
|
size: nextOverride.size || ''
|
||||||
|
};
|
||||||
|
const productOverrides = { ...current.productOverrides };
|
||||||
|
if (!normalizedOverride.familyKey && !normalizedOverride.color && !normalizedOverride.size) {
|
||||||
|
delete productOverrides[productId];
|
||||||
|
} else {
|
||||||
|
productOverrides[productId] = normalizedOverride;
|
||||||
|
}
|
||||||
|
return { ...current, productOverrides };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearProductOverride = (productId: string) => {
|
||||||
|
setCuttingSettings(current => {
|
||||||
|
const productOverrides = { ...current.productOverrides };
|
||||||
|
delete productOverrides[productId];
|
||||||
|
return { ...current, productOverrides };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const exportRows = () => {
|
const exportRows = () => {
|
||||||
exportToCSV(filteredRows.map(row => ({
|
exportToCSV(filteredRows.map(row => ({
|
||||||
@@ -241,11 +311,125 @@ const Cutting = () => {
|
|||||||
<Download size={16} className="text-brand-primary" />
|
<Download size={16} className="text-brand-primary" />
|
||||||
<span className="hidden sm:inline">Exportar</span>
|
<span className="hidden sm:inline">Exportar</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsSettingsOpen(current => !current)}
|
||||||
|
className="flex items-center justify-center gap-2 rounded-xl border border-dark-border bg-dark-card px-4 py-2.5 text-sm font-medium text-dark-text shadow-sm transition-colors hover:border-brand-primary cursor-pointer"
|
||||||
|
title="Configurar regras de corte"
|
||||||
|
>
|
||||||
|
<Settings2 size={16} className="text-brand-primary" />
|
||||||
|
<span className="hidden sm:inline">Regras</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RefreshStatus isRefreshing={isRefreshing} />
|
<RefreshStatus isRefreshing={isRefreshing} />
|
||||||
|
|
||||||
|
{isSettingsOpen && (
|
||||||
|
<div className="space-y-4 rounded-2xl border border-dark-border bg-dark-card p-4 shadow-sm">
|
||||||
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-bold text-dark-text">Regras de Corte</h2>
|
||||||
|
<p className="mt-1 text-xs font-semibold text-dark-muted">
|
||||||
|
{configuredYieldCount} rendimentos configurados · {productOverrideCount} correções de produto
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCuttingSettings({ familyYields: {}, productOverrides: {} })}
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
|
||||||
|
{CUT_FAMILY_RULES.map(rule => (
|
||||||
|
<label key={rule.key} className="rounded-xl border border-dark-border bg-dark-input p-3">
|
||||||
|
<span className="mb-2 flex items-center justify-between gap-2">
|
||||||
|
<span className="text-xs font-bold text-dark-text">{rule.materialLabel}</span>
|
||||||
|
<span className="text-[11px] font-semibold text-dark-muted">{rule.label}</span>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
value={cuttingSettings.familyYields[rule.key] || ''}
|
||||||
|
onChange={(event) => updateFamilyYield(rule.key, event.target.value)}
|
||||||
|
placeholder="un./rolo"
|
||||||
|
className="h-10 w-full rounded-lg border border-dark-border bg-dark-card px-3 text-sm font-bold text-dark-text outline-none transition-colors focus:border-brand-primary"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!!correctionRows.length && (
|
||||||
|
<div className="overflow-x-auto rounded-xl border border-dark-border">
|
||||||
|
<div className="grid grid-cols-[120px_1.4fr_150px_150px_120px_80px] gap-3 border-b border-dark-border bg-dark-header px-4 py-3 text-[10px] font-bold uppercase tracking-wider text-dark-muted">
|
||||||
|
<span>ID</span>
|
||||||
|
<span>Produto</span>
|
||||||
|
<span>Família</span>
|
||||||
|
<span>Cor</span>
|
||||||
|
<span>Tamanho</span>
|
||||||
|
<span className="text-right">Ações</span>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-dark-border">
|
||||||
|
{correctionRows.map(row => {
|
||||||
|
const override = cuttingSettings.productOverrides[row.id] || {};
|
||||||
|
return (
|
||||||
|
<div key={row.id} className="grid grid-cols-[120px_1.4fr_150px_150px_120px_80px] items-center gap-3 px-4 py-3">
|
||||||
|
<span className="font-mono text-[11px] text-dark-muted">#{row.id}</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate text-xs font-bold text-dark-text" title={row.name}>{row.name}</div>
|
||||||
|
<div className="mt-1 text-[10px] font-semibold text-amber-300">
|
||||||
|
{row.issues.map(issue => issueLabels[issue]).join(', ')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={override.familyKey || ''}
|
||||||
|
onChange={(event) => updateProductOverride(row.id, { familyKey: event.target.value as CutFamilyKey | '' })}
|
||||||
|
className="h-9 rounded-lg border border-dark-border bg-dark-input px-2 text-xs font-bold text-dark-text outline-none focus:border-brand-primary cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="">Auto</option>
|
||||||
|
{familyOptions.filter(option => option.value !== 'all').map(option => (
|
||||||
|
<option key={option.value} value={option.value}>{option.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={override.color || ''}
|
||||||
|
onChange={(event) => updateProductOverride(row.id, { color: event.target.value })}
|
||||||
|
placeholder={row.color || 'Cor'}
|
||||||
|
className="h-9 rounded-lg border border-dark-border bg-dark-input px-2 text-xs font-bold text-dark-text outline-none focus:border-brand-primary"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={override.size || ''}
|
||||||
|
onChange={(event) => updateProductOverride(row.id, { size: event.target.value })}
|
||||||
|
placeholder={row.size || 'Tam.'}
|
||||||
|
className="h-9 rounded-lg border border-dark-border bg-dark-input px-2 text-xs font-bold text-dark-text outline-none focus:border-brand-primary"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => clearProductOverride(row.id)}
|
||||||
|
className="inline-flex h-9 w-9 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="Limpar correção"
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||||
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user