Files
Autoparts-DB/pos/static/js/yonke.js
consultoria-as a386ca5b4a
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled
Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos
- F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado
- F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment
- F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings)
- F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant
- Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas
- Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant)
- Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant
- Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
2026-10-12 00:57:31 +00:00

3025 lines
148 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// /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/<id>/images DELETE /pieces/<id>/images/<image_id>
// PUT /pieces/<id>
// GET /vin/<vin>
// GET /donors
// GET /consignments POST /consignments
// GET /consignments/<id> POST /consignments/<id>/items
// PUT /consignments/<id>/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 '<tr><td colspan="' + colspan + '" class="ynk-empty">' + esc(text) + '</td></tr>';
}
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
? '<img class="ynk-thumb" src="' + esc(url) + '" alt="foto" loading="lazy" onerror="ynkThumbFallback(this)" />'
: '<div class="ynk-thumb ynk-thumb--empty">sin<br>foto</div>';
return '<div class="ynk-thumb-slot" title="Ver fotos" onclick="ynkOpenImages(' + item.id + ')">' + inner + '</div>';
}
// A broken image URL must not leave an unclickable blank cell.
window.ynkThumbFallback = function (img) {
var slot = img && img.parentNode;
if (slot) slot.innerHTML = '<div class="ynk-thumb ynk-thumb--empty">sin<br>foto</div>';
};
function gradeHtml(grade) {
if (!grade) return '<span class="ynk-grade ynk-grade--none">—</span>';
var g = String(grade).toUpperCase().charAt(0);
return '<span class="ynk-grade ynk-grade--' + esc(g) + '">' + esc(g) + '</span>';
}
function conditionBadge(condition) {
var c = (condition || '').toLowerCase();
var cls = c === 'nuevo' ? 'badge--ok' : (c === 'reconstruido' ? 'badge--transit' : 'badge--pending');
return '<span class="badge ' + cls + '">' + esc(c || '—') + '</span>';
}
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 '<span class="ynk-hint">—</span>';
return '<div>' + esc(veh || 'Vehículo sin datos') + '</div>' +
(vin ? '<div class="ynk-hint" style="font-family:var(--font-mono);font-size:0.7rem;">' + esc(vin) + '</div>' : '');
}
function renderPagination(el, pg, fnName) {
if (!el) return;
pg = pg || {};
if (!pg.total_pages || pg.total_pages <= 1) {
el.innerHTML = '';
return;
}
el.innerHTML =
'<button class="page-btn" ' + (pg.page <= 1 ? 'disabled' : 'onclick="window.' + fnName + '(' + (pg.page - 1) + ')"') + '>&lsaquo;</button>' +
'<span style="padding:0 var(--space-2);font-size:var(--text-caption);color:var(--color-text-muted);">' +
pg.page + ' / ' + pg.total_pages + '</span>' +
'<button class="page-btn" ' + (pg.page >= pg.total_pages ? 'disabled' : 'onclick="window.' + fnName + '(' + (pg.page + 1) + ')"') + '>&rsaquo;</button>';
}
// 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 '<tr>' +
'<td>' + thumbHtml(it) + '</td>' +
'<td class="td--mono">' + (esc(it.part_number) || '<span class="ynk-hint">—</span>') +
(it.is_unique_piece ? '<span class="ynk-uniq">1 de 1</span>' : '') + '</td>' +
'<td class="td--primary">' + esc(it.name) + '</td>' +
'<td>' + esc(it.brand) + '</td>' +
'<td>' + conditionBadge(it.condition) +
(it.ownership === 'consigna' ? ' <span class="badge badge--low">consigna</span>' : '') + '</td>' +
'<td style="text-align:center;">' + gradeHtml(it.condition_grade) + '</td>' +
'<td>' + donorHtml(it) + '</td>' +
'<td style="text-align:right" class="td--amount">' + fmtMoney(it.price_1) + '</td>' +
'<td style="text-align:right" class="td--amount">' + fmtMoney(it.cost) + '</td>' +
'<td style="text-align:right" class="td--primary">' + fmtInt(it.stock) + '</td>' +
'<td>' + esc(it.location) + '</td>' +
'<td style="white-space:nowrap;">' +
'<button class="btn btn--ghost btn--sm" onclick="ynkOpenPiece(' + it.id + ')">Ficha</button> ' +
'<button class="btn btn--ghost btn--sm" onclick="ynkOpenPiece(' + it.id + ', \'meli\')">MeLi</button> ' +
'<button class="btn btn--ghost btn--sm" onclick="ynkOpenLabel(' + it.id + ')">Etiqueta</button> ' +
'<button class="btn btn--ghost btn--sm" onclick="ynkOpenImages(' + it.id + ')">Fotos</button> ' +
(canEdit() ? '<button class="btn btn--ghost btn--sm" onclick="ynkOpenEditPiece(' + it.id + ')">Editar</button>' : '') +
'</td></tr>';
}
// =====================================================================
// 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 '<div class="ynk-photo"><img src="' + p.url + '" alt="foto ' + (i + 1) + '" />' +
'<button type="button" class="ynk-photo__remove" title="Quitar" onclick="ynkRemovePhoto(' + i + ')">&times;</button></div>';
}).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: <span class="ynk-sku">' + esc(sku) + '</span>' +
(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)… ' : '') +
'<span class="ynk-hint">(en ' + elapsed + 's)</span>');
}
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,
'<span class="ynk-hint">(' + n + ' foto(s) subidas, ' + elapsed + 's)</span>');
}
toast(n + ' foto(s) subidas', 'ok');
})
.catch(function (err) {
if (result) {
result.innerHTML = savedPieceHtml(item.id, sku,
'<span class="ynk-err">— no se pudieron subir las fotos: ' + esc(errMsg(err)) + '</span>');
}
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 '<tr>' +
'<td class="td--mono">' + (esc(d.donor_vin) || '<span class="ynk-hint">—</span>') + '</td>' +
'<td class="td--primary">' + (esc(d.donor_make) || '—') + '</td>' +
'<td>' + (esc(d.donor_model) || '—') + '</td>' +
'<td>' + (esc(d.donor_year) || '—') + '</td>' +
'<td style="text-align:right" class="td--primary">' + fmtInt(d.pieces) + '</td>' +
'<td style="text-align:right" class="td--amount">' + fmtMoney(d.total_value) + '</td>' +
'<td><button class="btn btn--ghost btn--sm" onclick="ynkDonorToPieces(' + i + ')">Ver piezas</button></td>' +
'</tr>';
}).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 '<tr>' +
'<td>' + thumbHtml(it) + '</td>' +
'<td class="td--mono">' + (esc(it.part_number) || '—') + '</td>' +
'<td class="td--primary">' + esc(it.name) + '</td>' +
'<td style="text-align:center;">' + gradeHtml(it.condition_grade) + '</td>' +
'<td>' + conditionBadge(it.condition) + '</td>' +
'<td>' + donorHtml(it) + '</td>' +
'<td style="text-align:right" class="td--amount">' + fmtMoney(it.price_1) + '</td>' +
'<td style="text-align:right" class="td--primary">' + fmtInt(it.stock) + '</td>' +
'<td>' + esc(it.location) + '</td>' +
'<td><button class="btn btn--ghost btn--sm" onclick="ynkOpenPieceFromSearch(' + it.id + ')">Editar</button></td>' +
'</tr>';
}).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 '<tr>' +
'<td class="td--mono">' + esc(c.id) + '</td>' +
'<td class="td--primary">' + esc(c.consignor_name) + '</td>' +
'<td class="td--mono">' + esc(c.consignor_rfc) + '</td>' +
'<td>' + esc(c.consignor_phone) + '</td>' +
'<td style="text-align:right">' + (c.commission_pct !== undefined && c.commission_pct !== null ? esc(c.commission_pct) + '%' : '—') + '</td>' +
'<td style="text-align:right">' + esc(pieces) + '</td>' +
'<td style="text-align:right" class="td--amount">' + fmtMoney(c.total_sold) + '</td>' +
'<td style="text-align:right" class="td--amount">' + fmtMoney(c.total_commission) + '</td>' +
'<td><span class="badge ' + statusCls + '">' + esc(status) + '</span></td>' +
'<td>' + esc((c.created_at || '').substring(0, 10) || '—') + '</td>' +
'<td style="white-space:nowrap;">' +
'<button class="btn btn--ghost btn--sm" onclick="ynkOpenConsignment(' + c.id + ')">Ver / liquidar</button> ' +
'<button class="btn btn--ghost btn--sm" onclick="ynkOpenStatement(' + c.id + ')">Estado de cuenta</button>' +
'</td></tr>';
}).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 = '<option value="">— Selecciona consignante —</option>' + open.map(function (c) {
return '<option value="' + esc(c.id) + '">' + esc('#' + c.id + ' · ' + (c.consignor_name || '')) + '</option>';
}).join('');
if (current) sel.value = current;
if (!open.length) {
sel.innerHTML = '<option value="">Sin consignaciones abiertas</option>';
}
})
.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 = '<strong>' + esc(c.consignor_name || '—') + '</strong>' +
(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 ? '<br><span>' + esc(c.notes) + '</span>' : '');
}
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 '<tr>' +
'<td class="td--mono">' + esc(it.inventory_id) + '</td>' +
'<td class="td--mono">' + esc(it.part_number) + '</td>' +
'<td class="td--primary">' + esc(it.name) + '</td>' +
'<td style="text-align:right" class="td--amount">' + fmtMoney(it.agreed_price) + '</td>' +
'<td style="text-align:right" class="td--amount">' + fmtMoney(it.commission_amount) + '</td>' +
'<td><span class="badge ' + cls + '">' + esc(status || '—') + '</span></td>' +
'</tr>';
}).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 = '<p class="ynk-hint">Liquidando…</p>';
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 = '<p class="ynk-err">No se pudo liquidar: ' + esc(errMsg(err)) + '</p>';
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 = '<div class="ynk-summary-card"><h4>Resumen de liquidación</h4>';
if (!keys.length) html += '<p class="ynk-hint">La consigna quedó marcada como liquidada.</p>';
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 += '<div class="ynk-kv"><span class="ynk-kv__k">' + esc(label) + '</span><span class="ynk-kv__v">' + shown + '</span></div>';
});
var items = Array.isArray(s.items) ? s.items : [];
if (items.length) {
html += '<div style="margin-top:var(--space-3);">' + items.map(function (it) {
return '<div class="ynk-kv"><span class="ynk-kv__k">' +
esc([it.part_number, it.name].filter(Boolean).join(' · ') || ('ID ' + (it.inventory_id || '—'))) +
'</span><span class="ynk-kv__v">' + fmtMoney(it.commission_amount || it.agreed_price) + '</span></div>';
}).join('') + '</div>';
}
return html + '</div>';
}
// =====================================================================
// 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 = '<p class="ynk-hint">Esta pieza no tiene fotos todavía.</p>';
return;
}
grid.innerHTML = imgs.map(function (im) {
return '<div class="ynk-photo"><img src="' + esc(im.url) + '" alt="foto" />' +
(im.is_primary ? '<span class="ynk-uniq" style="position:absolute;bottom:4px;left:4px;margin:0;">principal</span>' : '') +
(canEdit() && im.id ? '<button type="button" class="ynk-photo__remove" title="Eliminar" onclick="ynkDeleteImage(' + im.id + ')">&times;</button>' : '') +
'</div>';
}).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 '<span class="ynk-hint">—</span>';
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 '<p class="ynk-hint">Sin resultados.</p>';
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 '<pre class="ynk-hint" style="white-space:pre-wrap;">' + esc(JSON.stringify(items, null, 2)) + '</pre>';
}
return '<table class="data-table"><thead><tr>' + cols.map(function (c) {
return '<th>' + esc(fieldLabel(c)) + '</th>';
}).join('') + '</tr></thead><tbody>' + items.map(function (it) {
return '<tr>' + cols.map(function (c) {
return '<td>' + formatCell(c, it ? it[c] : null) + '</td>';
}).join('') + '</tr>';
}).join('') + '</tbody></table>';
}
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 = '<div class="ynk-summary-card"><h4>' + esc(title) + '</h4>';
if (!keys.length) html += '<p class="ynk-hint">Sin datos.</p>';
keys.forEach(function (k) {
var v = obj[k];
var shown = (typeof v === 'number' && isMoneyKey(k)) ? fmtMoney(v) : esc(v);
html += '<div class="ynk-kv"><span class="ynk-kv__k">' + esc(fieldLabel(k)) +
'</span><span class="ynk-kv__v">' + shown + '</span></div>';
});
return html + '</div>';
}
function anyCard(title, value) {
if (value === undefined) {
return '<div class="ynk-summary-card"><h4>' + esc(title) + '</h4><p class="ynk-hint">Sin datos.</p></div>';
}
if (Array.isArray(value)) {
return '<div class="ynk-summary-card"><h4>' + esc(title) + '</h4>' +
'<div style="max-height:360px;overflow:auto;">' + genericTableHtml(value) + '</div></div>';
}
if (value && typeof value === 'object') return kvHtml(value, title);
var shown = (typeof value === 'number' && isMoneyKey(title)) ? fmtMoney(value) : esc(value);
return '<div class="ynk-summary-card"><h4>' + esc(title) + '</h4>' +
'<div class="ynk-kv"><span class="ynk-kv__v">' + shown + '</span></div></div>';
}
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 =
'<div class="ynk-kv"><span class="ynk-kv__k">SKU / No. parte</span><span class="ynk-kv__v ynk-sku">' + (esc(it.part_number) || '—') + '</span></div>' +
'<div class="ynk-kv"><span class="ynk-kv__k">Marca</span><span class="ynk-kv__v">' + (esc(it.brand) || '—') + '</span></div>' +
'<div class="ynk-kv"><span class="ynk-kv__k">Condición / grado</span><span class="ynk-kv__v">' + esc(it.condition || '—') + ' · ' + gradeHtml(it.condition_grade) + '</span></div>' +
'<div class="ynk-kv"><span class="ynk-kv__k">Precio / costo</span><span class="ynk-kv__v">' + fmtMoney(it.price_1) + ' / ' + fmtMoney(it.cost) + '</span></div>' +
'<div class="ynk-kv"><span class="ynk-kv__k">Stock</span><span class="ynk-kv__v">' + fmtInt(it.stock) + (it.is_unique_piece ? ' (1 de 1)' : '') + '</span></div>' +
'<div class="ynk-kv"><span class="ynk-kv__k">Ubicación</span><span class="ynk-kv__v">' + (esc(it.location) || '—') + '</span></div>' +
'<div class="ynk-kv"><span class="ynk-kv__k">Propiedad</span><span class="ynk-kv__v">' + esc(it.ownership || 'propio') + '</span></div>' +
'<div class="ynk-kv"><span class="ynk-kv__k">Donador</span><span class="ynk-kv__v">' + (esc(veh) || '—') + (it.donor_vin ? ' · ' + esc(it.donor_vin) : '') + '</span></div>';
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 = '<span class="ynk-hint">Sin fotos.</span>'; return; }
grid.innerHTML = imgs.map(function (im) {
return '<div class="ynk-photo"><img src="' + esc(im.url) + '" alt="foto" loading="lazy" /></div>';
}).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('<span class="ynk-hint">Publicando en MercadoLibre…</span>');
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
? ' <a class="ynk-link" href="' + esc(d.permalink) + '" target="_blank" rel="noopener">' + esc(d.permalink) + '</a>'
: '';
pieceActionResult('<span class="ynk-ok">✔ Publicado en MeLi. Item: ' + esc(d.external_item_id || '—') + '</span>' + link);
toast('Pieza publicada en MercadoLibre', 'ok');
} else {
pieceActionResult('<span class="ynk-err">No se pudo publicar: ' + esc(d.error || 'la plataforma rechazó la publicación') + '</span>');
toast('No se pudo publicar en MeLi', 'error');
}
})
.catch(function (err) {
pieceActionResult('<span class="ynk-err">Error al publicar: ' + esc(errMsg(err)) + '</span>');
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('<span class="ynk-hint">Analizando la foto…</span>');
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('<span class="ynk-warn">La IA no reconoció la pieza' +
(d.error ? ': ' + esc(d.error) : '.') + '</span>');
return;
}
var html = 'Identificado: <strong>' + (esc(identifiedName) || '—') + '</strong>' +
(d.category ? ' · Categoría: ' + esc(d.category) : '') +
(conf ? ' · confianza ' + esc(conf) : '');
if (identifiedName) {
html += ' <button class="btn btn--primary btn--sm" type="button" onclick="ynkApplyIdentified()">Aplicar nombre a la pieza</button>';
}
pieceActionResult(html);
})
.catch(function (err) {
pieceActionResult('<span class="ynk-err">No se pudo identificar: ' + esc(errMsg(err)) + '</span>');
});
}
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('<span class="ynk-ok">✔ Nombre aplicado.</span>');
toast('Nombre actualizado', 'ok');
if (activeTab === 'piezas') loadPieces(1);
if (activeTab === 'donador' && donorView === 'pieces') loadDonorPieces(1);
})
.catch(function (err) {
pieceActionResult('<span class="ynk-err">No se pudo aplicar: ' + esc(errMsg(err)) + '</span>');
});
}
function ynkSuggestPrice() {
var id = currentDetailId;
if (!id) return;
pieceActionResult('<span class="ynk-hint">Calculando precio sugerido…</span>');
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('<span class="ynk-warn">No hay sugerencia disponible' +
(d.error ? ': ' + esc(d.error) : '.') + '</span>');
return;
}
var range = (d.low !== undefined && d.high !== undefined)
? ' · Rango: ' + fmtMoney(d.low) + ' – ' + fmtMoney(d.high) : '';
var html = '<strong>Sugerido: ' + fmtMoney(suggestedPrice) + '</strong>' + range +
(d.basis ? '<br><span class="ynk-hint">' + esc(d.basis) + '</span>' : '');
if (suggestedPrice !== null) {
html += ' <button class="btn btn--primary btn--sm" type="button" onclick="ynkApplySuggestedPrice()">Aplicar ' + fmtMoney(suggestedPrice) + '</button>';
}
pieceActionResult(html);
})
.catch(function (err) {
pieceActionResult('<span class="ynk-err">No se pudo sugerir precio: ' + esc(errMsg(err)) + '</span>');
});
}
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('<span class="ynk-ok">✔ Precio actualizado a ' + fmtMoney(suggestedPrice) + '.</span>');
if (activeTab === 'piezas') loadPieces(1);
})
.catch(function (err) {
pieceActionResult('<span class="ynk-err">No se pudo aplicar: ' + esc(errMsg(err)) + '</span>');
});
}
function ynkValidatePhoto(file) {
var id = currentDetailId;
if (!id || !file) return;
var fd = new FormData();
fd.append('file', file, file.name);
pieceActionResult('<span class="ynk-hint">Validando la foto…</span>');
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('<span class="ynk-ok">✔ La foto parece correcta.</span>');
} else {
var list = (issues && issues.length)
? issues.map(function (s) { return '<li>' + esc(typeof s === 'object' ? (s.message || JSON.stringify(s)) : s) + '</li>'; }).join('')
: '<li>' + esc(d.error || 'no cumple los requisitos') + '</li>';
pieceActionResult('<span class="ynk-warn">Revisar la foto:</span><ul style="margin:6px 0 0 18px;">' + list + '</ul>');
}
})
.catch(function (err) {
pieceActionResult('<span class="ynk-err">No se pudo validar: ' + esc(errMsg(err)) + '</span>');
});
}
function loadCompat(id) {
var box = document.getElementById('ynkCompatList');
if (!box) return;
box.innerHTML = '<span class="ynk-hint">Cargando compatibilidad…</span>';
apiFetch(API + '/pieces/' + id + '/compat')
.then(function (res) {
var items = anyList(res);
if (!items.length) { box.innerHTML = '<span class="ynk-hint">Sin compatibilidades registradas.</span>'; return; }
box.innerHTML = '<div class="table-wrapper">' +
genericTableHtml(items, ['make', 'model', 'year', 'engine']) + '</div>';
})
.catch(function (err) {
box.innerHTML = '<span class="ynk-err">No se pudo cargar la compatibilidad: ' + esc(errMsg(err)) + '</span>';
});
}
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 = '<p class="ynk-hint">Cargando estado de cuenta…</p>';
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 = '<p class="ynk-hint">Cargando estado de cuenta…</p>';
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 = '<strong>' + esc(s.consignor_name || '—') + '</strong>' +
(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'])
: '<p class="ynk-hint">Sin movimientos en el periodo.</p>';
if (s.totals && typeof s.totals === 'object') html += kvHtml(s.totals, 'Totales');
else if (s.totals !== undefined) html += '<p class="ynk-hint">Totales: ' + esc(s.totals) + '</p>';
body.innerHTML = html;
})
.catch(function (err) {
if (body) body.innerHTML = '<p class="ynk-err">No se pudo cargar el estado de cuenta: ' + esc(errMsg(err)) + '</p>';
});
}
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 '<tr>' +
'<td class="td--mono">' + esc(it.id) + '</td>' +
'<td class="td--mono">' + (esc(it.part_number) || '—') + '</td>' +
'<td class="td--primary">' + esc(it.name) + '</td>' +
'<td style="text-align:center;">' + gradeHtml(it.condition_grade) + '</td>' +
'<td style="text-align:right" class="td--amount">' + fmtMoney(it.price_1) + '</td>' +
'<td style="text-align:right" class="td--amount">' + fmtMoney(it.cost) + '</td>' +
'<td style="text-align:right" class="td--primary">' + fmtInt(it.days_in_yard) + '</td>' +
'</tr>';
}).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 = '<p class="ynk-hint">Cargando resumen…</p>';
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 || '<p class="ynk-hint">Sin datos.</p>';
var upd = document.getElementById('ynkRepSummaryUpdated');
if (upd) upd.textContent = 'Actualizado ' + new Date().toLocaleTimeString('es-MX');
})
.catch(function (err) {
box.innerHTML = '<p class="ynk-err">Error al cargar resumen: ' + esc(errMsg(err)) + '</p>';
});
}
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 '<div>' + it.pieces.map(function (p) {
return '<div class="ynk-kv"><span class="ynk-kv__k">#' + esc(p.id) + ' ' + esc(p.part_number || '') +
'</span><span class="ynk-kv__v">' + esc(p.name || '') + ' · ' + fmtMoney(p.price_1) + '</span></div>';
}).join('') + '</div>';
}
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 '<span class="ynk-hint">' + esc(parts.join(' · ')) + '</span>';
}
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 '<tr><td class="td--mono">' + (i + 1) + '</td>' +
'<td class="td--primary">' + duplicateSummary(it) + '</td>' +
'<td>' + duplicateDetail(it) + '</td></tr>';
}).join('');
} else {
body.innerHTML = '<tr><td colspan="3" style="padding:0;"><div class="table-wrapper">' +
genericTableHtml(items) + '</div></td></tr>';
}
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 += '<div class="ynk-ok">✔ Creadas: ' + esc(created) + ' pieza(s).</div>';
if (Array.isArray(errors) && errors.length) {
html += '<div class="ynk-err" style="margin-top:6px;">' + errors.length + ' error(es):</div>' +
'<div style="max-height:220px;overflow:auto;margin-top:4px;">' + 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 '<div class="ynk-kv"><span class="ynk-kv__k">' + esc(rowLabel) + '</span><span class="ynk-kv__v">' +
esc(e.error || e.message || JSON.stringify(e)) + '</span></div>';
}
return '<div class="ynk-kv"><span class="ynk-kv__v">' + esc(e) + '</span></div>';
}).join('') + '</div>';
} else if (errors && !Array.isArray(errors)) {
html += '<div class="ynk-err" style="margin-top:6px;">' + esc(errors) + '</div>';
}
if (!html) html = '<span class="ynk-hint">Respuesta: ' + esc(JSON.stringify(d)) + '</span>';
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 = '<option value="">Sin grado</option><option value="A">A</option>' +
'<option value="B">B</option><option value="C">C</option><option value="D">D</option>';
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 '<tr class="ynk-full-row">' +
'<td><input type="text" class="ynk-full-name" placeholder="Ej: Faro delantero izquierdo" autocomplete="off" /></td>' +
'<td><input type="number" class="ynk-full-price" step="0.01" min="0" placeholder="0.00" /></td>' +
'<td><input type="number" class="ynk-full-cost" step="0.01" min="0" placeholder="0.00" /></td>' +
'<td><select class="ynk-full-grade">' + GRADE_OPTS + '</select></td>' +
'<td><input type="text" class="ynk-full-location" placeholder="YON-A-1-1" autocomplete="off" /></td>' +
'<td><button class="btn btn--ghost btn--sm" type="button" title="Quitar" onclick="ynkFullRemoveRow(this)">&times;</button></td>' +
'</tr>';
}
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 <strong>' + esc(donorVin) + '</strong>: ' + esc(count) + ' pieza(s) creada(s).' +
(skus.length ? '<div class="ynk-hint" style="margin-top:4px;">SKUs: ' + skus.map(function (s) { return esc(s); }).join(', ') + '</div>' : '');
}
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 += '<div class="ynk-ok" style="margin-top:4px;">Costo prorrateado' +
(a.updated !== undefined ? ' en ' + esc(a.updated) + ' pieza(s)' : '') + '.</div>';
})
.catch(function (err) {
if (result) result.innerHTML += '<div class="ynk-err" style="margin-top:4px;">No se pudo prorratear el costo: ' + esc(errMsg(err)) + '</div>';
});
}
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/<id>
// POST /listings PATCH /listings/<id> DELETE /listings/<id>
// POST /orders GET /orders GET /orders/incoming
// GET /orders/<id> PUT /orders/<id>/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 '<span class="badge ' + meta.cls + '">' + esc(meta.label) + '</span>';
}
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 = '<p class="ynk-empty">Cargando catálogo…</p>';
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 = '<p class="ynk-empty">Sin piezas en mayoreo con esos filtros.</p>';
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 = '<p class="ynk-empty ynk-err">Error al cargar el catálogo: ' + esc(errMsg(err)) + '</p>';
toast('Error al cargar el catálogo de mayoreo: ' + errMsg(err), 'error');
});
}
function b2bCardHtml(it, i) {
var img = it.image_url
? '<img class="ynk-b2b-card__img" src="' + esc(it.image_url) + '" alt="foto" loading="lazy" onerror="ynkB2bImgFallback(this)" />'
: '<div class="ynk-b2b-card__img ynk-b2b-card__img--empty">sin foto</div>';
var veh = [it.donor_make, it.donor_model, it.donor_year].filter(Boolean).join(' ');
return '<div class="ynk-b2b-card">' + img +
'<div class="ynk-b2b-card__body">' +
'<div class="ynk-b2b-card__name">' + (esc(it.name) || 'Pieza sin nombre') + '</div>' +
'<div class="ynk-b2b-card__meta">' + gradeHtml(it.condition_grade) +
'<span>' + (esc(it.part_number) || 'sin No. parte') + '</span></div>' +
(veh ? '<div class="ynk-b2b-card__meta">Donador: ' + esc(veh) + '</div>' : '') +
'<div class="ynk-b2b-card__meta">Vendedor: ' + esc(b2bSellerLabel(it)) + '</div>' +
'<div class="ynk-b2b-card__price">' + fmtMoney(it.price_wholesale) + '</div>' +
'<div class="ynk-b2b-card__foot">' +
'<button class="btn btn--primary btn--sm" type="button" onclick="ynkB2bOpenBuy(' + i + ')">Comprar</button>' +
'</div>' +
'</div></div>';
}
window.ynkB2bImgFallback = function (img) {
var box = img && img.parentNode;
if (box) box.innerHTML = '<div class="ynk-b2b-card__img ynk-b2b-card__img--empty">sin foto</div>';
};
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) + ' <strong>' + (esc(it.name) || '—') + '</strong>' +
(it.part_number ? ' · ' + esc(it.part_number) : '') +
(veh ? '<br>Donador: ' + esc(veh) : '') +
'<br>Vendedor: ' + esc(b2bSellerLabel(it)) +
' · Mayoreo: <strong>' + fmtMoney(it.price_wholesale) + '</strong> 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 <strong>' + total + '</strong>' +
(o.status ? ' · estado: ' + b2bStatusBadge(o.status) : '') +
'<br><span class="ynk-hint">El vendedor la verá en “Pedidos › Recibidos”.</span>';
}
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
? '<img class="ynk-thumb" src="' + esc(it.image_url) + '" alt="foto" loading="lazy" onerror="ynkThumbFallback(this)" />'
: '<div class="ynk-thumb ynk-thumb--empty">sin<br>foto</div>';
var price = (it.price_wholesale === undefined || it.price_wholesale === null) ? '' : esc(it.price_wholesale);
return '<tr>' +
'<td>' + img + '</td>' +
'<td class="td--mono">' + (esc(it.part_number) || '—') + '</td>' +
'<td class="td--primary">' + (esc(it.name) || '—') + '</td>' +
'<td style="text-align:center;">' + gradeHtml(it.condition_grade) + '</td>' +
'<td>' + (esc(veh) || '<span class="ynk-hint">—</span>') + '</td>' +
'<td style="text-align:right;white-space:nowrap;">' +
'<input type="number" class="select-filter ynk-b2b-price-input" id="ynkB2bPrice' + esc(it.id) + '" step="0.01" min="0" value="' + price + '" /> ' +
'<button class="btn btn--ghost btn--sm" type="button" title="Guardar precio mayoreo" onclick="ynkB2bSavePrice(' + esc(it.id) + ')">Guardar</button>' +
'</td>' +
'<td><span class="badge ' + cls + '">' + esc(st || '—') + '</span></td>' +
'<td>' + (esc(String(it.created_at || '').substring(0, 10)) || '—') + '</td>' +
'<td style="white-space:nowrap;">' +
'<button class="btn btn--ghost btn--sm" type="button" onclick="ynkB2bRetire(' + esc(it.id) + ')">Retirar</button>' +
'</td></tr>';
}
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 = '<option value="">Cargando piezas…</option>';
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 = '<option value="">Sin piezas usadas en inventario</option>';
return;
}
sel.innerHTML = '<option value="">— Selecciona una pieza usada —</option>' + items.map(function (p) {
var label = '#' + p.id + ' · ' + (p.name || 'pieza') + (p.part_number ? ' (' + p.part_number + ')' : '');
return '<option value="' + esc(p.id) + '" data-price="' + esc(p.price_1 === undefined ? '' : p.price_1) + '">' +
esc(label) + '</option>';
}).join('');
})
.catch(function (err) {
sel.innerHTML = '<option value="">Error al cargar las piezas</option>';
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 '<tr>' +
'<td class="td--mono">' + esc(o.id) + '</td>' +
'<td class="td--primary">' + esc(b2bCounterparty(o, sellerSide)) + '</td>' +
'<td style="text-align:right;">' + fmtInt(count) + '</td>' +
'<td style="text-align:right;" class="td--amount">' + fmtMoney(o.total) + '</td>' +
'<td>' + b2bStatusBadge(o.status) + '</td>' +
'<td>' + (esc(String(o.created_at || '').substring(0, 10)) || '—') + '</td>' +
'<td style="white-space:nowrap;">' + b2bOrderActions(o, sellerSide, true) + '</td>' +
'</tr>';
}).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('<button class="btn btn--primary btn--sm" type="button" onclick="ynkB2bSetStatus(' + id + ', \'confirmed\')">Confirmar</button>');
}
if (s === 'pending' || s === 'new' || s === 'confirmed' || s === 'paid' || s === 'processing') {
btns.push('<button class="btn btn--secondary btn--sm" type="button" onclick="ynkB2bSetStatus(' + id + ', \'shipped\')">Marcar enviado</button>');
btns.push('<button class="btn btn--ghost btn--sm" type="button" onclick="ynkB2bSetStatus(' + id + ', \'cancelled\')">Cancelar</button>');
}
} else {
if (s === 'shipped' || s === 'sent' || s === 'confirmed' || s === 'processing') {
btns.push('<button class="btn btn--primary btn--sm" type="button" onclick="ynkB2bSetStatus(' + id + ', \'received\')">Recibido</button>');
}
if (s === 'pending' || s === 'new' || s === 'confirmed') {
btns.push('<button class="btn btn--ghost btn--sm" type="button" onclick="ynkB2bSetStatus(' + id + ', \'cancelled\')">Cancelar</button>');
}
}
if (includeDetail) {
btns.push('<button class="btn btn--ghost btn--sm" type="button" onclick="ynkB2bOpenOrder(' + id + ',' + (sellerSide ? 1 : 0) + ')">Detalle</button>');
}
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 = '<span class="ynk-err">No se pudo cargar el pedido: ' + esc(errMsg(err)) + '</span>';
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 = '<div class="ynk-summary-card"><h4>Pedido #' + esc(o.id !== undefined ? o.id : id) + '</h4>' +
'<div class="ynk-kv"><span class="ynk-kv__k">Estado</span><span class="ynk-kv__v">' + b2bStatusBadge(o.status) + '</span></div>' +
'<div class="ynk-kv"><span class="ynk-kv__k">' + (sellerSide ? 'Comprador' : 'Vendedor') + '</span><span class="ynk-kv__v">' +
esc(b2bCounterparty(o, sellerSide)) + '</span></div>' +
'<div class="ynk-kv"><span class="ynk-kv__k">Total</span><span class="ynk-kv__v">' + fmtMoney(o.total) + '</span></div>' +
(o.created_at ? '<div class="ynk-kv"><span class="ynk-kv__k">Creado</span><span class="ynk-kv__v">' +
esc(String(o.created_at).substring(0, 19).replace('T', ' ')) + '</span></div>' : '') +
(o.notes ? '<div class="ynk-kv"><span class="ynk-kv__k">Notas</span><span class="ynk-kv__v">' + esc(o.notes) + '</span></div>' : '') +
'</div>';
if (items.length) {
html += '<div class="table-wrapper" style="margin-top:var(--space-3);"><table class="data-table"><thead><tr>' +
'<th>Pieza</th><th>No. Parte</th><th style="text-align:right;">Cantidad</th>' +
'<th style="text-align:right;">P. unitario</th><th style="text-align:right;">Subtotal</th>' +
'</tr></thead><tbody>' + 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 '<tr>' +
'<td class="td--primary">' + (esc(it.name) || ('Pieza #' + esc(it.listing_id !== undefined ? it.listing_id : (it.inventory_id || '—')))) + '</td>' +
'<td class="td--mono">' + (esc(it.part_number) || '—') + '</td>' +
'<td style="text-align:right;">' + fmtInt(qty) + '</td>' +
'<td style="text-align:right;" class="td--amount">' + fmtMoney(unit) + '</td>' +
'<td style="text-align:right;" class="td--amount">' + fmtMoney(sub) + '</td>' +
'</tr>';
}).join('') + '</tbody></table></div>';
} else {
html += '<p class="ynk-hint" style="margin-top:var(--space-3);">El detalle de piezas no está disponible.</p>';
}
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();
}
})();