diff --git a/dashboard/admin.html b/dashboard/admin.html
new file mode 100644
index 0000000..1054eb3
--- /dev/null
+++ b/dashboard/admin.html
@@ -0,0 +1,1599 @@
+
+
+
+
+
+ Admin Panel - Autopartes DB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Partes Aftermarket
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Nombre |
+ Nombre (ES) |
+ Slug |
+ Icono |
+ Orden |
+ Acciones |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Nombre |
+ Nombre (ES) |
+ Categoría |
+ Orden |
+ Acciones |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Img |
+ Número OEM |
+ Nombre |
+ Grupo |
+ Acciones |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Nombre |
+ Tipo |
+ Calidad |
+ País |
+ Acciones |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Número |
+ Nombre |
+ OEM Ref |
+ Fabricante |
+ Calidad |
+ Precio |
+ Acciones |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Parte OEM |
+ Número Referencia |
+ Tipo |
+ Fuente |
+ Acciones |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Vehículo |
+ Parte |
+ Cantidad |
+ Posición |
+ Acciones |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ Selecciona un vehículo y agrega múltiples partes de una vez.
+
+
+
+
+
+
+ Vehículo seleccionado:
+ (MYE ID: )
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 partes seleccionadas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📄
+
+ Arrastra un archivo CSV aquí o haz clic para seleccionar
+
+
+
+
+
Vista previa (0 registros)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Descarga los datos en formato CSV para respaldo o importación en otros sistemas.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/admin.js b/dashboard/admin.js
new file mode 100644
index 0000000..43da4ef
--- /dev/null
+++ b/dashboard/admin.js
@@ -0,0 +1,1690 @@
+/**
+ * Admin Panel JavaScript
+ * CRUD operations and CSV import/export for Autopartes DB
+ */
+
+// 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;
+ }
+ }
+
+ // 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');
+}
+
+function editAftermarket(id) {
+ // TODO: Fetch and populate for edit
+ openAftermarketModal(id);
+}
+
+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');
+ const vehicles = await response.json();
+
+ select.innerHTML = '' +
+ vehicles.slice(0, 100).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}`);
+ const myeData = await myeResponse.json();
+
+ 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 => `
+
+
+
+
${part.oem_part_number}
+
${part.name}
+
${part.group_name || ''} · ${part.category_name || ''}
+
+
+
+ `).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();
+ }
+};
diff --git a/dashboard/customer-landing.html b/dashboard/customer-landing.html
index f96e5e9..a0af19f 100644
--- a/dashboard/customer-landing.html
+++ b/dashboard/customer-landing.html
@@ -113,6 +113,17 @@
width: 100%;
}
+ .nav-links a.admin-link {
+ color: var(--accent);
+ font-weight: 600;
+ opacity: 0.8;
+ transition: opacity 0.3s;
+ }
+
+ .nav-links a.admin-link:hover {
+ opacity: 1;
+ }
+
.header-actions {
display: flex;
align-items: center;
@@ -1061,6 +1072,7 @@
Marcas
Productos
Contacto
+ ⚡ Admin