import { api } from "../lib/api.js"; class RecommendationsCrud extends HTMLElement { static get observedAttributes() { return ["rule-type"]; } constructor() { super(); this.attachShadow({ mode: "open" }); this.items = []; this.selected = null; this.loading = false; this.searchQuery = ""; this.editMode = null; // 'create' | 'edit' | null // Cache de productos para el selector this.allProducts = []; this.productsLoaded = false; // Productos seleccionados en el formulario this.selectedTriggerProducts = []; this.selectedRecommendedProducts = []; // Items con qty/unit para reglas qty_per_person this.ruleItems = []; // Tipo de regla filtrado por atributo (crosssell o qty_per_person) this.filterRuleType = this.getAttribute("rule-type") || null; // Tipo de regla actual en el formulario this.currentRuleType = this.filterRuleType || "crosssell"; this.currentTriggerEvent = ""; this.shadowRoot.innerHTML = `
Reglas
Cargando...
Detalle
Selecciona una regla o crea una nueva
`; } connectedCallback() { // Leer atributo rule-type this.filterRuleType = this.getAttribute("rule-type") || null; this.currentRuleType = this.filterRuleType || "crosssell"; // Actualizar título según el tipo const listTitle = this.shadowRoot.getElementById("listTitle"); if (this.filterRuleType === "crosssell") { listTitle.textContent = "Reglas Cross-sell"; } else if (this.filterRuleType === "qty_per_person") { listTitle.textContent = "Reglas de Cantidades"; } else { listTitle.textContent = "Reglas de Recomendacion"; } this.shadowRoot.getElementById("search").oninput = (e) => { this.searchQuery = e.target.value; clearTimeout(this._searchTimer); this._searchTimer = setTimeout(() => this.load(), 300); }; this.shadowRoot.getElementById("newBtn").onclick = () => this.showCreateForm(); this.load(); this.loadProducts(); } attributeChangedCallback(name, oldValue, newValue) { if (name === "rule-type" && oldValue !== newValue) { this.filterRuleType = newValue; this.currentRuleType = newValue || "crosssell"; this.load(); } } async loadProducts() { if (this.productsLoaded) return; try { const data = await api.products({ limit: 2000 }); this.allProducts = (data.items || []).filter(p => p.stock_status === "instock"); this.productsLoaded = true; } catch (e) { console.error("Error loading products:", e); this.allProducts = []; } } async load() { this.loading = true; this.renderList(); try { const data = await api.recommendations({ q: this.searchQuery, limit: 200 }); let items = data.items || []; // Filtrar por tipo si está especificado if (this.filterRuleType) { items = items.filter(item => (item.rule_type || "crosssell") === this.filterRuleType); } this.items = items; this.loading = false; this.renderList(); } catch (e) { console.error("Error loading recommendations:", e); this.items = []; this.loading = false; this.renderList(); } } getProductName(id) { const p = this.allProducts.find(x => x.woo_product_id === id); return p?.name || `Producto #${id}`; } getProduct(id) { return this.allProducts.find(x => x.woo_product_id === id); } renderList() { const list = this.shadowRoot.getElementById("list"); if (this.loading) { list.innerHTML = `
Cargando...
`; return; } if (!this.items.length) { list.innerHTML = `
No se encontraron reglas
`; return; } list.innerHTML = ""; for (const item of this.items) { const el = document.createElement("div"); el.className = "item" + (this.selected?.id === item.id ? " active" : ""); const ruleType = item.rule_type || "crosssell"; const triggerEvent = item.trigger_event || ""; let contentHtml = ""; if (ruleType === "qty_per_person") { contentHtml = `
Evento: ${triggerEvent || "General"}
Cantidades por persona configuradas
`; } else { const triggerIds = item.trigger_product_ids || []; const triggerNames = triggerIds.slice(0, 3).map(id => this.getProductName(id)).join(", "); const triggerMore = triggerIds.length > 3 ? ` (+${triggerIds.length - 3})` : ""; const recoIds = item.recommended_product_ids || []; const recoNames = recoIds.slice(0, 3).map(id => this.getProductName(id)).join(", "); const recoMore = recoIds.length > 3 ? ` (+${recoIds.length - 3})` : ""; contentHtml = `
Cuando piden: ${triggerNames || "—"}${triggerMore}
→ Recomendar: ${recoNames || "—"}${recoMore}
`; } // Solo mostrar badge de tipo si no está filtrado const typeBadge = !this.filterRuleType ? `${ruleType === "qty_per_person" ? "Cantidades" : "Cross-sell"}` : ""; el.innerHTML = `
${item.rule_key} ${typeBadge} ${item.active ? "Activa" : "Inactiva"} P: ${item.priority}
${contentHtml} `; el.onclick = () => this.selectItem(item); list.appendChild(el); } } async selectItem(item) { // Cargar detalles incluyendo items try { const detail = await api.getRecommendation(item.id); this.selected = detail || item; } catch (e) { this.selected = item; } this.editMode = "edit"; this.currentRuleType = this.selected.rule_type || "crosssell"; this.currentTriggerEvent = this.selected.trigger_event || ""; this.selectedTriggerProducts = [...(this.selected.trigger_product_ids || [])]; this.selectedRecommendedProducts = [...(this.selected.recommended_product_ids || [])]; this.ruleItems = [...(this.selected.items || [])]; this.renderList(); this.renderForm(); } showCreateForm() { this.selected = null; this.editMode = "create"; // Usar el tipo filtrado si está definido this.currentRuleType = this.filterRuleType || "crosssell"; this.currentTriggerEvent = ""; this.selectedTriggerProducts = []; this.selectedRecommendedProducts = []; this.ruleItems = []; this.renderList(); this.renderForm(); } renderForm() { const form = this.shadowRoot.getElementById("form"); const title = this.shadowRoot.getElementById("formTitle"); if (!this.editMode) { title.textContent = "Detalle"; form.innerHTML = `
Selecciona una regla o crea una nueva
`; return; } const isCreate = this.editMode === "create"; title.textContent = isCreate ? "Nueva Regla" : "Editar Regla"; const rule_key = this.selected?.rule_key || ""; const active = this.selected?.active !== false; const priority = this.selected?.priority || 100; // Solo mostrar selector de tipo si no está filtrado por atributo const showTypeSelector = !this.filterRuleType; form.innerHTML = ` ${showTypeSelector ? `
Cross-sell
Si pide A, ofrecer B, C, D
Cantidades
Cantidad por persona por producto
` : ""}
Identificador unico, sin espacios
Productos que activan esta recomendacion
Productos a sugerir al cliente
Evento que activa esta regla de cantidades
${this.renderItemsRows()}
Producto Para Cantidad Unidad Razon
${!isCreate ? `` : ""}
`; // Setup event handlers this.shadowRoot.getElementById("saveBtn").onclick = () => this.save(); this.shadowRoot.getElementById("cancelBtn").onclick = () => this.cancel(); if (!isCreate) { this.shadowRoot.getElementById("deleteBtn").onclick = () => this.delete(); } // Rule type selector this.shadowRoot.querySelectorAll(".rule-type-btn").forEach(btn => { btn.onclick = () => { this.currentRuleType = btn.dataset.type; this.renderForm(); }; }); // Setup product selectors for crosssell if (this.currentRuleType === "crosssell") { this.setupProductSelector("trigger", this.selectedTriggerProducts); this.setupProductSelector("reco", this.selectedRecommendedProducts); } // Setup for qty_per_person if (this.currentRuleType === "qty_per_person") { this.setupItemsTable(); this.setupAddItemSelector(); } } renderItemsRows() { if (!this.ruleItems.length) { return `No hay productos configurados`; } return this.ruleItems.map((item, idx) => { const product = this.getProduct(item.woo_product_id); const name = product?.name || `Producto #${item.woo_product_id}`; const audience = item.audience_type || "adult"; return ` ${name} `; }).join(""); } setupItemsTable() { const tbody = this.shadowRoot.getElementById("itemsTableBody"); if (!tbody) return; // Handle audience changes tbody.querySelectorAll(".item-audience").forEach((select, idx) => { select.onchange = () => { this.ruleItems[idx].audience_type = select.value; }; }); // Handle qty/unit/reason changes tbody.querySelectorAll(".item-qty").forEach((input, idx) => { input.onchange = () => { this.ruleItems[idx].qty_per_person = parseFloat(input.value) || null; }; }); tbody.querySelectorAll(".item-unit").forEach((select, idx) => { select.onchange = () => { this.ruleItems[idx].unit = select.value; }; }); tbody.querySelectorAll(".item-reason").forEach((input, idx) => { input.onchange = () => { this.ruleItems[idx].reason = input.value || null; }; }); // Handle remove buttons tbody.querySelectorAll(".btn-remove").forEach(btn => { btn.onclick = () => { const idx = parseInt(btn.dataset.idx, 10); this.ruleItems.splice(idx, 1); this.renderForm(); }; }); } setupAddItemSelector() { const searchInput = this.shadowRoot.getElementById("itemSearch"); const dropdown = this.shadowRoot.getElementById("itemDropdown"); const addBtn = this.shadowRoot.getElementById("addItemBtn"); if (!searchInput || !dropdown) return; let selectedProductId = null; const renderDropdown = (query) => { const q = (query || "").toLowerCase().trim(); const existingIds = new Set(this.ruleItems.map(i => i.woo_product_id)); let filtered = this.allProducts.filter(p => !existingIds.has(p.woo_product_id)); if (q) { filtered = filtered.filter(p => p.name.toLowerCase().includes(q)); } filtered = filtered.slice(0, 30); if (!filtered.length) { dropdown.classList.remove("open"); return; } dropdown.innerHTML = filtered.map(p => `
${p.name} $${p.price || 0}
`).join(""); dropdown.querySelectorAll(".product-option").forEach(opt => { opt.onclick = () => { selectedProductId = parseInt(opt.dataset.id, 10); searchInput.value = this.getProductName(selectedProductId); dropdown.classList.remove("open"); }; }); dropdown.classList.add("open"); }; searchInput.oninput = () => { selectedProductId = null; clearTimeout(this._itemTimer); this._itemTimer = setTimeout(() => renderDropdown(searchInput.value), 150); }; searchInput.onfocus = () => renderDropdown(searchInput.value); addBtn.onclick = () => { if (!selectedProductId) { alert("Selecciona un producto primero"); return; } this.ruleItems.push({ woo_product_id: selectedProductId, audience_type: "adult", qty_per_person: null, unit: "kg", reason: null, display_order: this.ruleItems.length, }); searchInput.value = ""; selectedProductId = null; this.renderForm(); }; // Close on outside click document.addEventListener("click", (e) => { if (!this.shadowRoot.getElementById("itemSelector")?.contains(e.target)) { dropdown.classList.remove("open"); } }); } setupProductSelector(type, selectedIds) { const searchInput = this.shadowRoot.getElementById(`${type}Search`); const dropdown = this.shadowRoot.getElementById(`${type}Dropdown`); const selectedContainer = this.shadowRoot.getElementById(`${type}Selected`); if (!searchInput || !dropdown || !selectedContainer) return; const renderSelected = () => { const ids = type === "trigger" ? this.selectedTriggerProducts : this.selectedRecommendedProducts; if (!ids.length) { selectedContainer.innerHTML = `Ningun producto seleccionado`; return; } selectedContainer.innerHTML = ids.map(id => { const name = this.getProductName(id); return `${name}×`; }).join(""); selectedContainer.querySelectorAll(".remove").forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const id = parseInt(btn.parentElement.dataset.id, 10); if (type === "trigger") { this.selectedTriggerProducts = this.selectedTriggerProducts.filter(x => x !== id); } else { this.selectedRecommendedProducts = this.selectedRecommendedProducts.filter(x => x !== id); } renderSelected(); renderDropdown(searchInput.value); }; }); }; const renderDropdown = (query) => { const q = (query || "").toLowerCase().trim(); const selectedSet = new Set(type === "trigger" ? this.selectedTriggerProducts : this.selectedRecommendedProducts); let filtered = this.allProducts; if (q) { filtered = filtered.filter(p => p.name.toLowerCase().includes(q)); } filtered = filtered.slice(0, 50); if (!q && !filtered.length) { dropdown.classList.remove("open"); return; } dropdown.innerHTML = filtered.map(p => { const isSelected = selectedSet.has(p.woo_product_id); return `
${p.name} $${p.price || 0}
`; }).join(""); dropdown.querySelectorAll(".product-option").forEach(opt => { opt.onclick = () => { const id = parseInt(opt.dataset.id, 10); if (type === "trigger") { if (!this.selectedTriggerProducts.includes(id)) { this.selectedTriggerProducts.push(id); } } else { if (!this.selectedRecommendedProducts.includes(id)) { this.selectedRecommendedProducts.push(id); } } searchInput.value = ""; dropdown.classList.remove("open"); renderSelected(); }; }); dropdown.classList.add("open"); }; searchInput.oninput = () => { clearTimeout(this[`_${type}Timer`]); this[`_${type}Timer`] = setTimeout(() => renderDropdown(searchInput.value), 150); }; searchInput.onfocus = () => { if (searchInput.value || this.allProducts.length) { renderDropdown(searchInput.value); } }; document.addEventListener("click", (e) => { if (!this.shadowRoot.getElementById(`${type}Selector`)?.contains(e.target)) { dropdown.classList.remove("open"); } }); renderSelected(); } async save() { const ruleKey = this.shadowRoot.getElementById("ruleKeyInput").value.trim().toLowerCase().replace(/\s+/g, "_"); const priority = parseInt(this.shadowRoot.getElementById("priorityInput").value, 10) || 100; const active = this.shadowRoot.getElementById("activeInput").checked; if (!ruleKey) { alert("El nombre de la regla es requerido"); return; } const data = { rule_key: ruleKey, trigger: {}, queries: [], ask_slots: [], active, priority, rule_type: this.currentRuleType, trigger_event: null, trigger_product_ids: [], recommended_product_ids: [], items: [], }; if (this.currentRuleType === "crosssell") { if (!this.selectedTriggerProducts.length) { alert("Selecciona al menos un producto trigger"); return; } if (!this.selectedRecommendedProducts.length) { alert("Selecciona al menos un producto para recomendar"); return; } data.trigger_product_ids = this.selectedTriggerProducts; data.recommended_product_ids = this.selectedRecommendedProducts; } else { // qty_per_person const triggerEvent = this.shadowRoot.getElementById("triggerEventInput")?.value || null; data.trigger_event = triggerEvent; if (!this.ruleItems.length) { alert("Agrega al menos un producto con cantidad"); return; } data.items = this.ruleItems.map((item, idx) => ({ woo_product_id: item.woo_product_id, audience_type: item.audience_type || "adult", qty_per_person: item.qty_per_person, unit: item.unit || "kg", reason: item.reason || null, display_order: idx, })); } try { if (this.editMode === "create") { await api.createRecommendation(data); } else { await api.updateRecommendation(this.selected.id, data); } this.editMode = null; this.selected = null; await this.load(); this.renderForm(); } catch (e) { console.error("Error saving recommendation:", e); alert("Error guardando: " + (e.message || e)); } } async delete() { if (!this.selected?.id) return; if (!confirm(`¿Eliminar la regla "${this.selected.rule_key}"?`)) return; try { await api.deleteRecommendation(this.selected.id); this.editMode = null; this.selected = null; await this.load(); this.renderForm(); } catch (e) { console.error("Error deleting recommendation:", e); alert("Error eliminando: " + (e.message || e)); } } cancel() { this.editMode = null; this.selected = null; this.selectedTriggerProducts = []; this.selectedRecommendedProducts = []; this.ruleItems = []; this.renderList(); this.renderForm(); } } customElements.define("recommendations-crud", RecommendationsCrud);