/**
* Admin Panel JavaScript
* CRUD operations and CSV import/export for Nexus Autoparts
*/
// State
let currentPage = {
parts: 1,
aftermarket: 1,
crossref: 1,
fitment: 1
};
let categoriesCache = [];
let groupsCache = [];
let partsCache = [];
let manufacturersCache = [];
let brandsCache = [];
let pendingImportData = null;
let searchTimeout = null;
// CSV Format definitions
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'
}
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initSidebar();
initDropZone();
initImportTypeChange();
loadDashboard();
});
// Sidebar navigation
function initSidebar() {
const items = document.querySelectorAll('.sidebar-item');
items.forEach(item => {
item.addEventListener('click', () => {
const section = item.dataset.section;
showSection(section);
items.forEach(i => i.classList.remove('active'));
item.classList.add('active');
});
});
}
function showSection(sectionId) {
// Hide all sections
document.querySelectorAll('.admin-section').forEach(s => s.classList.remove('active'));
// Show selected section
const section = document.getElementById(`section-${sectionId}`);
if (section) {
section.classList.add('active');
// Load data for the section
switch (sectionId) {
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':
// Just show section, user uses search
break;
}
}
// Update sidebar active state
document.querySelectorAll('.sidebar-item').forEach(item => {
item.classList.toggle('active', item.dataset.section === sectionId);
});
}
// Modal functions
function openModal(modalId) {
document.getElementById(modalId).classList.add('active');
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
// Alert function
function showAlert(message, type = 'success') {
const container = document.getElementById('alertContainer');
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.innerHTML = `${type === 'success' ? '✓' : '✕'} ${message}`;
container.appendChild(alert);
setTimeout(() => alert.remove(), 5000);
}
// Debounce search
function debounceSearch(callback) {
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(callback, 300);
}
// ============================================================================
// Dashboard
// ============================================================================
async function loadDashboard() {
try {
const stats = await fetch('/api/admin/stats').then(r => r.json());
document.getElementById('statCategories').textContent = stats.categories || 0;
document.getElementById('statGroups').textContent = stats.groups || 0;
document.getElementById('statParts').textContent = stats.parts || 0;
document.getElementById('statAftermarket').textContent = stats.aftermarket || 0;
document.getElementById('statManufacturers').textContent = stats.manufacturers || 0;
document.getElementById('statFitment').textContent = stats.fitment || 0;
} catch (e) {
console.error('Error loading dashboard:', e);
}
}
// ============================================================================
// Categories CRUD
// ============================================================================
async function loadCategories() {
try {
const response = await fetch('/api/categories');
const categories = await response.json();
categoriesCache = flattenCategories(categories);
const tbody = document.getElementById('categoriesTable');
if (categoriesCache.length === 0) {
tbody.innerHTML = '
| No hay categorías |
';
return;
}
tbody.innerHTML = categoriesCache.map(cat => `
| ${cat.id} |
${cat.name} |
${cat.name_es || '-'} |
${cat.slug || '-'} |
${cat.icon_name || '-'} |
${cat.display_order || 0} |
|
`).join('');
// Update category selects
updateCategorySelects();
} catch (e) {
console.error('Error loading categories:', e);
showAlert('Error al cargar categorías', 'error');
}
}
function flattenCategories(categories, level = 0) {
let result = [];
categories.forEach(cat => {
result.push({ ...cat, level });
if (cat.children && cat.children.length > 0) {
result = result.concat(flattenCategories(cat.children, level + 1));
}
});
return result;
}
function updateCategorySelects() {
const selects = ['groupCategoryFilter', 'groupCategory'];
selects.forEach(id => {
const select = document.getElementById(id);
if (!select) return;
const currentValue = select.value;
const defaultOption = id.includes('Filter') ? '' : '';
select.innerHTML = defaultOption + categoriesCache.map(cat =>
``
).join('');
select.value = currentValue;
});
}
function openCategoryModal(id = null) {
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';
if (id) {
const cat = categoriesCache.find(c => c.id === id);
if (cat) {
document.getElementById('categoryId').value = cat.id;
document.getElementById('categoryName').value = cat.name;
document.getElementById('categoryNameEs').value = cat.name_es || '';
document.getElementById('categorySlug').value = cat.slug || '';
document.getElementById('categoryIcon').value = cat.icon_name || '';
document.getElementById('categoryOrder').value = cat.display_order || 0;
document.getElementById('categoryModalTitle').textContent = 'Editar Categoría';
}
}
openModal('categoryModal');
}
function editCategory(id) {
openCategoryModal(id);
}
async function saveCategory() {
const id = document.getElementById('categoryId').value;
const data = {
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 url = id ? `/api/admin/categories/${id}` : '/api/admin/categories';
const method = id ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Error al guardar');
}
closeModal('categoryModal');
showAlert(id ? 'Categoría actualizada' : 'Categoría creada');
loadCategories();
} catch (e) {
showAlert(e.message, 'error');
}
}
async function deleteCategory(id) {
if (!confirm('¿Estás seguro de eliminar esta categoría?')) return;
try {
const response = await fetch(`/api/admin/categories/${id}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Error al eliminar');
showAlert('Categoría eliminada');
loadCategories();
} catch (e) {
showAlert(e.message, 'error');
}
}
// ============================================================================
// Groups CRUD
// ============================================================================
async function loadGroups() {
try {
const categoryId = document.getElementById('groupCategoryFilter').value;
let url = '/api/admin/groups';
if (categoryId) url += `?category_id=${categoryId}`;
const response = await fetch(url);
const groups = await response.json();
groupsCache = groups;
const tbody = document.getElementById('groupsTable');
if (groups.length === 0) {
tbody.innerHTML = '| No hay grupos |
';
return;
}
tbody.innerHTML = groups.map(group => `
| ${group.id} |
${group.name} |
${group.name_es || '-'} |
${group.category_name || '-'} |
${group.display_order || 0} |
|
`).join('');
updateGroupSelects();
} catch (e) {
console.error('Error loading groups:', e);
showAlert('Error al cargar grupos', 'error');
}
}
function updateGroupSelects() {
const selects = ['partGroupFilter', 'partGroup'];
selects.forEach(id => {
const select = document.getElementById(id);
if (!select) return;
const currentValue = select.value;
const defaultOption = id.includes('Filter') ? '' : '';
select.innerHTML = defaultOption + groupsCache.map(g =>
``
).join('');
select.value = currentValue;
});
}
function openGroupModal(id = null) {
document.getElementById('groupId').value = '';
document.getElementById('groupName').value = '';
document.getElementById('groupNameEs').value = '';
document.getElementById('groupOrder').value = '0';
document.getElementById('groupModalTitle').textContent = 'Nuevo Grupo';
// Populate category select
const select = document.getElementById('groupCategory');
select.innerHTML = '' +
categoriesCache.map(cat => ``).join('');
if (id) {
const group = groupsCache.find(g => g.id === id);
if (group) {
document.getElementById('groupId').value = group.id;
document.getElementById('groupName').value = group.name;
document.getElementById('groupNameEs').value = group.name_es || '';
document.getElementById('groupCategory').value = group.category_id;
document.getElementById('groupOrder').value = group.display_order || 0;
document.getElementById('groupModalTitle').textContent = 'Editar Grupo';
}
}
openModal('groupModal');
}
function editGroup(id) {
openGroupModal(id);
}
async function saveGroup() {
const id = document.getElementById('groupId').value;
const data = {
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 url = id ? `/api/admin/groups/${id}` : '/api/admin/groups';
const method = id ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Error al guardar');
}
closeModal('groupModal');
showAlert(id ? 'Grupo actualizado' : 'Grupo creado');
loadGroups();
} catch (e) {
showAlert(e.message, 'error');
}
}
async function deleteGroup(id) {
if (!confirm('¿Estás seguro de eliminar este grupo?')) return;
try {
const response = await fetch(`/api/admin/groups/${id}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Error al eliminar');
showAlert('Grupo eliminado');
loadGroups();
} catch (e) {
showAlert(e.message, 'error');
}
}
// ============================================================================
// Parts CRUD
// ============================================================================
async function loadParts() {
try {
const search = document.getElementById('partSearch').value;
const groupId = document.getElementById('partGroupFilter').value;
let url = `/api/parts?page=${currentPage.parts}&per_page=20`;
if (search) url += `&search=${encodeURIComponent(search)}`;
if (groupId) url += `&group_id=${groupId}`;
const response = await fetch(url);
const result = await response.json();
partsCache = result.data || [];
const tbody = document.getElementById('partsTable');
if (partsCache.length === 0) {
tbody.innerHTML = '| No hay partes |
';
renderPagination('partsPagination', result.pagination, 'parts', loadParts);
return;
}
tbody.innerHTML = partsCache.map(part => `
| ${part.id} |
${part.image_url
? ` `
: '📷'}
|
${part.oem_part_number} |
${part.name} |
${part.group_name || '-'} |
|
`).join('');
renderPagination('partsPagination', result.pagination, 'parts', loadParts);
// Load groups if not already loaded
if (groupsCache.length === 0) {
await loadGroups();
}
} catch (e) {
console.error('Error loading parts:', e);
showAlert('Error al cargar partes', 'error');
}
}
function openPartModal(id = 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';
// Reset image preview
resetPartImagePreview();
// Populate group select
const select = document.getElementById('partGroup');
select.innerHTML = '' +
groupsCache.map(g => ``).join('');
if (id) {
fetchPartDetail(id);
}
openModal('partModal');
}
async function fetchPartDetail(id) {
try {
const response = await fetch(`/api/parts/${id}`);
const part = await response.json();
document.getElementById('partId').value = part.id;
document.getElementById('partOemNumber').value = part.oem_part_number;
document.getElementById('partName').value = part.name;
document.getElementById('partNameEs').value = part.name_es || '';
document.getElementById('partGroup').value = part.group_id;
document.getElementById('partDescription').value = part.description || '';
document.getElementById('partDescriptionEs').value = part.description_es || '';
document.getElementById('partModalTitle').textContent = 'Editar Parte OEM';
// Show existing image
const preview = document.getElementById('partImagePreview');
if (part.image_url) {
preview.innerHTML = `
`;
document.getElementById('partImageUrl').value = part.image_url;
} else {
resetPartImagePreview();
}
} catch (e) {
console.error('Error fetching part:', e);
}
}
function editPart(id) {
openPartModal(id);
}
async function savePart() {
const id = document.getElementById('partId').value;
// Get image URL (from file upload or manual URL input)
let imageUrl = document.getElementById('partImageUrl').value || null;
const imagePreview = document.getElementById('partImagePreview').querySelector('img');
if (imagePreview && imagePreview.src.startsWith('data:')) {
// If it's a base64 image from file upload, we'll upload it first
imageUrl = await uploadPartImage(imagePreview.src);
}
const data = {
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: imageUrl
};
try {
const url = id ? `/api/admin/parts/${id}` : '/api/admin/parts';
const method = id ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Error al guardar');
}
closeModal('partModal');
showAlert(id ? 'Parte actualizada' : 'Parte creada');
loadParts();
} catch (e) {
showAlert(e.message, 'error');
}
}
// Image handling functions
function previewPartImage(input) {
const preview = document.getElementById('partImagePreview');
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
preview.innerHTML = `
`;
};
reader.readAsDataURL(input.files[0]);
}
}
async function uploadPartImage(base64Data) {
try {
const response = await fetch('/api/admin/upload-image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64Data })
});
if (!response.ok) throw new Error('Error uploading image');
const result = await response.json();
return result.url;
} catch (e) {
console.error('Image upload failed:', e);
return null;
}
}
function resetPartImagePreview() {
const preview = document.getElementById('partImagePreview');
preview.innerHTML = '📷 Sin imagen';
document.getElementById('partImageUrl').value = '';
document.getElementById('partImageFile').value = '';
}
async function deletePart(id) {
if (!confirm('¿Estás seguro de eliminar esta parte?')) return;
try {
const response = await fetch(`/api/admin/parts/${id}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Error al eliminar');
showAlert('Parte eliminada');
loadParts();
} catch (e) {
showAlert(e.message, 'error');
}
}
// ============================================================================
// Manufacturers CRUD
// ============================================================================
async function loadManufacturers() {
try {
const response = await fetch('/api/manufacturers');
manufacturersCache = await response.json();
const tbody = document.getElementById('manufacturersTable');
if (manufacturersCache.length === 0) {
tbody.innerHTML = '| No hay fabricantes |
';
return;
}
tbody.innerHTML = manufacturersCache.map(m => `
| ${m.id} |
${m.name} |
${m.type || '-'} |
${m.quality_tier || '-'} |
${m.country || '-'} |
|
`).join('');
updateManufacturerSelects();
} catch (e) {
console.error('Error loading manufacturers:', e);
showAlert('Error al cargar fabricantes', 'error');
}
}
function updateManufacturerSelects() {
const select = document.getElementById('aftermarketManufacturerFilter');
if (select) {
const currentValue = select.value;
select.innerHTML = '' +
manufacturersCache.map(m => ``).join('');
select.value = currentValue;
}
const modalSelect = document.getElementById('aftermarketManufacturer');
if (modalSelect) {
modalSelect.innerHTML = '' +
manufacturersCache.map(m => ``).join('');
}
}
function openManufacturerModal(id = null) {
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';
if (id) {
const m = manufacturersCache.find(x => x.id === id);
if (m) {
document.getElementById('manufacturerId').value = m.id;
document.getElementById('manufacturerName').value = m.name;
document.getElementById('manufacturerType').value = m.type || 'aftermarket';
document.getElementById('manufacturerQuality').value = m.quality_tier || 'standard';
document.getElementById('manufacturerCountry').value = m.country || '';
document.getElementById('manufacturerWebsite').value = m.website || '';
document.getElementById('manufacturerModalTitle').textContent = 'Editar Fabricante';
}
}
openModal('manufacturerModal');
}
function editManufacturer(id) {
openManufacturerModal(id);
}
async function saveManufacturer() {
const id = document.getElementById('manufacturerId').value;
const data = {
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 url = id ? `/api/admin/manufacturers/${id}` : '/api/admin/manufacturers';
const method = id ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Error al guardar');
}
closeModal('manufacturerModal');
showAlert(id ? 'Fabricante actualizado' : 'Fabricante creado');
loadManufacturers();
} catch (e) {
showAlert(e.message, 'error');
}
}
async function deleteManufacturer(id) {
if (!confirm('¿Estás seguro de eliminar este fabricante?')) return;
try {
const response = await fetch(`/api/admin/manufacturers/${id}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Error al eliminar');
showAlert('Fabricante eliminado');
loadManufacturers();
} catch (e) {
showAlert(e.message, 'error');
}
}
// ============================================================================
// Aftermarket CRUD
// ============================================================================
async function loadAftermarket() {
try {
const search = document.getElementById('aftermarketSearch').value;
const manufacturerId = document.getElementById('aftermarketManufacturerFilter').value;
let url = `/api/aftermarket?page=${currentPage.aftermarket}&per_page=20`;
if (search) url += `&search=${encodeURIComponent(search)}`;
if (manufacturerId) url += `&manufacturer_id=${manufacturerId}`;
const response = await fetch(url);
const result = await response.json();
const tbody = document.getElementById('aftermarketTable');
if (!result.data || result.data.length === 0) {
tbody.innerHTML = '| No hay partes aftermarket |
';
renderPagination('aftermarketPagination', result.pagination, 'aftermarket', loadAftermarket);
return;
}
tbody.innerHTML = result.data.map(part => `
| ${part.id} |
${part.part_number} |
${part.name || '-'} |
${part.oem_part_number} |
${part.manufacturer_name} |
${part.quality_tier || '-'} |
${part.price_usd ? '$' + part.price_usd.toFixed(2) : '-'} |
|
`).join('');
renderPagination('aftermarketPagination', result.pagination, 'aftermarket', loadAftermarket);
} catch (e) {
console.error('Error loading aftermarket:', e);
showAlert('Error al cargar partes aftermarket', 'error');
}
}
async function openAftermarketModal(id = 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';
// Populate OEM parts select
await loadPartsForSelect('aftermarketOemPart');
// Populate manufacturers
updateManufacturerSelects();
openModal('aftermarketModal');
}
async function editAftermarket(id) {
await openAftermarketModal(id);
try {
const res = await fetch(`/api/aftermarket?search=&per_page=200`);
const data = await res.json();
const items = data.data || data;
const item = items.find(a => a.id === id);
if (item) {
document.getElementById('aftermarketId').value = item.id;
document.getElementById('aftermarketPartNumber').value = item.part_number || '';
document.getElementById('aftermarketName').value = item.name || '';
document.getElementById('aftermarketNameEs').value = item.name_es || '';
document.getElementById('aftermarketQuality').value = item.quality_tier || 'standard';
document.getElementById('aftermarketPrice').value = item.price_usd || '';
document.getElementById('aftermarketWarranty').value = item.warranty_months || '';
if (item.oem_part_id) document.getElementById('aftermarketOemPart').value = item.oem_part_id;
if (item.manufacturer_id) document.getElementById('aftermarketManufacturer').value = item.manufacturer_id;
document.getElementById('aftermarketModalTitle').textContent = 'Editar Parte Aftermarket';
}
} catch (e) {
console.error('Error loading aftermarket for edit:', e);
}
}
async function saveAftermarket() {
const id = document.getElementById('aftermarketId').value;
const data = {
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 url = id ? `/api/admin/aftermarket/${id}` : '/api/admin/aftermarket';
const method = id ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Error al guardar');
}
closeModal('aftermarketModal');
showAlert(id ? 'Parte actualizada' : 'Parte creada');
loadAftermarket();
} catch (e) {
showAlert(e.message, 'error');
}
}
async function deleteAftermarket(id) {
if (!confirm('¿Estás seguro de eliminar esta parte aftermarket?')) return;
try {
const response = await fetch(`/api/admin/aftermarket/${id}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Error al eliminar');
showAlert('Parte eliminada');
loadAftermarket();
} catch (e) {
showAlert(e.message, 'error');
}
}
// ============================================================================
// Cross-References CRUD
// ============================================================================
async function loadCrossRefs() {
try {
const response = await fetch(`/api/admin/crossref?page=${currentPage.crossref}&per_page=20`);
const result = await response.json();
const tbody = document.getElementById('crossrefTable');
if (!result.data || result.data.length === 0) {
tbody.innerHTML = '| No hay cross-references |
';
return;
}
tbody.innerHTML = result.data.map(ref => `
| ${ref.id} |
${ref.oem_part_number} - ${ref.part_name} |
${ref.cross_reference_number} |
${ref.reference_type} |
${ref.source || '-'} |
|
`).join('');
renderPagination('crossrefPagination', result.pagination, 'crossref', loadCrossRefs);
} catch (e) {
console.error('Error loading cross-refs:', e);
}
}
async function openCrossRefModal(id = 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(id) {
openCrossRefModal(id);
}
async function saveCrossRef() {
const id = document.getElementById('crossrefId').value;
const data = {
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 url = id ? `/api/admin/crossref/${id}` : '/api/admin/crossref';
const method = id ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Error al guardar');
}
closeModal('crossrefModal');
showAlert(id ? 'Referencia actualizada' : 'Referencia creada');
loadCrossRefs();
} catch (e) {
showAlert(e.message, 'error');
}
}
async function deleteCrossRef(id) {
if (!confirm('¿Estás seguro de eliminar esta referencia?')) return;
try {
const response = await fetch(`/api/admin/crossref/${id}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Error al eliminar');
showAlert('Referencia eliminada');
loadCrossRefs();
} catch (e) {
showAlert(e.message, 'error');
}
}
// ============================================================================
// Fitment CRUD
// ============================================================================
async function loadFitment() {
try {
const brand = document.getElementById('fitmentBrandFilter').value;
const model = document.getElementById('fitmentModelFilter').value;
let url = `/api/admin/fitment?page=${currentPage.fitment}&per_page=20`;
if (brand) url += `&brand=${encodeURIComponent(brand)}`;
if (model) url += `&model=${encodeURIComponent(model)}`;
const response = await fetch(url);
const result = await response.json();
const tbody = document.getElementById('fitmentTable');
if (!result.data || result.data.length === 0) {
tbody.innerHTML = '| No hay fitments |
';
renderPagination('fitmentPagination', result.pagination, 'fitment', loadFitment);
return;
}
tbody.innerHTML = result.data.map(f => `
| ${f.id} |
${f.brand} ${f.model} ${f.year} - ${f.engine} |
${f.oem_part_number} - ${f.part_name} |
${f.quantity_required} |
${f.position || '-'} |
|
`).join('');
renderPagination('fitmentPagination', result.pagination, 'fitment', loadFitment);
} catch (e) {
console.error('Error loading fitment:', e);
}
}
async function loadFitmentModels() {
const brand = document.getElementById('fitmentBrandFilter').value;
const modelSelect = document.getElementById('fitmentModelFilter');
if (!brand) {
modelSelect.innerHTML = '';
loadFitment();
return;
}
try {
const response = await fetch(`/api/models?brand=${encodeURIComponent(brand)}`);
const models = await response.json();
modelSelect.innerHTML = '' +
models.map(m => ``).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';
// Load vehicles
await loadVehiclesForSelect('fitmentVehicle');
// Load parts
await loadPartsForSelect('fitmentPart');
openModal('fitmentModal');
}
async function saveFitment() {
const id = document.getElementById('fitmentId').value;
const data = {
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 url = id ? `/api/admin/fitment/${id}` : '/api/admin/fitment';
const method = id ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Error al guardar');
}
closeModal('fitmentModal');
showAlert(id ? 'Fitment actualizado' : 'Fitment creado');
loadFitment();
} catch (e) {
showAlert(e.message, 'error');
}
}
async function deleteFitment(id) {
if (!confirm('¿Estás seguro de eliminar este fitment?')) return;
try {
const response = await fetch(`/api/admin/fitment/${id}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Error al eliminar');
showAlert('Fitment eliminado');
loadFitment();
} catch (e) {
showAlert(e.message, 'error');
}
}
// ============================================================================
// Helper functions for selects
// ============================================================================
async function loadPartsForSelect(selectId) {
const select = document.getElementById(selectId);
if (!select) return;
try {
const response = await fetch('/api/parts?per_page=100');
const result = await response.json();
select.innerHTML = '' +
(result.data || []).map(p =>
``
).join('');
} catch (e) {
console.error('Error loading parts for select:', e);
}
}
async function loadVehiclesForSelect(selectId) {
const select = document.getElementById(selectId);
if (!select) return;
try {
const response = await fetch('/api/model-year-engine?per_page=100');
const result = await response.json();
const vehicles = result.data || result;
select.innerHTML = '' +
vehicles.map(v =>
``
).join('');
} catch (e) {
console.error('Error loading vehicles for select:', e);
}
}
async function loadBrands() {
try {
const response = await fetch('/api/brands');
brandsCache = await response.json();
const select = document.getElementById('fitmentBrandFilter');
if (select) {
select.innerHTML = '' +
brandsCache.map(b => ``).join('');
}
} catch (e) {
console.error('Error loading brands:', e);
}
}
// ============================================================================
// Pagination
// ============================================================================
function renderPagination(containerId, pagination, pageKey, loadFunction) {
const container = document.getElementById(containerId);
if (!container || !pagination) {
container.innerHTML = '';
return;
}
const { page, total_pages } = pagination;
if (total_pages <= 1) {
container.innerHTML = '';
return;
}
let html = '';
// Previous button
html += ``;
// Page numbers
const startPage = Math.max(1, page - 2);
const endPage = Math.min(total_pages, page + 2);
for (let i = startPage; i <= endPage; i++) {
html += ``;
}
// Next button
html += ``;
container.innerHTML = html;
}
function goToPage(pageKey, page, loadFunctionName) {
currentPage[pageKey] = page;
window[loadFunctionName]();
}
// ============================================================================
// CSV Import/Export
// ============================================================================
function initDropZone() {
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('csvFile');
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file && file.name.endsWith('.csv')) {
handleCsvFile(file);
} else {
showAlert('Por favor selecciona un archivo CSV', 'error');
}
});
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
handleCsvFile(file);
}
});
}
function initImportTypeChange() {
const select = document.getElementById('importType');
select.addEventListener('change', updateCsvFormatHelp);
updateCsvFormatHelp();
}
function updateCsvFormatHelp() {
const type = document.getElementById('importType').value;
const format = csvFormats[type];
const helpDiv = document.getElementById('csvFormatHelp');
if (!format) return;
helpDiv.innerHTML = `
Columnas requeridas: ${format.required.join(', ')}
Todas las columnas: ${format.columns.join(', ')}
Ejemplo:
${format.example}
`;
}
function handleCsvFile(file) {
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target.result;
const rows = parseCSV(content);
if (rows.length < 2) {
showAlert('El archivo CSV debe tener al menos un encabezado y una fila de datos', 'error');
return;
}
pendingImportData = rows;
showImportPreview(rows);
};
reader.readAsText(file);
}
function parseCSV(content) {
const lines = content.split(/\r?\n/).filter(line => line.trim());
const rows = [];
for (const line of lines) {
const row = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
row.push(current.trim());
current = '';
} else {
current += char;
}
}
row.push(current.trim());
rows.push(row);
}
return rows;
}
function showImportPreview(rows) {
const headers = rows[0];
const data = rows.slice(1, 6); // Show first 5 rows
document.getElementById('previewCount').textContent = rows.length - 1;
document.getElementById('previewHead').innerHTML = '' +
headers.map(h => `| ${h} | `).join('') + '
';
document.getElementById('previewBody').innerHTML = data.map(row =>
'' + row.map(cell => `| ${cell} | `).join('') + '
'
).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 type = document.getElementById('importType').value;
const format = csvFormats[type];
const headers = pendingImportData[0];
const data = pendingImportData.slice(1);
// Validate required columns
for (const req of format.required) {
if (!headers.includes(req)) {
showAlert(`Falta columna requerida: ${req}`, 'error');
return;
}
}
// Convert to objects
const records = data.map(row => {
const obj = {};
headers.forEach((h, i) => {
let value = row[i] || null;
// Convert numeric fields
if (['id', 'category_id', 'group_id', 'part_id', 'oem_part_id', 'manufacturer_id',
'model_year_engine_id', 'display_order', 'quantity_required', 'warranty_months'].includes(h)) {
value = value ? parseInt(value) : null;
} else if (['weight_kg', 'price_usd'].includes(h)) {
value = value ? parseFloat(value) : null;
}
obj[h] = value;
});
return obj;
});
try {
const response = await fetch(`/api/admin/import/${type}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ records })
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'Error en la importación');
}
showAlert(`Importados ${result.imported} registros exitosamente`);
cancelImport();
loadDashboard();
} catch (e) {
showAlert(e.message, 'error');
}
}
async function exportData(type) {
try {
const response = await fetch(`/api/admin/export/${type}`);
const result = await response.json();
if (!result.data || result.data.length === 0) {
showAlert('No hay datos para exportar', 'error');
return;
}
// Convert to CSV
const headers = Object.keys(result.data[0]);
let csv = headers.join(',') + '\n';
for (const row of result.data) {
csv += headers.map(h => {
let val = row[h];
if (val === null || val === undefined) return '';
val = String(val);
if (val.includes(',') || val.includes('"') || val.includes('\n')) {
return '"' + val.replace(/"/g, '""') + '"';
}
return val;
}).join(',') + '\n';
}
// Download
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `${type}_export_${new Date().toISOString().slice(0, 10)}.csv`;
link.click();
showAlert(`Exportados ${result.data.length} registros`);
} catch (e) {
showAlert('Error al exportar: ' + e.message, 'error');
}
}
// Load brands on page load for fitment filter
loadBrands();
// ============================================================================
// Bulk Fitment Editor
// ============================================================================
let bulkSelectedMYEId = null;
let bulkAvailableParts = [];
async function initBulkEditor() {
// Populate bulk brand select
const bulkBrand = document.getElementById('bulkBrand');
if (bulkBrand && brandsCache.length > 0) {
bulkBrand.innerHTML = '' +
brandsCache.map(b => ``).join('');
}
// Populate categories for filter
if (categoriesCache.length === 0) {
await loadCategories();
}
const bulkCategory = document.getElementById('bulkCategory');
if (bulkCategory) {
bulkCategory.innerHTML = '' +
categoriesCache.map(c => ``).join('');
}
}
async function loadBulkModels() {
const brand = document.getElementById('bulkBrand').value;
const modelSelect = document.getElementById('bulkModel');
const yearSelect = document.getElementById('bulkYear');
const engineSelect = document.getElementById('bulkEngine');
// Reset dependent selects
modelSelect.innerHTML = '';
yearSelect.innerHTML = '';
engineSelect.innerHTML = '';
document.getElementById('bulkVehicleSelected').style.display = 'none';
if (!brand) return;
try {
const response = await fetch(`/api/models?brand=${encodeURIComponent(brand)}`);
const models = await response.json();
modelSelect.innerHTML = '' +
models.map(m => ``).join('');
} catch (e) {
console.error('Error loading models:', e);
}
}
async function loadBulkYears() {
const brand = document.getElementById('bulkBrand').value;
const model = document.getElementById('bulkModel').value;
const yearSelect = document.getElementById('bulkYear');
const engineSelect = document.getElementById('bulkEngine');
yearSelect.innerHTML = '';
engineSelect.innerHTML = '';
document.getElementById('bulkVehicleSelected').style.display = 'none';
if (!brand || !model) return;
try {
const response = await fetch(`/api/years?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}`);
const years = await response.json();
yearSelect.innerHTML = '' +
years.map(y => ``).join('');
} catch (e) {
console.error('Error loading years:', e);
}
}
async function loadBulkEngines() {
const brand = document.getElementById('bulkBrand').value;
const model = document.getElementById('bulkModel').value;
const year = document.getElementById('bulkYear').value;
const engineSelect = document.getElementById('bulkEngine');
engineSelect.innerHTML = '';
document.getElementById('bulkVehicleSelected').style.display = 'none';
if (!brand || !model || !year) return;
try {
const response = await fetch(`/api/engines?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}&year=${year}`);
const engines = await response.json();
// Get MYE IDs for each engine
const myeResponse = await fetch(`/api/model-year-engine?brand=${encodeURIComponent(brand)}&model=${encodeURIComponent(model)}&year=${year}&per_page=100`);
const myeResult = await myeResponse.json();
const myeData = myeResult.data || myeResult;
engineSelect.innerHTML = '' +
myeData.map(mye => ``).join('');
} catch (e) {
console.error('Error loading engines:', e);
}
}
async function selectBulkVehicle() {
const myeId = document.getElementById('bulkEngine').value;
if (!myeId) {
document.getElementById('bulkVehicleSelected').style.display = 'none';
return;
}
bulkSelectedMYEId = parseInt(myeId);
const brand = document.getElementById('bulkBrand').value;
const model = document.getElementById('bulkModel').value;
const year = document.getElementById('bulkYear').value;
const engine = document.getElementById('bulkEngine').options[document.getElementById('bulkEngine').selectedIndex].text;
document.getElementById('bulkVehicleName').textContent = `${brand} ${model} ${year} - ${engine}`;
document.getElementById('bulkMYEId').textContent = myeId;
document.getElementById('bulkVehicleSelected').style.display = 'block';
// Initialize categories and load parts
await initBulkEditor();
await loadBulkParts();
}
async function loadBulkParts() {
const container = document.getElementById('bulkPartsContainer');
const categoryId = document.getElementById('bulkCategory').value;
container.innerHTML = 'Cargando partes...
';
try {
let url = '/api/parts?per_page=100';
if (categoryId) url += `&category_id=${categoryId}`;
const response = await fetch(url);
const result = await response.json();
bulkAvailableParts = result.data || [];
if (bulkAvailableParts.length === 0) {
container.innerHTML = 'No hay partes disponibles en esta categoría.
';
return;
}
// Get existing fitments for this vehicle to mark them
const fitmentResponse = await fetch(`/api/admin/fitment?mye_id=${bulkSelectedMYEId}&per_page=500`);
const fitmentResult = await fitmentResponse.json();
const existingPartIds = new Set((fitmentResult.data || []).map(f => f.part_id));
container.innerHTML = bulkAvailableParts.map(part => `
`).join('');
updateBulkSelectedCount();
} catch (e) {
console.error('Error loading parts:', e);
container.innerHTML = 'Error al cargar partes
';
}
}
function toggleBulkPart(element, partId) {
const checkbox = element.querySelector('input[type="checkbox"]');
if (checkbox.disabled) return; // Already linked
checkbox.checked = !checkbox.checked;
element.classList.toggle('selected', checkbox.checked);
updateBulkSelectedCount();
}
function updateBulkSelectedCount() {
const checkboxes = document.querySelectorAll('#bulkPartsContainer input[type="checkbox"]:checked:not(:disabled)');
document.getElementById('bulkSelectedCount').textContent = `${checkboxes.length} partes seleccionadas`;
}
async function saveBulkFitments() {
if (!bulkSelectedMYEId) {
showAlert('Selecciona un vehículo primero', 'error');
return;
}
const selectedItems = document.querySelectorAll('#bulkPartsContainer .bulk-part-item');
const fitments = [];
selectedItems.forEach(item => {
const checkbox = item.querySelector('input[type="checkbox"]');
if (checkbox.checked && !checkbox.disabled) {
const partId = parseInt(checkbox.dataset.partId);
const quantity = parseInt(item.querySelector('.bulk-part-qty').value) || 1;
fitments.push({
model_year_engine_id: bulkSelectedMYEId,
part_id: partId,
quantity_required: quantity
});
}
});
if (fitments.length === 0) {
showAlert('Selecciona al menos una parte', 'error');
return;
}
try {
const response = await fetch('/api/admin/import/fitment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ records: fitments })
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'Error al guardar');
}
showAlert(`${result.imported} fitments creados exitosamente`);
await loadBulkParts(); // Refresh to show newly linked parts
loadFitment(); // Refresh fitment table
loadDashboard(); // Refresh stats
} catch (e) {
showAlert(e.message, 'error');
}
}
// Initialize bulk editor when fitment section is shown
const originalShowSection = showSection;
showSection = function(sectionId) {
originalShowSection(sectionId);
if (sectionId === 'fitment') {
initBulkEditor();
}
};
// ============================================================================
// Diagram Hotspot Editor
// ============================================================================
let currentEditorDiagramId = null;
let currentEditorHotspots = [];
let partSearchTimeout = null;
async function searchDiagramsAdmin() {
const q = document.getElementById('diagramSearchInput').value.trim();
const container = document.getElementById('diagramSearchResults');
if (!q) {
container.innerHTML = 'Ingresa un código de diagrama para buscar
';
return;
}
container.innerHTML = ' Buscando...
';
try {
const res = await fetch(`/api/diagrams/search?q=${encodeURIComponent(q)}`);
const diagrams = await res.json();
if (diagrams.length === 0) {
container.innerHTML = 'No se encontraron diagramas
';
return;
}
container.innerHTML = diagrams.map(d => {
const imgSrc = d.image_path ? '/' + d.image_path : `/static/diagrams/moog/${d.name}.jpg`;
return `
${d.name}
${d.name_es || d.source || ''}
`;
}).join('');
} catch (e) {
container.innerHTML = 'Error al buscar diagramas
';
}
}
async function openHotspotEditor(diagramId) {
currentEditorDiagramId = diagramId;
document.getElementById('hotspotEditorArea').style.display = 'block';
try {
const res = await fetch(`/api/diagrams/${diagramId}`);
const diagram = await res.json();
document.getElementById('hotspotEditorTitle').textContent = `${diagram.name} - ${diagram.name_es || diagram.group_name || ''}`;
const imgSrc = diagram.image_url || (diagram.image_path ? '/' + diagram.image_path : '');
document.getElementById('hotspotEditorImg').src = imgSrc;
currentEditorHotspots = diagram.hotspots || [];
renderEditorHotspots();
clearHotspotForm();
// Auto-set next callout number
const maxCallout = currentEditorHotspots.reduce((max, h) => Math.max(max, h.callout_number || 0), 0);
document.getElementById('hsCallout').value = maxCallout + 1;
// Scroll to editor
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(event) {
const img = event.target;
const rect = img.getBoundingClientRect();
const xPct = ((event.clientX - rect.left) / rect.width * 100).toFixed(2);
const yPct = ((event.clientY - rect.top) / rect.height * 100).toFixed(2);
document.getElementById('hsCoords').value = `${xPct},${yPct}`;
// Show temporary marker
renderEditorHotspots();
const container = document.getElementById('hotspotMarkersContainer');
const tempMarker = document.createElement('div');
tempMarker.style.cssText = `position:absolute;left:${xPct}%;top:${yPct}%;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`;
container.appendChild(tempMarker);
}
function renderEditorHotspots() {
const container = document.getElementById('hotspotMarkersContainer');
const list = document.getElementById('hotspotsList');
// Markers on image
container.innerHTML = currentEditorHotspots.map(h => {
const coords = (h.coords || '').split(',');
if (coords.length < 2) return '';
return `${h.callout_number || ''}
`;
}).join('');
// List
if (currentEditorHotspots.length === 0) {
list.innerHTML = 'No hay hotspots
';
return;
}
list.innerHTML = currentEditorHotspots.map(h => `
${h.callout_number || '?'}
${h.part_name || h.label || 'Sin parte'}
${h.part_number || ''} | ${h.coords}
`).join('');
}
function editHotspot(hotspotId) {
const hs = currentEditorHotspots.find(h => h.id === hotspotId);
if (!hs) return;
document.getElementById('hsEditId').value = hs.id;
document.getElementById('hsCoords').value = hs.coords || '';
document.getElementById('hsCallout').value = hs.callout_number || '';
document.getElementById('hsLabel').value = hs.label || '';
document.getElementById('hsPartId').value = hs.part_id || '';
document.getElementById('hsPartSearch').value = hs.part_name ? `${hs.part_number} - ${hs.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';
// Keep callout at next number
const maxCallout = currentEditorHotspots.reduce((max, h) => Math.max(max, h.callout_number || 0), 0);
document.getElementById('hsCallout').value = maxCallout + 1;
}
async function searchPartsForHotspot(query) {
clearTimeout(partSearchTimeout);
const select = document.getElementById('hsPartSelect');
if (!query || query.length < 2) {
select.style.display = 'none';
return;
}
partSearchTimeout = setTimeout(async () => {
try {
const res = await fetch(`/api/parts?search=${encodeURIComponent(query)}&per_page=20`);
const data = await res.json();
const parts = data.data || data;
if (parts.length === 0) {
select.innerHTML = '';
} else {
select.innerHTML = parts.map(p =>
``
).join('');
}
select.style.display = 'block';
select.onchange = function() {
const opt = select.options[select.selectedIndex];
document.getElementById('hsPartId').value = opt.value;
document.getElementById('hsPartSearch').value = opt.textContent;
select.style.display = 'none';
};
} catch (e) {
select.innerHTML = '';
select.style.display = 'block';
}
}, 300);
}
async function saveHotspot() {
const editId = document.getElementById('hsEditId').value;
const coords = document.getElementById('hsCoords').value.trim();
const callout = parseInt(document.getElementById('hsCallout').value) || null;
const partId = parseInt(document.getElementById('hsPartId').value) || null;
const label = document.getElementById('hsLabel').value.trim();
if (!coords) {
showAlert('Haz clic en la imagen para seleccionar posición', 'error');
return;
}
const body = {
diagram_id: currentEditorDiagramId,
coords: coords,
callout_number: callout,
part_id: partId,
label: label,
shape: 'circle',
color: '#e74c3c'
};
try {
let res;
if (editId) {
res = await fetch(`/api/admin/hotspots/${editId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
} else {
res = await fetch('/api/admin/hotspots', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
}
const result = await res.json();
if (!res.ok) throw new Error(result.error || 'Error al guardar');
showAlert(editId ? 'Hotspot actualizado' : 'Hotspot creado');
// Reload diagram to refresh hotspots
await openHotspotEditor(currentEditorDiagramId);
} catch (e) {
showAlert(e.message, 'error');
}
}
async function deleteHotspot(hotspotId) {
if (!confirm('Eliminar este hotspot?')) return;
try {
const res = await fetch(`/api/admin/hotspots/${hotspotId}`, { method: 'DELETE' });
const result = await res.json();
if (!res.ok) throw new Error(result.error || 'Error al eliminar');
showAlert('Hotspot eliminado');
await openHotspotEditor(currentEditorDiagramId);
} catch (e) {
showAlert(e.message, 'error');
}
}