- 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
3025 lines
148 KiB
JavaScript
3025 lines
148 KiB
JavaScript
// /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) + ')"') + '>‹</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) + ')"') + '>›</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 + ')">×</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 + ')">×</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)">×</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();
|
||
}
|
||
})();
|