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:
2026-03-18 22:25:32 +00:00
parent b1adf536f6
commit fe6542c45c
12 changed files with 4037 additions and 0 deletions

707
dashboard/captura.js Normal file
View 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">&#128203;</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 + ' &middot; ' + esc(v.engine) +
(v.trim_level ? ' &middot; ' + 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) + '">&laquo; 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 &raquo;</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">&#9664; Volver</button>' +
'<button class="btn btn-primary" id="btn-complete-vehicle">Terminado &#10003;</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">&#9660;</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">&#10005;</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">&#10003;</button>' +
'<button class="pr-btn pr-delete" title="Quitar">&#10005;</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">&#9989;</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) + ' &rsaquo; ' + 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">&#128247;</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) + ' &middot; ' + 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();
})();