341 lines
12 KiB
JavaScript
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);
|