// /home/Autopartes/pos/static/js/yonke.js // Nexus Yonke — used parts (partes usadas / deshuesadero) UI. // // Tabs: piezas (list + search), nueva (fast intake), donador (search by donor // vehicle), consignaciones (third-party parts on consignment), b2b (wholesale // marketplace between businesses). // // Backend contract: /pos/api/yonke/* // GET /pieces POST /pieces // POST /pieces//images DELETE /pieces//images/ // PUT /pieces/ // GET /vin/ // GET /donors // GET /consignments POST /consignments // GET /consignments/ POST /consignments//items // PUT /consignments//settle // // B2B contract: /pos/api/b2b (see the "MAYOREO B2B" section) (function () { 'use strict'; var API = '/pos/api/yonke'; // ─── Auth ──────────────────────────────────────────────────────────── function getToken() { return localStorage.getItem('pos_token') || localStorage.getItem('access_token') || ''; } if (!getToken()) { window.location.href = '/pos/login'; return; } function authHeaders() { return { 'Authorization': 'Bearer ' + getToken(), 'Content-Type': 'application/json' }; } // Reissue the JWT (same endpoint app-init.js uses) so an expired token or a // permission change does not kick the user straight back to the login page. var refreshPromise = null; function refreshToken() { if (!refreshPromise) { var hdrs = { 'Authorization': 'Bearer ' + getToken() }; refreshPromise = fetch('/pos/api/auth/refresh', { method: 'POST', headers: hdrs }) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (d) { if (d && d.token) { localStorage.setItem('pos_token', d.token); if (d.employee) { try { localStorage.setItem('pos_employee', JSON.stringify(d.employee)); } catch (e) {} } if (window.POS_USER) { window.POS_USER.token = d.token; if (d.permissions) window.POS_USER.permissions = d.permissions; } return d.token; } return null; }) .catch(function () { return null; }) .then(function (t) { refreshPromise = null; return t; }); } return refreshPromise; } function request(url, opts) { opts = opts || {}; var init = { method: opts.method || 'GET', headers: Object.assign(authHeaders(), opts.headers || {}) }; if (opts.body !== undefined) init.body = opts.body; if (opts.signal) init.signal = opts.signal; return fetch(url, init); } function parseResponse(resp) { if (!resp.ok) { return resp.json().catch(function () { return {}; }).then(function (body) { var msg = (body && (body.error || body.message)) || ('Error HTTP ' + resp.status); var err = new Error(msg); err.status = resp.status; err.body = body; throw err; }); } if (resp.status === 204) return null; var ct = resp.headers.get('content-type') || ''; if (ct.indexOf('json') === -1) return null; return resp.json().catch(function () { return null; }); } function apiFetch(url, opts) { opts = opts || {}; return request(url, opts).then(function (resp) { if (resp.status === 401) { return refreshToken().then(function (token) { if (!token) return redirectToLogin(); return request(url, opts).then(function (retry) { if (retry.status === 401) return redirectToLogin(); return parseResponse(retry); }); }); } return parseResponse(resp); }); } function redirectToLogin() { localStorage.removeItem('pos_token'); window.location.href = '/pos/login'; return null; } // Multipart upload — must NOT carry the JSON Content-Type. function apiUpload(url, formData) { return fetch(url, { method: 'POST', headers: { 'Authorization': 'Bearer ' + getToken() }, body: formData }).then(function (resp) { if (resp.status === 401) { return refreshToken().then(function (token) { if (!token) return redirectToLogin(); return fetch(url, { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData }).then(parseResponse); }); } return parseResponse(resp); }); } // ─── Helpers ───────────────────────────────────────────────────────── function esc(s) { if (s === null || s === undefined || s === '') return ''; var d = document.createElement('div'); d.textContent = String(s); return d.innerHTML; } function num(n) { return parseFloat(n) || 0; } function fmtMoney(n) { return '$' + num(n).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function fmtInt(n) { return parseInt(n, 10) || 0; } function qs(params) { var sp = new URLSearchParams(); Object.keys(params).forEach(function (k) { var v = params[k]; if (v === '' || v === null || v === undefined || (typeof v === 'number' && isNaN(v))) return; sp.set(k, v); }); return sp.toString(); } function asList(res) { if (!res) return []; if (Array.isArray(res)) return res; if (Array.isArray(res.data)) return res.data; if (Array.isArray(res.items)) return res.items; if (res.data && Array.isArray(res.data.items)) return res.data.items; return []; } function unwrap(res) { if (res && res.data && typeof res.data === 'object' && !Array.isArray(res.data)) return res.data; return res || {}; } function val(id) { var el = document.getElementById(id); return el ? el.value.trim() : ''; } function numOrNull(id) { var v = val(id); if (v === '') return null; var n = parseFloat(v); return isNaN(n) ? null : n; } function intOrNull(id) { var n = numOrNull(id); return n === null ? null : Math.round(n); } function openModal(id) { var el = document.getElementById(id); if (el) el.classList.add('is-open'); } function toast(msg, type) { if (typeof window.showToast === 'function') window.showToast(msg, type || 'ok'); } function errMsg(err) { if (!err) return 'Error desconocido'; if (err.status === 403) return 'Sin permiso para esta acción (403)'; if (err.status === 404) return 'No encontrado (404)'; return err.message || 'Error de red'; } function emptyRow(colspan, text) { return '' + esc(text) + ''; } function primaryImage(item) { var imgs = (item && item.images) || []; for (var i = 0; i < imgs.length; i++) { if (imgs[i].is_primary) return imgs[i].url; } if (imgs.length) return imgs[0].url; return item && item.image_url ? item.image_url : ''; } function thumbHtml(item) { var url = primaryImage(item); var inner = url ? 'foto' : '
sin
foto
'; return '
' + inner + '
'; } // A broken image URL must not leave an unclickable blank cell. window.ynkThumbFallback = function (img) { var slot = img && img.parentNode; if (slot) slot.innerHTML = '
sin
foto
'; }; function gradeHtml(grade) { if (!grade) return '—'; var g = String(grade).toUpperCase().charAt(0); return '' + esc(g) + ''; } function conditionBadge(condition) { var c = (condition || '').toLowerCase(); var cls = c === 'nuevo' ? 'badge--ok' : (c === 'reconstruido' ? 'badge--transit' : 'badge--pending'); return '' + esc(c || '—') + ''; } function donorHtml(item) { var veh = [item.donor_make, item.donor_model, item.donor_year].filter(Boolean).join(' '); var vin = item.donor_vin || ''; if (!veh && !vin) return '—'; return '
' + esc(veh || 'Vehículo sin datos') + '
' + (vin ? '
' + esc(vin) + '
' : ''); } function renderPagination(el, pg, fnName) { if (!el) return; pg = pg || {}; if (!pg.total_pages || pg.total_pages <= 1) { el.innerHTML = ''; return; } el.innerHTML = '' + '' + pg.page + ' / ' + pg.total_pages + '' + ''; } // Permissions — the server enforces them; this only hides buttons. var user = window.POS_USER || {}; var role = (user.role || '').toLowerCase(); var perms = user.permissions || []; function hasPerm(p) { return role === 'owner' || role === 'admin' || perms.indexOf(p) !== -1; } function canEdit() { return role === 'owner' || role === 'admin' || hasPerm('yonke.manage') || hasPerm('yonke.edit') || hasPerm('inventory.edit'); } // ─── Tab switching ─────────────────────────────────────────────────── var TABS = { piezas: 'panel-piezas', nueva: 'panel-nueva', donador: 'panel-donador', consignaciones: 'panel-consignaciones', b2b: 'panel-b2b', reportes: 'panel-reportes' }; var activeTab = 'piezas'; function switchTab(name) { if (!TABS[name]) return; activeTab = name; document.querySelectorAll('.tab-btn').forEach(function (btn) { var on = btn.getAttribute('aria-controls') === TABS[name]; btn.classList.toggle('is-active', on); btn.setAttribute('aria-selected', on ? 'true' : 'false'); }); document.querySelectorAll('.tab-panel').forEach(function (p) { p.classList.toggle('is-active', p.id === TABS[name]); }); var panels = document.getElementById('tab-panels'); if (panels) panels.scrollTop = 0; if (window.location.hash !== '#' + name) { try { history.replaceState(null, '', '#' + name); } catch (e) {} } if (name === 'piezas') loadPieces(1); if (name === 'nueva') { loadConsignmentOptions(); fillConsignmentSelect('ynkFullConsignmentId'); if (intakeMode === 'full') { var fullRows = document.getElementById('ynkFullPiecesBody'); if (fullRows && !fullRows.children.length) { for (var fr = 0; fr < 3; fr++) ynkFullAddRow(); } } startIntakeTimer(); } else { stopIntakeTimer(); } if (name === 'donador') ynkSetDonorView(donorView); if (name === 'consignaciones') loadConsignments(); if (name === 'b2b') ynkB2bInit(); if (name === 'reportes') ynkSetReportView(reportView); } // ─── Summary cards ─────────────────────────────────────────────────── function setStat(id, value) { var el = document.getElementById(id); if (el) el.textContent = value === null || value === undefined ? '--' : value; } function refreshSummary() { apiFetch(API + '/pieces?per_page=1') .then(function (res) { if (!res) return; var total = res.pagination ? res.pagination.total : asList(res).length; setStat('ynkStatPieces', fmtInt(total)); var b = document.getElementById('badge-piezas'); if (b) b.textContent = fmtInt(total); }) .catch(function () { setStat('ynkStatPieces', '--'); }); apiFetch(API + '/pieces?ownership=consigna&per_page=1') .then(function (res) { if (!res) return; var total = res.pagination ? res.pagination.total : asList(res).length; setStat('ynkStatConsignPieces', fmtInt(total)); }) .catch(function () { setStat('ynkStatConsignPieces', '--'); }); apiFetch(API + '/donors?page=1') .then(function (res) { if (!res) return; var total = res.pagination ? res.pagination.total : asList(res).length; setStat('ynkStatDonors', fmtInt(total)); var b = document.getElementById('badge-donadores'); if (b) b.textContent = fmtInt(total); }) .catch(function () { setStat('ynkStatDonors', '--'); }); } // ===================================================================== // TAB 1 — PIEZAS USADAS // ===================================================================== var pieceById = {}; var piecesAbort = null; function loadPieces(page) { var body = document.getElementById('ynkPiecesBody'); if (!body) return; body.innerHTML = emptyRow(12, 'Cargando piezas…'); var params = { q: val('ynkQ'), condition: document.getElementById('ynkFiltCondition').value, grade: document.getElementById('ynkFiltGrade').value, ownership: document.getElementById('ynkFiltOwnership').value, page: page || 1, per_page: document.getElementById('ynkFiltPerPage').value || 50 }; if (piecesAbort) piecesAbort.abort(); piecesAbort = new AbortController(); apiFetch(API + '/pieces?' + qs(params), { signal: piecesAbort.signal }) .then(function (res) { piecesAbort = null; if (!res) return; var items = asList(res); if (!items.length) { body.innerHTML = emptyRow(12, params.q || params.condition || params.grade || params.ownership ? 'Sin piezas que coincidan con los filtros.' : 'Aún no hay piezas usadas. Usa "Nueva pieza" para dar de alta la primera.'); document.getElementById('ynkPiecesPagination').innerHTML = ''; document.getElementById('ynkPiecesFooter').textContent = '0 piezas'; return; } pieceById = {}; body.innerHTML = items.map(pieceRow).join(''); var pg = res.pagination || {}; document.getElementById('ynkPiecesFooter').textContent = fmtInt(pg.total !== undefined ? pg.total : items.length) + ' piezas'; renderPagination(document.getElementById('ynkPiecesPagination'), pg, 'ynkPiecesPage'); }) .catch(function (err) { if (!err || err.name === 'AbortError') return; piecesAbort = null; body.innerHTML = emptyRow(12, 'Error al cargar piezas: ' + errMsg(err)); toast('Error al cargar piezas: ' + errMsg(err), 'error'); }); } function pieceRow(it) { pieceById[it.id] = it; return '' + '' + thumbHtml(it) + '' + '' + (esc(it.part_number) || '—') + (it.is_unique_piece ? '1 de 1' : '') + '' + '' + esc(it.name) + '' + '' + esc(it.brand) + '' + '' + conditionBadge(it.condition) + (it.ownership === 'consigna' ? ' consigna' : '') + '' + '' + gradeHtml(it.condition_grade) + '' + '' + donorHtml(it) + '' + '' + fmtMoney(it.price_1) + '' + '' + fmtMoney(it.cost) + '' + '' + fmtInt(it.stock) + '' + '' + esc(it.location) + '' + '' + ' ' + ' ' + ' ' + ' ' + (canEdit() ? '' : '') + ''; } // ===================================================================== // TAB 2 — NUEVA PIEZA (alta rápida) // ===================================================================== var pendingPhotos = []; // [{file, url}] var intakeTimer = null; var intakeStart = 0; function startIntakeTimer() { if (intakeTimer) return; intakeStart = Date.now(); var el = document.getElementById('ynkTimer'); if (el) el.textContent = '⏱ 0s'; intakeTimer = setInterval(function () { var el2 = document.getElementById('ynkTimer'); if (el2) el2.textContent = '⏱ ' + Math.round((Date.now() - intakeStart) / 1000) + 's'; }, 1000); } function stopIntakeTimer() { if (intakeTimer) { clearInterval(intakeTimer); intakeTimer = null; } } function intakeElapsed() { return Math.round((Date.now() - intakeStart) / 1000); } // The API only accepts these formats (see yonke_bp.ALLOWED_EXTENSIONS). var ALLOWED_IMAGE = /^image\/(jpeg|jpg|png|webp)$/i; function isAllowedImage(f) { return ALLOWED_IMAGE.test(f.type || '') || /\.(jpe?g|png|webp)$/i.test(f.name || ''); } function addPhotos(fileList) { var skipped = 0; Array.prototype.slice.call(fileList || []).forEach(function (f) { if (!isAllowedImage(f)) { skipped++; return; } pendingPhotos.push({ file: f, url: URL.createObjectURL(f) }); }); renderPhotoPreview(); if (skipped) showToast(skipped + ' archivo(s) omitidos: solo JPG, PNG o WEBP.', 'warn'); } function renderPhotoPreview() { var grid = document.getElementById('ynkPhotoPreview'); var counter = document.getElementById('ynkPhotoCount'); if (counter) { counter.textContent = pendingPhotos.length ? pendingPhotos.length + ' foto(s) listas para subir' : 'Sin fotos seleccionadas'; } if (!grid) return; grid.innerHTML = pendingPhotos.map(function (p, i) { return '
foto ' + (i + 1) + '' + '
'; }).join(''); } function ynkRemovePhoto(i) { var p = pendingPhotos[i]; if (!p) return; try { URL.revokeObjectURL(p.url); } catch (e) {} pendingPhotos.splice(i, 1); renderPhotoPreview(); } function ynkDecodeVin() { var vin = val('ynkVin').toUpperCase(); var status = document.getElementById('ynkVinStatus'); if (!vin) { if (status) { status.textContent = 'Escribe un VIN primero.'; status.className = 'ynk-hint ynk-warn'; } return; } var btn = document.getElementById('ynkDecodeBtn'); if (btn) btn.disabled = true; if (status) { status.textContent = 'Decodificando ' + vin + '…'; status.className = 'ynk-hint'; } apiFetch(API + '/vin/' + encodeURIComponent(vin)) .then(function (d) { if (!d) return; var filled = []; if (d.make) { document.getElementById('ynkDonorMake').value = d.make; filled.push(d.make); } if (d.model) { document.getElementById('ynkDonorModel').value = d.model; filled.push(d.model); } if (d.year) { document.getElementById('ynkDonorYear').value = d.year; filled.push(d.year); } if (d.error && !filled.length) { if (status) { status.textContent = 'No se pudo decodificar: ' + d.error; status.className = 'ynk-hint ynk-err'; } return; } var detail = [d.engine, d.body_type, d.fuel_type, d.drive_type, d.trim].filter(Boolean).join(' · '); if (status) { status.textContent = (filled.length ? 'Autocompletado: ' + filled.join(' ') : 'VIN sin datos de vehículo.') + (detail ? ' — ' + detail : '') + (d.error ? ' (' + d.error + ')' : ''); status.className = 'ynk-hint' + (filled.length ? ' ynk-ok' : ' ynk-warn'); } }) .catch(function (err) { if (status) { status.textContent = 'No se pudo decodificar: ' + errMsg(err); status.className = 'ynk-hint ynk-err'; } }) .then(function () { if (btn) btn.disabled = false; }); } function buildPiecePayload() { var payload = { name: val('ynkName'), price_1: numOrNull('ynkPrice'), condition: document.getElementById('ynkCondition').value || 'usado', is_unique_piece: !!document.getElementById('ynkUnique').checked }; var partNumber = val('ynkPartNumber'); if (partNumber) payload.part_number = partNumber; var brand = val('ynkBrand'); if (brand) payload.brand = brand; var cost = numOrNull('ynkCost'); if (cost !== null) payload.cost = cost; var grade = document.getElementById('ynkGrade').value; if (grade) payload.condition_grade = grade; var vin = val('ynkVin'); if (vin) payload.donor_vin = vin.toUpperCase(); var make = val('ynkDonorMake'); if (make) payload.donor_make = make; var model = val('ynkDonorModel'); if (model) payload.donor_model = model; var year = intOrNull('ynkDonorYear'); if (year !== null) payload.donor_year = year; var km = intOrNull('ynkDonorKm'); if (km !== null) payload.donor_mileage_km = km; payload.ownership = document.getElementById('ynkOwnership').value || 'propio'; if (payload.ownership === 'consigna') { var cid = document.getElementById('ynkConsignmentId').value; if (cid) payload.consignment_id = parseInt(cid, 10); } var location = val('ynkLocation'); if (location) payload.location = location; var warranty = intOrNull('ynkWarranty'); if (warranty !== null) payload.warranty_days = warranty; var stock = intOrNull('ynkInitialStock'); if (stock !== null) payload.initial_stock = stock; var serial = val('ynkSerial'); if (serial) payload.serial_number = serial; return payload; } function uploadImages(pieceId) { if (!pendingPhotos.length) return Promise.resolve(null); var fd = new FormData(); pendingPhotos.forEach(function (p) { fd.append('file', p.file, p.file.name); }); return apiUpload(API + '/pieces/' + pieceId + '/images', fd); } function savedPieceHtml(id, sku, tail) { return '✔ Pieza #' + id + ' creada — SKU: ' + esc(sku) + '' + (tail ? ' ' + tail : ''); } function ynkSubmitPiece() { var result = document.getElementById('ynkSaveResult'); var btn = document.getElementById('ynkSaveBtn'); var payload = buildPiecePayload(); if (!payload.name) { if (result) { result.className = 'ynk-save-result ynk-err'; result.textContent = 'El nombre de la pieza es obligatorio.'; } document.getElementById('ynkName').focus(); return; } if (payload.price_1 === null) { if (result) { result.className = 'ynk-save-result ynk-err'; result.textContent = 'El precio de venta es obligatorio.'; } document.getElementById('ynkPrice').focus(); return; } var photoCount = pendingPhotos.length; var elapsed = intakeElapsed(); stopIntakeTimer(); if (btn) { btn.disabled = true; btn.textContent = 'Guardando…'; } if (result) { result.className = 'ynk-save-result'; result.textContent = 'Creando pieza…'; } apiFetch(API + '/pieces', { method: 'POST', body: JSON.stringify(payload) }) .then(function (item) { if (!item) return; var sku = item.part_number || '(sin SKU)'; if (result) { result.className = 'ynk-save-result ynk-ok'; result.innerHTML = savedPieceHtml(item.id, sku, (photoCount ? '· subiendo ' + photoCount + ' foto(s)… ' : '') + '(en ' + elapsed + 's)'); } toast('Pieza creada — SKU ' + sku, 'ok'); var after = photoCount ? uploadImages(item.id) .then(function (resp) { var n = resp && resp.images ? resp.images.length : photoCount; if (result) { result.innerHTML = savedPieceHtml(item.id, sku, '(' + n + ' foto(s) subidas, ' + elapsed + 's)'); } toast(n + ' foto(s) subidas', 'ok'); }) .catch(function (err) { if (result) { result.innerHTML = savedPieceHtml(item.id, sku, '— no se pudieron subir las fotos: ' + esc(errMsg(err)) + ''); } toast('La pieza se creó, pero fallaron las fotos: ' + errMsg(err), 'warn'); }) : Promise.resolve(); return after.then(function () { resetPieceForm(); refreshSummary(); }); }) .catch(function (err) { if (result) { result.className = 'ynk-save-result ynk-err'; result.textContent = 'No se pudo crear la pieza: ' + errMsg(err); } toast('Error al crear la pieza: ' + errMsg(err), 'error'); }) .then(function () { if (btn) { btn.disabled = false; btn.textContent = 'Guardar pieza'; } }); } function resetPieceForm() { ['ynkName', 'ynkPartNumber', 'ynkBrand', 'ynkPrice', 'ynkCost', 'ynkVin', 'ynkDonorMake', 'ynkDonorModel', 'ynkDonorYear', 'ynkDonorKm', 'ynkLocation', 'ynkWarranty', 'ynkSerial'] .forEach(function (id) { var el = document.getElementById(id); if (el) el.value = ''; }); document.getElementById('ynkCondition').value = 'usado'; document.getElementById('ynkGrade').value = ''; document.getElementById('ynkUnique').checked = true; document.getElementById('ynkOwnership').value = 'propio'; document.getElementById('ynkInitialStock').value = '1'; document.getElementById('ynkConsignmentWrap').style.display = 'none'; var status = document.getElementById('ynkVinStatus'); if (status) { status.textContent = 'Decodifica el VIN para autocompletar marca, modelo y año.'; status.className = 'ynk-hint'; } pendingPhotos.forEach(function (p) { try { URL.revokeObjectURL(p.url); } catch (e) {} }); pendingPhotos = []; renderPhotoPreview(); var timerEl = document.getElementById('ynkTimer'); if (timerEl) timerEl.textContent = '—'; var nameEl = document.getElementById('ynkName'); if (nameEl) nameEl.focus(); } // ===================================================================== // TAB 3 — POR DONADOR // ===================================================================== var donorView = 'donors'; var donors = []; var donorsAbort = null; var donorPiecesAbort = null; function ynkSetDonorView(view) { donorView = view === 'pieces' ? 'pieces' : 'donors'; var segD = document.getElementById('ynkSegDonors'); var segP = document.getElementById('ynkSegPieces'); if (segD) segD.classList.toggle('is-active', donorView === 'donors'); if (segP) segP.classList.toggle('is-active', donorView === 'pieces'); var vd = document.getElementById('ynkDonorViewDonors'); var vp = document.getElementById('ynkDonorViewPieces'); if (vd) vd.style.display = donorView === 'donors' ? '' : 'none'; if (vp) vp.style.display = donorView === 'pieces' ? '' : 'none'; if (donorView === 'donors') loadDonors(1); else { loadDonorPieces(1); refreshSummary(); } } function loadDonors(page) { var body = document.getElementById('ynkDonorsBody'); if (!body) return; body.innerHTML = emptyRow(7, 'Cargando donadores…'); if (donorsAbort) donorsAbort.abort(); donorsAbort = new AbortController(); var params = { q: val('ynkDonorQ'), page: page || 1 }; apiFetch(API + '/donors?' + qs(params), { signal: donorsAbort.signal }) .then(function (res) { donorsAbort = null; if (!res) return; donors = asList(res); if (!donors.length) { body.innerHTML = emptyRow(7, 'Sin donadores registrados. Da de alta una pieza con VIN del donador para verla aquí.'); document.getElementById('ynkDonorsFooter').textContent = '0 donadores'; document.getElementById('ynkDonorsPagination').innerHTML = ''; return; } body.innerHTML = donors.map(function (d, i) { return '' + '' + (esc(d.donor_vin) || '—') + '' + '' + (esc(d.donor_make) || '—') + '' + '' + (esc(d.donor_model) || '—') + '' + '' + (esc(d.donor_year) || '—') + '' + '' + fmtInt(d.pieces) + '' + '' + fmtMoney(d.total_value) + '' + '' + ''; }).join(''); var pg = res.pagination || {}; document.getElementById('ynkDonorsFooter').textContent = fmtInt(pg.total !== undefined ? pg.total : donors.length) + ' donadores'; renderPagination(document.getElementById('ynkDonorsPagination'), pg, 'ynkDonorsPage'); }) .catch(function (err) { if (!err || err.name === 'AbortError') return; donorsAbort = null; body.innerHTML = emptyRow(7, 'Error al cargar donadores: ' + errMsg(err)); toast('Error al cargar donadores: ' + errMsg(err), 'error'); }); } function ynkDonorToPieces(i) { var d = donors[i]; if (!d) return; document.getElementById('ynkDonorMake').value = d.donor_make || ''; document.getElementById('ynkDonorModel').value = d.donor_model || ''; document.getElementById('ynkDonorYear').value = d.donor_year || ''; document.getElementById('ynkDonorVin').value = d.donor_vin || ''; document.getElementById('ynkDonorPieceQ').value = ''; ynkSetDonorView('pieces'); } function loadDonorPieces(page) { var body = document.getElementById('ynkDonorPiecesBody'); if (!body) return; body.innerHTML = emptyRow(10, 'Buscando piezas del donador…'); if (donorPiecesAbort) donorPiecesAbort.abort(); donorPiecesAbort = new AbortController(); var params = { donor_make: val('ynkDonorMake'), donor_model: val('ynkDonorModel'), donor_year: val('ynkDonorYear'), vin: val('ynkDonorVin'), q: val('ynkDonorPieceQ'), page: page || 1, per_page: 50 }; apiFetch(API + '/pieces?' + qs(params), { signal: donorPiecesAbort.signal }) .then(function (res) { donorPiecesAbort = null; if (!res) return; var items = asList(res); if (!items.length) { body.innerHTML = emptyRow(10, 'No hay piezas de ese donador con los filtros actuales.'); document.getElementById('ynkDonorPiecesFooter').textContent = '0 piezas'; document.getElementById('ynkDonorPiecesPagination').innerHTML = ''; return; } body.innerHTML = items.map(function (it) { pieceById[it.id] = it; return '' + '' + thumbHtml(it) + '' + '' + (esc(it.part_number) || '—') + '' + '' + esc(it.name) + '' + '' + gradeHtml(it.condition_grade) + '' + '' + conditionBadge(it.condition) + '' + '' + donorHtml(it) + '' + '' + fmtMoney(it.price_1) + '' + '' + fmtInt(it.stock) + '' + '' + esc(it.location) + '' + '' + ''; }).join(''); var pg = res.pagination || {}; document.getElementById('ynkDonorPiecesFooter').textContent = fmtInt(pg.total !== undefined ? pg.total : items.length) + ' piezas'; renderPagination(document.getElementById('ynkDonorPiecesPagination'), pg, 'ynkDonorPiecesPage'); }) .catch(function (err) { if (!err || err.name === 'AbortError') return; donorPiecesAbort = null; body.innerHTML = emptyRow(10, 'Error al buscar piezas: ' + errMsg(err)); toast('Error al buscar piezas del donador: ' + errMsg(err), 'error'); }); } function ynkOpenPieceFromSearch(id) { var it = pieceById[id]; if (!it) { toast('Recarga la pestaña "Piezas usadas" para editar esta pieza.', 'warn'); return; } openEditPiece(it); } // ===================================================================== // TAB 4 — CONSIGNACIONES // ===================================================================== var consignments = []; var currentConsignmentId = null; function loadConsignments() { var body = document.getElementById('ynkConsignBody'); if (!body) return; body.innerHTML = emptyRow(11, 'Cargando consignaciones…'); apiFetch(API + '/consignments') .then(function (res) { if (!res) return; consignments = asList(res); renderConsignments(); loadConsignmentOptions(); }) .catch(function (err) { body.innerHTML = emptyRow(11, 'Error al cargar consignaciones: ' + errMsg(err)); toast('Error al cargar consignaciones: ' + errMsg(err), 'error'); }); } function renderConsignments() { var body = document.getElementById('ynkConsignBody'); if (!body) return; var q = val('ynkConsignQ').toLowerCase(); var rows = consignments.filter(function (c) { if (!q) return true; return [(c.consignor_name || ''), (c.consignor_rfc || ''), (c.consignor_phone || '')] .join(' ').toLowerCase().indexOf(q) !== -1; }); var open = consignments.filter(function (c) { return (c.status || 'open') === 'open'; }).length; setStat('ynkStatConsignments', open + ' / ' + consignments.length); var sub = document.getElementById('ynkStatConsignmentsSub'); if (sub) sub.textContent = 'Abiertas / total'; var badge = document.getElementById('badge-consignaciones'); if (badge) badge.textContent = open; if (!rows.length) { body.innerHTML = emptyRow(11, consignments.length ? 'Ninguna consigna coincide con el filtro.' : 'Sin consignaciones registradas todavía.'); document.getElementById('ynkConsignFooter').textContent = '0 consignaciones'; return; } body.innerHTML = rows.map(function (c) { var status = (c.status || 'open').toLowerCase(); var statusCls = status === 'open' ? 'badge--pending' : (status === 'settled' ? 'badge--ok' : 'badge--complete'); var pieces = c.pieces !== undefined ? c.pieces : (c.items_count !== undefined ? c.items_count : ((c.items || []).length || 0)); return '' + '' + esc(c.id) + '' + '' + esc(c.consignor_name) + '' + '' + esc(c.consignor_rfc) + '' + '' + esc(c.consignor_phone) + '' + '' + (c.commission_pct !== undefined && c.commission_pct !== null ? esc(c.commission_pct) + '%' : '—') + '' + '' + esc(pieces) + '' + '' + fmtMoney(c.total_sold) + '' + '' + fmtMoney(c.total_commission) + '' + '' + esc(status) + '' + '' + esc((c.created_at || '').substring(0, 10) || '—') + '' + '' + ' ' + '' + ''; }).join(''); document.getElementById('ynkConsignFooter').textContent = rows.length + ' consignaciones'; } function loadConsignmentOptions() { fillConsignmentSelect('ynkConsignmentId'); } function fillConsignmentSelect(selId) { var sel = document.getElementById(selId); if (!sel) return; apiFetch(API + '/consignments') .then(function (res) { if (!res) return; var all = asList(res); if (!consignments.length) consignments = all; var open = all.filter(function (c) { return (c.status || 'open') === 'open'; }); var current = sel.value; sel.innerHTML = '' + open.map(function (c) { return ''; }).join(''); if (current) sel.value = current; if (!open.length) { sel.innerHTML = ''; } }) .catch(function () { /* silent — the select is optional */ }); } function ynkOpenConsignmentModal() { ['ynkConsName', 'ynkConsRfc', 'ynkConsPhone', 'ynkConsCommission', 'ynkConsNotes'] .forEach(function (id) { var el = document.getElementById(id); if (el) el.value = ''; }); var res = document.getElementById('ynkConsResult'); if (res) { res.textContent = ''; res.className = 'ynk-save-result'; } openModal('ynkConsignModal'); setTimeout(function () { document.getElementById('ynkConsName').focus(); }, 50); } function ynkCreateConsignment() { var name = val('ynkConsName'); var res = document.getElementById('ynkConsResult'); var btn = document.getElementById('ynkConsCreateBtn'); if (!name) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'El nombre del consignante es obligatorio.'; } return; } var payload = { consignor_name: name }; var rfc = val('ynkConsRfc'); if (rfc) payload.consignor_rfc = rfc; var phone = val('ynkConsPhone'); if (phone) payload.consignor_phone = phone; var pct = numOrNull('ynkConsCommission'); if (pct !== null) payload.commission_pct = pct; var notes = val('ynkConsNotes'); if (notes) payload.notes = notes; if (btn) btn.disabled = true; if (res) { res.className = 'ynk-save-result'; res.textContent = 'Creando…'; } apiFetch(API + '/consignments', { method: 'POST', body: JSON.stringify(payload) }) .then(function (c) { if (!c) return; var created = unwrap(c); toast('Consignación creada: ' + (created.consignor_name || name), 'ok'); ynkCloseModal('ynkConsignModal'); loadConsignments(); }) .catch(function (err) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'No se pudo crear: ' + errMsg(err); } toast('Error al crear consignación: ' + errMsg(err), 'error'); }) .then(function () { if (btn) btn.disabled = false; }); } function ynkOpenConsignment(id) { currentConsignmentId = id; document.getElementById('ynkConsignDetailTitle').textContent = 'Consignación #' + id; document.getElementById('ynkConsignDetailInfo').textContent = 'Cargando…'; document.getElementById('ynkConsignItemsBody').innerHTML = emptyRow(6, 'Cargando piezas…'); document.getElementById('ynkSettleResult').innerHTML = ''; ['ynkItemInvId', 'ynkItemAgreed', 'ynkItemCommission'].forEach(function (i) { var el = document.getElementById(i); if (el) el.value = ''; }); var r = document.getElementById('ynkConsItemResult'); if (r) { r.textContent = ''; r.className = 'ynk-save-result'; } openModal('ynkConsignDetailModal'); loadConsignmentDetail(id); } function loadConsignmentDetail(id) { return apiFetch(API + '/consignments/' + id) .then(function (res) { if (!res) return null; var c = unwrap(res); var info = document.getElementById('ynkConsignDetailInfo'); var items = Array.isArray(c.items) ? c.items : []; if (info) { info.innerHTML = '' + esc(c.consignor_name || '—') + '' + (c.consignor_rfc ? ' · RFC ' + esc(c.consignor_rfc) : '') + (c.consignor_phone ? ' · Tel ' + esc(c.consignor_phone) : '') + (c.commission_pct !== undefined && c.commission_pct !== null ? ' · Comisión ' + esc(c.commission_pct) + '%' : '') + ' · Estado: ' + esc(c.status || 'open') + (c.notes ? '
' + esc(c.notes) + '' : ''); } var body = document.getElementById('ynkConsignItemsBody'); if (!items.length) { body.innerHTML = emptyRow(6, 'Esta consigna todavía no tiene piezas.'); } else { body.innerHTML = items.map(function (it) { var status = (it.status || '').toLowerCase(); var cls = status === 'sold' ? 'badge--ok' : (status === 'settled' ? 'badge--complete' : 'badge--pending'); return '' + '' + esc(it.inventory_id) + '' + '' + esc(it.part_number) + '' + '' + esc(it.name) + '' + '' + fmtMoney(it.agreed_price) + '' + '' + fmtMoney(it.commission_amount) + '' + '' + esc(status || '—') + '' + ''; }).join(''); } return c; }) .catch(function (err) { document.getElementById('ynkConsignItemsBody').innerHTML = emptyRow(6, 'Error al cargar el detalle: ' + errMsg(err)); toast('Error al cargar la consignación: ' + errMsg(err), 'error'); return null; }); } function ynkAddConsignmentItem() { if (!currentConsignmentId) return; var invId = intOrNull('ynkItemInvId'); var agreed = numOrNull('ynkItemAgreed'); var res = document.getElementById('ynkConsItemResult'); if (!invId) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'El ID de inventario es obligatorio.'; } return; } if (agreed === null) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'El precio acordado es obligatorio.'; } return; } var payload = { inventory_id: invId, agreed_price: agreed }; var commission = numOrNull('ynkItemCommission'); if (commission !== null) payload.commission_amount = commission; if (res) { res.className = 'ynk-save-result'; res.textContent = 'Agregando pieza…'; } apiFetch(API + '/consignments/' + currentConsignmentId + '/items', { method: 'POST', body: JSON.stringify(payload) }) .then(function () { if (res) { res.className = 'ynk-save-result ynk-ok'; res.textContent = 'Pieza agregada a la consigna.'; } toast('Pieza agregada a la consigna', 'ok'); ['ynkItemInvId', 'ynkItemAgreed', 'ynkItemCommission'].forEach(function (i) { var el = document.getElementById(i); if (el) el.value = ''; }); loadConsignmentDetail(currentConsignmentId); loadConsignments(); }) .catch(function (err) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'No se pudo agregar: ' + errMsg(err); } toast('Error al agregar la pieza: ' + errMsg(err), 'error'); }); } function ynkSettleConsignment() { if (!currentConsignmentId) return; if (!window.confirm('¿Liquidar la consignación #' + currentConsignmentId + '? Esta acción marca la consigna como liquidada.')) return; var btn = document.getElementById('ynkSettleBtn'); var box = document.getElementById('ynkSettleResult'); if (btn) btn.disabled = true; if (box) box.innerHTML = '

