Files
botino/public/components/quantities-crud.js
2026-01-18 18:28:28 -03:00

341 lines
12 KiB
JavaScript

import { api } from "../lib/api.js";
const FIXED_EVENTS = [
{ id: "asado", label: "Asado / Parrilla" },
{ id: "horno", label: "Horno" },
{ id: "sanguches", label: "Sanguches" },
];
const PERSON_TYPES = [
{ id: "adult", label: "Adulto" },
{ id: "child", label: "Niño" },
];
class QuantitiesCrud extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.products = [];
this.ruleCounts = new Map();
this.selectedProduct = null;
this.productRules = [];
this.loading = false;
this.searchQuery = "";
this.saving = false;
this.shadowRoot.innerHTML = `
<style>
:host { display:block; height:100%; padding:16px; overflow:hidden; }
* { box-sizing:border-box; font-family:system-ui,Segoe UI,Roboto,Arial; }
.container { display:grid; grid-template-columns:1fr 1fr; gap:16px; height:100%; }
.panel { background:#121823; border:1px solid #1e2a3a; border-radius:10px; padding:16px; overflow:hidden; display:flex; flex-direction:column; }
.panel-title { font-size:14px; font-weight:700; color:#8aa0b5; text-transform:uppercase; letter-spacing:.4px; margin-bottom:12px; }
.toolbar { display:flex; gap:8px; margin-bottom:12px; }
input, select { background:#0f1520; color:#e7eef7; border:1px solid #253245; border-radius:8px; padding:8px 12px; font-size:13px; width:100%; }
input:focus, select:focus { outline:none; border-color:#1f6feb; }
button { cursor:pointer; background:#1f6feb; color:#fff; border:none; border-radius:8px; padding:8px 16px; font-size:13px; }
button:hover { background:#1a5fd0; }
button:disabled { opacity:.5; cursor:not-allowed; }
button.secondary { background:#253245; }
button.secondary:hover { background:#2d3e52; }
.list { flex:1; overflow-y:auto; }
.item { background:#0f1520; border:1px solid #253245; border-radius:8px; padding:12px; margin-bottom:8px; cursor:pointer; transition:all .15s; display:flex; justify-content:space-between; align-items:center; }
.item:hover { border-color:#1f6feb; }
.item.active { border-color:#1f6feb; background:#111b2a; }
.item-name { font-weight:500; color:#e7eef7; }
.item-price { font-size:12px; color:#8aa0b5; }
.badge { display:inline-flex; align-items:center; justify-content:center; min-width:20px; height:20px; padding:0 6px; border-radius:999px; font-size:11px; font-weight:600; background:#1f6feb; color:#fff; }
.badge.empty { background:#253245; color:#8aa0b5; }
.form { flex:1; overflow-y:auto; }
.form-empty { color:#8aa0b5; text-align:center; padding:40px; }
.product-header { margin-bottom:16px; }
.product-name { font-size:18px; font-weight:600; color:#e7eef7; margin-bottom:4px; }
.product-price { font-size:14px; color:#8aa0b5; }
.qty-grid { width:100%; border-collapse:collapse; }
.qty-grid th { text-align:left; font-size:12px; color:#8aa0b5; padding:10px 8px; border-bottom:1px solid #253245; }
.qty-grid td { padding:8px; border-bottom:1px solid #1e2a3a; }
.qty-grid .event-label { font-size:13px; color:#e7eef7; font-weight:500; }
.qty-grid input { width:70px; padding:6px 8px; font-size:12px; text-align:center; }
.qty-grid select { width:70px; padding:6px 4px; font-size:11px; }
.cell-group { display:flex; gap:4px; align-items:center; }
.actions { display:flex; gap:8px; margin-top:16px; }
.loading { text-align:center; padding:40px; color:#8aa0b5; }
.status { font-size:12px; color:#2ecc71; margin-left:auto; }
.status.error { color:#e74c3c; }
</style>
<div class="container">
<div class="panel">
<div class="panel-title">Productos</div>
<div class="toolbar">
<input type="text" id="search" placeholder="Buscar producto..." />
</div>
<div class="list" id="list">
<div class="loading">Cargando...</div>
</div>
</div>
<div class="panel">
<div class="panel-title">Cantidades por Persona</div>
<div class="form" id="form">
<div class="form-empty">Selecciona un producto para configurar cantidades</div>
</div>
</div>
</div>
`;
}
connectedCallback() {
this.shadowRoot.getElementById("search").oninput = (e) => {
this.searchQuery = e.target.value;
this.renderList();
};
this.load();
}
async load() {
this.loading = true;
this.renderList();
try {
// Cargar productos y cantidades en paralelo
const [productsData, quantitiesData] = await Promise.all([
api.products({ limit: 2000 }),
api.listQuantities(),
]);
this.products = (productsData.items || []).filter(p => p.stock_status === "instock");
// Crear mapa de conteo de reglas
this.ruleCounts = new Map();
for (const c of (quantitiesData.counts || [])) {
this.ruleCounts.set(c.woo_product_id, parseInt(c.rule_count, 10));
}
this.loading = false;
this.renderList();
} catch (e) {
console.error("Error loading:", e);
this.loading = false;
this.renderList();
}
}
renderList() {
const list = this.shadowRoot.getElementById("list");
if (this.loading) {
list.innerHTML = `<div class="loading">Cargando...</div>`;
return;
}
const q = this.searchQuery.toLowerCase().trim();
let filtered = this.products;
if (q) {
filtered = filtered.filter(p => p.name.toLowerCase().includes(q));
}
if (!filtered.length) {
list.innerHTML = `<div class="loading">No se encontraron productos</div>`;
return;
}
// Ordenar: primero los que tienen reglas, luego por nombre
filtered.sort((a, b) => {
const countA = this.ruleCounts.get(a.woo_product_id) || 0;
const countB = this.ruleCounts.get(b.woo_product_id) || 0;
if (countA !== countB) return countB - countA;
return a.name.localeCompare(b.name);
});
list.innerHTML = "";
for (const product of filtered.slice(0, 100)) {
const el = document.createElement("div");
el.className = "item" + (this.selectedProduct?.woo_product_id === product.woo_product_id ? " active" : "");
const ruleCount = this.ruleCounts.get(product.woo_product_id) || 0;
el.innerHTML = `
<div>
<div class="item-name">${product.name}</div>
<div class="item-price">$${product.price || 0} / ${product.sell_unit || 'kg'}</div>
</div>
<span class="badge ${ruleCount === 0 ? 'empty' : ''}">${ruleCount}</span>
`;
el.onclick = () => this.selectProduct(product);
list.appendChild(el);
}
}
async selectProduct(product) {
this.selectedProduct = product;
this.renderList();
// Cargar reglas del producto
try {
const data = await api.getProductQuantities(product.woo_product_id);
this.productRules = data.rules || [];
this.renderForm();
} catch (e) {
console.error("Error loading product rules:", e);
this.productRules = [];
this.renderForm();
}
}
renderForm() {
const form = this.shadowRoot.getElementById("form");
if (!this.selectedProduct) {
form.innerHTML = `<div class="form-empty">Selecciona un producto para configurar cantidades</div>`;
return;
}
const p = this.selectedProduct;
// Crear mapa de reglas existentes: "event_type:person_type" -> rule
const ruleMap = new Map();
for (const rule of this.productRules) {
const key = `${rule.event_type}:${rule.person_type}`;
ruleMap.set(key, rule);
}
// Generar filas de la grilla
const rows = FIXED_EVENTS.map(event => {
const cells = PERSON_TYPES.map(person => {
const key = `${event.id}:${person.id}`;
const rule = ruleMap.get(key);
const qty = rule?.qty_per_person ?? "";
const unit = rule?.unit || "kg";
return `
<td>
<div class="cell-group">
<input type="number"
data-event="${event.id}"
data-person="${person.id}"
class="qty-input"
value="${qty}"
step="0.01"
min="0"
placeholder="-" />
<select data-event="${event.id}" data-person="${person.id}" class="unit-select">
<option value="kg" ${unit === "kg" ? "selected" : ""}>kg</option>
<option value="g" ${unit === "g" ? "selected" : ""}>g</option>
<option value="unidad" ${unit === "unidad" ? "selected" : ""}>u</option>
</select>
</div>
</td>
`;
}).join("");
return `
<tr>
<td class="event-label">${event.label}</td>
${cells}
</tr>
`;
}).join("");
form.innerHTML = `
<div class="product-header">
<div class="product-name">${p.name}</div>
<div class="product-price">$${p.price || 0} / ${p.sell_unit || 'kg'}</div>
</div>
<table class="qty-grid">
<thead>
<tr>
<th>Evento</th>
${PERSON_TYPES.map(pt => `<th>${pt.label}</th>`).join("")}
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
<div class="actions">
<button id="saveBtn" ${this.saving ? "disabled" : ""}>Guardar</button>
<button id="clearBtn" class="secondary">Limpiar</button>
<span class="status" id="status"></span>
</div>
`;
this.shadowRoot.getElementById("saveBtn").onclick = () => this.save();
this.shadowRoot.getElementById("clearBtn").onclick = () => this.clear();
}
async save() {
if (!this.selectedProduct || this.saving) return;
// Recolectar valores de la grilla ANTES de renderizar
const rules = [];
const qtyInputs = this.shadowRoot.querySelectorAll(".qty-input");
for (const input of qtyInputs) {
const eventType = input.dataset.event;
const personType = input.dataset.person;
const qty = parseFloat(input.value);
if (!isNaN(qty) && qty > 0) {
const unitSelect = this.shadowRoot.querySelector(`.unit-select[data-event="${eventType}"][data-person="${personType}"]`);
const unit = unitSelect?.value || "kg";
rules.push({
event_type: eventType,
person_type: personType,
qty_per_person: qty,
unit,
});
}
}
this.saving = true;
this.renderForm();
try {
await api.saveProductQuantities(this.selectedProduct.woo_product_id, rules);
// Actualizar conteo local
this.ruleCounts.set(this.selectedProduct.woo_product_id, rules.length);
this.productRules = rules;
this.saving = false;
this.renderList();
this.renderForm();
const status = this.shadowRoot.getElementById("status");
status.textContent = "Guardado";
status.className = "status";
setTimeout(() => { status.textContent = ""; }, 2000);
} catch (e) {
console.error("Error saving:", e);
this.saving = false;
this.renderForm();
const status = this.shadowRoot.getElementById("status");
status.textContent = "Error al guardar";
status.className = "status error";
}
}
clear() {
const qtyInputs = this.shadowRoot.querySelectorAll(".qty-input");
for (const input of qtyInputs) {
input.value = "";
}
}
}
customElements.define("quantities-crud", QuantitiesCrud);