feat(F-133): completed feature

This commit is contained in:
chattie
2026-08-21 18:44:42 +02:00
parent 15d6ebb1f3
commit 5289ae2717
10 changed files with 243 additions and 46 deletions

View File

@@ -50,32 +50,17 @@ export function PriceStockSection({ productId }: { productId?: string }) {
const [eanMsg, setEanMsg] = useState('');
// Peso y compra mínima (F-102)
// Peso se gestiona en gramos (UI: dropdown + custom). Se convierte a kg al persistir.
// Peso se gestiona en gramos. La UI es un <input type="number" list="…"> con
// <datalist> que ofrece los presets como sugerencias pero permite cualquier
// valor (custom) sin necesidad de un segundo campo (F-133). Se convierte a
// kg al persistir.
const WEIGHT_PRESETS_GR = [100, 150, 200, 250, 300, 350, 500, 740] as const;
const [unitWeightGr, setUnitWeightGr] = useState('1');
const [customWeightGr, setCustomWeightGr] = useState('');
const [, setCustomWeightGr] = useState(''); // legacy, mantenida para no romper el handler
const [minPurchaseQty, setMinPurchaseQty] = useState('1');
const [savingProductMeta, setSavingProductMeta] = useState(false);
const [metaMsg, setMetaMsg] = useState('');
// Determina si el valor actual coincide con un preset o si requiere custom.
const weightIsCustom = !WEIGHT_PRESETS_GR.map(String).includes(unitWeightGr);
const weightPresetValue = weightIsCustom ? 'custom' : unitWeightGr;
const onWeightPresetChange = (value: string) => {
if (value === 'custom') {
// Mantén el valor actual en customWeightGr para que el usuario no pierda lo escrito.
setCustomWeightGr(unitWeightGr || '');
// Marca custom sin asignar un valor concreto hasta que el usuario edite.
if (!WEIGHT_PRESETS_GR.map(String).includes(unitWeightGr)) {
setUnitWeightGr('');
}
} else {
setUnitWeightGr(value);
setCustomWeightGr('');
}
};
useEffect(() => {
taxApi.list().then(({ items }) => setTaxRates(items.filter((r) => r.active))).catch(() => {});
}, []);
@@ -369,29 +354,30 @@ export function PriceStockSection({ productId }: { productId?: string }) {
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">Peso unitario (Gr)</label>
<select
value={weightPresetValue}
onChange={(e) => onWeightPresetChange(e.target.value)}
{/* F-133: input único con datalist. El usuario puede escribir cualquier
valor (custom) o elegir uno de los presets del desplegable nativo
del navegador. Sin segundo campo separado, sin romper el grid. */}
<input
type="number"
list="weight-presets-list"
min={1}
max={100000}
value={unitWeightGr}
onChange={(e) => {
setUnitWeightGr(e.target.value);
setCustomWeightGr('');
}}
onBlur={saveProductMeta}
onKeyDown={(e) => { if (e.key === 'Enter') saveProductMeta(); }}
disabled={savingProductMeta || pending}
placeholder="Gramos"
className="w-full px-3 py-2 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none bg-white disabled:opacity-50"
>
/>
<datalist id="weight-presets-list">
{WEIGHT_PRESETS_GR.map(g => (
<option key={g} value={String(g)}>{g} g</option>
<option key={g} value={g} label={`${g} g`} />
))}
<option value="custom">Personalizado</option>
</select>
{weightIsCustom && (
<input
type="text" inputMode="decimal" value={customWeightGr || unitWeightGr}
onChange={(e) => { setCustomWeightGr(e.target.value); setUnitWeightGr(e.target.value); }}
onBlur={saveProductMeta}
onKeyDown={(e) => { if (e.key === 'Enter') saveProductMeta(); }}
disabled={savingProductMeta || pending}
placeholder="Gramos"
className="mt-2 w-full px-3 py-2 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none bg-white disabled:opacity-50"
/>
)}
</datalist>
</div>
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">Compra mínima (uds.)</label>

File diff suppressed because one or more lines are too long