Files
Autoparts-DB/dashboard/admin.min.js
consultoria-as 21959f1b37 FASE 7d: Lazy Loading + Minificación + Auto-serve minified
Cambios implementados:

1. Lazy loading de imágenes:
   - catalog.js: loading="lazy" decoding="async" en part cards y detail panel
   - inventory.js: lazy loading en imagen de detalle de item

2. Minificación de assets:
   - scripts/minify-assets.sh: minifica JS (terser) y CSS para POS y Dashboard
   - 25 archivos .min.js + 5 .min.css generados en pos/static/
   - 14 archivos .min.js + 8 .min.css generados en dashboard/

3. Nginx auto-serve minified:
   - try_files $1.min.js antes de servir .js original
   - try_files $1.min.css antes de servir .css original
   - Transparente para los templates HTML (cero cambios en HTML)

4. Cache warming script:
   - scripts/warm_vehicle_cache.py: pobla Redis con vehicle info por batches
   - Mitiga DISTINCT ON + 4 JOINs sobre 2B filas
   - Corre en background, procesa ~1.5M parts

Tests: 73/73 pasando
2026-04-27 08:34:24 +00:00

1 line
53 KiB
JavaScript

let currentPage={parts:1,aftermarket:1,crossref:1,fitment:1},categoriesCache=[],groupsCache=[],partsCache=[],manufacturersCache=[],brandsCache=[],pendingImportData=null,searchTimeout=null;const csvFormats={parts:{columns:["oem_part_number","name","name_es","group_id","description","description_es","weight_kg","material"],required:["oem_part_number","name","group_id"],example:"oem_part_number,name,name_es,group_id,description,description_es,weight_kg,material\n04465-33450,Brake Pad Set Front,Pastillas de Freno Delanteras,5,Front disc brake pads,Pastillas de freno de disco delanteras,1.2,Ceramic"},aftermarket:{columns:["oem_part_id","manufacturer_id","part_number","name","name_es","quality_tier","price_usd","warranty_months"],required:["oem_part_id","manufacturer_id","part_number"],example:"oem_part_id,manufacturer_id,part_number,name,name_es,quality_tier,price_usd,warranty_months\n1,3,BP-1234,Brake Pad Set,Pastillas de Freno,premium,45.99,24"},manufacturers:{columns:["name","type","quality_tier","country","website"],required:["name","type"],example:"name,type,quality_tier,country,website\nBrembo,aftermarket,premium,Italy,https://www.brembo.com"},categories:{columns:["name","name_es","slug","icon_name","display_order"],required:["name"],example:"name,name_es,slug,icon_name,display_order\nBrake System,Sistema de Frenos,brakes,brake,1"},groups:{columns:["category_id","name","name_es","display_order"],required:["category_id","name"],example:"category_id,name,name_es,display_order\n1,Brake Pads,Pastillas de Freno,1"},crossref:{columns:["part_id","cross_reference_number","reference_type","source","notes"],required:["part_id","cross_reference_number","reference_type"],example:"part_id,cross_reference_number,reference_type,source,notes\n1,D1210,interchange,Manufacturer,Compatible replacement"},fitment:{columns:["model_year_engine_id","part_id","quantity_required","position","fitment_notes"],required:["model_year_engine_id","part_id"],example:"model_year_engine_id,part_id,quantity_required,position,fitment_notes\n1,1,2,front,Fits all trims"}};function initSidebar(){const e=document.querySelectorAll(".sidebar-item");e.forEach((t=>{t.addEventListener("click",(()=>{showSection(t.dataset.section),e.forEach((e=>e.classList.remove("active"))),t.classList.add("active")}))}))}function showSection(e){document.querySelectorAll(".admin-section").forEach((e=>e.classList.remove("active")));const t=document.getElementById(`section-${e}`);if(t)switch(t.classList.add("active"),e){case"dashboard":loadDashboard();break;case"categories":loadCategories();break;case"groups":loadGroups();break;case"parts":loadParts();break;case"manufacturers":loadManufacturers();break;case"aftermarket":loadAftermarket();break;case"crossref":loadCrossRefs();break;case"fitment":loadFitment();break;case"diagrams":break;case"users":loadUsers()}document.querySelectorAll(".sidebar-item").forEach((t=>{t.classList.toggle("active",t.dataset.section===e)}))}function openModal(e){document.getElementById(e).classList.add("active")}function closeModal(e){document.getElementById(e).classList.remove("active")}function showAlert(e,t="success"){const a=document.getElementById("alertContainer"),n=document.createElement("div");n.className=`alert alert-${t}`,n.innerHTML=`<span>${"success"===t?"✓":"✕"}</span> ${e}`,a.appendChild(n),setTimeout((()=>n.remove()),5e3)}function debounceSearch(e){searchTimeout&&clearTimeout(searchTimeout),searchTimeout=setTimeout(e,300)}async function loadDashboard(){try{const e=await fetch("/api/admin/stats").then((e=>e.json()));document.getElementById("statCategories").textContent=e.categories||0,document.getElementById("statGroups").textContent=e.groups||0,document.getElementById("statParts").textContent=e.parts||0,document.getElementById("statAftermarket").textContent=e.aftermarket||0,document.getElementById("statManufacturers").textContent=e.manufacturers||0,document.getElementById("statFitment").textContent=e.fitment||0}catch(e){console.error("Error loading dashboard:",e)}}async function loadCategories(){try{const e=await fetch("/api/categories"),t=await e.json();categoriesCache=flattenCategories(t);const a=document.getElementById("categoriesTable");if(0===categoriesCache.length)return void(a.innerHTML='<tr><td colspan="7" style="text-align: center; color: var(--text-secondary);">No hay categorías</td></tr>');a.innerHTML=categoriesCache.map((e=>`\n <tr>\n <td>${e.id}</td>\n <td>${e.name}</td>\n <td>${e.name_es||"-"}</td>\n <td>${e.slug||"-"}</td>\n <td>${e.icon_name||"-"}</td>\n <td>${e.display_order||0}</td>\n <td class="actions-cell">\n <button class="btn btn-sm btn-secondary" onclick="editCategory(${e.id})">Editar</button>\n <button class="btn btn-sm btn-danger" onclick="deleteCategory(${e.id})">Eliminar</button>\n </td>\n </tr>\n `)).join(""),updateCategorySelects()}catch(e){console.error("Error loading categories:",e),showAlert("Error al cargar categorías","error")}}function flattenCategories(e,t=0){let a=[];return e.forEach((e=>{a.push({...e,level:t}),e.children&&e.children.length>0&&(a=a.concat(flattenCategories(e.children,t+1)))})),a}function updateCategorySelects(){["groupCategoryFilter","groupCategory"].forEach((e=>{const t=document.getElementById(e);if(!t)return;const a=t.value,n=e.includes("Filter")?'<option value="">Todas las categorías</option>':'<option value="">Selecciona categoría...</option>';t.innerHTML=n+categoriesCache.map((e=>`<option value="${e.id}">${"—".repeat(e.level||0)} ${e.name}</option>`)).join(""),t.value=a}))}function openCategoryModal(e=null){if(document.getElementById("categoryId").value="",document.getElementById("categoryName").value="",document.getElementById("categoryNameEs").value="",document.getElementById("categorySlug").value="",document.getElementById("categoryIcon").value="",document.getElementById("categoryOrder").value="0",document.getElementById("categoryModalTitle").textContent="Nueva Categoría",e){const t=categoriesCache.find((t=>t.id===e));t&&(document.getElementById("categoryId").value=t.id,document.getElementById("categoryName").value=t.name,document.getElementById("categoryNameEs").value=t.name_es||"",document.getElementById("categorySlug").value=t.slug||"",document.getElementById("categoryIcon").value=t.icon_name||"",document.getElementById("categoryOrder").value=t.display_order||0,document.getElementById("categoryModalTitle").textContent="Editar Categoría")}openModal("categoryModal")}function editCategory(e){openCategoryModal(e)}async function saveCategory(){const e=document.getElementById("categoryId").value,t={name:document.getElementById("categoryName").value,name_es:document.getElementById("categoryNameEs").value||null,slug:document.getElementById("categorySlug").value||null,icon_name:document.getElementById("categoryIcon").value||null,display_order:parseInt(document.getElementById("categoryOrder").value)||0};try{const a=e?`/api/admin/categories/${e}`:"/api/admin/categories",n=e?"PUT":"POST",r=await fetch(a,{method:n,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("categoryModal"),showAlert(e?"Categoría actualizada":"Categoría creada"),loadCategories()}catch(e){showAlert(e.message,"error")}}async function deleteCategory(e){if(confirm("¿Estás seguro de eliminar esta categoría?"))try{if(!(await fetch(`/api/admin/categories/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Categoría eliminada"),loadCategories()}catch(e){showAlert(e.message,"error")}}async function loadGroups(){try{const e=document.getElementById("groupCategoryFilter").value;let t="/api/admin/groups";e&&(t+=`?category_id=${e}`);const a=await fetch(t),n=await a.json();groupsCache=n;const r=document.getElementById("groupsTable");if(0===n.length)return void(r.innerHTML='<tr><td colspan="6" style="text-align: center; color: var(--text-secondary);">No hay grupos</td></tr>');r.innerHTML=n.map((e=>`\n <tr>\n <td>${e.id}</td>\n <td>${e.name}</td>\n <td>${e.name_es||"-"}</td>\n <td>${e.category_name||"-"}</td>\n <td>${e.display_order||0}</td>\n <td class="actions-cell">\n <button class="btn btn-sm btn-secondary" onclick="editGroup(${e.id})">Editar</button>\n <button class="btn btn-sm btn-danger" onclick="deleteGroup(${e.id})">Eliminar</button>\n </td>\n </tr>\n `)).join(""),updateGroupSelects()}catch(e){console.error("Error loading groups:",e),showAlert("Error al cargar grupos","error")}}function updateGroupSelects(){["partGroupFilter","partGroup"].forEach((e=>{const t=document.getElementById(e);if(!t)return;const a=t.value,n=e.includes("Filter")?'<option value="">Todos los grupos</option>':'<option value="">Selecciona grupo...</option>';t.innerHTML=n+groupsCache.map((e=>`<option value="${e.id}">${e.name} (${e.category_name||"Sin categoría"})</option>`)).join(""),t.value=a}))}function openGroupModal(e=null){document.getElementById("groupId").value="",document.getElementById("groupName").value="",document.getElementById("groupNameEs").value="",document.getElementById("groupOrder").value="0",document.getElementById("groupModalTitle").textContent="Nuevo Grupo";if(document.getElementById("groupCategory").innerHTML='<option value="">Selecciona categoría...</option>'+categoriesCache.map((e=>`<option value="${e.id}">${e.name}</option>`)).join(""),e){const t=groupsCache.find((t=>t.id===e));t&&(document.getElementById("groupId").value=t.id,document.getElementById("groupName").value=t.name,document.getElementById("groupNameEs").value=t.name_es||"",document.getElementById("groupCategory").value=t.category_id,document.getElementById("groupOrder").value=t.display_order||0,document.getElementById("groupModalTitle").textContent="Editar Grupo")}openModal("groupModal")}function editGroup(e){openGroupModal(e)}async function saveGroup(){const e=document.getElementById("groupId").value,t={category_id:parseInt(document.getElementById("groupCategory").value),name:document.getElementById("groupName").value,name_es:document.getElementById("groupNameEs").value||null,display_order:parseInt(document.getElementById("groupOrder").value)||0};try{const a=e?`/api/admin/groups/${e}`:"/api/admin/groups",n=e?"PUT":"POST",r=await fetch(a,{method:n,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("groupModal"),showAlert(e?"Grupo actualizado":"Grupo creado"),loadGroups()}catch(e){showAlert(e.message,"error")}}async function deleteGroup(e){if(confirm("¿Estás seguro de eliminar este grupo?"))try{if(!(await fetch(`/api/admin/groups/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Grupo eliminado"),loadGroups()}catch(e){showAlert(e.message,"error")}}async function loadParts(){try{const e=document.getElementById("partSearch").value,t=document.getElementById("partGroupFilter").value;let a=`/api/parts?page=${currentPage.parts}&per_page=20`;e&&(a+=`&search=${encodeURIComponent(e)}`),t&&(a+=`&group_id=${t}`);const n=await fetch(a),r=await n.json();partsCache=r.data||[];const o=document.getElementById("partsTable");if(0===partsCache.length)return o.innerHTML='<tr><td colspan="6" style="text-align: center; color: var(--text-secondary);">No hay partes</td></tr>',void renderPagination("partsPagination",r.pagination,"parts",loadParts);o.innerHTML=partsCache.map((e=>`\n <tr>\n <td>${e.id}</td>\n <td>\n ${e.image_url?`<img src="${e.image_url}" alt="${e.name}" class="part-thumbnail">`:'<span style="color: var(--text-secondary);">📷</span>'}\n </td>\n <td><code>${e.oem_part_number}</code></td>\n <td>${e.name}</td>\n <td>${e.group_name||"-"}</td>\n <td class="actions-cell">\n <button class="btn btn-sm btn-secondary" onclick="editPart(${e.id})">Editar</button>\n <button class="btn btn-sm btn-danger" onclick="deletePart(${e.id})">Eliminar</button>\n </td>\n </tr>\n `)).join(""),renderPagination("partsPagination",r.pagination,"parts",loadParts),0===groupsCache.length&&await loadGroups()}catch(e){console.error("Error loading parts:",e),showAlert("Error al cargar partes","error")}}function openPartModal(e=null){document.getElementById("partId").value="",document.getElementById("partOemNumber").value="",document.getElementById("partName").value="",document.getElementById("partNameEs").value="",document.getElementById("partDescription").value="",document.getElementById("partDescriptionEs").value="",document.getElementById("partWeight").value="",document.getElementById("partMaterial").value="",document.getElementById("partModalTitle").textContent="Nueva Parte OEM",resetPartImagePreview();document.getElementById("partGroup").innerHTML='<option value="">Selecciona grupo...</option>'+groupsCache.map((e=>`<option value="${e.id}">${e.name}</option>`)).join(""),e&&fetchPartDetail(e),openModal("partModal")}async function fetchPartDetail(e){try{const t=await fetch(`/api/parts/${e}`),a=await t.json();document.getElementById("partId").value=a.id,document.getElementById("partOemNumber").value=a.oem_part_number,document.getElementById("partName").value=a.name,document.getElementById("partNameEs").value=a.name_es||"",document.getElementById("partGroup").value=a.group_id,document.getElementById("partDescription").value=a.description||"",document.getElementById("partDescriptionEs").value=a.description_es||"",document.getElementById("partModalTitle").textContent="Editar Parte OEM";const n=document.getElementById("partImagePreview");a.image_url?(n.innerHTML=`<img src="${a.image_url}" alt="${a.name}">`,document.getElementById("partImageUrl").value=a.image_url):resetPartImagePreview()}catch(e){console.error("Error fetching part:",e)}}function editPart(e){openPartModal(e)}async function savePart(){const e=document.getElementById("partId").value;let t=document.getElementById("partImageUrl").value||null;const a=document.getElementById("partImagePreview").querySelector("img");a&&a.src.startsWith("data:")&&(t=await uploadPartImage(a.src));const n={oem_part_number:document.getElementById("partOemNumber").value,name:document.getElementById("partName").value,name_es:document.getElementById("partNameEs").value||null,group_id:parseInt(document.getElementById("partGroup").value),description:document.getElementById("partDescription").value||null,description_es:document.getElementById("partDescriptionEs").value||null,weight_kg:parseFloat(document.getElementById("partWeight").value)||null,material:document.getElementById("partMaterial").value||null,image_url:t};try{const t=e?`/api/admin/parts/${e}`:"/api/admin/parts",a=e?"PUT":"POST",r=await fetch(t,{method:a,headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("partModal"),showAlert(e?"Parte actualizada":"Parte creada"),loadParts()}catch(e){showAlert(e.message,"error")}}function previewPartImage(e){const t=document.getElementById("partImagePreview");if(e.files&&e.files[0]){const a=new FileReader;a.onload=function(e){t.innerHTML=`<img src="${e.target.result}" alt="Preview">`},a.readAsDataURL(e.files[0])}}async function uploadPartImage(e){try{const t=await fetch("/api/admin/upload-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({image:e})});if(!t.ok)throw new Error("Error uploading image");return(await t.json()).url}catch(e){return console.error("Image upload failed:",e),null}}function resetPartImagePreview(){document.getElementById("partImagePreview").innerHTML='<span class="image-placeholder">📷 Sin imagen</span>',document.getElementById("partImageUrl").value="",document.getElementById("partImageFile").value=""}async function deletePart(e){if(confirm("¿Estás seguro de eliminar esta parte?"))try{if(!(await fetch(`/api/admin/parts/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Parte eliminada"),loadParts()}catch(e){showAlert(e.message,"error")}}async function loadManufacturers(){try{const e=await fetch("/api/manufacturers");manufacturersCache=await e.json();const t=document.getElementById("manufacturersTable");if(0===manufacturersCache.length)return void(t.innerHTML='<tr><td colspan="6" style="text-align: center; color: var(--text-secondary);">No hay fabricantes</td></tr>');t.innerHTML=manufacturersCache.map((e=>`\n <tr>\n <td>${e.id}</td>\n <td>${e.name}</td>\n <td>${e.type||"-"}</td>\n <td><span class="badge badge-${e.quality_tier||"standard"}">${e.quality_tier||"-"}</span></td>\n <td>${e.country||"-"}</td>\n <td class="actions-cell">\n <button class="btn btn-sm btn-secondary" onclick="editManufacturer(${e.id})">Editar</button>\n <button class="btn btn-sm btn-danger" onclick="deleteManufacturer(${e.id})">Eliminar</button>\n </td>\n </tr>\n `)).join(""),updateManufacturerSelects()}catch(e){console.error("Error loading manufacturers:",e),showAlert("Error al cargar fabricantes","error")}}function updateManufacturerSelects(){const e=document.getElementById("aftermarketManufacturerFilter");if(e){const t=e.value;e.innerHTML='<option value="">Todos los fabricantes</option>'+manufacturersCache.map((e=>`<option value="${e.id}">${e.name}</option>`)).join(""),e.value=t}const t=document.getElementById("aftermarketManufacturer");t&&(t.innerHTML='<option value="">Selecciona fabricante...</option>'+manufacturersCache.map((e=>`<option value="${e.id}">${e.name}</option>`)).join(""))}function openManufacturerModal(e=null){if(document.getElementById("manufacturerId").value="",document.getElementById("manufacturerName").value="",document.getElementById("manufacturerType").value="aftermarket",document.getElementById("manufacturerQuality").value="standard",document.getElementById("manufacturerCountry").value="",document.getElementById("manufacturerWebsite").value="",document.getElementById("manufacturerModalTitle").textContent="Nuevo Fabricante",e){const t=manufacturersCache.find((t=>t.id===e));t&&(document.getElementById("manufacturerId").value=t.id,document.getElementById("manufacturerName").value=t.name,document.getElementById("manufacturerType").value=t.type||"aftermarket",document.getElementById("manufacturerQuality").value=t.quality_tier||"standard",document.getElementById("manufacturerCountry").value=t.country||"",document.getElementById("manufacturerWebsite").value=t.website||"",document.getElementById("manufacturerModalTitle").textContent="Editar Fabricante")}openModal("manufacturerModal")}function editManufacturer(e){openManufacturerModal(e)}async function saveManufacturer(){const e=document.getElementById("manufacturerId").value,t={name:document.getElementById("manufacturerName").value,type:document.getElementById("manufacturerType").value,quality_tier:document.getElementById("manufacturerQuality").value,country:document.getElementById("manufacturerCountry").value||null,website:document.getElementById("manufacturerWebsite").value||null};try{const a=e?`/api/admin/manufacturers/${e}`:"/api/admin/manufacturers",n=e?"PUT":"POST",r=await fetch(a,{method:n,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("manufacturerModal"),showAlert(e?"Fabricante actualizado":"Fabricante creado"),loadManufacturers()}catch(e){showAlert(e.message,"error")}}async function deleteManufacturer(e){if(confirm("¿Estás seguro de eliminar este fabricante?"))try{if(!(await fetch(`/api/admin/manufacturers/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Fabricante eliminado"),loadManufacturers()}catch(e){showAlert(e.message,"error")}}async function loadAftermarket(){try{const e=document.getElementById("aftermarketSearch").value,t=document.getElementById("aftermarketManufacturerFilter").value;let a=`/api/aftermarket?page=${currentPage.aftermarket}&per_page=20`;e&&(a+=`&search=${encodeURIComponent(e)}`),t&&(a+=`&manufacturer_id=${t}`);const n=await fetch(a),r=await n.json(),o=document.getElementById("aftermarketTable");if(!r.data||0===r.data.length)return o.innerHTML='<tr><td colspan="8" style="text-align: center; color: var(--text-secondary);">No hay partes aftermarket</td></tr>',void renderPagination("aftermarketPagination",r.pagination,"aftermarket",loadAftermarket);o.innerHTML=r.data.map((e=>`\n <tr>\n <td>${e.id}</td>\n <td><code>${e.part_number}</code></td>\n <td>${e.name||"-"}</td>\n <td><code>${e.oem_part_number}</code></td>\n <td>${e.manufacturer_name}</td>\n <td><span class="badge badge-${e.quality_tier||"standard"}">${e.quality_tier||"-"}</span></td>\n <td>${e.price_usd?"$"+e.price_usd.toFixed(2):"-"}</td>\n <td class="actions-cell">\n <button class="btn btn-sm btn-secondary" onclick="editAftermarket(${e.id})">Editar</button>\n <button class="btn btn-sm btn-danger" onclick="deleteAftermarket(${e.id})">Eliminar</button>\n </td>\n </tr>\n `)).join(""),renderPagination("aftermarketPagination",r.pagination,"aftermarket",loadAftermarket)}catch(e){console.error("Error loading aftermarket:",e),showAlert("Error al cargar partes aftermarket","error")}}async function openAftermarketModal(e=null){document.getElementById("aftermarketId").value="",document.getElementById("aftermarketPartNumber").value="",document.getElementById("aftermarketName").value="",document.getElementById("aftermarketNameEs").value="",document.getElementById("aftermarketQuality").value="standard",document.getElementById("aftermarketPrice").value="",document.getElementById("aftermarketWarranty").value="",document.getElementById("aftermarketModalTitle").textContent="Nueva Parte Aftermarket",await loadPartsForSelect("aftermarketOemPart"),updateManufacturerSelects(),openModal("aftermarketModal")}async function editAftermarket(e){await openAftermarketModal(e);try{const t=await fetch("/api/aftermarket?search=&per_page=200"),a=await t.json(),n=(a.data||a).find((t=>t.id===e));n&&(document.getElementById("aftermarketId").value=n.id,document.getElementById("aftermarketPartNumber").value=n.part_number||"",document.getElementById("aftermarketName").value=n.name||"",document.getElementById("aftermarketNameEs").value=n.name_es||"",document.getElementById("aftermarketQuality").value=n.quality_tier||"standard",document.getElementById("aftermarketPrice").value=n.price_usd||"",document.getElementById("aftermarketWarranty").value=n.warranty_months||"",n.oem_part_id&&(document.getElementById("aftermarketOemPart").value=n.oem_part_id),n.manufacturer_id&&(document.getElementById("aftermarketManufacturer").value=n.manufacturer_id),document.getElementById("aftermarketModalTitle").textContent="Editar Parte Aftermarket")}catch(e){console.error("Error loading aftermarket for edit:",e)}}async function saveAftermarket(){const e=document.getElementById("aftermarketId").value,t={oem_part_id:parseInt(document.getElementById("aftermarketOemPart").value),manufacturer_id:parseInt(document.getElementById("aftermarketManufacturer").value),part_number:document.getElementById("aftermarketPartNumber").value,name:document.getElementById("aftermarketName").value||null,name_es:document.getElementById("aftermarketNameEs").value||null,quality_tier:document.getElementById("aftermarketQuality").value,price_usd:parseFloat(document.getElementById("aftermarketPrice").value)||null,warranty_months:parseInt(document.getElementById("aftermarketWarranty").value)||null};try{const a=e?`/api/admin/aftermarket/${e}`:"/api/admin/aftermarket",n=e?"PUT":"POST",r=await fetch(a,{method:n,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("aftermarketModal"),showAlert(e?"Parte actualizada":"Parte creada"),loadAftermarket()}catch(e){showAlert(e.message,"error")}}async function deleteAftermarket(e){if(confirm("¿Estás seguro de eliminar esta parte aftermarket?"))try{if(!(await fetch(`/api/admin/aftermarket/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Parte eliminada"),loadAftermarket()}catch(e){showAlert(e.message,"error")}}async function loadCrossRefs(){try{const e=await fetch(`/api/admin/crossref?page=${currentPage.crossref}&per_page=20`),t=await e.json(),a=document.getElementById("crossrefTable");if(!t.data||0===t.data.length)return void(a.innerHTML='<tr><td colspan="6" style="text-align: center; color: var(--text-secondary);">No hay cross-references</td></tr>');a.innerHTML=t.data.map((e=>`\n <tr>\n <td>${e.id}</td>\n <td><code>${e.oem_part_number}</code> - ${e.part_name}</td>\n <td><code>${e.cross_reference_number}</code></td>\n <td>${e.reference_type}</td>\n <td>${e.source||"-"}</td>\n <td class="actions-cell">\n <button class="btn btn-sm btn-secondary" onclick="editCrossRef(${e.id})">Editar</button>\n <button class="btn btn-sm btn-danger" onclick="deleteCrossRef(${e.id})">Eliminar</button>\n </td>\n </tr>\n `)).join(""),renderPagination("crossrefPagination",t.pagination,"crossref",loadCrossRefs)}catch(e){console.error("Error loading cross-refs:",e)}}async function openCrossRefModal(e=null){document.getElementById("crossrefId").value="",document.getElementById("crossrefNumber").value="",document.getElementById("crossrefType").value="interchange",document.getElementById("crossrefSource").value="",document.getElementById("crossrefNotes").value="",document.getElementById("crossrefModalTitle").textContent="Nueva Cross-Reference",await loadPartsForSelect("crossrefPart"),openModal("crossrefModal")}function editCrossRef(e){openCrossRefModal(e)}async function saveCrossRef(){const e=document.getElementById("crossrefId").value,t={part_id:parseInt(document.getElementById("crossrefPart").value),cross_reference_number:document.getElementById("crossrefNumber").value,reference_type:document.getElementById("crossrefType").value,source:document.getElementById("crossrefSource").value||null,notes:document.getElementById("crossrefNotes").value||null};try{const a=e?`/api/admin/crossref/${e}`:"/api/admin/crossref",n=e?"PUT":"POST",r=await fetch(a,{method:n,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("crossrefModal"),showAlert(e?"Referencia actualizada":"Referencia creada"),loadCrossRefs()}catch(e){showAlert(e.message,"error")}}async function deleteCrossRef(e){if(confirm("¿Estás seguro de eliminar esta referencia?"))try{if(!(await fetch(`/api/admin/crossref/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Referencia eliminada"),loadCrossRefs()}catch(e){showAlert(e.message,"error")}}async function loadFitment(){try{const e=document.getElementById("fitmentBrandFilter").value,t=document.getElementById("fitmentModelFilter").value;let a=`/api/admin/fitment?page=${currentPage.fitment}&per_page=20`;e&&(a+=`&brand=${encodeURIComponent(e)}`),t&&(a+=`&model=${encodeURIComponent(t)}`);const n=await fetch(a),r=await n.json(),o=document.getElementById("fitmentTable");if(!r.data||0===r.data.length)return o.innerHTML='<tr><td colspan="6" style="text-align: center; color: var(--text-secondary);">No hay fitments</td></tr>',void renderPagination("fitmentPagination",r.pagination,"fitment",loadFitment);o.innerHTML=r.data.map((e=>`\n <tr>\n <td>${e.id}</td>\n <td>${e.brand} ${e.model} ${e.year} - ${e.engine}</td>\n <td><code>${e.oem_part_number}</code> - ${e.part_name}</td>\n <td>${e.quantity_required}</td>\n <td>${e.position||"-"}</td>\n <td class="actions-cell">\n <button class="btn btn-sm btn-danger" onclick="deleteFitment(${e.id})">Eliminar</button>\n </td>\n </tr>\n `)).join(""),renderPagination("fitmentPagination",r.pagination,"fitment",loadFitment)}catch(e){console.error("Error loading fitment:",e)}}async function loadFitmentModels(){const e=document.getElementById("fitmentBrandFilter").value,t=document.getElementById("fitmentModelFilter");if(!e)return t.innerHTML='<option value="">Todos los modelos</option>',void loadFitment();try{const a=await fetch(`/api/models?brand=${encodeURIComponent(e)}`),n=await a.json();t.innerHTML='<option value="">Todos los modelos</option>'+n.map((e=>`<option value="${e}">${e}</option>`)).join(""),loadFitment()}catch(e){console.error("Error loading models:",e)}}async function openFitmentModal(){document.getElementById("fitmentId").value="",document.getElementById("fitmentQuantity").value="1",document.getElementById("fitmentPosition").value="",document.getElementById("fitmentNotes").value="",document.getElementById("fitmentModalTitle").textContent="Nuevo Fitment",await loadVehiclesForSelect("fitmentVehicle"),await loadPartsForSelect("fitmentPart"),openModal("fitmentModal")}async function saveFitment(){const e=document.getElementById("fitmentId").value,t={model_year_engine_id:parseInt(document.getElementById("fitmentVehicle").value),part_id:parseInt(document.getElementById("fitmentPart").value),quantity_required:parseInt(document.getElementById("fitmentQuantity").value)||1,position:document.getElementById("fitmentPosition").value||null,fitment_notes:document.getElementById("fitmentNotes").value||null};try{const a=e?`/api/admin/fitment/${e}`:"/api/admin/fitment",n=e?"PUT":"POST",r=await fetch(a,{method:n,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("fitmentModal"),showAlert(e?"Fitment actualizado":"Fitment creado"),loadFitment()}catch(e){showAlert(e.message,"error")}}async function deleteFitment(e){if(confirm("¿Estás seguro de eliminar este fitment?"))try{if(!(await fetch(`/api/admin/fitment/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Fitment eliminado"),loadFitment()}catch(e){showAlert(e.message,"error")}}async function loadPartsForSelect(e){const t=document.getElementById(e);if(t)try{const e=await fetch("/api/parts?per_page=100"),a=await e.json();t.innerHTML='<option value="">Selecciona parte...</option>'+(a.data||[]).map((e=>`<option value="${e.id}">${e.oem_part_number} - ${e.name}</option>`)).join("")}catch(e){console.error("Error loading parts for select:",e)}}async function loadVehiclesForSelect(e){const t=document.getElementById(e);if(t)try{const e=await fetch("/api/model-year-engine?per_page=100"),a=await e.json(),n=a.data||a;t.innerHTML='<option value="">Selecciona vehículo...</option>'+n.map((e=>`<option value="${e.id}">${e.brand} ${e.model} ${e.year} - ${e.engine}</option>`)).join("")}catch(e){console.error("Error loading vehicles for select:",e)}}async function loadBrands(){try{const e=await fetch("/api/brands");brandsCache=await e.json();const t=document.getElementById("fitmentBrandFilter");t&&(t.innerHTML='<option value="">Selecciona marca...</option>'+brandsCache.map((e=>`<option value="${e}">${e}</option>`)).join(""))}catch(e){console.error("Error loading brands:",e)}}function renderPagination(e,t,a,n){const r=document.getElementById(e);if(!r||!t)return void(r.innerHTML="");const{page:o,total_pages:i}=t;if(i<=1)return void(r.innerHTML="");let d="";d+=`<button ${o<=1?"disabled":""} onclick="goToPage('${a}', ${o-1}, '${n.name}')">← Anterior</button>`;const l=Math.max(1,o-2),c=Math.min(i,o+2);for(let e=l;e<=c;e++)d+=`<button class="${e===o?"active":""}" onclick="goToPage('${a}', ${e}, '${n.name}')">${e}</button>`;d+=`<button ${o>=i?"disabled":""} onclick="goToPage('${a}', ${o+1}, '${n.name}')">Siguiente →</button>`,r.innerHTML=d}function goToPage(e,t,a){currentPage[e]=t,window[a]()}function initDropZone(){const e=document.getElementById("dropZone"),t=document.getElementById("csvFile");e.addEventListener("click",(()=>t.click())),e.addEventListener("dragover",(t=>{t.preventDefault(),e.classList.add("dragover")})),e.addEventListener("dragleave",(()=>{e.classList.remove("dragover")})),e.addEventListener("drop",(t=>{t.preventDefault(),e.classList.remove("dragover");const a=t.dataTransfer.files[0];a&&a.name.endsWith(".csv")?handleCsvFile(a):showAlert("Por favor selecciona un archivo CSV","error")})),t.addEventListener("change",(e=>{const t=e.target.files[0];t&&handleCsvFile(t)}))}function initImportTypeChange(){document.getElementById("importType").addEventListener("change",updateCsvFormatHelp),updateCsvFormatHelp()}function updateCsvFormatHelp(){const e=document.getElementById("importType").value,t=csvFormats[e],a=document.getElementById("csvFormatHelp");t&&(a.innerHTML=`\n <p style="margin-bottom: 0.5rem;"><strong>Columnas requeridas:</strong> ${t.required.join(", ")}</p>\n <p style="margin-bottom: 0.5rem;"><strong>Todas las columnas:</strong> ${t.columns.join(", ")}</p>\n <p style="margin-bottom: 0.5rem;"><strong>Ejemplo:</strong></p>\n <pre style="background: var(--bg-tertiary); padding: 1rem; border-radius: 8px; overflow-x: auto; font-size: 0.85rem;">${t.example}</pre>\n `)}function handleCsvFile(e){const t=new FileReader;t.onload=e=>{const t=parseCSV(e.target.result);t.length<2?showAlert("El archivo CSV debe tener al menos un encabezado y una fila de datos","error"):(pendingImportData=t,showImportPreview(t))},t.readAsText(e)}function parseCSV(e){const t=e.split(/\r?\n/).filter((e=>e.trim())),a=[];for(const e of t){const t=[];let n="",r=!1;for(let a=0;a<e.length;a++){const o=e[a];'"'===o?r=!r:","!==o||r?n+=o:(t.push(n.trim()),n="")}t.push(n.trim()),a.push(t)}return a}function showImportPreview(e){const t=e[0],a=e.slice(1,6);document.getElementById("previewCount").textContent=e.length-1,document.getElementById("previewHead").innerHTML="<tr>"+t.map((e=>`<th>${e}</th>`)).join("")+"</tr>",document.getElementById("previewBody").innerHTML=a.map((e=>"<tr>"+e.map((e=>`<td>${e}</td>`)).join("")+"</tr>")).join(""),document.getElementById("importPreview").style.display="block"}function cancelImport(){pendingImportData=null,document.getElementById("importPreview").style.display="none",document.getElementById("csvFile").value=""}async function executeImport(){if(!pendingImportData)return;const e=document.getElementById("importType").value,t=csvFormats[e],a=pendingImportData[0],n=pendingImportData.slice(1);for(const e of t.required)if(!a.includes(e))return void showAlert(`Falta columna requerida: ${e}`,"error");const r=n.map((e=>{const t={};return a.forEach(((a,n)=>{let r=e[n]||null;["id","category_id","group_id","part_id","oem_part_id","manufacturer_id","model_year_engine_id","display_order","quantity_required","warranty_months"].includes(a)?r=r?parseInt(r):null:["weight_kg","price_usd"].includes(a)&&(r=r?parseFloat(r):null),t[a]=r})),t}));try{const t=await fetch(`/api/admin/import/${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({records:r})}),a=await t.json();if(!t.ok)throw new Error(a.error||"Error en la importación");showAlert(`Importados ${a.imported} registros exitosamente`),cancelImport(),loadDashboard()}catch(e){showAlert(e.message,"error")}}async function exportData(e){try{const t=await fetch(`/api/admin/export/${e}`),a=await t.json();if(!a.data||0===a.data.length)return void showAlert("No hay datos para exportar","error");const n=Object.keys(a.data[0]);let r=n.join(",")+"\n";for(const e of a.data)r+=n.map((t=>{let a=e[t];return null==a?"":(a=String(a),a.includes(",")||a.includes('"')||a.includes("\n")?'"'+a.replace(/"/g,'""')+'"':a)})).join(",")+"\n";const o=new Blob([r],{type:"text/csv;charset=utf-8;"}),i=document.createElement("a");i.href=URL.createObjectURL(o),i.download=`${e}_export_${(new Date).toISOString().slice(0,10)}.csv`,i.click(),showAlert(`Exportados ${a.data.length} registros`)}catch(e){showAlert("Error al exportar: "+e.message,"error")}}document.addEventListener("DOMContentLoaded",(()=>{initSidebar(),initDropZone(),initImportTypeChange(),loadDashboard()})),loadBrands();let bulkSelectedMYEId=null,bulkAvailableParts=[];async function initBulkEditor(){const e=document.getElementById("bulkBrand");e&&brandsCache.length>0&&(e.innerHTML='<option value="">Selecciona marca...</option>'+brandsCache.map((e=>`<option value="${e}">${e}</option>`)).join("")),0===categoriesCache.length&&await loadCategories();const t=document.getElementById("bulkCategory");t&&(t.innerHTML='<option value="">Todas las categorías</option>'+categoriesCache.map((e=>`<option value="${e.id}">${e.name}</option>`)).join(""))}async function loadBulkModels(){const e=document.getElementById("bulkBrand").value,t=document.getElementById("bulkModel"),a=document.getElementById("bulkYear"),n=document.getElementById("bulkEngine");if(t.innerHTML='<option value="">Selecciona modelo...</option>',a.innerHTML='<option value="">Selecciona año...</option>',n.innerHTML='<option value="">Selecciona motor...</option>',document.getElementById("bulkVehicleSelected").style.display="none",e)try{const a=await fetch(`/api/models?brand=${encodeURIComponent(e)}`),n=await a.json();t.innerHTML='<option value="">Selecciona modelo...</option>'+n.map((e=>`<option value="${e}">${e}</option>`)).join("")}catch(e){console.error("Error loading models:",e)}}async function loadBulkYears(){const e=document.getElementById("bulkBrand").value,t=document.getElementById("bulkModel").value,a=document.getElementById("bulkYear"),n=document.getElementById("bulkEngine");if(a.innerHTML='<option value="">Selecciona año...</option>',n.innerHTML='<option value="">Selecciona motor...</option>',document.getElementById("bulkVehicleSelected").style.display="none",e&&t)try{const n=await fetch(`/api/years?brand=${encodeURIComponent(e)}&model=${encodeURIComponent(t)}`),r=await n.json();a.innerHTML='<option value="">Selecciona año...</option>'+r.map((e=>`<option value="${e}">${e}</option>`)).join("")}catch(e){console.error("Error loading years:",e)}}async function loadBulkEngines(){const e=document.getElementById("bulkBrand").value,t=document.getElementById("bulkModel").value,a=document.getElementById("bulkYear").value,n=document.getElementById("bulkEngine");if(n.innerHTML='<option value="">Selecciona motor...</option>',document.getElementById("bulkVehicleSelected").style.display="none",e&&t&&a)try{const r=await fetch(`/api/engines?brand=${encodeURIComponent(e)}&model=${encodeURIComponent(t)}&year=${a}`),o=(await r.json(),await fetch(`/api/model-year-engine?brand=${encodeURIComponent(e)}&model=${encodeURIComponent(t)}&year=${a}&per_page=100`)),i=await o.json(),d=i.data||i;n.innerHTML='<option value="">Selecciona motor...</option>'+d.map((e=>`<option value="${e.id}">${e.engine}</option>`)).join("")}catch(e){console.error("Error loading engines:",e)}}async function selectBulkVehicle(){const e=document.getElementById("bulkEngine").value;if(!e)return void(document.getElementById("bulkVehicleSelected").style.display="none");bulkSelectedMYEId=parseInt(e);const t=document.getElementById("bulkBrand").value,a=document.getElementById("bulkModel").value,n=document.getElementById("bulkYear").value,r=document.getElementById("bulkEngine").options[document.getElementById("bulkEngine").selectedIndex].text;document.getElementById("bulkVehicleName").textContent=`${t} ${a} ${n} - ${r}`,document.getElementById("bulkMYEId").textContent=e,document.getElementById("bulkVehicleSelected").style.display="block",await initBulkEditor(),await loadBulkParts()}async function loadBulkParts(){const e=document.getElementById("bulkPartsContainer"),t=document.getElementById("bulkCategory").value;e.innerHTML='<p style="color: var(--text-secondary);">Cargando partes...</p>';try{let a="/api/parts?per_page=100";t&&(a+=`&category_id=${t}`);const n=await fetch(a),r=await n.json();if(bulkAvailableParts=r.data||[],0===bulkAvailableParts.length)return void(e.innerHTML='<p style="color: var(--text-secondary);">No hay partes disponibles en esta categoría.</p>');const o=await fetch(`/api/admin/fitment?mye_id=${bulkSelectedMYEId}&per_page=500`),i=await o.json(),d=new Set((i.data||[]).map((e=>e.part_id)));e.innerHTML=bulkAvailableParts.map((e=>`\n <div class="bulk-part-item ${d.has(e.id)?"selected":""}" onclick="toggleBulkPart(this, ${e.id})">\n <input type="checkbox" ${d.has(e.id)?"checked disabled":""} data-part-id="${e.id}">\n <div class="bulk-part-info">\n <div class="bulk-part-number">${e.oem_part_number}</div>\n <div class="bulk-part-name">${e.name}</div>\n <div class="bulk-part-group">${e.group_name||""} · ${e.category_name||""}</div>\n </div>\n <input type="number" class="form-input bulk-part-qty" value="1" min="1" placeholder="Cant."\n onclick="event.stopPropagation()" ${d.has(e.id)?"disabled":""}>\n </div>\n `)).join(""),updateBulkSelectedCount()}catch(t){console.error("Error loading parts:",t),e.innerHTML='<p style="color: var(--danger);">Error al cargar partes</p>'}}function toggleBulkPart(e,t){const a=e.querySelector('input[type="checkbox"]');a.disabled||(a.checked=!a.checked,e.classList.toggle("selected",a.checked),updateBulkSelectedCount())}function updateBulkSelectedCount(){const e=document.querySelectorAll('#bulkPartsContainer input[type="checkbox"]:checked:not(:disabled)');document.getElementById("bulkSelectedCount").textContent=`${e.length} partes seleccionadas`}async function saveBulkFitments(){if(!bulkSelectedMYEId)return void showAlert("Selecciona un vehículo primero","error");const e=document.querySelectorAll("#bulkPartsContainer .bulk-part-item"),t=[];if(e.forEach((e=>{const a=e.querySelector('input[type="checkbox"]');if(a.checked&&!a.disabled){const n=parseInt(a.dataset.partId),r=parseInt(e.querySelector(".bulk-part-qty").value)||1;t.push({model_year_engine_id:bulkSelectedMYEId,part_id:n,quantity_required:r})}})),0!==t.length)try{const e=await fetch("/api/admin/import/fitment",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({records:t})}),a=await e.json();if(!e.ok)throw new Error(a.error||"Error al guardar");showAlert(`${a.imported} fitments creados exitosamente`),await loadBulkParts(),loadFitment(),loadDashboard()}catch(e){showAlert(e.message,"error")}else showAlert("Selecciona al menos una parte","error")}const originalShowSection=showSection;showSection=function(e){originalShowSection(e),"fitment"===e&&initBulkEditor()};let currentEditorDiagramId=null,currentEditorHotspots=[],partSearchTimeout=null;async function searchDiagramsAdmin(){const e=document.getElementById("diagramSearchInput").value.trim(),t=document.getElementById("diagramSearchResults");if(e){t.innerHTML='<p style="color:var(--text-secondary);grid-column:1/-1"><i class="fas fa-spinner fa-spin"></i> Buscando...</p>';try{const a=await fetch(`/api/diagrams/search?q=${encodeURIComponent(e)}`),n=await a.json();if(0===n.length)return void(t.innerHTML='<p style="color:var(--text-secondary);grid-column:1/-1">No se encontraron diagramas</p>');t.innerHTML=n.map((e=>{const t=e.image_path?"/"+e.image_path:`/static/diagrams/moog/${e.name}.jpg`;return`\n <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:8px;overflow:hidden;cursor:pointer;transition:border-color 0.2s"\n onclick="openHotspotEditor(${e.id})"\n onmouseover="this.style.borderColor='var(--accent)'"\n onmouseout="this.style.borderColor='var(--border)'">\n <img src="${t}" alt="${e.name}" style="width:100%;height:120px;object-fit:contain;background:#f0f0f0;display:block"\n onerror="this.style.display='none'">\n <div style="padding:0.5rem 0.65rem">\n <div style="font-weight:600;color:var(--accent)">${e.name}</div>\n <div style="font-size:0.8rem;color:var(--text-secondary)">${e.name_es||e.source||""}</div>\n </div>\n </div>`})).join("")}catch(e){t.innerHTML='<p style="color:#e74c3c;grid-column:1/-1">Error al buscar diagramas</p>'}}else t.innerHTML='<p style="color:var(--text-secondary);grid-column:1/-1">Ingresa un código de diagrama para buscar</p>'}async function openHotspotEditor(e){currentEditorDiagramId=e,document.getElementById("hotspotEditorArea").style.display="block";try{const t=await fetch(`/api/diagrams/${e}`),a=await t.json();document.getElementById("hotspotEditorTitle").textContent=`${a.name} - ${a.name_es||a.group_name||""}`;const n=a.image_url||(a.image_path?"/"+a.image_path:"");document.getElementById("hotspotEditorImg").src=n,currentEditorHotspots=a.hotspots||[],renderEditorHotspots(),clearHotspotForm();const r=currentEditorHotspots.reduce(((e,t)=>Math.max(e,t.callout_number||0)),0);document.getElementById("hsCallout").value=r+1,document.getElementById("hotspotEditorArea").scrollIntoView({behavior:"smooth"})}catch(e){showAlert("Error al cargar diagrama","error")}}function closeHotspotEditor(){document.getElementById("hotspotEditorArea").style.display="none",currentEditorDiagramId=null,currentEditorHotspots=[]}function onHotspotImageClick(e){const t=e.target.getBoundingClientRect(),a=((e.clientX-t.left)/t.width*100).toFixed(2),n=((e.clientY-t.top)/t.height*100).toFixed(2);document.getElementById("hsCoords").value=`${a},${n}`,renderEditorHotspots();const r=document.getElementById("hotspotMarkersContainer"),o=document.createElement("div");o.style.cssText=`position:absolute;left:${a}%;top:${n}%;width:24px;height:24px;border-radius:50%;background:rgba(46,204,113,0.5);border:2px solid #2ecc71;transform:translate(-50%,-50%);pointer-events:none;z-index:10`,r.appendChild(o)}function renderEditorHotspots(){const e=document.getElementById("hotspotMarkersContainer"),t=document.getElementById("hotspotsList");e.innerHTML=currentEditorHotspots.map((e=>{const t=(e.coords||"").split(",");return t.length<2?"":`<div style="position:absolute;left:${t[0]}%;top:${t[1]}%;width:24px;height:24px;border-radius:50%;background:rgba(231,76,60,0.4);border:2px solid #e74c3c;transform:translate(-50%,-50%);display:flex;align-items:center;justify-content:center;font-size:0.6rem;font-weight:700;color:white;pointer-events:auto;cursor:pointer" onclick="editHotspot(${e.id})" title="${e.label||e.part_name||""}">${e.callout_number||""}</div>`})).join(""),0!==currentEditorHotspots.length?t.innerHTML=currentEditorHotspots.map((e=>`\n <div style="background:var(--bg-hover);border:1px solid var(--border);border-radius:6px;padding:0.5rem;margin-bottom:0.4rem;display:flex;align-items:center;gap:0.5rem">\n <span style="background:var(--accent);color:white;width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:0.65rem;font-weight:700;flex-shrink:0">${e.callout_number||"?"}</span>\n <div style="flex:1;min-width:0">\n <div style="font-size:0.82rem;font-weight:500">${e.part_name||e.label||"Sin parte"}</div>\n <div style="font-size:0.72rem;color:var(--text-secondary)">${e.part_number||""} | ${e.coords}</div>\n </div>\n <button class="btn btn-secondary" style="padding:0.2rem 0.5rem;font-size:0.75rem" onclick="editHotspot(${e.id})">Editar</button>\n <button class="btn" style="padding:0.2rem 0.5rem;font-size:0.75rem;background:#e74c3c;color:white;border:none;border-radius:4px;cursor:pointer" onclick="deleteHotspot(${e.id})">Borrar</button>\n </div>\n `)).join(""):t.innerHTML='<p style="color:var(--text-secondary);font-size:0.85rem">No hay hotspots</p>'}function editHotspot(e){const t=currentEditorHotspots.find((t=>t.id===e));t&&(document.getElementById("hsEditId").value=t.id,document.getElementById("hsCoords").value=t.coords||"",document.getElementById("hsCallout").value=t.callout_number||"",document.getElementById("hsLabel").value=t.label||"",document.getElementById("hsPartId").value=t.part_id||"",document.getElementById("hsPartSearch").value=t.part_name?`${t.part_number} - ${t.part_name}`:"")}function clearHotspotForm(){document.getElementById("hsEditId").value="",document.getElementById("hsCoords").value="",document.getElementById("hsLabel").value="",document.getElementById("hsPartId").value="",document.getElementById("hsPartSearch").value="",document.getElementById("hsPartSelect").style.display="none";const e=currentEditorHotspots.reduce(((e,t)=>Math.max(e,t.callout_number||0)),0);document.getElementById("hsCallout").value=e+1}async function searchPartsForHotspot(e){clearTimeout(partSearchTimeout);const t=document.getElementById("hsPartSelect");!e||e.length<2?t.style.display="none":partSearchTimeout=setTimeout((async()=>{try{const a=await fetch(`/api/parts?search=${encodeURIComponent(e)}&per_page=20`),n=await a.json(),r=n.data||n;0===r.length?t.innerHTML="<option disabled>Sin resultados</option>":t.innerHTML=r.map((e=>`<option value="${e.id}">${e.oem_part_number} - ${e.name_es||e.name}</option>`)).join(""),t.style.display="block",t.onchange=function(){const e=t.options[t.selectedIndex];document.getElementById("hsPartId").value=e.value,document.getElementById("hsPartSearch").value=e.textContent,t.style.display="none"}}catch(e){t.innerHTML="<option disabled>Error buscando</option>",t.style.display="block"}}),300)}async function saveHotspot(){const e=document.getElementById("hsEditId").value,t=document.getElementById("hsCoords").value.trim(),a=parseInt(document.getElementById("hsCallout").value)||null,n=parseInt(document.getElementById("hsPartId").value)||null,r=document.getElementById("hsLabel").value.trim();if(!t)return void showAlert("Haz clic en la imagen para seleccionar posición","error");const o={diagram_id:currentEditorDiagramId,coords:t,callout_number:a,part_id:n,label:r,shape:"circle",color:"#e74c3c"};try{let t;t=e?await fetch(`/api/admin/hotspots/${e}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}):await fetch("/api/admin/hotspots",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});const a=await t.json();if(!t.ok)throw new Error(a.error||"Error al guardar");showAlert(e?"Hotspot actualizado":"Hotspot creado"),await openHotspotEditor(currentEditorDiagramId)}catch(e){showAlert(e.message,"error")}}async function deleteHotspot(e){if(confirm("Eliminar este hotspot?"))try{const t=await fetch(`/api/admin/hotspots/${e}`,{method:"DELETE"}),a=await t.json();if(!t.ok)throw new Error(a.error||"Error al eliminar");showAlert("Hotspot eliminado"),await openHotspotEditor(currentEditorDiagramId)}catch(e){showAlert(e.message,"error")}}const roleBadgeColors={ADMIN:"#3b82f6",OWNER:"#8b5cf6",TALLER:"#22c55e",BODEGA:"#f59e0b"};function formatDate(e){if(!e)return'<span style="color:var(--text-secondary)">Nunca</span>';var t=new Date(e);return isNaN(t.getTime())?e:t.toLocaleDateString("es-MX",{year:"numeric",month:"short",day:"numeric"})+" "+t.toLocaleTimeString("es-MX",{hour:"2-digit",minute:"2-digit"})}function getRoleBadge(e){return'<span style="background:'+(roleBadgeColors[e]||"#6b7280")+'; color:#fff; padding:2px 8px; border-radius:10px; font-size:0.75rem; font-weight:600;">'+(e||"N/A")+"</span>"}function getActiveBadge(e){return e?'<span style="background:var(--success); color:#000; padding:2px 8px; border-radius:10px; font-size:0.75rem; font-weight:600;">Activo</span>':'<span style="background:#ef4444; color:#fff; padding:2px 8px; border-radius:10px; font-size:0.75rem; font-weight:600;">Inactivo</span>'}async function loadUsers(){var e=localStorage.getItem("access_token"),t=document.getElementById("usersTable");t.innerHTML='<tr><td colspan="7" class="loading"><div class="spinner"></div></td></tr>';try{var a=await fetch("/api/admin/users",{headers:{Authorization:"Bearer "+e}});if(!a.ok)throw new Error("Error al cargar usuarios ("+a.status+")");var n=await a.json(),r=Array.isArray(n)?n:n.data||[],o=r.filter((function(e){return!e.is_active})).length,i=document.getElementById("pendingUsersBadge");if(i&&(o>0?(i.textContent=o,i.style.display="inline-block"):i.style.display="none"),0===r.length)return void(t.innerHTML='<tr><td colspan="7" style="text-align:center; color:var(--text-secondary); padding:2rem;">No hay usuarios registrados</td></tr>');t.innerHTML=r.map((function(e){var t=e.is_active?"Desactivar":"Activar",a=e.is_active?"btn-secondary":"btn-primary";return"<tr><td>"+(e.name||e.nombre||"-")+"</td><td>"+(e.email||"-")+"</td><td>"+(e.business_name||e.negocio||"-")+"</td><td>"+getRoleBadge(e.role||e.rol)+"</td><td>"+getActiveBadge(e.is_active)+"</td><td>"+formatDate(e.last_login||e.ultimo_login)+'</td><td><button class="btn '+a+'" style="font-size:0.8rem; padding:4px 10px;" onclick="toggleUserActive('+e.id+", "+e.is_active+')">'+t+"</button></td></tr>"})).join("")}catch(e){t.innerHTML='<tr><td colspan="7" style="text-align:center; color:#ef4444; padding:2rem;">'+e.message+"</td></tr>"}}async function toggleUserActive(e,t){var a=localStorage.getItem("access_token");if(confirm("¿Seguro que deseas "+(t?"desactivar":"activar")+" este usuario?"))try{var n=await fetch("/api/admin/users/"+e+"/activate",{method:"PUT",headers:{"Content-Type":"application/json",Authorization:"Bearer "+a},body:JSON.stringify({is_active:!t})});if(!n.ok){var r=await n.json();throw new Error(r.error||"Error al actualizar usuario")}showAlert("Usuario "+(t?"desactivado":"activado")+" correctamente"),loadUsers()}catch(e){showAlert(e.message,"error")}}