Liquidando…

'; apiFetch(API + '/consignments/' + currentConsignmentId + '/settle', { method: 'PUT', body: JSON.stringify({}) }) .then(function (summary) { if (!summary) return; var s = unwrap(summary); if (box) box.innerHTML = renderSettlementSummary(s); toast('Consignación liquidada', 'ok'); loadConsignmentDetail(currentConsignmentId); loadConsignments(); }) .catch(function (err) { if (box) box.innerHTML = '

No se pudo liquidar: ' + esc(errMsg(err)) + '

'; toast('Error al liquidar: ' + errMsg(err), 'error'); }) .then(function () { if (btn) btn.disabled = false; }); } // The settle response shape is not fixed by the contract, so render any // scalar key it returns with a friendly label, and any items[] as a table. var SUMMARY_LABELS = { settled_count: 'Piezas liquidadas', count: 'Piezas liquidadas', items_settled: 'Piezas liquidadas', items_count: 'Piezas liquidadas', total_sold: 'Total precio acordado', total_agreed: 'Total precio acordado', agreed_total: 'Total precio acordado', total_commission: 'Comisión total', commission_total: 'Comisión total', total_payout: 'A pagar al consignante', payout_total: 'A pagar al consignante', consignor_payout: 'A pagar al consignante', total_to_pay_consignor: 'A pagar al consignante', consignor_name: 'Consignante', total: 'Total', status: 'Estado', message: 'Resultado' }; var MONEY_KEYS = /(price|amount|total|commission|payout|value|subtotal)/i; function renderSettlementSummary(s) { var keys = Object.keys(s || {}).filter(function (k) { return k !== 'items' && s[k] !== null && typeof s[k] !== 'object'; }); var html = '

