Add manual SKU planning overrides
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m18s

This commit is contained in:
Cauê Faleiros
2026-07-16 11:48:52 -03:00
parent 422828f343
commit 8eeadf00ae
11 changed files with 444 additions and 34 deletions

View File

@@ -0,0 +1,201 @@
import { useState } from 'react';
import { RotateCcw, Save, X } from 'lucide-react';
import { CUT_FAMILY_RULES, type CutFamilyKey } from '../analytics/cutting';
import { editableProductTypeOptions, getProductTypeConfig, resolveProductType, type ProductTypeKey } from '../productClassification';
import type { CutProductOverride } from '../types';
import ProductTypeBadge from './ProductTypeBadge';
type SkuPlanningModalProduct = {
id: string;
name: string;
color?: string;
size?: string;
};
type SkuPlanningModalProps = {
product: SkuPlanningModalProduct;
override?: CutProductOverride;
isSaving?: boolean;
onClose: () => void;
onSave: (override: CutProductOverride | null) => void | Promise<void>;
};
const familyOptions: Array<{ value: CutFamilyKey | ''; label: string }> = [
{ value: '', label: 'Auto' },
...CUT_FAMILY_RULES.map(rule => ({ value: rule.key, label: `${rule.materialLabel} · ${rule.label}` })),
{ value: 'OUTROS', label: 'Sem regra' }
];
const buildOverride = ({
familyKey,
color,
size,
productType,
planningNotes
}: Required<Pick<CutProductOverride, 'color' | 'size' | 'planningNotes'>> & {
familyKey: CutFamilyKey | '';
productType: ProductTypeKey | '';
}): CutProductOverride | null => {
const next: CutProductOverride = {
familyKey,
color: color.trim(),
size: size.trim().toUpperCase(),
productType,
planningNotes: planningNotes.trim()
};
if (!next.familyKey && !next.color && !next.size && !next.productType && !next.planningNotes) {
return null;
}
return next;
};
const SkuPlanningModal = ({ product, override, isSaving = false, onClose, onSave }: SkuPlanningModalProps) => {
const [familyKey, setFamilyKey] = useState<CutFamilyKey | ''>(override?.familyKey || '');
const [color, setColor] = useState(override?.color || '');
const [size, setSize] = useState(override?.size || '');
const [productType, setProductType] = useState<ProductTypeKey | ''>(override?.productType || '');
const [planningNotes, setPlanningNotes] = useState(override?.planningNotes || '');
const resolvedType = resolveProductType(product.name, { productType });
const resolvedConfig = getProductTypeConfig(resolvedType);
const handleSave = () => {
void onSave(buildOverride({ familyKey, color, size, productType, planningNotes }));
};
const handleClear = () => {
void onSave(null);
};
return (
<div
className="fixed inset-0 z-50 flex h-dvh items-center justify-center overflow-y-auto bg-zinc-950/80 p-4 backdrop-blur-md dark:bg-black/75"
role="dialog"
aria-modal="true"
aria-label={`Editar planejamento do SKU ${product.id}`}
onClick={onClose}
>
<div
className="w-full max-w-2xl 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">
<div className="font-mono text-[10px] font-bold uppercase tracking-widest text-dark-muted">SKU #{product.id}</div>
<h2 className="mt-1 truncate text-lg font-bold text-dark-text" title={product.name}>{product.name}</h2>
<div className="mt-2 flex flex-wrap items-center gap-2">
<ProductTypeBadge type={resolvedType} />
<span className="text-xs font-semibold text-dark-muted">{resolvedConfig.description}</span>
</div>
</div>
<button
type="button"
onClick={onClose}
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 cursor-pointer"
title="Fechar"
aria-label="Fechar"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="grid grid-cols-1 gap-4 p-5 md:grid-cols-2">
<label className="space-y-2">
<span className="text-xs font-bold uppercase tracking-widest text-dark-muted">Tipo de produto</span>
<select
value={productType}
onChange={(event) => setProductType(event.target.value as ProductTypeKey | '')}
className="h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text outline-none transition-colors focus:border-brand-primary cursor-pointer"
>
<option value="">Auto ({getProductTypeConfig(resolveProductType(product.name)).label})</option>
{editableProductTypeOptions.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</label>
<label className="space-y-2">
<span className="text-xs font-bold uppercase tracking-widest text-dark-muted">Família de corte</span>
<select
value={familyKey}
onChange={(event) => setFamilyKey(event.target.value as CutFamilyKey | '')}
className="h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text outline-none transition-colors focus:border-brand-primary cursor-pointer"
>
{familyOptions.map(option => (
<option key={option.value || 'auto'} value={option.value}>{option.label}</option>
))}
</select>
</label>
<label className="space-y-2">
<span className="text-xs font-bold uppercase tracking-widest text-dark-muted">Cor</span>
<input
type="text"
value={color}
onChange={(event) => setColor(event.target.value)}
placeholder={product.color || 'Auto pelo nome'}
className="h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text outline-none transition-colors placeholder:text-dark-muted focus:border-brand-primary"
/>
</label>
<label className="space-y-2">
<span className="text-xs font-bold uppercase tracking-widest text-dark-muted">Tamanho</span>
<input
type="text"
value={size}
onChange={(event) => setSize(event.target.value)}
placeholder={product.size || 'Auto pelo nome'}
className="h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold uppercase text-dark-text outline-none transition-colors placeholder:normal-case placeholder:text-dark-muted focus:border-brand-primary"
/>
</label>
<label className="space-y-2 md:col-span-2">
<span className="text-xs font-bold uppercase tracking-widest text-dark-muted">Notas de planejamento</span>
<textarea
value={planningNotes}
onChange={(event) => setPlanningNotes(event.target.value)}
rows={3}
placeholder="Ex.: revisar consumo, material usado, regra temporária..."
className="w-full resize-none rounded-lg border border-dark-border bg-dark-input px-3 py-2 text-sm font-semibold text-dark-text outline-none transition-colors placeholder:text-dark-muted focus:border-brand-primary"
/>
</label>
</div>
<div className="flex flex-col gap-2 border-t border-dark-border p-5 sm:flex-row sm:items-center sm:justify-between">
<button
type="button"
onClick={handleClear}
disabled={isSaving}
className="inline-flex items-center justify-center gap-2 rounded-xl border border-dark-border bg-dark-input px-4 py-2.5 text-sm 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"
>
<RotateCcw className="h-4 w-4" />
Limpar override
</button>
<div className="flex gap-2 sm:justify-end">
<button
type="button"
onClick={onClose}
disabled={isSaving}
className="inline-flex flex-1 items-center justify-center rounded-xl border border-dark-border bg-dark-input px-4 py-2.5 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary disabled:cursor-not-allowed disabled:opacity-50 sm:flex-none cursor-pointer"
>
Cancelar
</button>
<button
type="button"
onClick={handleSave}
disabled={isSaving}
className="inline-flex flex-1 items-center justify-center gap-2 rounded-xl border border-brand-primary/30 bg-brand-primary/15 px-4 py-2.5 text-sm font-bold text-brand-primary transition-colors hover:border-brand-primary disabled:cursor-not-allowed disabled:opacity-50 sm:flex-none cursor-pointer"
>
<Save className="h-4 w-4" />
{isSaving ? 'Salvando' : 'Salvar'}
</button>
</div>
</div>
</div>
</div>
);
};
export default SkuPlanningModal;