Productos: allowed_order_units + fix sync que pisaba overrides

Introduce un campo nuevo allowed_order_units por producto (array kg/unit)
que separa "cómo se vende" de "cómo se puede pedir". Productos sell_unit=unit
solo aceptan unit (regla dura aplicada por código y handler). Productos
sell_unit=kg defaultean a ambos, editable desde la UI con checkboxes.

Fixea bug crítico de wooSnapshot.upsertSnapshotItems que pisaba raw entero
en cada sync, perdiendo overrides locales (_sell_unit_override,
_categories_override y el nuevo _allowed_order_units). El UPDATE ahora
mergea preservando los overrides.

Las tools del agente (addToCart, setQuantity, selectCandidate) validan
unit contra allowed_order_units y devuelven unit_not_allowed para que el
LLM re-pregunte. workingMemory expone allowed_order_units y needs_unit_choice.
buildLineItems agrega meta_data unit/sell_unit_product/unit_mode_mismatch
y omite subtotal/total cuando hay mismatch para que Woo recalcule.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lucas Tettamanti
2026-05-18 00:40:12 -03:00
parent c786e47160
commit 423a99ab48
18 changed files with 431 additions and 72 deletions

View File

@@ -458,6 +458,24 @@ class ProductsCrud extends HTMLElement {
Define si este producto se vende por peso o por unidad
</div>
</div>
<div class="field">
<label>Unidades aceptadas al pedir</label>
<div id="allowedUnitsBox" style="display:flex;gap:14px;align-items:center;">
<label style="display:inline-flex;gap:6px;align-items:center;font-size:13px;">
<input type="checkbox" id="cbAllowKg" ${this.computeAllowedUI(p, currentUnit).includes("kg") ? "checked" : ""} ${currentUnit === "unit" ? "disabled" : ""}>
Permitir pedir por kg
</label>
<label style="display:inline-flex;gap:6px;align-items:center;font-size:13px;">
<input type="checkbox" id="cbAllowUnit" ${this.computeAllowedUI(p, currentUnit).includes("unit") ? "checked" : ""} ${currentUnit === "unit" ? "disabled" : ""}>
Permitir pedir por unidad
</label>
</div>
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;" id="allowedUnitsHint">
${currentUnit === "unit"
? 'Producto por unidad: solo se acepta pedir por unidad.'
: 'Para productos vendidos por kg, permitir también pedido por unidad (ej: milanesas, hamburguesas).'}
</div>
</div>
<div class="field">
<label>Categorías actuales</label>
<div id="currentCategories" style="display:flex;flex-wrap:wrap;gap:6px;min-height:30px;margin-bottom:8px;">
@@ -504,7 +522,40 @@ class ProductsCrud extends HTMLElement {
// Bind save button
this.shadowRoot.getElementById("saveProduct").onclick = () => this.saveProduct();
// Bind sell_unit select: si cambia a "unit", forzar checks; si pasa a "kg", rehabilitar.
const sellUnitSel = this.shadowRoot.getElementById("sellUnit");
const cbKg = this.shadowRoot.getElementById("cbAllowKg");
const cbUnit = this.shadowRoot.getElementById("cbAllowUnit");
const hintEl = this.shadowRoot.getElementById("allowedUnitsHint");
sellUnitSel.onchange = () => {
if (sellUnitSel.value === "unit") {
cbKg.checked = false;
cbUnit.checked = true;
cbKg.disabled = true;
cbUnit.disabled = true;
hintEl.textContent = 'Producto por unidad: solo se acepta pedir por unidad.';
} else {
cbKg.disabled = false;
cbUnit.disabled = false;
// Default permisivo al pasar a kg
if (!cbKg.checked && !cbUnit.checked) {
cbKg.checked = true;
cbUnit.checked = true;
}
hintEl.textContent = 'Para productos vendidos por kg, permitir también pedido por unidad (ej: milanesas, hamburguesas).';
}
};
const validateAtLeastOne = (e) => {
if (!cbKg.checked && !cbUnit.checked) {
modal.warn("Tenés que dejar al menos una unidad aceptada.");
// Revertir: marcar el que se acaba de desmarcar
e.target.checked = true;
}
};
cbKg.addEventListener("change", validateAtLeastOne);
cbUnit.addEventListener("change", validateAtLeastOne);
// Bind add category button
this.shadowRoot.getElementById("addCatBtn").onclick = () => this.addCategoryToProduct();
@@ -558,22 +609,25 @@ class ProductsCrud extends HTMLElement {
async autoSaveCategories() {
if (this.selectedItems.length !== 1) return;
const p = this.selectedItems[0];
const categories = this.getCurrentProductCategories();
const sellUnitSelect = this.shadowRoot.getElementById("sellUnit");
const sell_unit = sellUnitSelect?.value || p.sell_unit || 'kg';
const allowed_order_units = this.getCurrentAllowedOrderUnits();
try {
await api.updateProduct(p.woo_product_id, { sell_unit, categories });
await api.updateProduct(p.woo_product_id, { sell_unit, categories, allowed_order_units });
// Actualizar localmente
p.categories = JSON.stringify(categories.map(name => ({ name })));
if (allowed_order_units) p.allowed_order_units = allowed_order_units;
const idx = this.items.findIndex(i => i.woo_product_id === p.woo_product_id);
if (idx >= 0) {
this.items[idx].categories = p.categories;
if (allowed_order_units) this.items[idx].allowed_order_units = allowed_order_units;
}
// Mostrar feedback breve
const status = this.shadowRoot.getElementById("saveStatus");
if (status) {
@@ -604,31 +658,52 @@ class ProductsCrud extends HTMLElement {
return [...tags].map(t => t.dataset.category);
}
computeAllowedUI(p, currentUnit) {
if (currentUnit === "unit") return ["unit"];
if (Array.isArray(p?.allowed_order_units) && p.allowed_order_units.length > 0) {
return p.allowed_order_units;
}
return ["kg", "unit"];
}
getCurrentAllowedOrderUnits() {
const cbKg = this.shadowRoot.getElementById("cbAllowKg");
const cbUnit = this.shadowRoot.getElementById("cbAllowUnit");
if (!cbKg || !cbUnit) return undefined;
const list = [];
if (cbKg.checked) list.push("kg");
if (cbUnit.checked) list.push("unit");
return list.length ? list : ["unit"];
}
async saveProduct() {
if (this.selectedItems.length !== 1) return;
const p = this.selectedItems[0];
const btn = this.shadowRoot.getElementById("saveProduct");
const sellUnitSelect = this.shadowRoot.getElementById("sellUnit");
const sell_unit = sellUnitSelect.value;
const categories = this.getCurrentProductCategories();
const allowed_order_units = this.getCurrentAllowedOrderUnits();
btn.disabled = true;
btn.textContent = "Guardando...";
try {
await api.updateProduct(p.woo_product_id, { sell_unit, categories });
await api.updateProduct(p.woo_product_id, { sell_unit, categories, allowed_order_units });
// Actualizar localmente
p.sell_unit = sell_unit;
p.categories = JSON.stringify(categories.map(name => ({ name })));
if (allowed_order_units) p.allowed_order_units = sell_unit === "unit" ? ["unit"] : allowed_order_units;
const idx = this.items.findIndex(i => i.woo_product_id === p.woo_product_id);
if (idx >= 0) {
this.items[idx].sell_unit = sell_unit;
this.items[idx].categories = p.categories;
if (allowed_order_units) this.items[idx].allowed_order_units = p.allowed_order_units;
}
btn.textContent = "Guardado!";
this.renderList();
setTimeout(() => { btn.textContent = "Guardar cambios"; btn.disabled = false; }, 1500);