Resumen de liquidación

'; if (!keys.length) html += '

La consigna quedó marcada como liquidada.

'; keys.forEach(function (k) { var label = SUMMARY_LABELS[k] || k.replace(/_/g, ' '); var v = s[k]; var shown = (typeof v === 'number' && MONEY_KEYS.test(k)) ? fmtMoney(v) : esc(v); html += '
' + esc(label) + '' + shown + '
'; }); var items = Array.isArray(s.items) ? s.items : []; if (items.length) { html += '
' + items.map(function (it) { return '
' + esc([it.part_number, it.name].filter(Boolean).join(' · ') || ('ID ' + (it.inventory_id || '—'))) + '' + fmtMoney(it.commission_amount || it.agreed_price) + '
'; }).join('') + '
'; } return html + '
'; } // ===================================================================== // EDIT PIECE (modal) // ===================================================================== var editTarget = null; function ynkOpenEditPiece(id) { var it = pieceById[id]; if (!it) return; openEditPiece(it); } function openEditPiece(it) { editTarget = it; document.getElementById('ynkEditId').value = it.id; document.getElementById('ynkEditName').value = it.name || ''; document.getElementById('ynkEditPart').value = it.part_number || ''; document.getElementById('ynkEditBrand').value = it.brand || ''; document.getElementById('ynkEditPrice').value = it.price_1 !== undefined && it.price_1 !== null ? it.price_1 : ''; document.getElementById('ynkEditCost').value = it.cost !== undefined && it.cost !== null ? it.cost : ''; document.getElementById('ynkEditCondition').value = it.condition || 'usado'; document.getElementById('ynkEditGrade').value = it.condition_grade || ''; document.getElementById('ynkEditOwnership').value = it.ownership || 'propio'; document.getElementById('ynkEditLocation').value = it.location || ''; document.getElementById('ynkEditVin').value = it.donor_vin || ''; document.getElementById('ynkEditMake').value = it.donor_make || ''; document.getElementById('ynkEditModel').value = it.donor_model || ''; document.getElementById('ynkEditYear').value = it.donor_year || ''; document.getElementById('ynkEditKm').value = it.donor_mileage_km || ''; document.getElementById('ynkEditUnique').checked = !!it.is_unique_piece; var res = document.getElementById('ynkEditResult'); if (res) { res.textContent = ''; res.className = 'ynk-save-result'; } openModal('ynkEditModal'); } function ynkSaveEditPiece() { var id = document.getElementById('ynkEditId').value; if (!id) return; var res = document.getElementById('ynkEditResult'); var btn = document.getElementById('ynkEditSaveBtn'); var name = val('ynkEditName'); var price = numOrNull('ynkEditPrice'); if (!name) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'El nombre es obligatorio.'; } return; } if (price === null) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'El precio es obligatorio.'; } return; } var payload = { name: name, price_1: price, condition: document.getElementById('ynkEditCondition').value, ownership: document.getElementById('ynkEditOwnership').value, is_unique_piece: !!document.getElementById('ynkEditUnique').checked, condition_grade: document.getElementById('ynkEditGrade').value || null, location: val('ynkEditLocation') || null, donor_vin: (val('ynkEditVin') || '').toUpperCase() || null, donor_make: val('ynkEditMake') || null, donor_model: val('ynkEditModel') || null, donor_year: intOrNull('ynkEditYear'), donor_mileage_km: intOrNull('ynkEditKm'), cost: numOrNull('ynkEditCost') }; if (btn) btn.disabled = true; if (res) { res.className = 'ynk-save-result'; res.textContent = 'Guardando…'; } apiFetch(API + '/pieces/' + id, { method: 'PUT', body: JSON.stringify(payload) }) .then(function (updated) { var u = updated ? unwrap(updated) : null; if (u && u.id) { pieceById[u.id] = u; } else if (editTarget) { pieceById[editTarget.id] = Object.assign({}, editTarget, payload); } toast('Pieza actualizada', 'ok'); ynkCloseModal('ynkEditModal'); if (activeTab === 'piezas') loadPieces(1); if (activeTab === 'donador' && donorView === 'pieces') loadDonorPieces(1); refreshSummary(); }) .catch(function (err) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'No se pudo guardar: ' + errMsg(err); } toast('Error al guardar la pieza: ' + errMsg(err), 'error'); }) .then(function () { if (btn) btn.disabled = false; }); } // ===================================================================== // PIECE IMAGES (modal) // ===================================================================== var imagesTargetId = null; function ynkOpenImages(id) { var it = pieceById[id]; imagesTargetId = id; document.getElementById('ynkImagesTitle').textContent = 'Fotos · ' + (it && it.name ? it.name : 'pieza #' + id); var status = document.getElementById('ynkImagesStatus'); if (status) status.textContent = ''; renderImagesGrid(it); openModal('ynkImagesModal'); } function renderImagesGrid(it) { var grid = document.getElementById('ynkImagesGrid'); if (!grid) return; var imgs = (it && it.images) || []; if (!imgs.length && it && it.image_url) imgs = [{ id: null, url: it.image_url, is_primary: true }]; if (!imgs.length) { grid.innerHTML = '

