Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos - F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado - F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment - F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings) - F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant - Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas - Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant) - Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant - Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
This commit is contained in:
@@ -112,7 +112,8 @@ const Config = (() => {
|
||||
warehouse: 'Almacenista',
|
||||
accountant: 'Contador',
|
||||
workshop: 'Taller',
|
||||
mechanic: 'Mecanico'
|
||||
mechanic: 'Mecanico',
|
||||
driver: 'Repartidor'
|
||||
};
|
||||
|
||||
var ROLE_BADGE = {
|
||||
@@ -123,7 +124,8 @@ const Config = (() => {
|
||||
warehouse: 'badge--yellow',
|
||||
accountant: 'badge--purple',
|
||||
workshop: 'badge--orange',
|
||||
mechanic: 'badge--teal'
|
||||
mechanic: 'badge--teal',
|
||||
driver: 'badge--gray'
|
||||
};
|
||||
|
||||
function roleBadge(role) {
|
||||
@@ -513,6 +515,15 @@ const Config = (() => {
|
||||
setChecked('receipt-show-iva', d.show_iva_breakdown);
|
||||
setChecked('receipt-show-payment', d.show_payment_details);
|
||||
setChecked('receipt-show-employee', d.show_employee);
|
||||
setChecked('receipt-auto-print', d.auto_print);
|
||||
fetch(API + '/invoice-settings', { headers: headers() }).then(function (r) { return r.json(); }).then(function (s) {
|
||||
var a = document.getElementById('cfg-invoice-auto');
|
||||
var b = document.getElementById('cfg-invoice-email');
|
||||
var c = document.getElementById('cfg-invoice-global');
|
||||
if (a) a.checked = !!s.invoice_auto;
|
||||
if (b) b.checked = !!s.invoice_email;
|
||||
if (c) c.checked = !!s.invoice_global;
|
||||
}).catch(function () {});
|
||||
renderReceiptLogoThumb();
|
||||
} catch (e) {
|
||||
console.error('Config.loadReceiptConfig:', e);
|
||||
@@ -599,13 +610,26 @@ const Config = (() => {
|
||||
show_iva_breakdown: getChecked('receipt-show-iva'),
|
||||
show_payment_details: getChecked('receipt-show-payment'),
|
||||
show_employee: getChecked('receipt-show-employee'),
|
||||
invoice_auto: document.getElementById('cfg-invoice-auto') ? document.getElementById('cfg-invoice-auto').checked : undefined,
|
||||
invoice_email: document.getElementById('cfg-invoice-email') ? document.getElementById('cfg-invoice-email').checked : undefined,
|
||||
invoice_global: document.getElementById('cfg-invoice-global') ? document.getElementById('cfg-invoice-global').checked : undefined,
|
||||
auto_print: getChecked('receipt-auto-print'),
|
||||
};
|
||||
try {
|
||||
var inv = {};
|
||||
['invoice_auto', 'invoice_email', 'invoice_global'].forEach(function (k) {
|
||||
if (data[k] !== undefined) { inv[k] = data[k]; delete data[k]; }
|
||||
});
|
||||
var res = await fetch(API + '/receipt', {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (Object.keys(inv).length) {
|
||||
await fetch(API + '/invoice-settings', {
|
||||
method: 'PUT', headers: headers(), body: JSON.stringify(inv),
|
||||
});
|
||||
}
|
||||
if (!res.ok) {
|
||||
var err = await res.json().catch(function() { return { error: res.statusText }; });
|
||||
throw new Error(err.error || 'Error al guardar');
|
||||
@@ -628,6 +652,7 @@ const Config = (() => {
|
||||
await saveAllowedBrands();
|
||||
await saveModules();
|
||||
await saveSalesSettings();
|
||||
await saveAlertContacts();
|
||||
await saveReceiptConfig();
|
||||
toast('Configuración guardada', 'ok');
|
||||
} catch (e) {
|
||||
@@ -842,6 +867,53 @@ const Config = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tier Discounts (Taller / Mayoreo)
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadTierDiscounts() {
|
||||
try {
|
||||
var res = await fetch('/pos/api/inventory/tier-discounts', { headers: headers() });
|
||||
if (!res.ok) return;
|
||||
var d = await res.json();
|
||||
(d.data || []).forEach(function(t) {
|
||||
var el = document.getElementById('cfg-tier-disc-' + t.tier_id);
|
||||
if (el) el.value = t.discount_pct;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Config.loadTierDiscounts:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTierDiscounts() {
|
||||
var el2 = document.getElementById('cfg-tier-disc-2');
|
||||
var el3 = document.getElementById('cfg-tier-disc-3');
|
||||
var statusEl = document.getElementById('tier-discount-status');
|
||||
var btn = document.getElementById('btn-save-tier-discounts');
|
||||
if (!el2 || !el3) return;
|
||||
|
||||
var d2 = Math.max(0, Math.min(100, parseFloat(el2.value) || 0));
|
||||
var d3 = Math.max(0, Math.min(100, parseFloat(el3.value) || 0));
|
||||
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; }
|
||||
try {
|
||||
var res = await fetch('/pos/api/inventory/tier-discounts', {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({ discount_pct_2: d2, discount_pct_3: d3 })
|
||||
});
|
||||
if (!res.ok) {
|
||||
var err = await res.json().catch(function() { return { error: res.statusText }; });
|
||||
throw new Error(err.error || 'Save failed');
|
||||
}
|
||||
toast('Descuentos por tipo de cliente guardados');
|
||||
if (statusEl) statusEl.textContent = 'Taller -' + d2 + '% · Mayoreo -' + d3 + '%';
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Guardar Descuentos'; }
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Vehicle Compatibility Source
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -1059,6 +1131,50 @@ const Config = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Alert contacts (per-business WhatsApp alerts)
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadAlertContacts() {
|
||||
try {
|
||||
var res = await fetch(API + '/alert-contacts', { headers: headers() });
|
||||
if (!res.ok) return;
|
||||
var data = await res.json();
|
||||
var input = document.getElementById('alert-phones');
|
||||
if (input) input.value = (data.alert_phones || []).join(', ');
|
||||
var cb = document.getElementById('cfg-alert-whatsapp-enabled');
|
||||
if (cb) cb.checked = data.alert_whatsapp_enabled === true;
|
||||
} catch (e) {
|
||||
console.error('Config.loadAlertContacts:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAlertContacts() {
|
||||
if (!checkAuth()) return;
|
||||
var input = document.getElementById('alert-phones');
|
||||
var cb = document.getElementById('cfg-alert-whatsapp-enabled');
|
||||
if (!input && !cb) return;
|
||||
var btn = document.getElementById('btn-save-alert-contacts');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Guardando...'; }
|
||||
try {
|
||||
var res = await fetch(API + '/alert-contacts', {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({
|
||||
alert_phones: input ? input.value : '',
|
||||
alert_whatsapp_enabled: cb ? cb.checked : false,
|
||||
}),
|
||||
});
|
||||
var data = await res.json().catch(function() { return { error: res.statusText }; });
|
||||
if (!res.ok) throw new Error(data.error || 'Error al guardar');
|
||||
if (input) input.value = (data.alert_phones || []).join(', ');
|
||||
toast('Contactos de alerta guardados', 'ok');
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Guardar alertas'; }
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tab navigation
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -1293,6 +1409,12 @@ const Config = (() => {
|
||||
btnBrands.addEventListener('click', saveAllowedBrands);
|
||||
}
|
||||
|
||||
// Alert contacts save button (per-business WhatsApp alerts)
|
||||
var btnAlertContacts = document.getElementById('btn-save-alert-contacts');
|
||||
if (btnAlertContacts) {
|
||||
btnAlertContacts.addEventListener('click', saveAlertContacts);
|
||||
}
|
||||
|
||||
// Kiosk mode toggle
|
||||
var kioskToggle = document.getElementById('cfg-kiosk-mode');
|
||||
if (kioskToggle && window.NexusKiosk) {
|
||||
@@ -1322,7 +1444,9 @@ const Config = (() => {
|
||||
loadAllowedBrands();
|
||||
loadModules();
|
||||
loadSalesSettings();
|
||||
loadAlertContacts();
|
||||
loadReceiptConfig();
|
||||
loadTierDiscounts();
|
||||
if (isAdmin) {
|
||||
loadRolePermissions();
|
||||
loadWorkshopPermissions();
|
||||
@@ -1353,8 +1477,10 @@ const Config = (() => {
|
||||
deleteBranch,
|
||||
loadBusiness, saveBusiness, saveTaxParams, saveAll,
|
||||
loadCurrency, saveCurrency,
|
||||
loadTierDiscounts, saveTierDiscounts,
|
||||
loadVehicleCompatSource, saveVehicleCompatSource,
|
||||
loadModules, saveModules,
|
||||
loadAlertContacts, saveAlertContacts,
|
||||
loadReceiptConfig, saveReceiptConfig, handleReceiptLogo, removeReceiptLogo,
|
||||
openModal, closeModal, openBranchModal, editBranch,
|
||||
switchTab, loadRolePermissions, renderRolePermissions, saveRolePermissions,
|
||||
|
||||
Reference in New Issue
Block a user