import { api } from "../lib/api.js"; import { on } from "../lib/bus.js"; import { navigateToItem } from "../lib/router.js"; import { modal } from "../lib/modal.js"; class ProductsCrud extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); this.items = []; this.selectedItems = []; // Array de productos seleccionados this.lastClickedIndex = -1; // Para Shift+Click this.loading = false; this.searchQuery = ""; this.stockFilter = false; this.shadowRoot.innerHTML = `
Productos
Total
En Stock
Cargando productos...
Detalle
Seleccioná un producto para ver detalles
`; } connectedCallback() { this.shadowRoot.getElementById("search").oninput = (e) => { this.searchQuery = e.target.value; clearTimeout(this._searchTimer); this._searchTimer = setTimeout(() => this.load(), 300); }; this.shadowRoot.getElementById("syncBtn").onclick = () => this.syncFromWoo(); // Stats click handlers this.shadowRoot.getElementById("statTotal").onclick = () => { this.stockFilter = false; this.renderList(); this.updateStatStyles(); }; this.shadowRoot.getElementById("statStock").onclick = () => { this.stockFilter = !this.stockFilter; this.renderList(); this.updateStatStyles(); }; // Escuchar cambios de ruta para deep-linking this._unsubRouter = on("router:viewChanged", ({ view, params }) => { if (view === "products" && params.id) { this.selectProductById(params.id); } }); this.load(); } disconnectedCallback() { this._unsubRouter?.(); } updateStatStyles() { const statTotal = this.shadowRoot.getElementById("statTotal"); const statStock = this.shadowRoot.getElementById("statStock"); statTotal.classList.toggle("active", !this.stockFilter); statStock.classList.toggle("active", this.stockFilter); } async load() { this.loading = true; this.renderList(); try { const data = await api.products({ q: this.searchQuery, limit: 2000 }); this.items = data.items || []; this.loading = false; this.renderList(); this.renderStats(); // Si hay un producto pendiente de selección (deep-link), seleccionarlo if (this._pendingProductId) { const product = this.items.find(p => p.woo_product_id === this._pendingProductId); if (product) { this.selectedItems = [product]; this.renderList(); this.renderDetail(); } this._pendingProductId = null; } } catch (e) { console.error("Error loading products:", e); this.items = []; this.loading = false; this.renderList(); } } async syncFromWoo() { // Mostrar confirmación antes de sincronizar const confirmed = await modal.confirm( "⚠️ Resincronización de emergencia\n\n" + "Esto reimportará TODOS los productos desde WooCommerce y sobrescribirá los datos locales.\n\n" + "Usar solo si la plataforma estuvo caída, los webhooks no funcionaron, o necesitás una sincronización completa.\n\n" + "¿Continuar?" ); if (!confirmed) return; const btn = this.shadowRoot.getElementById("syncBtn"); btn.disabled = true; btn.textContent = "Sincronizando..."; try { const result = await api.syncFromWoo(); if (result.ok) { modal.success(`Sincronización completada: ${result.synced} productos importados`); } else { modal.error("Error: " + (result.error || "Error desconocido")); } await this.load(); } catch (e) { console.error("Error syncing products:", e); modal.error("Error sincronizando: " + (e.message || e)); } finally { btn.disabled = false; btn.textContent = "Resincronizar"; } } renderStats() { const total = this.items.length; const inStock = this.items.filter(p => p.stock_status === "instock" || p.payload?.stock_status === "instock").length; this.shadowRoot.getElementById("totalCount").textContent = total; this.shadowRoot.getElementById("inStockCount").textContent = inStock; } /** * Extrae todas las categorías únicas de todos los productos */ getAllCategories() { const categoriesSet = new Set(); for (const p of this.items) { try { if (p.categories) { const cats = typeof p.categories === 'string' ? JSON.parse(p.categories) : p.categories; if (Array.isArray(cats)) { cats.forEach(c => { const name = c.name || c; if (name) categoriesSet.add(name); }); } } else if (p.payload?.categories && Array.isArray(p.payload.categories)) { p.payload.categories.forEach(c => { const name = c.name || c; if (name) categoriesSet.add(name); }); } } catch (e) { /* ignore parse errors */ } } return [...categoriesSet].sort((a, b) => a.localeCompare(b)); } renderList() { const list = this.shadowRoot.getElementById("list"); if (this.loading) { list.innerHTML = `
Cargando productos...
`; return; } // Filter items based on stock filter const filteredItems = this.stockFilter ? this.items.filter(p => p.stock_status === "instock" || p.payload?.stock_status === "instock") : this.items; if (!filteredItems.length) { list.innerHTML = `
No se encontraron productos
`; return; } list.innerHTML = ""; this._filteredItems = filteredItems; // Guardar referencia para Shift+Click for (let i = 0; i < filteredItems.length; i++) { const item = filteredItems[i]; const el = document.createElement("div"); const isSelected = this.selectedItems.some(s => s.woo_product_id === item.woo_product_id); const isSingleSelected = isSelected && this.selectedItems.length === 1; el.className = "item" + (isSelected ? " selected" : "") + (isSingleSelected ? " active" : ""); el.dataset.index = i; const price = item.price != null ? `$${Number(item.price).toLocaleString()}` : "—"; // SKU: puede venir de item.sku, item.payload.SKU (mayúsculas), o item.slug const sku = item.sku || item.payload?.SKU || item.payload?.sku || item.slug || "—"; const stock = item.stock_status || item.payload?.stock_status || "unknown"; const stockBadge = stock === "instock" ? `En stock` : `Sin stock`; // Mostrar unidad actual si está definida const unit = item.sell_unit || item.payload?._sell_unit_override; const unitBadge = unit ? `${unit === 'unit' ? 'Unidad' : 'Kg'}` : ''; el.innerHTML = `
${item.name || "Sin nombre"} ${stockBadge} ${unitBadge}
${price} · SKU: ${sku} · ID: ${item.woo_product_id}
`; el.onclick = (e) => this.handleItemClick(e, item, i); list.appendChild(el); } } handleItemClick(e, item, index) { console.log("[products-crud] handleItemClick", { shift: e.shiftKey, ctrl: e.ctrlKey, index, item: item?.name }); let updateUrl = false; if (e.shiftKey && this.lastClickedIndex >= 0) { // Shift+Click: seleccionar rango const start = Math.min(this.lastClickedIndex, index); const end = Math.max(this.lastClickedIndex, index); for (let i = start; i <= end; i++) { const rangeItem = this._filteredItems[i]; if (!this.selectedItems.some(s => s.woo_product_id === rangeItem.woo_product_id)) { this.selectedItems.push(rangeItem); } } } else if (e.ctrlKey || e.metaKey) { // Ctrl+Click: toggle individual const idx = this.selectedItems.findIndex(s => s.woo_product_id === item.woo_product_id); if (idx >= 0) { this.selectedItems.splice(idx, 1); } else { this.selectedItems.push(item); } } else { // Click normal: selección única this.selectedItems = [item]; updateUrl = true; } console.log("[products-crud] after click, selectedItems:", this.selectedItems.length, this.selectedItems.map(s => s.name)); this.lastClickedIndex = index; this.renderList(); try { this.renderDetail(); } catch (err) { console.error("[products-crud] Error in renderDetail:", err); const detail = this.shadowRoot.getElementById("detail"); detail.innerHTML = `
Error: ${err.message}
`; } // Scroll detail panel to top const detail = this.shadowRoot.getElementById("detail"); if (detail) detail.scrollTop = 0; // Actualizar URL solo en selección única if (updateUrl && this.selectedItems.length === 1) { navigateToItem("products", item.woo_product_id); } } selectProductById(productId) { const id = parseInt(productId); if (!id) return; // Buscar en los items cargados const product = this.items.find(p => p.woo_product_id === id); if (product) { this.selectedItems = [product]; this.renderList(); this.renderDetail(); } else { // Guardar el ID pendiente para seleccionar después de cargar this._pendingProductId = id; } } escapeHtml(str) { if (!str) return ""; return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } renderDetail() { const detail = this.shadowRoot.getElementById("detail"); console.log("[products-crud] renderDetail called, selectedItems:", this.selectedItems.length); if (!this.selectedItems.length) { detail.innerHTML = `
Seleccioná un producto para ver detalles
`; return; } // Si hay múltiples seleccionados, mostrar vista de edición masiva if (this.selectedItems.length > 1) { console.log("[products-crud] rendering multi detail"); this.renderMultiDetail(); return; } const p = this.selectedItems[0]; console.log("[products-crud] rendering single product:", p?.name, p?.woo_product_id); if (!p) { detail.innerHTML = `
Error: producto no encontrado
`; return; } // Categorías: pueden estar en p.categories (string JSON) o p.payload.categories (array) let categoriesArray = []; try { if (p.categories) { const cats = typeof p.categories === 'string' ? JSON.parse(p.categories) : p.categories; categoriesArray = Array.isArray(cats) ? cats.map(c => c.name || c) : [String(cats)]; } else if (p.payload?.categories && Array.isArray(p.payload.categories)) { categoriesArray = p.payload.categories.map(c => c.name || c); } } catch (e) { categoriesArray = p.categories ? [String(p.categories)] : []; } const categoriesText = this.escapeHtml(categoriesArray.join(", ")); const rawAttrs = p.attributes_normalized || p.payload?.attributes; const attrsArray = Array.isArray(rawAttrs) ? rawAttrs : []; const attributes = attrsArray.map(a => `${a.name}: ${a.options?.join(", ")}`).join("; ") || "—"; // Determinar unidad actual (de payload o inferida) const currentUnit = p.sell_unit || p.payload?._sell_unit_override || this.inferUnit(p); // Stock info const stockStatus = p.stock_status || p.payload?.stock_status || "unknown"; const stockQty = p.stock_qty ?? p.stock_quantity ?? p.payload?.stock_quantity ?? null; const stockBadgeClass = stockStatus === "instock" ? "stock" : "nostock"; const stockText = stockStatus === "instock" ? "En stock" : (stockStatus === "outofstock" ? "Sin stock" : stockStatus); const stockQtyText = stockQty !== null ? ` (${stockQty} disponibles)` : ""; const productName = this.escapeHtml(p.name || "—"); const productSku = this.escapeHtml(p.sku || p.payload?.SKU || p.payload?.sku || p.slug || "—"); detail.innerHTML = `
${productName}
${p.woo_product_id}
${productSku}
${p.price != null ? `$${Number(p.price).toLocaleString()} ${p.currency || ""}` : "—"}
${stockText}${stockQtyText}
Define si este producto se vende por peso o por unidad
${categoriesArray.length > 0 ? categoriesArray.map(cat => ` ${this.escapeHtml(cat)} × `).join("") : 'Sin categorías' }
${this.escapeHtml(attributes)}
${p.refreshed_at ? new Date(p.refreshed_at).toLocaleString() : "—"}
${this.escapeHtml(JSON.stringify(p.payload || {}, null, 2))}
`; // Bind save button this.shadowRoot.getElementById("saveProduct").onclick = () => this.saveProduct(); // Bind add category button this.shadowRoot.getElementById("addCatBtn").onclick = () => this.addCategoryToProduct(); // Bind remove category clicks this.shadowRoot.querySelectorAll(".remove-cat").forEach(el => { el.onclick = (e) => { e.stopPropagation(); const tag = el.closest(".category-tag"); if (tag) tag.remove(); // Actualizar el select para mostrar la categoría removida this.updateCategorySelect(); // Auto-guardar al quitar categoría this.autoSaveCategories(); }; }); } async addCategoryToProduct() { const select = this.shadowRoot.getElementById("addCategorySelect"); const container = this.shadowRoot.getElementById("currentCategories"); const categoryName = select.value; if (!categoryName) return; // Crear el tag const tag = document.createElement("span"); tag.className = "category-tag"; tag.dataset.category = categoryName; tag.style = "display:inline-flex;align-items:center;gap:4px;background:#1a3a5c;color:#7eb8e7;padding:4px 8px;border-radius:6px;font-size:12px;"; tag.innerHTML = `${this.escapeHtml(categoryName)}×`; // Bind remove tag.querySelector(".remove-cat").onclick = (e) => { e.stopPropagation(); tag.remove(); this.updateCategorySelect(); this.autoSaveCategories(); // Auto-guardar al quitar }; // Remover el mensaje "Sin categorías" si existe const emptyMsg = container.querySelector('span[style*="color:#8aa0b5"]'); if (emptyMsg) emptyMsg.remove(); container.appendChild(tag); select.value = ""; this.updateCategorySelect(); // Auto-guardar al agregar await this.autoSaveCategories(); } 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'; try { await api.updateProduct(p.woo_product_id, { sell_unit, categories }); // Actualizar localmente p.categories = JSON.stringify(categories.map(name => ({ name }))); const idx = this.items.findIndex(i => i.woo_product_id === p.woo_product_id); if (idx >= 0) { this.items[idx].categories = p.categories; } // Mostrar feedback breve const status = this.shadowRoot.getElementById("saveStatus"); if (status) { status.textContent = "Guardado"; setTimeout(() => { status.textContent = ""; }, 1500); } } catch (e) { console.error("Error auto-saving categories:", e); } } updateCategorySelect() { const select = this.shadowRoot.getElementById("addCategorySelect"); const currentTags = this.shadowRoot.querySelectorAll(".category-tag"); const currentCategories = [...currentTags].map(t => t.dataset.category); const allCategories = this.getAllCategories(); // Rebuild options select.innerHTML = `` + allCategories .filter(c => !currentCategories.includes(c)) .map(cat => ``) .join(""); } getCurrentProductCategories() { const tags = this.shadowRoot.querySelectorAll(".category-tag"); return [...tags].map(t => t.dataset.category); } 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(); btn.disabled = true; btn.textContent = "Guardando..."; try { await api.updateProduct(p.woo_product_id, { sell_unit, categories }); // Actualizar localmente p.sell_unit = sell_unit; p.categories = JSON.stringify(categories.map(name => ({ name }))); 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; } btn.textContent = "Guardado!"; this.renderList(); setTimeout(() => { btn.textContent = "Guardar cambios"; btn.disabled = false; }, 1500); } catch (e) { console.error("Error saving product:", e); modal.error("Error guardando: " + (e.message || e)); btn.textContent = "Guardar cambios"; btn.disabled = false; } } renderMultiDetail() { const detail = this.shadowRoot.getElementById("detail"); const count = this.selectedItems.length; const names = this.selectedItems.slice(0, 5).map(p => p.name).join(", "); const moreText = count > 5 ? ` y ${count - 5} más...` : ""; // Contar stock const inStockCount = this.selectedItems.filter(p => (p.stock_status || p.payload?.stock_status) === "instock").length; detail.innerHTML = `
${count} productos
${names}${moreText}
${inStockCount} en stock ${count - inStockCount} sin stock
Se aplicará a todos los productos seleccionados
Se agregará esta categoría a todos los productos seleccionados
`; this.shadowRoot.getElementById("saveUnit").onclick = () => this.saveProductUnit(); this.shadowRoot.getElementById("addCategory").onclick = () => this.addCategoryToSelected(); this.shadowRoot.getElementById("clearSelection").onclick = () => { this.selectedItems = []; this.lastClickedIndex = -1; this.renderList(); this.renderDetail(); }; } async addCategoryToSelected() { if (!this.selectedItems.length) return; const select = this.shadowRoot.getElementById("addCategorySelect"); const btn = this.shadowRoot.getElementById("addCategory"); const categoryName = select.value; if (!categoryName) { modal.warn("Seleccioná una categoría"); return; } const count = this.selectedItems.length; btn.disabled = true; btn.textContent = "Guardando..."; try { for (const p of this.selectedItems) { // Obtener categorías actuales let currentCats = []; try { if (p.categories) { const cats = typeof p.categories === 'string' ? JSON.parse(p.categories) : p.categories; currentCats = Array.isArray(cats) ? cats.map(c => c.name || c) : [String(cats)]; } else if (p.payload?.categories) { currentCats = p.payload.categories.map(c => c.name || c); } } catch (e) { currentCats = []; } // Agregar nueva categoría si no existe if (!currentCats.includes(categoryName)) { currentCats.push(categoryName); } // Guardar await api.updateProduct(p.woo_product_id, { sell_unit: p.sell_unit || 'kg', categories: currentCats }); // Actualizar localmente p.categories = JSON.stringify(currentCats.map(name => ({ name }))); const idx = this.items.findIndex(i => i.woo_product_id === p.woo_product_id); if (idx >= 0) this.items[idx].categories = p.categories; } select.value = ""; btn.textContent = `Agregado a ${count}!`; setTimeout(() => { btn.textContent = "Agregar"; btn.disabled = false; }, 1500); } catch (e) { console.error("Error adding category:", e); modal.error("Error agregando categoría: " + (e.message || e)); btn.textContent = "Agregar"; btn.disabled = false; } } inferUnit(p) { const name = String(p.name || "").toLowerCase(); const cats = (p.payload?.categories || []).map(c => String(c.name || c.slug || "").toLowerCase()); const allText = name + " " + cats.join(" "); // Productos que típicamente se venden por unidad if (/chimichurri|provoleta|queso|pan|salsa|aderezo|condimento|especias?|vino|vinos|bebida|cerveza|gaseosa|whisky|ron|gin|vodka|fernet/i.test(allText)) { return "unit"; } return "kg"; } async saveProductUnit() { if (!this.selectedItems.length) return; const select = this.shadowRoot.getElementById("sellUnit"); const btn = this.shadowRoot.getElementById("saveUnit"); const unit = select.value; const count = this.selectedItems.length; btn.disabled = true; btn.textContent = "Guardando..."; try { // IDs de todos los productos seleccionados const wooProductIds = this.selectedItems.map(p => p.woo_product_id); // Un solo request para todos await api.updateProductsUnit(wooProductIds, { sell_unit: unit }); // Actualizar localmente for (const p of this.selectedItems) { p.sell_unit = unit; const idx = this.items.findIndex(i => i.woo_product_id === p.woo_product_id); if (idx >= 0) this.items[idx].sell_unit = unit; } btn.textContent = `Guardado ${count}!`; this.renderList(); // Actualizar badges en lista setTimeout(() => { btn.textContent = count > 1 ? `Guardar para ${count}` : "Guardar"; btn.disabled = false; }, 1500); } catch (e) { console.error("Error saving product unit:", e); modal.error("Error guardando: " + (e.message || e)); btn.textContent = count > 1 ? `Guardar para ${count}` : "Guardar"; btn.disabled = false; } } } customElements.define("products-crud", ProductsCrud);