Esta pieza no tiene fotos todavía.

'; return; } grid.innerHTML = imgs.map(function (im) { return '
foto' + (im.is_primary ? 'principal' : '') + (canEdit() && im.id ? '' : '') + '
'; }).join(''); } function ynkUploadImagesTo(id) { return function (fileList) { var files = Array.prototype.slice.call(fileList || []).filter(isAllowedImage); if (!files.length) return; var status = document.getElementById('ynkImagesStatus'); if (status) status.textContent = 'Subiendo ' + files.length + ' foto(s)…'; var fd = new FormData(); files.forEach(function (f) { fd.append('file', f, f.name); }); apiUpload(API + '/pieces/' + id + '/images', fd) .then(function (resp) { var it = pieceById[id] || {}; if (resp && Array.isArray(resp.images)) it.images = resp.images; pieceById[id] = it; renderImagesGrid(it); if (status) status.textContent = 'Fotos actualizadas.'; toast('Fotos actualizadas', 'ok'); if (activeTab === 'piezas') loadPieces(1); }) .catch(function (err) { if (status) status.textContent = 'Error al subir: ' + errMsg(err); toast('Error al subir fotos: ' + errMsg(err), 'error'); }); }; } function ynkDeleteImage(imageId) { var id = imagesTargetId; if (!id || !imageId) return; if (!window.confirm('¿Eliminar esta foto?')) return; apiFetch(API + '/pieces/' + id + '/images/' + imageId, { method: 'DELETE' }) .then(function () { var it = pieceById[id] || {}; it.images = (it.images || []).filter(function (im) { return im.id !== imageId; }); pieceById[id] = it; renderImagesGrid(it); toast('Foto eliminada', 'ok'); if (activeTab === 'piezas') loadPieces(1); }) .catch(function (err) { toast('Error al eliminar la foto: ' + errMsg(err), 'error'); }); } // ===================================================================== // SHARED HELPERS — tolerant parsing + generic rendering // The newer report/compat/duplicate endpoints do not pin every response // key, so parse defensively and render whatever comes back. // ===================================================================== var LIST_KEYS = ['data', 'items', 'rows', 'results', 'vehicles', 'compat', 'compatibilities', 'compatibility', 'duplicates', 'groups', 'pieces']; function anyList(res) { if (!res) return []; if (Array.isArray(res)) return res; for (var i = 0; i < LIST_KEYS.length; i++) { if (Array.isArray(res[LIST_KEYS[i]])) return res[LIST_KEYS[i]]; } if (res.data && typeof res.data === 'object') { for (var j = 0; j < LIST_KEYS.length; j++) { if (Array.isArray(res.data[LIST_KEYS[j]])) return res.data[LIST_KEYS[j]]; } } return []; } var FIELD_LABELS = { id: 'ID', part_number: 'No. Parte', name: 'Nombre', brand: 'Marca', condition: 'Condición', condition_grade: 'Grado', grade: 'Grado', price_1: 'Precio', price: 'Precio', cost: 'Costo', stock: 'Stock', location: 'Ubicación', ownership: 'Propiedad', days_in_yard: 'Días en patio', donor_vin: 'VIN', donor_make: 'Marca', donor_model: 'Modelo', donor_year: 'Año', make: 'Marca', model: 'Modelo', year: 'Año', engine: 'Motor', inventory_id: 'ID Inv.', pieces: 'Piezas', count: 'Cantidad', qty: 'Cantidad', total: 'Total', total_value: 'Valor', value: 'Valor', reason: 'Motivo', score: 'Similitud', similarity: 'Similitud', status: 'Estado', agreed_price: 'Precio acordado', commission_amount: 'Comisión' }; function fieldLabel(k) { return FIELD_LABELS[k] || String(k).replace(/_/g, ' '); } function isMoneyKey(k) { return /(price|cost|amount|total|value|margin|margen|capital|payout|commission|precio|costo)/i.test(k); } function formatCell(k, v) { if (v === null || v === undefined || v === '') return '—'; if (k === 'condition_grade' || k === 'grade') return gradeHtml(v); if (typeof v === 'number') return isMoneyKey(k) ? fmtMoney(v) : esc(v); if (typeof v === 'object') return esc(JSON.stringify(v)); return esc(v); } function genericTableHtml(items, columns) { if (!items || !items.length) return '

Sin resultados.

'; var present = {}; items.forEach(function (it) { if (it && typeof it === 'object') Object.keys(it).forEach(function (k) { present[k] = 1; }); }); var cols = (columns || []).filter(function (c) { return present[c]; }); if (!cols.length) { var seen = {}; items.forEach(function (it) { if (!it || typeof it !== 'object') return; Object.keys(it).forEach(function (k) { if (k !== 'images' && !seen[k]) { seen[k] = 1; cols.push(k); } }); }); } if (!cols.length) { return '
' + esc(JSON.stringify(items, null, 2)) + '
'; } return '' + cols.map(function (c) { return ''; }).join('') + '' + items.map(function (it) { return '' + cols.map(function (c) { return ''; }).join('') + ''; }).join('') + '
' + esc(fieldLabel(c)) + '
' + formatCell(c, it ? it[c] : null) + '
'; } function kvHtml(obj, title) { var keys = Object.keys(obj || {}).filter(function (k) { var v = obj[k]; return v === null || v === undefined || typeof v !== 'object'; }); var html = '

' + esc(title) + '

'; if (!keys.length) html += '

Sin datos.

'; keys.forEach(function (k) { var v = obj[k]; var shown = (typeof v === 'number' && isMoneyKey(k)) ? fmtMoney(v) : esc(v); html += '
' + esc(fieldLabel(k)) + '' + shown + '
'; }); return html + '
'; } function anyCard(title, value) { if (value === undefined) { return '

' + esc(title) + '

Sin datos.

'; } if (Array.isArray(value)) { return '

' + esc(title) + '

' + '
' + genericTableHtml(value) + '
'; } if (value && typeof value === 'object') return kvHtml(value, title); var shown = (typeof value === 'number' && isMoneyKey(title)) ? fmtMoney(value) : esc(value); return '

' + esc(title) + '

