feat: add captura, POS, cuentas, and tienda pages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
707
dashboard/captura.js
Normal file
707
dashboard/captura.js
Normal file
@@ -0,0 +1,707 @@
|
||||
/**
|
||||
* captura.js — Data entry logic for Nexus Autoparts
|
||||
* 3 sections: OEM Parts, Aftermarket/Interchange, Images
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var API = '';
|
||||
var currentMye = null; // selected vehicle MYE id
|
||||
var currentVehicle = null; // vehicle info object
|
||||
var vehicleParts = []; // existing parts for current vehicle
|
||||
var manufacturers = []; // cached manufacturer list
|
||||
var vehicleStatus = 'pending';
|
||||
var vehiclePage = 1;
|
||||
|
||||
// ================================================================
|
||||
// Utility
|
||||
// ================================================================
|
||||
|
||||
function toast(msg, type) {
|
||||
var el = document.createElement('div');
|
||||
el.className = 'toast ' + (type || 'success');
|
||||
el.textContent = msg;
|
||||
document.body.appendChild(el);
|
||||
setTimeout(function () { el.remove(); }, 3000);
|
||||
}
|
||||
|
||||
function api(path, opts) {
|
||||
opts = opts || {};
|
||||
return fetch(API + path, opts).then(function (r) {
|
||||
if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'Error'); });
|
||||
return r.json();
|
||||
});
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
if (!s) return '';
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Tab Switching
|
||||
// ================================================================
|
||||
|
||||
document.querySelectorAll('.captura-tab').forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
document.querySelectorAll('.captura-tab').forEach(function (t) { t.classList.remove('active'); });
|
||||
document.querySelectorAll('.captura-section').forEach(function (s) { s.classList.remove('active'); });
|
||||
tab.classList.add('active');
|
||||
var target = tab.getAttribute('data-tab');
|
||||
document.getElementById('section-' + target).classList.add('active');
|
||||
|
||||
if (target === 'aftermarket') loadPartsWithoutAftermarket();
|
||||
if (target === 'images') loadPartsWithoutImage();
|
||||
});
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// SECTION 1: OEM Parts
|
||||
// ================================================================
|
||||
|
||||
// --- Status tabs ---
|
||||
document.querySelectorAll('.status-tab').forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
document.querySelectorAll('.status-tab').forEach(function (t) { t.classList.remove('active'); });
|
||||
tab.classList.add('active');
|
||||
vehicleStatus = tab.getAttribute('data-status');
|
||||
vehiclePage = 1;
|
||||
loadVehicles();
|
||||
});
|
||||
});
|
||||
|
||||
// --- Brand filter ---
|
||||
function loadBrands() {
|
||||
api('/api/brands').then(function (brands) {
|
||||
var sel = document.getElementById('oem-brand-filter');
|
||||
brands.forEach(function (b) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = b;
|
||||
opt.textContent = b;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('oem-brand-filter').addEventListener('change', function () {
|
||||
vehiclePage = 1;
|
||||
loadVehicles();
|
||||
});
|
||||
|
||||
var modelTimer = null;
|
||||
document.getElementById('oem-model-filter').addEventListener('input', function () {
|
||||
clearTimeout(modelTimer);
|
||||
modelTimer = setTimeout(function () {
|
||||
vehiclePage = 1;
|
||||
loadVehicles();
|
||||
}, 400);
|
||||
});
|
||||
|
||||
// --- Load vehicles ---
|
||||
function loadVehicles() {
|
||||
var brand = document.getElementById('oem-brand-filter').value;
|
||||
var model = document.getElementById('oem-model-filter').value;
|
||||
var list = document.getElementById('oem-vehicle-list');
|
||||
list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
|
||||
var endpoint = vehicleStatus === 'pending'
|
||||
? '/api/captura/vehicles/pending'
|
||||
: '/api/captura/vehicles/in-progress';
|
||||
|
||||
var params = '?page=' + vehiclePage + '&per_page=30';
|
||||
if (brand) params += '&brand=' + encodeURIComponent(brand);
|
||||
if (model) params += '&model=' + encodeURIComponent(model);
|
||||
|
||||
api(endpoint + params).then(function (res) {
|
||||
var data = res.data || [];
|
||||
if (data.length === 0) {
|
||||
list.innerHTML = '<div class="empty-state"><div class="es-icon">📋</div><div class="es-text">No hay vehiculos ' +
|
||||
(vehicleStatus === 'pending' ? 'pendientes' : 'en progreso') + '</div></div>';
|
||||
document.getElementById('oem-vehicle-pagination').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = data.map(function (v) {
|
||||
return '<div class="vehicle-card" data-mye="' + v.id_mye + '">' +
|
||||
'<div class="vc-brand">' + esc(v.brand) + '</div>' +
|
||||
'<div class="vc-model">' + esc(v.model) + '</div>' +
|
||||
'<div class="vc-details">' + v.year + ' · ' + esc(v.engine) +
|
||||
(v.trim_level ? ' · ' + esc(v.trim_level) : '') + '</div>' +
|
||||
(v.parts_count ? '<div class="vc-parts-count">' + v.parts_count + ' partes registradas</div>' : '') +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
// Click handler for vehicle cards
|
||||
list.querySelectorAll('.vehicle-card').forEach(function (card) {
|
||||
card.addEventListener('click', function () {
|
||||
selectVehicle(parseInt(card.getAttribute('data-mye')));
|
||||
});
|
||||
});
|
||||
|
||||
// Pagination
|
||||
renderPagination('oem-vehicle-pagination', res.pagination, function (p) {
|
||||
vehiclePage = p;
|
||||
loadVehicles();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderPagination(containerId, pag, onPage) {
|
||||
var c = document.getElementById(containerId);
|
||||
if (!pag || pag.total_pages <= 1) { c.innerHTML = ''; return; }
|
||||
c.innerHTML = '<button ' + (pag.page <= 1 ? 'disabled' : '') + ' data-p="' + (pag.page - 1) + '">« Anterior</button>' +
|
||||
'<span class="page-info">Pag ' + pag.page + ' de ' + pag.total_pages + ' (' + pag.total + ' total)</span>' +
|
||||
'<button ' + (pag.page >= pag.total_pages ? 'disabled' : '') + ' data-p="' + (pag.page + 1) + '">Siguiente »</button>';
|
||||
c.querySelectorAll('button').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
onPage(parseInt(btn.getAttribute('data-p')));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Select vehicle and show part entry ---
|
||||
function selectVehicle(myeId) {
|
||||
currentMye = myeId;
|
||||
document.getElementById('oem-vehicle-select').style.display = 'none';
|
||||
document.getElementById('oem-part-entry').style.display = 'block';
|
||||
|
||||
// Mark as in_progress
|
||||
api('/api/captura/vehicles/' + myeId + '/status', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'in_progress' })
|
||||
});
|
||||
|
||||
loadVehicleParts(myeId);
|
||||
}
|
||||
|
||||
function loadVehicleParts(myeId) {
|
||||
api('/api/captura/vehicles/' + myeId + '/parts').then(function (res) {
|
||||
currentVehicle = res.vehicle;
|
||||
vehicleParts = res.parts || [];
|
||||
|
||||
// Render vehicle header
|
||||
var hdr = document.getElementById('oem-vehicle-header');
|
||||
hdr.innerHTML = '<div class="vh-info">' +
|
||||
'<div><div class="vh-label">Marca</div><div class="vh-value vh-brand">' + esc(currentVehicle.brand) + '</div></div>' +
|
||||
'<div><div class="vh-label">Modelo</div><div class="vh-value">' + esc(currentVehicle.model) + '</div></div>' +
|
||||
'<div><div class="vh-label">Ano</div><div class="vh-value">' + currentVehicle.year + '</div></div>' +
|
||||
'<div><div class="vh-label">Motor</div><div class="vh-value">' + esc(currentVehicle.engine) + '</div></div>' +
|
||||
(currentVehicle.trim_level ? '<div><div class="vh-label">Trim</div><div class="vh-value">' + esc(currentVehicle.trim_level) + '</div></div>' : '') +
|
||||
'</div>' +
|
||||
'<div class="vh-actions">' +
|
||||
'<button class="btn btn-secondary" id="btn-back-vehicles">◀ Volver</button>' +
|
||||
'<button class="btn btn-primary" id="btn-complete-vehicle">Terminado ✓</button>' +
|
||||
'</div>';
|
||||
|
||||
document.getElementById('btn-back-vehicles').addEventListener('click', backToVehicles);
|
||||
document.getElementById('btn-complete-vehicle').addEventListener('click', completeVehicle);
|
||||
|
||||
// Build groups by category
|
||||
renderGroups(res.groups, vehicleParts);
|
||||
updateProgress();
|
||||
});
|
||||
}
|
||||
|
||||
function backToVehicles() {
|
||||
document.getElementById('oem-vehicle-select').style.display = 'block';
|
||||
document.getElementById('oem-part-entry').style.display = 'none';
|
||||
currentMye = null;
|
||||
loadVehicles();
|
||||
}
|
||||
|
||||
function completeVehicle() {
|
||||
if (vehicleParts.length === 0) {
|
||||
toast('Registra al menos una parte antes de marcar como terminado', 'error');
|
||||
return;
|
||||
}
|
||||
api('/api/captura/vehicles/' + currentMye + '/status', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'completed' })
|
||||
}).then(function () {
|
||||
toast('Vehiculo completado');
|
||||
backToVehicles();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Render groups/categories ---
|
||||
function renderGroups(groups, parts) {
|
||||
var container = document.getElementById('oem-groups-container');
|
||||
// Group by category
|
||||
var categories = {};
|
||||
groups.forEach(function (g) {
|
||||
if (!categories[g.category]) {
|
||||
categories[g.category] = { id: g.id_part_category, groups: [] };
|
||||
}
|
||||
categories[g.category].groups.push(g);
|
||||
});
|
||||
|
||||
var html = '';
|
||||
Object.keys(categories).forEach(function (catName) {
|
||||
var cat = categories[catName];
|
||||
var catParts = parts.filter(function (p) {
|
||||
return cat.groups.some(function (g) { return g.id_part_group === p.group_id; });
|
||||
});
|
||||
|
||||
html += '<div class="category-section">' +
|
||||
'<div class="category-header" data-cat="' + cat.id + '">' +
|
||||
'<h3>' + esc(catName) + ' (' + catParts.length + ')</h3>' +
|
||||
'<span class="cat-toggle">▼</span></div>' +
|
||||
'<div class="category-body" data-cat-body="' + cat.id + '">';
|
||||
|
||||
cat.groups.forEach(function (g) {
|
||||
var groupParts = parts.filter(function (p) { return p.group_id === g.id_part_group; });
|
||||
html += '<div class="group-section" data-group="' + g.id_part_group + '">' +
|
||||
'<div class="group-name">' + esc(g.group_name) + '</div>' +
|
||||
'<div class="part-rows" data-group-parts="' + g.id_part_group + '">';
|
||||
|
||||
groupParts.forEach(function (p) {
|
||||
html += savedPartRow(p);
|
||||
});
|
||||
|
||||
html += '</div>' +
|
||||
'<button class="btn-add-part" data-group-id="' + g.id_part_group + '">+ Agregar pieza</button>' +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
html += '</div></div>';
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Category toggle
|
||||
container.querySelectorAll('.category-header').forEach(function (ch) {
|
||||
ch.addEventListener('click', function () {
|
||||
var catId = ch.getAttribute('data-cat');
|
||||
var body = container.querySelector('[data-cat-body="' + catId + '"]');
|
||||
ch.classList.toggle('collapsed');
|
||||
body.classList.toggle('collapsed');
|
||||
});
|
||||
});
|
||||
|
||||
// Add part buttons
|
||||
container.querySelectorAll('.btn-add-part').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
addPartRow(parseInt(btn.getAttribute('data-group-id')), btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function savedPartRow(p) {
|
||||
return '<div class="part-row saved" data-fitment-id="' + p.id_vehicle_part + '">' +
|
||||
'<input class="pr-oem" value="' + esc(p.oem_part_number) + '" readonly>' +
|
||||
'<input class="pr-name" value="' + esc(p.name_part || '') + '" readonly>' +
|
||||
'<input class="pr-qty" value="' + (p.quantity_required || 1) + '" readonly>' +
|
||||
'<button class="pr-btn pr-delete" title="Eliminar">✕</button>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function addPartRow(groupId, addBtn) {
|
||||
var rowsContainer = document.querySelector('[data-group-parts="' + groupId + '"]');
|
||||
var row = document.createElement('div');
|
||||
row.className = 'part-row';
|
||||
row.innerHTML = '<input class="pr-oem" placeholder="# OEM" data-group="' + groupId + '">' +
|
||||
'<input class="pr-name" placeholder="Nombre pieza">' +
|
||||
'<input class="pr-qty" value="1" type="number" min="1">' +
|
||||
'<button class="pr-btn pr-save" title="Guardar">✓</button>' +
|
||||
'<button class="pr-btn pr-delete" title="Quitar">✕</button>';
|
||||
|
||||
rowsContainer.appendChild(row);
|
||||
|
||||
// Focus OEM field
|
||||
row.querySelector('.pr-oem').focus();
|
||||
|
||||
// OEM blur: check if exists
|
||||
row.querySelector('.pr-oem').addEventListener('blur', function () {
|
||||
var oem = this.value.trim();
|
||||
if (!oem) return;
|
||||
api('/api/captura/parts/check-oem?oem=' + encodeURIComponent(oem)).then(function (res) {
|
||||
if (res.exists) {
|
||||
row.querySelector('.pr-name').value = res.part.name_part || '';
|
||||
row.querySelector('.pr-name').style.borderColor = 'var(--success)';
|
||||
row.dataset.existingPartId = res.part.id_part;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Save
|
||||
row.querySelector('.pr-save').addEventListener('click', function () {
|
||||
savePart(row, groupId);
|
||||
});
|
||||
|
||||
// Delete (unsaved)
|
||||
row.querySelector('.pr-delete').addEventListener('click', function () {
|
||||
row.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function savePart(row, groupId) {
|
||||
var oem = row.querySelector('.pr-oem').value.trim();
|
||||
var name = row.querySelector('.pr-name').value.trim();
|
||||
var qty = parseInt(row.querySelector('.pr-qty').value) || 1;
|
||||
|
||||
if (!oem) {
|
||||
toast('Ingresa el numero OEM', 'error');
|
||||
row.querySelector('.pr-oem').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
var saveBtn = row.querySelector('.pr-save');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = '...';
|
||||
|
||||
// Check if part already exists
|
||||
var existingId = row.dataset.existingPartId;
|
||||
|
||||
function createFitment(partId) {
|
||||
api('/api/admin/fitment', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model_year_engine_id: currentMye,
|
||||
part_id: partId,
|
||||
quantity_required: qty
|
||||
})
|
||||
}).then(function (res) {
|
||||
// Replace row with saved version
|
||||
var newPart = {
|
||||
id_vehicle_part: res.id,
|
||||
part_id: partId,
|
||||
oem_part_number: oem,
|
||||
name_part: name,
|
||||
quantity_required: qty,
|
||||
group_id: groupId
|
||||
};
|
||||
vehicleParts.push(newPart);
|
||||
row.outerHTML = savedPartRow(newPart);
|
||||
updateProgress();
|
||||
toast('Parte guardada: ' + oem);
|
||||
|
||||
// Re-attach delete handlers
|
||||
attachDeleteHandlers();
|
||||
}).catch(function (err) {
|
||||
toast(err.message, 'error');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = '\u2713';
|
||||
});
|
||||
}
|
||||
|
||||
if (existingId) {
|
||||
createFitment(parseInt(existingId));
|
||||
} else {
|
||||
// Create part first
|
||||
api('/api/admin/parts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
oem_part_number: oem,
|
||||
name: name || oem,
|
||||
group_id: groupId
|
||||
})
|
||||
}).then(function (res) {
|
||||
createFitment(res.id);
|
||||
}).catch(function (err) {
|
||||
toast(err.message, 'error');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = '\u2713';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function attachDeleteHandlers() {
|
||||
document.querySelectorAll('.part-row.saved .pr-delete').forEach(function (btn) {
|
||||
btn.onclick = function () {
|
||||
var row = btn.closest('.part-row');
|
||||
var fitmentId = row.getAttribute('data-fitment-id');
|
||||
if (!fitmentId) { row.remove(); return; }
|
||||
|
||||
api('/api/admin/fitment/' + fitmentId, { method: 'DELETE' }).then(function () {
|
||||
vehicleParts = vehicleParts.filter(function (p) {
|
||||
return p.id_vehicle_part !== parseInt(fitmentId);
|
||||
});
|
||||
row.remove();
|
||||
updateProgress();
|
||||
toast('Parte eliminada');
|
||||
}).catch(function (err) {
|
||||
toast(err.message, 'error');
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
var count = vehicleParts.length;
|
||||
var totalGroups = 63;
|
||||
var pct = Math.min(100, Math.round((count / totalGroups) * 100));
|
||||
document.getElementById('oem-progress-fill').style.width = pct + '%';
|
||||
document.getElementById('oem-progress-text').textContent = count + ' partes registradas';
|
||||
|
||||
// Update category counts
|
||||
document.querySelectorAll('.category-header h3').forEach(function (h3) {
|
||||
var catSection = h3.closest('.category-section');
|
||||
var rows = catSection.querySelectorAll('.part-row.saved');
|
||||
var catName = h3.textContent.replace(/\s*\(\d+\)$/, '');
|
||||
h3.textContent = catName + ' (' + rows.length + ')';
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SECTION 2: Aftermarket / Interchange
|
||||
// ================================================================
|
||||
|
||||
var aftermarketPage = 1;
|
||||
|
||||
function loadPartsWithoutAftermarket(page) {
|
||||
page = page || 1;
|
||||
aftermarketPage = page;
|
||||
var search = document.getElementById('aftermarket-search').value;
|
||||
var list = document.getElementById('aftermarket-list');
|
||||
list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
|
||||
var params = '?page=' + page + '&per_page=20';
|
||||
if (search) params += '&search=' + encodeURIComponent(search);
|
||||
|
||||
api('/api/captura/parts/without-aftermarket' + params).then(function (res) {
|
||||
var data = res.data || [];
|
||||
if (data.length === 0) {
|
||||
list.innerHTML = '<div class="empty-state"><div class="es-icon">✅</div><div class="es-text">No hay piezas sin intercambios</div></div>';
|
||||
document.getElementById('aftermarket-pagination').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = data.map(function (p) {
|
||||
return '<div class="part-detail-card" data-part-id="' + p.id_part + '">' +
|
||||
'<div class="pdc-header">' +
|
||||
'<div><span class="pdc-oem">' + esc(p.oem_part_number) + '</span>' +
|
||||
' <span class="pdc-name">' + esc(p.name_part) + '</span></div>' +
|
||||
'<span class="pdc-group">' + esc(p.category) + ' › ' + esc(p.group_name) + '</span></div>' +
|
||||
'<div class="aftermarket-existing" data-af-list="' + p.id_part + '"></div>' +
|
||||
'<div class="aftermarket-form" data-af-form="' + p.id_part + '">' +
|
||||
'<div class="af-field"><label>Fabricante</label>' +
|
||||
'<select class="af-manufacturer">' + manufacturerOptions() + '</select></div>' +
|
||||
'<div class="af-field"><label># Aftermarket</label>' +
|
||||
'<input class="af-partnum" placeholder="Ej: MK1234"></div>' +
|
||||
'<div class="af-field"><label>Nombre</label>' +
|
||||
'<input class="af-name" placeholder="Nombre pieza"></div>' +
|
||||
'<div class="af-field"><label>Calidad</label>' +
|
||||
'<select class="af-quality">' +
|
||||
'<option value="standard">Standard</option>' +
|
||||
'<option value="economy">Economy</option>' +
|
||||
'<option value="oem">OEM</option>' +
|
||||
'<option value="premium">Premium</option></select></div>' +
|
||||
'<div class="af-field"><label>Precio USD</label>' +
|
||||
'<input class="af-price" type="number" step="0.01" placeholder="0.00" style="width:80px"></div>' +
|
||||
'<div class="af-field"><label>Garantia (meses)</label>' +
|
||||
'<input class="af-warranty" type="number" placeholder="12" style="width:70px"></div>' +
|
||||
'<button class="btn btn-primary af-save-btn" style="padding:0.4rem 1rem">+ Agregar</button>' +
|
||||
'</div></div>';
|
||||
}).join('');
|
||||
|
||||
// Load existing aftermarket for each part
|
||||
data.forEach(function (p) {
|
||||
loadPartAftermarket(p.id_part);
|
||||
});
|
||||
|
||||
// Save handlers
|
||||
list.querySelectorAll('.af-save-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var card = btn.closest('.part-detail-card');
|
||||
saveAftermarket(card);
|
||||
});
|
||||
});
|
||||
|
||||
renderPagination('aftermarket-pagination', res.pagination, function (p) {
|
||||
loadPartsWithoutAftermarket(p);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function manufacturerOptions() {
|
||||
return manufacturers.map(function (m) {
|
||||
return '<option value="' + m.id + '">' + esc(m.name) + '</option>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function loadPartAftermarket(partId) {
|
||||
api('/api/captura/parts/' + partId + '/aftermarket').then(function (items) {
|
||||
var container = document.querySelector('[data-af-list="' + partId + '"]');
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = '<p style="font-size:0.8rem;color:var(--text-secondary);margin-bottom:0.5rem">Sin intercambios registrados</p>';
|
||||
return;
|
||||
}
|
||||
var html = '<table class="aftermarket-table"><thead><tr>' +
|
||||
'<th>Fabricante</th><th># Parte</th><th>Nombre</th><th>Calidad</th><th>Precio</th><th>Garantia</th></tr></thead><tbody>';
|
||||
items.forEach(function (a) {
|
||||
html += '<tr><td>' + esc(a.manufacturer) + '</td><td>' + esc(a.part_number) +
|
||||
'</td><td>' + esc(a.name || '') + '</td><td>' + esc(a.quality || '') +
|
||||
'</td><td>' + (a.price_usd ? '$' + a.price_usd : '') +
|
||||
'</td><td>' + (a.warranty_months || '') + '</td></tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
});
|
||||
}
|
||||
|
||||
function saveAftermarket(card) {
|
||||
var partId = card.getAttribute('data-part-id');
|
||||
var manufacturer = card.querySelector('.af-manufacturer').value;
|
||||
var partNumber = card.querySelector('.af-partnum').value.trim();
|
||||
var name = card.querySelector('.af-name').value.trim();
|
||||
var quality = card.querySelector('.af-quality').value;
|
||||
var price = card.querySelector('.af-price').value;
|
||||
var warranty = card.querySelector('.af-warranty').value;
|
||||
|
||||
if (!partNumber) {
|
||||
toast('Ingresa el numero de parte aftermarket', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
api('/api/admin/aftermarket', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
oem_part_id: parseInt(partId),
|
||||
manufacturer_id: parseInt(manufacturer),
|
||||
part_number: partNumber,
|
||||
name: name,
|
||||
quality_tier: quality,
|
||||
price_usd: price ? parseFloat(price) : null,
|
||||
warranty_months: warranty ? parseInt(warranty) : null
|
||||
})
|
||||
}).then(function () {
|
||||
toast('Intercambio guardado: ' + partNumber);
|
||||
// Clear form
|
||||
card.querySelector('.af-partnum').value = '';
|
||||
card.querySelector('.af-name').value = '';
|
||||
card.querySelector('.af-price').value = '';
|
||||
card.querySelector('.af-warranty').value = '';
|
||||
// Reload aftermarket list
|
||||
loadPartAftermarket(parseInt(partId));
|
||||
}).catch(function (err) {
|
||||
toast(err.message, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SECTION 3: Images
|
||||
// ================================================================
|
||||
|
||||
var imagePage = 1;
|
||||
|
||||
function loadPartsWithoutImage(page) {
|
||||
page = page || 1;
|
||||
imagePage = page;
|
||||
var search = document.getElementById('image-search').value;
|
||||
var list = document.getElementById('image-list');
|
||||
list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
|
||||
var params = '?page=' + page + '&per_page=20';
|
||||
if (search) params += '&search=' + encodeURIComponent(search);
|
||||
|
||||
api('/api/captura/parts/without-image' + params).then(function (res) {
|
||||
var data = res.data || [];
|
||||
if (data.length === 0) {
|
||||
list.innerHTML = '<div class="empty-state"><div class="es-icon">📷</div><div class="es-text">No hay piezas sin imagen</div></div>';
|
||||
document.getElementById('image-pagination').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = data.map(function (p) {
|
||||
return '<div class="image-card" data-part-id="' + p.id_part + '">' +
|
||||
'<div class="ic-preview"><span>Sin imagen</span></div>' +
|
||||
'<div class="ic-info">' +
|
||||
'<div class="ic-oem">' + esc(p.oem_part_number) + '</div>' +
|
||||
'<div class="ic-name">' + esc(p.name_part) + ' · ' + esc(p.group_name) + '</div>' +
|
||||
'<div class="ic-upload">' +
|
||||
'<input type="file" accept="image/jpeg,image/png,image/webp" class="ic-file-input">' +
|
||||
'<button class="btn btn-primary ic-upload-btn" style="padding:0.3rem 0.8rem;font-size:0.8rem" disabled>Subir</button>' +
|
||||
'</div></div></div>';
|
||||
}).join('');
|
||||
|
||||
// File input change → enable upload button and show preview
|
||||
list.querySelectorAll('.ic-file-input').forEach(function (input) {
|
||||
input.addEventListener('change', function () {
|
||||
var card = input.closest('.image-card');
|
||||
var btn = card.querySelector('.ic-upload-btn');
|
||||
var preview = card.querySelector('.ic-preview');
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
btn.disabled = false;
|
||||
|
||||
// Show preview
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
preview.innerHTML = '<img src="' + e.target.result + '">';
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Upload button
|
||||
list.querySelectorAll('.ic-upload-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var card = btn.closest('.image-card');
|
||||
uploadImage(card);
|
||||
});
|
||||
});
|
||||
|
||||
renderPagination('image-pagination', res.pagination, function (p) {
|
||||
loadPartsWithoutImage(p);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function uploadImage(card) {
|
||||
var partId = card.getAttribute('data-part-id');
|
||||
var fileInput = card.querySelector('.ic-file-input');
|
||||
var btn = card.querySelector('.ic-upload-btn');
|
||||
|
||||
if (!fileInput.files || !fileInput.files[0]) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Subiendo...';
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('image', fileInput.files[0]);
|
||||
|
||||
fetch(API + '/api/captura/parts/' + partId + '/image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if (res.error) throw new Error(res.error);
|
||||
toast('Imagen subida correctamente');
|
||||
// Remove card from list
|
||||
card.style.opacity = '0.3';
|
||||
setTimeout(function () { card.remove(); }, 500);
|
||||
}).catch(function (err) {
|
||||
toast(err.message, 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Subir';
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Init
|
||||
// ================================================================
|
||||
|
||||
function init() {
|
||||
loadBrands();
|
||||
loadVehicles();
|
||||
|
||||
// Pre-load manufacturers for Section 2
|
||||
api('/api/captura/manufacturers').then(function (data) {
|
||||
manufacturers = data;
|
||||
});
|
||||
}
|
||||
|
||||
// Make functions globally accessible for inline onclick handlers
|
||||
window.loadPartsWithoutAftermarket = loadPartsWithoutAftermarket;
|
||||
window.loadPartsWithoutImage = loadPartsWithoutImage;
|
||||
|
||||
init();
|
||||
})();
|
||||
Reference in New Issue
Block a user