' + '
' + shown + '
'; } function summaryInline(summary) { if (summary === null || summary === undefined || summary === '') return ''; if (typeof summary !== 'object') return String(summary); var parts = Object.keys(summary).filter(function (k) { return summary[k] === null || typeof summary[k] !== 'object'; }).map(function (k) { var v = summary[k]; return fieldLabel(k) + ': ' + ((typeof v === 'number' && isMoneyKey(k)) ? fmtMoney(v) : v); }); return parts.join(' · '); } // ===================================================================== // PIECE DETAIL / ACTIONS (ficha) // ===================================================================== var currentDetailId = null; var identifiedName = ''; var suggestedPrice = null; function fetchPiece(id) { return apiFetch(API + '/pieces/' + id) .then(function (res) { return unwrap(res); }) .catch(function (err) { toast('No se pudo cargar la pieza: ' + errMsg(err), 'error'); return null; }); } function ynkOpenPiece(id, action) { var it = pieceById[id]; if (it) { renderPieceDetail(it); } else { document.getElementById('ynkPieceTitle').textContent = 'Pieza #' + id; document.getElementById('ynkPieceInfo').textContent = 'Cargando…'; document.getElementById('ynkPiecePhotos').innerHTML = ''; } openModal('ynkPieceModal'); var p = it ? Promise.resolve(it) : fetchPiece(id); p.then(function (full) { if (!full || !full.id) return; pieceById[full.id] = full; renderPieceDetail(full); if (action === 'meli') ynkPublishMeli(); }).catch(function () {}); } function renderPieceDetail(it) { currentDetailId = it.id; document.getElementById('ynkPieceTitle').textContent = it.name || ('Pieza #' + it.id); var veh = [it.donor_make, it.donor_model, it.donor_year].filter(Boolean).join(' '); var info = document.getElementById('ynkPieceInfo'); info.innerHTML = '
SKU / No. parte' + (esc(it.part_number) || '—') + '
' + '
Marca' + (esc(it.brand) || '—') + '
' + '
Condición / grado' + esc(it.condition || '—') + ' · ' + gradeHtml(it.condition_grade) + '
' + '
Precio / costo' + fmtMoney(it.price_1) + ' / ' + fmtMoney(it.cost) + '
' + '
Stock' + fmtInt(it.stock) + (it.is_unique_piece ? ' (1 de 1)' : '') + '
' + '
Ubicación' + (esc(it.location) || '—') + '
' + '
Propiedad' + esc(it.ownership || 'propio') + '
' + '
Donador' + (esc(veh) || '—') + (it.donor_vin ? ' · ' + esc(it.donor_vin) : '') + '
'; renderPiecePhotos(it); pieceActionResult('', ''); var pres = document.getElementById('ynkCompatResult'); if (pres) { pres.textContent = ''; pres.className = 'ynk-save-result'; } ['ynkCompatMake', 'ynkCompatModel', 'ynkCompatYear', 'ynkCompatEngine'].forEach(function (i) { var el = document.getElementById(i); if (el) el.value = ''; }); loadCompat(it.id); } function renderPiecePhotos(it) { var grid = document.getElementById('ynkPiecePhotos'); if (!grid) return; var imgs = (it && it.images) || []; if (!imgs.length && it && it.image_url) imgs = [{ url: it.image_url }]; if (!imgs.length) { grid.innerHTML = 'Sin fotos.'; return; } grid.innerHTML = imgs.map(function (im) { return '
foto
'; }).join(''); } function pieceActionResult(html, cls) { var res = document.getElementById('ynkPieceActionResult'); if (!res) return; res.className = 'ynk-save-result' + (cls ? ' ' + cls : ''); res.innerHTML = html; } function ynkPublishMeli() { var id = currentDetailId; if (!id) return; var body = {}; var cat = val('ynkMeliCategory'); if (cat) body.category_id = cat; var price = numOrNull('ynkMeliPrice'); if (price !== null) body.price = price; var stock = intOrNull('ynkMeliStock'); if (stock !== null) body.stock = stock; pieceActionResult('Publicando en MercadoLibre…'); apiFetch(API + '/pieces/' + id + '/publish-meli', { method: 'POST', body: JSON.stringify(body) }) .then(function (d) { d = unwrap(d) || {}; if (d.ok) { var link = d.permalink ? ' ' + esc(d.permalink) + '' : ''; pieceActionResult('✔ Publicado en MeLi. Item: ' + esc(d.external_item_id || '—') + '' + link); toast('Pieza publicada en MercadoLibre', 'ok'); } else { pieceActionResult('No se pudo publicar: ' + esc(d.error || 'la plataforma rechazó la publicación') + ''); toast('No se pudo publicar en MeLi', 'error'); } }) .catch(function (err) { pieceActionResult('Error al publicar: ' + esc(errMsg(err)) + ''); toast('Error al publicar en MeLi: ' + errMsg(err), 'error'); }); } function ynkOpenLabel(id) { var pid = id || currentDetailId; if (!pid) return; window.open(API + '/pieces/' + pid + '/label', '_blank', 'noopener'); } function ynkEditFromDetail() { var it = pieceById[currentDetailId]; if (!it) return; ynkCloseModal('ynkPieceModal'); openEditPiece(it); } function ynkIdentifyFile(file) { var id = currentDetailId; if (!id || !file) return; var fd = new FormData(); fd.append('file', file, file.name); pieceActionResult('Analizando la foto…'); apiUpload(API + '/pieces/' + id + '/identify', fd) .then(function (raw) { var d = unwrap(raw) || {}; identifiedName = (d.name || '').trim(); var conf = (typeof d.confidence === 'number') ? Math.round(d.confidence * 100) + '%' : ''; if (!identifiedName && !d.category) { pieceActionResult('La IA no reconoció la pieza' + (d.error ? ': ' + esc(d.error) : '.') + ''); return; } var html = 'Identificado: ' + (esc(identifiedName) || '—') + '' + (d.category ? ' · Categoría: ' + esc(d.category) : '') + (conf ? ' · confianza ' + esc(conf) : ''); if (identifiedName) { html += ' '; } pieceActionResult(html); }) .catch(function (err) { pieceActionResult('No se pudo identificar: ' + esc(errMsg(err)) + ''); }); } function ynkApplyIdentified() { var id = currentDetailId; if (!id || !identifiedName) return; apiFetch(API + '/pieces/' + id, { method: 'PUT', body: JSON.stringify({ name: identifiedName }) }) .then(function (u) { var item = unwrap(u); if (item && item.id) pieceById[item.id] = item; else if (pieceById[id]) pieceById[id].name = identifiedName; pieceActionResult('✔ Nombre aplicado.'); toast('Nombre actualizado', 'ok'); if (activeTab === 'piezas') loadPieces(1); if (activeTab === 'donador' && donorView === 'pieces') loadDonorPieces(1); }) .catch(function (err) { pieceActionResult('No se pudo aplicar: ' + esc(errMsg(err)) + ''); }); } function ynkSuggestPrice() { var id = currentDetailId; if (!id) return; pieceActionResult('Calculando precio sugerido…'); apiFetch(API + '/pieces/' + id + '/price-suggest', { method: 'POST', body: JSON.stringify({}) }) .then(function (raw) { var d = unwrap(raw) || {}; suggestedPrice = (d.suggested_price === undefined || d.suggested_price === null) ? null : d.suggested_price; if (suggestedPrice === null && d.low === undefined) { pieceActionResult('No hay sugerencia disponible' + (d.error ? ': ' + esc(d.error) : '.') + ''); return; } var range = (d.low !== undefined && d.high !== undefined) ? ' · Rango: ' + fmtMoney(d.low) + ' – ' + fmtMoney(d.high) : ''; var html = 'Sugerido: ' + fmtMoney(suggestedPrice) + '' + range + (d.basis ? '
' + esc(d.basis) + '' : ''); if (suggestedPrice !== null) { html += ' '; } pieceActionResult(html); }) .catch(function (err) { pieceActionResult('No se pudo sugerir precio: ' + esc(errMsg(err)) + ''); }); } function ynkApplySuggestedPrice() { var id = currentDetailId; if (!id || suggestedPrice === null) return; apiFetch(API + '/pieces/' + id, { method: 'PUT', body: JSON.stringify({ price_1: suggestedPrice }) }) .then(function (u) { var item = unwrap(u); if (item && item.id) pieceById[item.id] = item; else if (pieceById[id]) pieceById[id].price_1 = suggestedPrice; toast('Precio aplicado: ' + fmtMoney(suggestedPrice), 'ok'); pieceActionResult('✔ Precio actualizado a ' + fmtMoney(suggestedPrice) + '.'); if (activeTab === 'piezas') loadPieces(1); }) .catch(function (err) { pieceActionResult('No se pudo aplicar: ' + esc(errMsg(err)) + ''); }); } function ynkValidatePhoto(file) { var id = currentDetailId; if (!id || !file) return; var fd = new FormData(); fd.append('file', file, file.name); pieceActionResult('Validando la foto…'); apiUpload(API + '/pieces/' + id + '/validate-photo', fd) .then(function (raw) { var d = unwrap(raw) || {}; var issues = d.issues || d.problems || []; if (d.ok && !(issues && issues.length)) { pieceActionResult('✔ La foto parece correcta.'); } else { var list = (issues && issues.length) ? issues.map(function (s) { return '
  • ' + esc(typeof s === 'object' ? (s.message || JSON.stringify(s)) : s) + '
  • '; }).join('') : '
  • ' + esc(d.error || 'no cumple los requisitos') + '
  • '; pieceActionResult('Revisar la foto:
      ' + list + '
    '); } }) .catch(function (err) { pieceActionResult('No se pudo validar: ' + esc(errMsg(err)) + ''); }); } function loadCompat(id) { var box = document.getElementById('ynkCompatList'); if (!box) return; box.innerHTML = 'Cargando compatibilidad…'; apiFetch(API + '/pieces/' + id + '/compat') .then(function (res) { var items = anyList(res); if (!items.length) { box.innerHTML = 'Sin compatibilidades registradas.'; return; } box.innerHTML = '
    ' + genericTableHtml(items, ['make', 'model', 'year', 'engine']) + '
    '; }) .catch(function (err) { box.innerHTML = 'No se pudo cargar la compatibilidad: ' + esc(errMsg(err)) + ''; }); } function ynkAddCompat() { var id = currentDetailId; if (!id) return; var make = val('ynkCompatMake'), model = val('ynkCompatModel'); var year = intOrNull('ynkCompatYear'), engine = val('ynkCompatEngine'); var res = document.getElementById('ynkCompatResult'); if (!make || !model || year === null) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'Marca, modelo y año son obligatorios.'; } return; } var body = { make: make, model: model, year: year }; if (engine) body.engine = engine; if (res) { res.className = 'ynk-save-result'; res.textContent = 'Agregando…'; } apiFetch(API + '/pieces/' + id + '/compat', { method: 'POST', body: JSON.stringify(body) }) .then(function () { if (res) { res.className = 'ynk-save-result ynk-ok'; res.textContent = '✔ Compatibilidad agregada.'; } toast('Compatibilidad agregada', 'ok'); ['ynkCompatMake', 'ynkCompatModel', 'ynkCompatYear', 'ynkCompatEngine'].forEach(function (i) { var el = document.getElementById(i); if (el) el.value = ''; }); loadCompat(id); }) .catch(function (err) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'No se pudo agregar: ' + errMsg(err); } }); } // ===================================================================== // CONSIGNMENT STATEMENT (estado de cuenta) // ===================================================================== var currentStatementId = null; function ynkOpenStatement(id) { currentStatementId = id; document.getElementById('ynkStatementTitle').textContent = 'Estado de cuenta · Consignación #' + id; document.getElementById('ynkStatementInfo').textContent = ''; document.getElementById('ynkStatementBody').innerHTML = '

    Cargando estado de cuenta…

    '; var from = document.getElementById('ynkStmtFrom'); var to = document.getElementById('ynkStmtTo'); if (from && !from.value) { var d = new Date(); from.value = new Date(d.getFullYear(), d.getMonth(), 1).toISOString().substring(0, 10); to.value = new Date(d.getFullYear(), d.getMonth() + 1, 0).toISOString().substring(0, 10); } openModal('ynkStatementModal'); ynkLoadStatement(); } function statementParams() { return qs({ from: val('ynkStmtFrom'), to: val('ynkStmtTo') }); } function ynkLoadStatement() { if (!currentStatementId) return; var body = document.getElementById('ynkStatementBody'); if (body) body.innerHTML = '

    Cargando estado de cuenta…

    '; var params = statementParams(); apiFetch(API + '/consignments/' + currentStatementId + '/statement' + (params ? '?' + params : '')) .then(function (res) { var s = unwrap(res) || {}; var info = document.getElementById('ynkStatementInfo'); if (info) { var period = s.period && typeof s.period === 'object' ? JSON.stringify(s.period) : s.period; info.innerHTML = '' + esc(s.consignor_name || '—') + '' + (period ? ' · Periodo: ' + esc(period) : ''); } if (!body) return; var items = anyList(res); var html = items.length ? genericTableHtml(items, ['inventory_id', 'part_number', 'name', 'condition_grade', 'price_1', 'cost', 'agreed_price', 'commission_amount', 'status']) : '

    Sin movimientos en el periodo.

    '; if (s.totals && typeof s.totals === 'object') html += kvHtml(s.totals, 'Totales'); else if (s.totals !== undefined) html += '

    Totales: ' + esc(s.totals) + '

    '; body.innerHTML = html; }) .catch(function (err) { if (body) body.innerHTML = '

    No se pudo cargar el estado de cuenta: ' + esc(errMsg(err)) + '

    '; }); } function ynkPrintStatement() { if (!currentStatementId) return; var params = statementParams(); window.open(API + '/consignments/' + currentStatementId + '/statement/print' + (params ? '?' + params : ''), '_blank', 'noopener'); } // ===================================================================== // TAB 5 — REPORTES // ===================================================================== var reportView = 'aging'; function ynkSetReportView(view) { reportView = view === 'summary' ? 'summary' : (view === 'duplicates' ? 'duplicates' : 'aging'); var segs = { aging: 'ynkRepSegAging', summary: 'ynkRepSegSummary', duplicates: 'ynkRepSegDup' }; Object.keys(segs).forEach(function (k) { var el = document.getElementById(segs[k]); if (el) el.classList.toggle('is-active', k === reportView); }); document.getElementById('ynkRepAging').style.display = reportView === 'aging' ? '' : 'none'; document.getElementById('ynkRepSummary').style.display = reportView === 'summary' ? '' : 'none'; document.getElementById('ynkRepDup').style.display = reportView === 'duplicates' ? '' : 'none'; if (reportView === 'aging') ynkLoadAging(); else if (reportView === 'summary') ynkLoadReportSummary(); else ynkLoadDuplicates(); } function ynkLoadAging() { var body = document.getElementById('ynkAgingBody'); if (!body) return; var days = intOrNull('ynkAgingDays'); if (days === null) days = 90; body.innerHTML = emptyRow(7, 'Cargando antigüedad…'); apiFetch(API + '/reports/aging?days=' + days) .then(function (res) { res = res || {}; var items = Array.isArray(res.data) ? res.data : anyList(res); var summary = document.getElementById('ynkAgingSummary'); if (summary) summary.textContent = summaryInline(res.summary); if (!items.length) { body.innerHTML = emptyRow(7, 'Sin piezas con ' + days + '+ días en patio.'); document.getElementById('ynkAgingFooter').textContent = '0 piezas'; return; } body.innerHTML = items.map(function (it) { return '' + '' + esc(it.id) + '' + '' + (esc(it.part_number) || '—') + '' + '' + esc(it.name) + '' + '' + gradeHtml(it.condition_grade) + '' + '' + fmtMoney(it.price_1) + '' + '' + fmtMoney(it.cost) + '' + '' + fmtInt(it.days_in_yard) + '' + ''; }).join(''); document.getElementById('ynkAgingFooter').textContent = items.length + ' pieza(s)'; }) .catch(function (err) { body.innerHTML = emptyRow(7, 'Error al cargar antigüedad: ' + errMsg(err)); }); } function ynkLoadReportSummary() { var box = document.getElementById('ynkRepSummaryBody'); if (!box) return; box.innerHTML = '

    Cargando resumen…

    '; apiFetch(API + '/reports/summary') .then(function (res) { var s = unwrap(res) || {}; var known = ['capital', 'margin', 'without_photo', 'by_grade', 'by_donor']; var hasKnown = known.some(function (k) { return s[k] !== undefined; }); var html = ''; if (hasKnown) { html += anyCard('Capital en inventario', s.capital); html += anyCard('Margen', s.margin); html += anyCard('Sin foto', s.without_photo); html += anyCard('Piezas por grado', s.by_grade); html += anyCard('Piezas por donador', s.by_donor); } else { html = anyCard('Resumen', s); } box.innerHTML = html || '

    Sin datos.

    '; var upd = document.getElementById('ynkRepSummaryUpdated'); if (upd) upd.textContent = 'Actualizado ' + new Date().toLocaleTimeString('es-MX'); }) .catch(function (err) { box.innerHTML = '

    Error al cargar resumen: ' + esc(errMsg(err)) + '

    '; }); } function duplicateSummary(it) { if (!it || typeof it !== 'object') return esc(it); var label = it.name || it.part_number || it.key || it.signature || it.label; return esc(label || 'Grupo') + (it.count !== undefined ? ' (' + esc(it.count) + ')' : ''); } function duplicateDetail(it) { if (it && Array.isArray(it.pieces) && it.pieces.length) { return '
    ' + it.pieces.map(function (p) { return '
    #' + esc(p.id) + ' ' + esc(p.part_number || '') + '' + esc(p.name || '') + ' · ' + fmtMoney(p.price_1) + '
    '; }).join('') + '
    '; } if (it && typeof it === 'object') { var parts = Object.keys(it).filter(function (k) { var v = it[k]; return v === null || typeof v !== 'object'; }).map(function (k) { var v = it[k]; return fieldLabel(k) + ': ' + ((typeof v === 'number' && isMoneyKey(k)) ? fmtMoney(v) : v); }); return '' + esc(parts.join(' · ')) + ''; } return esc(it); } function ynkLoadDuplicates() { var body = document.getElementById('ynkDupBody'); if (!body) return; var th = numOrNull('ynkDupThreshold'); body.innerHTML = emptyRow(3, 'Buscando duplicados…'); apiFetch(API + '/duplicates' + (th !== null ? '?threshold=' + th : '')) .then(function (res) { var items = anyList(res); if (!items.length) { body.innerHTML = emptyRow(3, 'No se encontraron duplicados con ese umbral.'); document.getElementById('ynkDupFooter').textContent = '0 coincidencias'; return; } var grouped = items.some(function (it) { return it && Array.isArray(it.pieces); }); if (grouped) { body.innerHTML = items.map(function (it, i) { return '' + (i + 1) + '' + '' + duplicateSummary(it) + '' + '' + duplicateDetail(it) + ''; }).join(''); } else { body.innerHTML = '
    ' + genericTableHtml(items) + '
    '; } document.getElementById('ynkDupFooter').textContent = items.length + ' coincidencia(s)'; }) .catch(function (err) { body.innerHTML = emptyRow(3, 'Error al buscar duplicados: ' + errMsg(err)); }); } // ===================================================================== // CSV IMPORT (pieces/bulk) // ===================================================================== function ynkOpenImportModal() { var f = document.getElementById('ynkImportFile'); if (f) f.value = ''; var n = document.getElementById('ynkImportFileName'); if (n) n.textContent = 'Sin archivo seleccionado'; var r = document.getElementById('ynkImportResult'); if (r) { r.textContent = ''; r.className = 'ynk-save-result'; } openModal('ynkImportModal'); } function postImport(field, file) { var fd = new FormData(); fd.append(field, file, file.name); return apiUpload(API + '/pieces/bulk', fd); } function renderImportResult(resp) { var res = document.getElementById('ynkImportResult'); if (!res) return; var d = unwrap(resp) || {}; var created = d.created !== undefined ? d.created : (d.inserted !== undefined ? d.inserted : (d.imported !== undefined ? d.imported : (d.count !== undefined ? d.count : undefined))); var errors = d.errors || d.error_rows || d.failed || []; var html = ''; if (created !== undefined) html += '
    ✔ Creadas: ' + esc(created) + ' pieza(s).
    '; if (Array.isArray(errors) && errors.length) { html += '
    ' + errors.length + ' error(es):
    ' + '
    ' + errors.map(function (e) { if (e && typeof e === 'object') { var rowLabel = e.row !== undefined ? 'Fila ' + e.row : (e.line !== undefined ? 'Línea ' + e.line : '—'); return '
    ' + esc(rowLabel) + '' + esc(e.error || e.message || JSON.stringify(e)) + '
    '; } return '
    ' + esc(e) + '
    '; }).join('') + '
    '; } else if (errors && !Array.isArray(errors)) { html += '
    ' + esc(errors) + '
    '; } if (!html) html = 'Respuesta: ' + esc(JSON.stringify(d)) + ''; res.className = 'ynk-save-result'; res.innerHTML = html; } function ynkSubmitImport() { var input = document.getElementById('ynkImportFile'); var file = input && input.files && input.files[0]; var res = document.getElementById('ynkImportResult'); var btn = document.getElementById('ynkImportBtn'); if (!file) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'Selecciona un archivo CSV.'; } return; } if (btn) { btn.disabled = true; btn.textContent = 'Importando…'; } if (res) { res.className = 'ynk-save-result'; res.textContent = 'Importando ' + file.name + '…'; } postImport('file', file) .catch(function (err) { // A missing-file 400 usually means the backend reads the part // under the `csv` field instead of `file`; retry once. Only for // a missing-file error, so nothing is imported twice. var msg = ((err && err.body && (err.body.error || err.body.message)) || '').toLowerCase(); if (err && err.status === 400 && /file|csv|multipart|missing|required|no se recib/.test(msg)) { return postImport('csv', file); } throw err; }) .then(function (resp) { renderImportResult(resp); toast('Importación procesada', 'ok'); refreshSummary(); if (activeTab === 'piezas') loadPieces(1); }) .catch(function (err) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'No se pudo importar: ' + errMsg(err); } toast('Error al importar: ' + errMsg(err), 'error'); }) .then(function () { if (btn) { btn.disabled = false; btn.textContent = 'Importar'; } }); } // ===================================================================== // INTAKE MODE — single piece / full donor (varias piezas) // ===================================================================== var intakeMode = 'single'; var GRADE_OPTS = '' + ''; function ynkSetIntakeMode(mode) { intakeMode = mode === 'full' ? 'full' : 'single'; document.getElementById('ynkSegSingle').classList.toggle('is-active', intakeMode === 'single'); document.getElementById('ynkSegFull').classList.toggle('is-active', intakeMode === 'full'); document.getElementById('ynkNuevaSimple').style.display = intakeMode === 'single' ? '' : 'none'; document.getElementById('ynkNuevaFull').style.display = intakeMode === 'full' ? '' : 'none'; if (intakeMode === 'full') { fillConsignmentSelect('ynkFullConsignmentId'); var rows = document.getElementById('ynkFullPiecesBody'); if (rows && !rows.children.length) { for (var i = 0; i < 3; i++) ynkFullAddRow(); } } } function ynkFullRowHtml() { return '' + '' + '' + '' + '' + '' + '' + ''; } function ynkFullAddRow() { var body = document.getElementById('ynkFullPiecesBody'); if (!body) return; body.insertAdjacentHTML('beforeend', ynkFullRowHtml()); } function ynkFullRemoveRow(btn) { var row = btn && btn.closest ? btn.closest('tr') : null; if (row && row.parentNode) row.parentNode.removeChild(row); } function ynkFullClearRows() { var body = document.getElementById('ynkFullPiecesBody'); if (body) body.innerHTML = ''; ynkFullAddRow(); } function ynkFullResetForm() { ynkFullClearRows(); var r = document.getElementById('ynkFullResult'); if (r) { r.textContent = ''; r.className = 'ynk-save-result'; } } function ynkFullDecodeVin() { var vin = val('ynkFullVin').toUpperCase(); var status = document.getElementById('ynkFullVinStatus'); if (!vin) { if (status) { status.textContent = 'Escribe un VIN primero.'; status.className = 'ynk-hint ynk-warn'; } return; } var btn = document.getElementById('ynkFullDecodeBtn'); if (btn) btn.disabled = true; if (status) { status.textContent = 'Decodificando ' + vin + '…'; status.className = 'ynk-hint'; } apiFetch(API + '/vin/' + encodeURIComponent(vin)) .then(function (d) { if (!d) return; var filled = []; if (d.make) { document.getElementById('ynkFullMake').value = d.make; filled.push(d.make); } if (d.model) { document.getElementById('ynkFullModel').value = d.model; filled.push(d.model); } if (d.year) { document.getElementById('ynkFullYear').value = d.year; filled.push(d.year); } if (status) { status.textContent = filled.length ? ('Autocompletado: ' + filled.join(' ')) : (d.error ? 'No se pudo decodificar: ' + d.error : 'VIN sin datos de vehículo.'); status.className = 'ynk-hint' + (filled.length ? ' ynk-ok' : ' ynk-warn'); } }) .catch(function (err) { if (status) { status.textContent = 'No se pudo decodificar: ' + errMsg(err); status.className = 'ynk-hint ynk-err'; } }) .then(function () { if (btn) btn.disabled = false; }); } function ynkReadFullRows() { var rows = document.querySelectorAll('#ynkFullPiecesBody tr.ynk-full-row'); var out = []; Array.prototype.forEach.call(rows, function (tr) { var name = (tr.querySelector('.ynk-full-name').value || '').trim(); var priceStr = (tr.querySelector('.ynk-full-price').value || '').trim(); if (!name && priceStr === '') return; // skip fully blank rows var piece = { name: name, price_1: priceStr === '' ? null : parseFloat(priceStr) }; var cost = (tr.querySelector('.ynk-full-cost').value || '').trim(); if (cost !== '') piece.cost = parseFloat(cost); var grade = tr.querySelector('.ynk-full-grade').value; if (grade) piece.condition_grade = grade; var loc = (tr.querySelector('.ynk-full-location').value || '').trim(); if (loc) piece.location = loc; out.push(piece); }); return out; } function ynkSubmitDonorFull() { var result = document.getElementById('ynkFullResult'); var btn = document.getElementById('ynkFullSaveBtn'); var vin = val('ynkFullVin').toUpperCase(); var make = val('ynkFullMake'); var model = val('ynkFullModel'); var year = intOrNull('ynkFullYear'); function fail(msg) { if (result) { result.className = 'ynk-save-result ynk-err'; result.textContent = msg; } } if (!vin || !make || !model || year === null) { fail('VIN, marca, modelo y año del donador son obligatorios.'); return; } var pieces = ynkReadFullRows(); if (!pieces.length) { fail('Agrega al menos una pieza con nombre y precio.'); return; } var bad = pieces.filter(function (p) { return !p.name || p.price_1 === null || isNaN(p.price_1); }); if (bad.length) { fail('Cada pieza necesita nombre y precio válidos.'); return; } var body = { vin: vin, make: make, model: model, year: year, pieces: pieces }; var mileage = intOrNull('ynkFullMileage'); if (mileage !== null) body.mileage_km = mileage; var color = val('ynkFullColor'); if (color) body.color = color; var ownership = document.getElementById('ynkFullOwnership').value; if (ownership) body.ownership = ownership; if (ownership === 'consigna') { var cid = document.getElementById('ynkFullConsignmentId').value; if (cid) body.consignment_id = parseInt(cid, 10); } if (btn) { btn.disabled = true; btn.textContent = 'Registrando…'; } if (result) { result.className = 'ynk-save-result'; result.textContent = 'Registrando ' + pieces.length + ' pieza(s) del donador ' + vin + '…'; } apiFetch(API + '/donors/full', { method: 'POST', body: JSON.stringify(body) }) .then(function (res) { var d = unwrap(res) || {}; var created = Array.isArray(d.pieces) ? d.pieces : []; var skus = created.map(function (p) { return p.part_number; }).filter(Boolean); var count = d.count !== undefined ? d.count : skus.length; var donorVin = d.donor_vin || vin; if (result) { result.className = 'ynk-save-result ynk-ok'; result.innerHTML = '✔ Donador ' + esc(donorVin) + ': ' + esc(count) + ' pieza(s) creada(s).' + (skus.length ? '
    SKUs: ' + skus.map(function (s) { return esc(s); }).join(', ') + '
    ' : ''); } toast(count + ' pieza(s) creadas', 'ok'); ynkFullClearRows(); var totalCost = numOrNull('ynkFullTotalCost'); if (totalCost !== null && totalCost > 0 && window.confirm('¿Prorratear el costo total ' + fmtMoney(totalCost) + ' entre las piezas del donador? Esto actualiza el costo de todas sus piezas.')) { apiFetch(API + '/donors/' + encodeURIComponent(donorVin) + '/allocate-cost', { method: 'POST', body: JSON.stringify({ total_cost: totalCost }) }) .then(function (r2) { var a = unwrap(r2) || {}; if (result) result.innerHTML += '
    Costo prorrateado' + (a.updated !== undefined ? ' en ' + esc(a.updated) + ' pieza(s)' : '') + '.
    '; }) .catch(function (err) { if (result) result.innerHTML += '
    No se pudo prorratear el costo: ' + esc(errMsg(err)) + '
    '; }); } refreshSummary(); if (activeTab === 'piezas') loadPieces(1); }) .catch(function (err) { if (result) { result.className = 'ynk-save-result ynk-err'; result.textContent = 'No se pudo registrar el donador: ' + errMsg(err); } toast('Error al registrar el donador: ' + errMsg(err), 'error'); }) .then(function () { if (btn) { btn.disabled = false; btn.textContent = 'Registrar donador y piezas'; } }); } function ynkSuggestGradeFromPhoto() { var status = document.getElementById('ynkGradeSuggestStatus'); var btn = document.getElementById('ynkGradeSuggestBtn'); if (!pendingPhotos.length) { if (status) { status.textContent = 'Primero elige al menos una foto.'; status.className = 'ynk-hint ynk-warn'; } return; } var fd = new FormData(); fd.append('file', pendingPhotos[0].file, pendingPhotos[0].file.name); var nm = val('ynkName'); if (nm) fd.append('name', nm); var cond = document.getElementById('ynkCondition').value; if (cond) fd.append('condition', cond); if (btn) btn.disabled = true; if (status) { status.textContent = 'Analizando la foto con IA…'; status.className = 'ynk-hint'; } apiUpload(API + '/grade-suggest', fd) .then(function (raw) { var d = unwrap(raw) || {}; var g = d.condition_grade_suggested; if (g) { document.getElementById('ynkGrade').value = g; var conf = (typeof d.confidence === 'number') ? ' (confianza ' + Math.round(d.confidence * 100) + '%)' : ''; if (status) { status.textContent = 'Grado sugerido: ' + g + conf + (d.notes ? ' — ' + d.notes : ''); status.className = 'ynk-hint ynk-ok'; } } else if (status) { status.textContent = 'La IA no pudo sugerir un grado' + (d.error ? ': ' + d.error : '.'); status.className = 'ynk-hint ynk-warn'; } }) .catch(function (err) { if (status) { status.textContent = 'No se pudo sugerir grado: ' + errMsg(err); status.className = 'ynk-hint ynk-err'; } }) .then(function () { if (btn) btn.disabled = false; }); } // ===================================================================== // TAB 6 — MAYOREO B2B // Flujo: publicas una pieza (vender) → otro negocio la compra (catálogo) // → el vendedor confirma y marca enviado → el comprador marca recibido. // // Contract: /pos/api/b2b // GET /my-listings GET /catalog GET /listings/ // POST /listings PATCH /listings/ DELETE /listings/ // POST /orders GET /orders GET /orders/incoming // GET /orders/ PUT /orders//status // GET /seller/status // ===================================================================== var B2B = '/pos/api/b2b'; var b2bView = 'catalog'; // catalog | listings | orders var b2bOrdersView = 'incoming'; // incoming (vendedor) | purchases (comprador) var b2bInitDone = false; var b2bSeller = null; // {is_seller, listings_count, incoming_orders} var b2bCatalog = []; var b2bListings = []; var b2bOrders = []; var b2bCatalogAbort = null; var b2bBuyTarget = null; // listing being purchased var b2bOrderDetail = null; // {id, side} of the open detail modal function setDisplay(id, show) { var el = document.getElementById(id); if (el) el.style.display = show ? '' : 'none'; } function b2bCounterparty(o, sellerSide) { if (sellerSide) { return o.buyer_name || o.buyer_tenant_name || (o.buyer_tenant_id ? '#' + o.buyer_tenant_id : 'Comprador'); } return o.seller_name || o.seller_tenant_name || (o.seller_tenant_id ? '#' + o.seller_tenant_id : 'Vendedor'); } function b2bSellerLabel(it) { if (it.seller_name) return it.seller_name; if (it.seller_tenant_id) return '#' + it.seller_tenant_id; return '—'; } var B2B_ORDER_STATUS = { pending: { label: 'Pendiente', cls: 'badge--pending' }, new: { label: 'Nuevo', cls: 'badge--pending' }, paid: { label: 'Pagado', cls: 'badge--transit' }, processing: { label: 'En proceso', cls: 'badge--transit' }, confirmed: { label: 'Confirmado', cls: 'badge--transit' }, shipped: { label: 'Enviado', cls: 'badge--new' }, sent: { label: 'Enviado', cls: 'badge--new' }, received: { label: 'Recibido', cls: 'badge--ok' }, delivered: { label: 'Entregado', cls: 'badge--ok' }, completed: { label: 'Completado', cls: 'badge--ok' }, cancelled: { label: 'Cancelado', cls: 'badge--cancelled' }, canceled: { label: 'Cancelado', cls: 'badge--cancelled' } }; function b2bStatusBadge(status) { var s = (status || '').toLowerCase(); var meta = B2B_ORDER_STATUS[s] || { label: status || '—', cls: 'badge--pending' }; return '' + esc(meta.label) + ''; } function ynkB2bInit() { if (b2bInitDone) { ynkB2bView(b2bView); return; } b2bInitDone = true; var q = document.getElementById('ynkB2bQ'); if (q) q.addEventListener('input', debounce(function () { loadB2bCatalog(1); }, 350)); ['ynkB2bMake', 'ynkB2bModel', 'ynkB2bYear', 'ynkB2bGrade'].forEach(function (id) { var el = document.getElementById(id); if (el) el.addEventListener('change', function () { loadB2bCatalog(1); }); }); var pick = document.getElementById('ynkB2bPublishPiece'); if (pick) { pick.addEventListener('change', function () { var opt = this.options[this.selectedIndex]; var p = opt ? opt.getAttribute('data-price') : ''; var priceEl = document.getElementById('ynkB2bPublishPrice'); if (priceEl) priceEl.value = (p === null || p === undefined || p === '') ? '' : p; }); } loadB2bSellerStatus().then(refreshB2bBadge); ynkB2bView(b2bView); } function ynkB2bView(view) { b2bView = (view === 'listings' || view === 'orders') ? view : 'catalog'; var segs = { catalog: 'ynkB2bSegCatalog', listings: 'ynkB2bSegListings', orders: 'ynkB2bSegOrders' }; Object.keys(segs).forEach(function (k) { var el = document.getElementById(segs[k]); if (!el) return; el.classList.toggle('is-active', k === b2bView); el.setAttribute('aria-selected', k === b2bView ? 'true' : 'false'); }); setDisplay('ynkB2bCatalog', b2bView === 'catalog'); setDisplay('ynkB2bListings', b2bView === 'listings'); setDisplay('ynkB2bOrders', b2bView === 'orders'); if (b2bView === 'catalog') loadB2bCatalog(1); else if (b2bView === 'listings') loadB2bListings(); else ynkB2bOrdersView(b2bOrdersView); } function ynkB2bRefresh() { toast('Actualizando mayoreo…', 'info'); loadB2bSellerStatus().then(refreshB2bBadge); if (b2bView === 'catalog') loadB2bCatalog(1); else if (b2bView === 'listings') loadB2bListings(); else loadB2bOrders(); } // ─── Vendedor / estado de la cuenta ────────────────────────────────── function loadB2bSellerStatus() { return apiFetch(B2B + '/seller/status') .then(function (res) { b2bSeller = unwrap(res) || null; renderB2bSellerStatus(); refreshB2bBadge(); return b2bSeller; }) .catch(function () { b2bSeller = null; renderB2bSellerStatus(); refreshB2bBadge(); return null; }); } function renderB2bSellerStatus() { var box = document.getElementById('ynkB2bSellerStatus'); var btn = document.getElementById('ynkB2bPublishBtn'); if (!box) return; if (!b2bSeller) { box.textContent = ''; return; } var isSeller = !!b2bSeller.is_seller; box.className = 'ynk-hint' + (isSeller ? ' ynk-ok' : ' ynk-warn'); box.textContent = isSeller ? '✔ Habilitado para vender en mayoreo · ' + fmtInt(b2bSeller.listings_count) + ' publicación(es) · ' + fmtInt(b2bSeller.incoming_orders) + ' pedido(s) recibido(s)' : 'Tu negocio no está habilitado para vender en mayoreo (is_seller: false). Puedes comprar en el catálogo, ' + 'pero para publicar piezas contacta al administrador.'; if (btn) { btn.disabled = !isSeller; btn.title = isSeller ? '' : 'Tu negocio no está habilitado para vender en mayoreo'; } } function refreshB2bBadge() { var b = document.getElementById('badge-b2b'); if (!b) return; b.textContent = b2bSeller ? fmtInt(b2bSeller.incoming_orders) : '0'; } // ─── Catálogo (comprar) ────────────────────────────────────────────── function loadB2bCatalog(page) { var grid = document.getElementById('ynkB2bCatalogGrid'); if (!grid) return; grid.innerHTML = '

    Cargando catálogo…

    '; var params = { q: val('ynkB2bQ'), make: val('ynkB2bMake'), model: val('ynkB2bModel'), year: val('ynkB2bYear'), grade: val('ynkB2bGrade'), page: page || 1, per_page: 24 }; if (b2bCatalogAbort) b2bCatalogAbort.abort(); b2bCatalogAbort = new AbortController(); apiFetch(B2B + '/catalog?' + qs(params), { signal: b2bCatalogAbort.signal }) .then(function (res) { b2bCatalogAbort = null; if (!res) return; b2bCatalog = asList(res); var footer = document.getElementById('ynkB2bCatalogFooter'); var pager = document.getElementById('ynkB2bCatalogPagination'); if (!b2bCatalog.length) { grid.innerHTML = '

    Sin piezas en mayoreo con esos filtros.

    '; if (footer) footer.textContent = '0 piezas'; if (pager) pager.innerHTML = ''; return; } grid.innerHTML = b2bCatalog.map(b2bCardHtml).join(''); var pg = res.pagination || {}; if (footer) footer.textContent = fmtInt(pg.total !== undefined ? pg.total : b2bCatalog.length) + ' piezas en mayoreo'; renderPagination(pager, pg, 'ynkB2bCatalogPage'); }) .catch(function (err) { if (!err || err.name === 'AbortError') return; b2bCatalogAbort = null; grid.innerHTML = '

    Error al cargar el catálogo: ' + esc(errMsg(err)) + '

    '; toast('Error al cargar el catálogo de mayoreo: ' + errMsg(err), 'error'); }); } function b2bCardHtml(it, i) { var img = it.image_url ? 'foto' : '
    sin foto
    '; var veh = [it.donor_make, it.donor_model, it.donor_year].filter(Boolean).join(' '); return '
    ' + img + '
    ' + '
    ' + (esc(it.name) || 'Pieza sin nombre') + '
    ' + '
    ' + gradeHtml(it.condition_grade) + '' + (esc(it.part_number) || 'sin No. parte') + '
    ' + (veh ? '
    Donador: ' + esc(veh) + '
    ' : '') + '
    Vendedor: ' + esc(b2bSellerLabel(it)) + '
    ' + '
    ' + fmtMoney(it.price_wholesale) + '
    ' + '
    ' + '' + '
    ' + '
    '; } window.ynkB2bImgFallback = function (img) { var box = img && img.parentNode; if (box) box.innerHTML = '
    sin foto
    '; }; function loadB2bCatalogUi() { loadB2bCatalog(1); } function ynkB2bClearCatalogFilters() { ['ynkB2bQ', 'ynkB2bMake', 'ynkB2bModel', 'ynkB2bYear', 'ynkB2bGrade'].forEach(function (id) { var el = document.getElementById(id); if (el) el.value = ''; }); loadB2bCatalog(1); } // ─── Comprar (crear orden) ─────────────────────────────────────────── function ynkB2bOpenBuy(i) { var it = b2bCatalog[i]; if (!it) return; b2bBuyTarget = it; var title = document.getElementById('ynkB2bBuyTitle'); if (title) title.textContent = 'Comprar: ' + (it.name || 'pieza'); var info = document.getElementById('ynkB2bBuyInfo'); if (info) { var veh = [it.donor_make, it.donor_model, it.donor_year].filter(Boolean).join(' '); info.innerHTML = gradeHtml(it.condition_grade) + ' ' + (esc(it.name) || '—') + '' + (it.part_number ? ' · ' + esc(it.part_number) : '') + (veh ? '
    Donador: ' + esc(veh) : '') + '
    Vendedor: ' + esc(b2bSellerLabel(it)) + ' · Mayoreo: ' + fmtMoney(it.price_wholesale) + ' c/u'; } var qty = document.getElementById('ynkB2bBuyQty'); if (qty) qty.value = '1'; var notes = document.getElementById('ynkB2bBuyNotes'); if (notes) notes.value = ''; var res = document.getElementById('ynkB2bBuyResult'); if (res) { res.className = 'ynk-save-result'; res.innerHTML = ''; } ynkB2bUpdateBuyTotal(); openModal('ynkB2bBuyModal'); } function ynkB2bUpdateBuyTotal() { var el = document.getElementById('ynkB2bBuyTotal'); if (!el) return; var qty = intOrNull('ynkB2bBuyQty') || 0; var price = b2bBuyTarget ? num(b2bBuyTarget.price_wholesale) : 0; el.textContent = fmtMoney(qty * price); } function ynkB2bSubmitOrder() { if (!b2bBuyTarget) return; var res = document.getElementById('ynkB2bBuyResult'); var btn = document.getElementById('ynkB2bBuyBtn'); var qty = intOrNull('ynkB2bBuyQty'); if (!qty || qty < 1) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'La cantidad debe ser al menos 1.'; } return; } var body = { items: [{ listing_id: b2bBuyTarget.id, quantity: qty }] }; var notes = val('ynkB2bBuyNotes'); if (notes) body.notes = notes; if (btn) { btn.disabled = true; btn.textContent = 'Creando orden…'; } if (res) { res.className = 'ynk-save-result'; res.textContent = 'Creando la orden…'; } apiFetch(B2B + '/orders', { method: 'POST', body: JSON.stringify(body) }) .then(function (raw) { var o = unwrap(raw) || {}; var total = (o.total !== undefined && o.total !== null) ? fmtMoney(o.total) : fmtMoney(qty * num(b2bBuyTarget.price_wholesale)); if (res) { res.className = 'ynk-save-result ynk-ok'; res.innerHTML = '✔ Orden #' + esc(o.id !== undefined ? o.id : '—') + ' creada — total ' + total + '' + (o.status ? ' · estado: ' + b2bStatusBadge(o.status) : '') + '
    El vendedor la verá en “Pedidos › Recibidos”.'; } toast('Orden #' + (o.id !== undefined ? o.id : '') + ' creada por ' + total, 'ok'); loadB2bSellerStatus(); }) .catch(function (err) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'No se pudo crear la orden: ' + errMsg(err); } toast('Error al crear la orden: ' + errMsg(err), 'error'); }) .then(function () { if (btn) { btn.disabled = false; btn.textContent = 'Crear orden'; } }); } // ─── Mis publicaciones (vender) ────────────────────────────────────── function loadB2bListings() { var body = document.getElementById('ynkB2bListingsBody'); if (!body) return; body.innerHTML = emptyRow(9, 'Cargando publicaciones…'); loadB2bSellerStatus(); apiFetch(B2B + '/my-listings') .then(function (res) { b2bListings = asList(res); renderB2bListings(); }) .catch(function (err) { body.innerHTML = emptyRow(9, 'Error al cargar publicaciones: ' + errMsg(err)); toast('Error al cargar tus publicaciones: ' + errMsg(err), 'error'); }); } function renderB2bListings() { var body = document.getElementById('ynkB2bListingsBody'); if (!body) return; var footer = document.getElementById('ynkB2bListingsFooter'); if (!b2bListings.length) { body.innerHTML = emptyRow(9, 'Aún no publicas piezas en mayoreo. Usa “Publicar pieza en mayoreo”.'); if (footer) footer.textContent = '0 publicaciones'; return; } body.innerHTML = b2bListings.map(b2bListingRow).join(''); if (footer) footer.textContent = b2bListings.length + ' publicación(es)'; } function b2bListingRow(it) { var veh = [it.donor_make, it.donor_model, it.donor_year].filter(Boolean).join(' '); var st = (it.status || '').toLowerCase(); var cls = (st === 'active' || st === 'published' || st === 'available') ? 'badge--ok' : (st === 'sold' ? 'badge--complete' : (st === 'paused' || st === 'pending' ? 'badge--pending' : 'badge--cancelled')); var img = it.image_url ? 'foto' : '
    sin
    foto
    '; var price = (it.price_wholesale === undefined || it.price_wholesale === null) ? '' : esc(it.price_wholesale); return '' + '' + img + '' + '' + (esc(it.part_number) || '—') + '' + '' + (esc(it.name) || '—') + '' + '' + gradeHtml(it.condition_grade) + '' + '' + (esc(veh) || '—') + '' + '' + ' ' + '' + '' + '' + esc(st || '—') + '' + '' + (esc(String(it.created_at || '').substring(0, 10)) || '—') + '' + '' + '' + ''; } function ynkB2bSavePrice(id) { var el = document.getElementById('ynkB2bPrice' + id); if (!el) return; var price = parseFloat(el.value); if (isNaN(price) || price < 0) { toast('Precio mayoreo inválido', 'warn'); return; } apiFetch(B2B + '/listings/' + id, { method: 'PATCH', body: JSON.stringify({ price_wholesale: price }) }) .then(function () { toast('Precio mayoreo actualizado: ' + fmtMoney(price), 'ok'); loadB2bListings(); }) .catch(function (err) { toast('No se pudo actualizar el precio: ' + errMsg(err), 'error'); }); } function ynkB2bRetire(id) { if (!window.confirm('¿Retirar esta pieza del mayoreo? Los compradores dejarán de verla.')) return; apiFetch(B2B + '/listings/' + id, { method: 'DELETE' }) .then(function () { toast('Publicación retirada', 'ok'); loadB2bListings(); loadB2bSellerStatus(); }) .catch(function (err) { toast('No se pudo retirar la publicación: ' + errMsg(err), 'error'); }); } function ynkB2bOpenPublish() { if (b2bSeller && !b2bSeller.is_seller) { toast('Tu negocio no está habilitado para vender en mayoreo.', 'warn'); return; } var sel = document.getElementById('ynkB2bPublishPiece'); if (sel) sel.innerHTML = ''; var price = document.getElementById('ynkB2bPublishPrice'); if (price) price.value = ''; var notes = document.getElementById('ynkB2bPublishNotes'); if (notes) notes.value = ''; var res = document.getElementById('ynkB2bPublishResult'); if (res) { res.className = 'ynk-save-result'; res.innerHTML = ''; } openModal('ynkB2bPublishModal'); loadB2bInventoryOptions(); } function loadB2bInventoryOptions() { var sel = document.getElementById('ynkB2bPublishPiece'); if (!sel) return; apiFetch(API + '/pieces?per_page=200') .then(function (res) { var items = asList(res); if (!items.length) { sel.innerHTML = ''; return; } sel.innerHTML = '' + items.map(function (p) { var label = '#' + p.id + ' · ' + (p.name || 'pieza') + (p.part_number ? ' (' + p.part_number + ')' : ''); return ''; }).join(''); }) .catch(function (err) { sel.innerHTML = ''; toast('No se pudieron cargar las piezas del inventario: ' + errMsg(err), 'error'); }); } function ynkB2bSubmitPublish() { var sel = document.getElementById('ynkB2bPublishPiece'); var invId = sel ? sel.value : ''; var price = numOrNull('ynkB2bPublishPrice'); var res = document.getElementById('ynkB2bPublishResult'); var btn = document.getElementById('ynkB2bPublishSubmitBtn'); if (!invId) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'Selecciona una pieza del inventario.'; } return; } if (price === null || price < 0) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'Indica un precio de mayoreo válido.'; } return; } var body = { inventory_id: parseInt(invId, 10), price_wholesale: price }; var notes = val('ynkB2bPublishNotes'); if (notes) body.notes = notes; if (btn) { btn.disabled = true; btn.textContent = 'Publicando…'; } if (res) { res.className = 'ynk-save-result'; res.textContent = 'Publicando…'; } apiFetch(B2B + '/listings', { method: 'POST', body: JSON.stringify(body) }) .then(function () { toast('Pieza publicada en mayoreo', 'ok'); ynkCloseModal('ynkB2bPublishModal'); ynkB2bView('listings'); }) .catch(function (err) { if (res) { res.className = 'ynk-save-result ynk-err'; res.textContent = 'No se pudo publicar: ' + errMsg(err); } toast('Error al publicar en mayoreo: ' + errMsg(err), 'error'); }) .then(function () { if (btn) { btn.disabled = false; btn.textContent = 'Publicar'; } }); } // ─── Pedidos (vendedor y comprador) ────────────────────────────────── function ynkB2bOrdersView(view) { b2bOrdersView = view === 'purchases' ? 'purchases' : 'incoming'; var inc = document.getElementById('ynkB2bSegIncoming'); var pur = document.getElementById('ynkB2bSegPurchases'); if (inc) { inc.classList.toggle('is-active', b2bOrdersView === 'incoming'); inc.setAttribute('aria-selected', b2bOrdersView === 'incoming' ? 'true' : 'false'); } if (pur) { pur.classList.toggle('is-active', b2bOrdersView === 'purchases'); pur.setAttribute('aria-selected', b2bOrdersView === 'purchases' ? 'true' : 'false'); } var sellerSide = b2bOrdersView === 'incoming'; var head = document.getElementById('ynkB2bOrdersCounterpartyHead'); if (head) head.textContent = sellerSide ? 'Comprador' : 'Vendedor'; var hint = document.getElementById('ynkB2bOrdersHint'); if (hint) hint.textContent = sellerSide ? 'Pedidos que otros negocios te hacen.' : 'Pedidos que hiciste a otros negocios.'; loadB2bOrders(); } function loadB2bOrders() { var body = document.getElementById('ynkB2bOrdersBody'); if (!body) return; body.innerHTML = emptyRow(7, 'Cargando pedidos…'); var sellerSide = b2bOrdersView === 'incoming'; apiFetch(B2B + (sellerSide ? '/orders/incoming' : '/orders')) .then(function (res) { b2bOrders = asList(res); renderB2bOrders(); }) .catch(function (err) { body.innerHTML = emptyRow(7, 'Error al cargar pedidos: ' + errMsg(err)); toast('Error al cargar los pedidos de mayoreo: ' + errMsg(err), 'error'); }); } function renderB2bOrders() { var body = document.getElementById('ynkB2bOrdersBody'); if (!body) return; var sellerSide = b2bOrdersView === 'incoming'; var footer = document.getElementById('ynkB2bOrdersFooter'); if (!b2bOrders.length) { body.innerHTML = emptyRow(7, sellerSide ? 'Todavía no has recibido pedidos de mayoreo.' : 'Todavía no has hecho compras de mayoreo.'); if (footer) footer.textContent = '0 pedidos'; return; } body.innerHTML = b2bOrders.map(function (o) { var items = b2bItems(o); var count = o.items_count !== undefined ? o.items_count : items.length; return '' + '' + esc(o.id) + '' + '' + esc(b2bCounterparty(o, sellerSide)) + '' + '' + fmtInt(count) + '' + '' + fmtMoney(o.total) + '' + '' + b2bStatusBadge(o.status) + '' + '' + (esc(String(o.created_at || '').substring(0, 10)) || '—') + '' + '' + b2bOrderActions(o, sellerSide, true) + '' + ''; }).join(''); if (footer) footer.textContent = b2bOrders.length + ' pedido(s)'; } function b2bItems(o) { var arr = o.items || o.order_items || o.lines || []; return Array.isArray(arr) ? arr : []; } // Action buttons depend on the order status and which side we are on. function b2bOrderActions(o, sellerSide, includeDetail) { var s = (o.status || '').toLowerCase(); var id = o.id; var btns = []; if (sellerSide) { if (s === 'pending' || s === 'new') { btns.push(''); } if (s === 'pending' || s === 'new' || s === 'confirmed' || s === 'paid' || s === 'processing') { btns.push(''); btns.push(''); } } else { if (s === 'shipped' || s === 'sent' || s === 'confirmed' || s === 'processing') { btns.push(''); } if (s === 'pending' || s === 'new' || s === 'confirmed') { btns.push(''); } } if (includeDetail) { btns.push(''); } return btns.join(' '); } function ynkB2bSetStatus(id, status) { var labels = { confirmed: 'confirmado', shipped: 'marcado como enviado', received: 'marcado como recibido', cancelled: 'cancelado' }; if (status === 'cancelled' && !window.confirm('¿Cancelar el pedido #' + id + '?')) return; apiFetch(B2B + '/orders/' + id + '/status', { method: 'PUT', body: JSON.stringify({ status: status }) }) .then(function () { toast('Pedido #' + id + ' ' + (labels[status] || status), 'ok'); loadB2bOrders(); loadB2bSellerStatus(); if (b2bOrderDetail && b2bOrderDetail.id === id) { ynkB2bOpenOrder(id, b2bOrderDetail.side); } }) .catch(function (err) { toast('No se pudo actualizar el pedido: ' + errMsg(err), 'error'); }); } function ynkB2bOpenOrder(id, side) { b2bOrderDetail = { id: id, side: side ? 1 : 0 }; var title = document.getElementById('ynkB2bOrderTitle'); if (title) title.textContent = 'Pedido #' + id; var body = document.getElementById('ynkB2bOrderBody'); if (body) body.innerHTML = 'Cargando pedido…'; var actions = document.getElementById('ynkB2bOrderActions'); if (actions) actions.innerHTML = ''; openModal('ynkB2bOrderModal'); apiFetch(B2B + '/orders/' + id) .then(function (raw) { var o = unwrap(raw) || {}; renderB2bOrderDetail(o, id, side); }) .catch(function (err) { if (body) body.innerHTML = 'No se pudo cargar el pedido: ' + esc(errMsg(err)) + ''; toast('Error al cargar el pedido: ' + errMsg(err), 'error'); }); } function renderB2bOrderDetail(o, id, side) { var sellerSide = !!side; var items = b2bItems(o); var body = document.getElementById('ynkB2bOrderBody'); var actions = document.getElementById('ynkB2bOrderActions'); var html = '

    Pedido #' + esc(o.id !== undefined ? o.id : id) + '

    ' + '
    Estado' + b2bStatusBadge(o.status) + '
    ' + '
    ' + (sellerSide ? 'Comprador' : 'Vendedor') + '' + esc(b2bCounterparty(o, sellerSide)) + '
    ' + '
    Total' + fmtMoney(o.total) + '
    ' + (o.created_at ? '
    Creado' + esc(String(o.created_at).substring(0, 19).replace('T', ' ')) + '
    ' : '') + (o.notes ? '
    Notas' + esc(o.notes) + '
    ' : '') + '
    '; if (items.length) { html += '
    ' + '' + '' + '' + items.map(function (it) { var qty = it.quantity !== undefined ? it.quantity : (it.qty !== undefined ? it.qty : 1); var unit = it.unit_price !== undefined ? it.unit_price : (it.price_wholesale !== undefined ? it.price_wholesale : it.price); var sub = it.subtotal !== undefined ? it.subtotal : (num(unit) * num(qty)); return '' + '' + '' + '' + '' + '' + ''; }).join('') + '
    PiezaNo. ParteCantidadP. unitarioSubtotal
    ' + (esc(it.name) || ('Pieza #' + esc(it.listing_id !== undefined ? it.listing_id : (it.inventory_id || '—')))) + '' + (esc(it.part_number) || '—') + '' + fmtInt(qty) + '' + fmtMoney(unit) + '' + fmtMoney(sub) + '
    '; } else { html += '

    El detalle de piezas no está disponible.

    '; } if (body) body.innerHTML = html; if (actions) actions.innerHTML = b2bOrderActions(o, sellerSide, false); } // ===================================================================== // GLOBAL EXPORTS (used by inline onclick handlers in yonke.html) // ===================================================================== window.switchTab = switchTab; window.ynkRefreshPieces = function () { refreshSummary(); loadPieces(1); toast('Lista actualizada', 'info'); }; window.ynkPiecesPage = function (p) { loadPieces(p); }; window.ynkResetPieceFilters = function () { document.getElementById('ynkQ').value = ''; document.getElementById('ynkFiltCondition').value = ''; document.getElementById('ynkFiltGrade').value = ''; document.getElementById('ynkFiltOwnership').value = ''; loadPieces(1); }; window.ynkDecodeVin = ynkDecodeVin; window.ynkRemovePhoto = ynkRemovePhoto; window.ynkSubmitPiece = ynkSubmitPiece; window.ynkResetPieceForm = function () { resetPieceForm(); stopIntakeTimer(); startIntakeTimer(); }; window.ynkSetDonorView = ynkSetDonorView; window.ynkSearchDonors = function (p) { loadDonors(p || 1); }; window.ynkDonorsPage = function (p) { loadDonors(p); }; window.ynkDonorToPieces = ynkDonorToPieces; window.ynkSearchDonorPieces = function (p) { loadDonorPieces(p || 1); }; window.ynkDonorPiecesPage = function (p) { loadDonorPieces(p); }; window.ynkClearDonorPieceFilters = function () { ['ynkDonorMake', 'ynkDonorModel', 'ynkDonorYear', 'ynkDonorVin', 'ynkDonorPieceQ'] .forEach(function (i) { var el = document.getElementById(i); if (el) el.value = ''; }); loadDonorPieces(1); }; window.ynkLoadConsignments = loadConsignments; window.ynkOpenConsignmentModal = ynkOpenConsignmentModal; window.ynkCreateConsignment = ynkCreateConsignment; window.ynkOpenConsignment = ynkOpenConsignment; window.ynkAddConsignmentItem = ynkAddConsignmentItem; window.ynkSettleConsignment = ynkSettleConsignment; window.ynkOpenEditPiece = ynkOpenEditPiece; window.ynkOpenPieceFromSearch = ynkOpenPieceFromSearch; window.ynkSaveEditPiece = ynkSaveEditPiece; window.ynkOpenImages = ynkOpenImages; window.ynkDeleteImage = ynkDeleteImage; // Piece detail / actions window.ynkOpenPiece = ynkOpenPiece; window.ynkOpenLabel = ynkOpenLabel; window.ynkPublishMeli = ynkPublishMeli; window.ynkSuggestPrice = ynkSuggestPrice; window.ynkApplySuggestedPrice = ynkApplySuggestedPrice; window.ynkApplyIdentified = ynkApplyIdentified; window.ynkAddCompat = ynkAddCompat; window.ynkEditFromDetail = ynkEditFromDetail; // Intake modes window.ynkSetIntakeMode = ynkSetIntakeMode; window.ynkFullAddRow = ynkFullAddRow; window.ynkFullRemoveRow = ynkFullRemoveRow; window.ynkFullClearRows = ynkFullClearRows; window.ynkFullResetForm = ynkFullResetForm; window.ynkFullDecodeVin = ynkFullDecodeVin; window.ynkSubmitDonorFull = ynkSubmitDonorFull; window.ynkSuggestGradeFromPhoto = ynkSuggestGradeFromPhoto; // Consignment statement window.ynkOpenStatement = ynkOpenStatement; window.ynkLoadStatement = ynkLoadStatement; window.ynkPrintStatement = ynkPrintStatement; // Reports window.ynkSetReportView = ynkSetReportView; window.ynkLoadAging = ynkLoadAging; window.ynkLoadReportSummary = ynkLoadReportSummary; window.ynkLoadDuplicates = ynkLoadDuplicates; // Mayoreo B2B window.ynkB2bView = ynkB2bView; window.ynkB2bRefresh = ynkB2bRefresh; window.ynkB2bOrdersView = ynkB2bOrdersView; window.ynkB2bOpenBuy = ynkB2bOpenBuy; window.ynkB2bUpdateBuyTotal = ynkB2bUpdateBuyTotal; window.ynkB2bSubmitOrder = ynkB2bSubmitOrder; window.ynkB2bOpenPublish = ynkB2bOpenPublish; window.ynkB2bSubmitPublish = ynkB2bSubmitPublish; window.ynkB2bSavePrice = ynkB2bSavePrice; window.ynkB2bRetire = ynkB2bRetire; window.ynkB2bSetStatus = ynkB2bSetStatus; window.ynkB2bOpenOrder = ynkB2bOpenOrder; window.ynkB2bCatalogPage = function (p) { loadB2bCatalog(p); }; window.ynkB2bClearCatalogFilters = ynkB2bClearCatalogFilters; window.loadB2bCatalogUi = loadB2bCatalogUi; // CSV import window.ynkOpenImportModal = ynkOpenImportModal; window.ynkSubmitImport = ynkSubmitImport; window.ynkCloseModal = function (id) { var el = document.getElementById(id); if (el) el.classList.remove('is-open'); }; // ===================================================================== // INIT // ===================================================================== function debounce(fn, ms) { var t = null; return function () { clearTimeout(t); t = setTimeout(fn, ms); }; } function init() { // Overlay click closes modals document.querySelectorAll('.inv-modal-overlay').forEach(function (ov) { ov.addEventListener('click', function (e) { if (e.target === ov) ov.classList.remove('is-open'); }); }); // Tab 1 — filters var q = document.getElementById('ynkQ'); if (q) q.addEventListener('input', debounce(function () { loadPieces(1); }, 350)); ['ynkFiltCondition', 'ynkFiltGrade', 'ynkFiltOwnership'].forEach(function (id) { var el = document.getElementById(id); if (el) el.addEventListener('change', function () { loadPieces(1); }); }); var perPage = document.getElementById('ynkFiltPerPage'); if (perPage) perPage.addEventListener('change', function () { loadPieces(1); }); // Tab 2 — intake var photos = document.getElementById('ynkPhotos'); if (photos) { photos.addEventListener('change', function () { addPhotos(this.files); this.value = ''; }); } var ownership = document.getElementById('ynkOwnership'); if (ownership) { ownership.addEventListener('change', function () { document.getElementById('ynkConsignmentWrap').style.display = this.value === 'consigna' ? '' : 'none'; }); } var vin = document.getElementById('ynkVin'); if (vin) { vin.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); ynkDecodeVin(); } }); } var panelNueva = document.getElementById('panel-nueva'); if (panelNueva) { panelNueva.addEventListener('keydown', function (e) { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); if (intakeMode === 'full') ynkSubmitDonorFull(); else ynkSubmitPiece(); } }); } // Tab 2b — full donor form var fullOwnership = document.getElementById('ynkFullOwnership'); if (fullOwnership) { fullOwnership.addEventListener('change', function () { document.getElementById('ynkFullConsignmentWrap').style.display = this.value === 'consigna' ? '' : 'none'; }); } var fullVin = document.getElementById('ynkFullVin'); if (fullVin) { fullVin.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); ynkFullDecodeVin(); } }); } // Piece detail — photo-driven tools var idInput = document.getElementById('ynkIdentifyInput'); if (idInput) { idInput.addEventListener('change', function () { if (this.files && this.files[0]) ynkIdentifyFile(this.files[0]); this.value = ''; }); } var valInput = document.getElementById('ynkValidateInput'); if (valInput) { valInput.addEventListener('change', function () { if (this.files && this.files[0]) ynkValidatePhoto(this.files[0]); this.value = ''; }); } // CSV import picker var importFile = document.getElementById('ynkImportFile'); if (importFile) { importFile.addEventListener('change', function () { var n = document.getElementById('ynkImportFileName'); if (n) n.textContent = (this.files && this.files[0]) ? this.files[0].name : 'Sin archivo seleccionado'; }); } // Tab 3 — donors var dq = document.getElementById('ynkDonorQ'); if (dq) dq.addEventListener('input', debounce(function () { loadDonors(1); }, 350)); var dpq = document.getElementById('ynkDonorPieceQ'); if (dpq) dpq.addEventListener('input', debounce(function () { loadDonorPieces(1); }, 350)); // Tab 4 — consignments var cq = document.getElementById('ynkConsignQ'); if (cq) cq.addEventListener('input', debounce(renderConsignments, 200)); // Images modal upload var imgInput = document.getElementById('ynkImagesInput'); if (imgInput) { imgInput.addEventListener('change', function () { if (!imagesTargetId) return; var fn = ynkUploadImagesTo(imagesTargetId); fn(this.files); this.value = ''; }); } // Hash deep links (#donador, #nueva, ...) var hash = (window.location.hash || '').replace('#', ''); switchTab(TABS[hash] ? hash : 'piezas'); refreshSummary(); window.addEventListener('hashchange', function () { var h = (window.location.hash || '').replace('#', ''); if (TABS[h] && h !== activeTab) switchTab(h); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();