feat(pos): integrate design system into POS — payment modal, F-keys, ticket, margins
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ const POS = (() => {
|
|||||||
let canViewCost = false;
|
let canViewCost = false;
|
||||||
let employeeMaxDiscount = 100;
|
let employeeMaxDiscount = 100;
|
||||||
let lastSaleId = null;
|
let lastSaleId = null;
|
||||||
|
let lastSaleData = null;
|
||||||
let searchTimeout = null;
|
let searchTimeout = null;
|
||||||
let customerSearchTimeout = null;
|
let customerSearchTimeout = null;
|
||||||
|
|
||||||
@@ -39,6 +40,16 @@ const POS = (() => {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showToast(msg) {
|
||||||
|
const container = document.getElementById('toastContainer');
|
||||||
|
if (!container) return;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'toast';
|
||||||
|
el.textContent = msg;
|
||||||
|
container.appendChild(el);
|
||||||
|
setTimeout(() => el.remove(), 2100);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Init ────────────────────────────
|
// ─── Init ────────────────────────────
|
||||||
async function init() {
|
async function init() {
|
||||||
// Parse JWT to get employee info
|
// Parse JWT to get employee info
|
||||||
@@ -49,10 +60,19 @@ const POS = (() => {
|
|||||||
canViewCost = (payload.permissions || []).includes('pos.view_cost');
|
canViewCost = (payload.permissions || []).includes('pos.view_cost');
|
||||||
employeeMaxDiscount = payload.max_discount_pct || 100;
|
employeeMaxDiscount = payload.max_discount_pct || 100;
|
||||||
|
|
||||||
// Show cost/margin columns if permission
|
// Show cost/margin columns and toggle button if permission
|
||||||
if (canViewCost) {
|
if (canViewCost) {
|
||||||
document.getElementById('thCost').style.display = '';
|
document.getElementById('thCost').style.display = '';
|
||||||
document.getElementById('thMargin').style.display = '';
|
document.getElementById('thMargin').style.display = '';
|
||||||
|
const costToggle = document.getElementById('costToggle');
|
||||||
|
if (costToggle) costToggle.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set avatar initials
|
||||||
|
const avatar = document.querySelector('.status-bar__user-avatar');
|
||||||
|
if (avatar && payload.name) {
|
||||||
|
const parts = payload.name.split(' ');
|
||||||
|
avatar.textContent = parts.map(p => p[0]).join('').substring(0, 2).toUpperCase();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Could not parse token:', e);
|
console.warn('Could not parse token:', e);
|
||||||
@@ -87,11 +107,9 @@ const POS = (() => {
|
|||||||
currentRegister = data.register;
|
currentRegister = data.register;
|
||||||
document.getElementById('registerInfo').innerHTML =
|
document.getElementById('registerInfo').innerHTML =
|
||||||
`<span>Caja #${data.register.register_number}</span>`;
|
`<span>Caja #${data.register.register_number}</span>`;
|
||||||
document.getElementById('registerInfo').classList.remove('no-register');
|
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('registerInfo').innerHTML =
|
document.getElementById('registerInfo').innerHTML =
|
||||||
'<span>Sin caja abierta</span>';
|
'<span>Sin caja abierta</span>';
|
||||||
document.getElementById('registerInfo').classList.add('no-register');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Register check failed:', e);
|
console.warn('Register check failed:', e);
|
||||||
@@ -100,7 +118,6 @@ const POS = (() => {
|
|||||||
|
|
||||||
// ─── Cart ────────────────────────────
|
// ─── Cart ────────────────────────────
|
||||||
function addToCart(item) {
|
function addToCart(item) {
|
||||||
// Check if item already in cart
|
|
||||||
const existing = cart.find(c => c.inventory_id === item.inventory_id);
|
const existing = cart.find(c => c.inventory_id === item.inventory_id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.quantity += (item.quantity || 1);
|
existing.quantity += (item.quantity || 1);
|
||||||
@@ -121,6 +138,7 @@ const POS = (() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
renderCart();
|
renderCart();
|
||||||
|
showToast(`${item.name || 'Articulo'} agregado`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFromCart(index) {
|
function removeFromCart(index) {
|
||||||
@@ -150,36 +168,39 @@ const POS = (() => {
|
|||||||
const lineDiscount = lineGross * item.discount_pct / 100;
|
const lineDiscount = lineGross * item.discount_pct / 100;
|
||||||
const lineSubtotal = lineGross - lineDiscount;
|
const lineSubtotal = lineGross - lineDiscount;
|
||||||
|
|
||||||
const costHtml = canViewCost ? `<td class="num">${fmt(item.unit_cost)}</td>` : '';
|
const costHtml = canViewCost ? `<td class="num" style="text-align:right;padding:var(--space-2);font-family:var(--font-mono);color:var(--color-text-muted);font-size:var(--text-caption);">${fmt(item.unit_cost)}</td>` : '';
|
||||||
let marginHtml = '';
|
let marginHtml = '';
|
||||||
if (canViewCost) {
|
if (canViewCost) {
|
||||||
const effectivePrice = item.unit_price * (1 - item.discount_pct / 100);
|
const effectivePrice = item.unit_price * (1 - item.discount_pct / 100);
|
||||||
const marginPct = effectivePrice > 0
|
const marginPct = effectivePrice > 0
|
||||||
? ((effectivePrice - item.unit_cost) / effectivePrice * 100).toFixed(1)
|
? ((effectivePrice - item.unit_cost) / effectivePrice * 100).toFixed(1)
|
||||||
: '0.0';
|
: '0.0';
|
||||||
const cls = parseFloat(marginPct) < 5 ? 'margin-warning' : 'margin-info';
|
const marginVal = parseFloat(marginPct);
|
||||||
marginHtml = `<td class="num"><span class="${cls}">${marginPct}%</span></td>`;
|
const cls = marginVal > 30 ? 'margin-high' : marginVal > 15 ? 'margin-mid' : 'margin-low';
|
||||||
|
const color = marginVal > 30 ? 'var(--color-success)' : marginVal > 15 ? 'var(--color-warning)' : 'var(--color-error)';
|
||||||
|
marginHtml = `<td style="text-align:center;padding:var(--space-2);"><span style="font-family:var(--font-mono);font-size:var(--text-caption);font-weight:var(--font-weight-bold);color:${color};">${marginPct}%</span></td>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `<tr class="${i === selectedRow ? 'selected' : ''}" onclick="POS.selectRow(${i})">
|
html += `<tr style="border-bottom:1px solid var(--color-border);cursor:pointer;${i === selectedRow ? 'background:var(--color-primary-muted);' : ''}" onclick="POS.selectRow(${i})">
|
||||||
<td>${i + 1}</td>
|
<td style="padding:var(--space-2);color:var(--color-text-muted);">${i + 1}</td>
|
||||||
<td>
|
<td style="padding:var(--space-2);">
|
||||||
<div class="part-name">${item.name}</div>
|
<div style="font-weight:var(--font-weight-semibold);color:var(--color-text-primary);">${item.name}</div>
|
||||||
<div class="part-number">${item.part_number} | Stock: ${item.stock}</div>
|
<div style="font-family:var(--font-mono);font-size:var(--text-caption);color:var(--color-text-muted);">${item.part_number} | Stock: ${item.stock}</div>
|
||||||
</td>
|
</td>
|
||||||
<td><input type="number" class="qty-input" value="${item.quantity}" min="1"
|
<td style="text-align:center;padding:var(--space-2);"><input type="number" style="width:50px;text-align:center;font-family:var(--font-mono);background:var(--color-bg-base);color:var(--color-text-primary);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:2px 4px;" value="${item.quantity}" min="1"
|
||||||
onchange="POS.updateQty(${i}, this.value)" onclick="event.stopPropagation()"></td>
|
onchange="POS.updateQty(${i}, this.value)" onclick="event.stopPropagation()"></td>
|
||||||
<td class="num">${fmt(item.unit_price)}</td>
|
<td style="text-align:right;padding:var(--space-2);font-family:var(--font-mono);">${fmt(item.unit_price)}</td>
|
||||||
<td><input type="number" class="discount-input" value="${item.discount_pct}" min="0" max="100" step="0.5"
|
<td style="text-align:center;padding:var(--space-2);"><input type="number" style="width:45px;text-align:center;font-family:var(--font-mono);background:var(--color-bg-base);color:var(--color-text-primary);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:2px 4px;" value="${item.discount_pct}" min="0" max="100" step="0.5"
|
||||||
onchange="POS.updateDiscount(${i}, this.value)" onclick="event.stopPropagation()">%</td>
|
onchange="POS.updateDiscount(${i}, this.value)" onclick="event.stopPropagation()">%</td>
|
||||||
<td class="num">${fmt(lineSubtotal)}</td>
|
<td style="text-align:right;padding:var(--space-2);font-family:var(--font-mono);font-weight:var(--font-weight-bold);">${fmt(lineSubtotal)}</td>
|
||||||
${costHtml}
|
${costHtml}
|
||||||
${marginHtml}
|
${marginHtml}
|
||||||
<td><button class="btn-remove" onclick="event.stopPropagation(); POS.removeFromCart(${i})">×</button></td>
|
<td style="text-align:center;"><button style="background:transparent;border:1px solid var(--color-border);border-radius:var(--radius-sm);width:24px;height:24px;cursor:pointer;color:var(--color-text-muted);font-size:14px;" onclick="event.stopPropagation(); POS.removeFromCart(${i})">×</button></td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
tbody.innerHTML = html;
|
tbody.innerHTML = html;
|
||||||
updateTotals();
|
updateTotals();
|
||||||
|
updateAvgMargin();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateQty(index, val) {
|
function updateQty(index, val) {
|
||||||
@@ -230,6 +251,26 @@ const POS = (() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateAvgMargin() {
|
||||||
|
const avgEl = document.getElementById('avgMargin');
|
||||||
|
if (!avgEl || !canViewCost) return;
|
||||||
|
let totalRevenue = 0, totalCost = 0;
|
||||||
|
cart.forEach(item => {
|
||||||
|
if (item.unit_cost > 0) {
|
||||||
|
const effectivePrice = item.unit_price * (1 - item.discount_pct / 100);
|
||||||
|
totalRevenue += effectivePrice * item.quantity;
|
||||||
|
totalCost += item.unit_cost * item.quantity;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (totalRevenue > 0) {
|
||||||
|
const avg = ((totalRevenue - totalCost) / totalRevenue * 100);
|
||||||
|
avgEl.textContent = avg.toFixed(1) + '%';
|
||||||
|
avgEl.style.color = avg > 30 ? 'var(--color-success)' : avg > 15 ? 'var(--color-warning)' : 'var(--color-error)';
|
||||||
|
} else {
|
||||||
|
avgEl.textContent = '--';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getTotal() {
|
function getTotal() {
|
||||||
let subtotal = 0, taxTotal = 0;
|
let subtotal = 0, taxTotal = 0;
|
||||||
cart.forEach(item => {
|
cart.forEach(item => {
|
||||||
@@ -243,9 +284,14 @@ const POS = (() => {
|
|||||||
return Math.round((subtotal + taxTotal) * 100) / 100;
|
return Math.round((subtotal + taxTotal) * 100) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getItemCount() {
|
||||||
|
return cart.reduce((sum, item) => sum + item.quantity, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Search ──────────────────────────
|
// ─── Search ──────────────────────────
|
||||||
function setupSearch() {
|
function setupSearch() {
|
||||||
const input = document.getElementById('itemSearch');
|
const input = document.getElementById('itemSearch');
|
||||||
|
if (!input) return;
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener('input', () => {
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
searchTimeout = setTimeout(() => searchItems(input.value.trim()), 300);
|
searchTimeout = setTimeout(() => searchItems(input.value.trim()), 300);
|
||||||
@@ -271,31 +317,29 @@ const POS = (() => {
|
|||||||
const totals = document.getElementById('totalsPanel');
|
const totals = document.getElementById('totalsPanel');
|
||||||
|
|
||||||
if (data.data.length === 0) {
|
if (data.data.length === 0) {
|
||||||
container.innerHTML = '<div style="padding:20px;text-align:center;color:#999;">Sin resultados</div>';
|
container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--color-text-muted);">Sin resultados</div>';
|
||||||
} else {
|
} else {
|
||||||
let html = '';
|
let html = '';
|
||||||
data.data.forEach(item => {
|
data.data.forEach(item => {
|
||||||
// Determine price for current customer
|
|
||||||
let price = item.price_1;
|
let price = item.price_1;
|
||||||
if (currentCustomer) {
|
if (currentCustomer) {
|
||||||
const tier = currentCustomer.price_tier || 1;
|
const tier = currentCustomer.price_tier || 1;
|
||||||
price = tier === 3 ? item.price_3 : tier === 2 ? item.price_2 : item.price_1;
|
price = tier === 3 ? item.price_3 : tier === 2 ? item.price_2 : item.price_1;
|
||||||
}
|
}
|
||||||
const stockClass = item.stock <= 0 ? 'zero' : '';
|
html += `<div style="padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--color-border);cursor:pointer;display:flex;justify-content:space-between;align-items:center;transition:var(--transition-fast);" onmouseover="this.style.background='var(--color-primary-muted)'" onmouseout="this.style.background=''" onclick='POS.addFromSearch(${JSON.stringify(item).replace(/'/g, "'")}, ${price})'>
|
||||||
html += `<div class="search-result-item" onclick='POS.addFromSearch(${JSON.stringify(item).replace(/'/g, "'")}, ${price})'>
|
|
||||||
<div>
|
<div>
|
||||||
<div class="sr-name">${item.name}</div>
|
<div style="font-weight:var(--font-weight-semibold);">${item.name}</div>
|
||||||
<div class="sr-pn">${item.part_number} | ${item.brand || ''}</div>
|
<div style="font-family:var(--font-mono);font-size:var(--text-caption);color:var(--color-text-muted);">${item.part_number} | ${item.brand || ''}</div>
|
||||||
<div class="sr-stock ${stockClass}">Stock: ${item.stock}</div>
|
<div style="font-size:var(--text-caption);color:${item.stock <= 0 ? 'var(--color-error)' : 'var(--color-text-muted)'};">Stock: ${item.stock}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sr-price">${fmt(price)}</div>
|
<div style="font-family:var(--font-mono);font-weight:var(--font-weight-bold);color:var(--color-primary);">${fmt(price)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.classList.add('active');
|
container.style.display = '';
|
||||||
totals.classList.add('hidden');
|
totals.style.display = 'none';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Search error:', e);
|
console.error('Search error:', e);
|
||||||
}
|
}
|
||||||
@@ -317,13 +361,16 @@ const POS = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hideSearchResults() {
|
function hideSearchResults() {
|
||||||
document.getElementById('searchResults').classList.remove('active');
|
const sr = document.getElementById('searchResults');
|
||||||
document.getElementById('totalsPanel').classList.remove('hidden');
|
const tp = document.getElementById('totalsPanel');
|
||||||
|
if (sr) sr.style.display = 'none';
|
||||||
|
if (tp) tp.style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Customer Search ─────────────────
|
// ─── Customer Search ─────────────────
|
||||||
function setupCustomerSearch() {
|
function setupCustomerSearch() {
|
||||||
const input = document.getElementById('customerSearch');
|
const input = document.getElementById('customerSearch');
|
||||||
|
if (!input) return;
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener('input', () => {
|
||||||
clearTimeout(customerSearchTimeout);
|
clearTimeout(customerSearchTimeout);
|
||||||
customerSearchTimeout = setTimeout(() => searchCustomers(input.value.trim()), 300);
|
customerSearchTimeout = setTimeout(() => searchCustomers(input.value.trim()), 300);
|
||||||
@@ -347,14 +394,14 @@ const POS = (() => {
|
|||||||
const ac = document.getElementById('customerAutocomplete');
|
const ac = document.getElementById('customerAutocomplete');
|
||||||
|
|
||||||
if (data.data.length === 0) {
|
if (data.data.length === 0) {
|
||||||
ac.innerHTML = '<div class="ac-item" style="color:#999;">Sin resultados</div>';
|
ac.innerHTML = '<div style="padding:var(--space-3);color:var(--color-text-muted);">Sin resultados</div>';
|
||||||
} else {
|
} else {
|
||||||
let html = '';
|
let html = '';
|
||||||
data.data.forEach(c => {
|
data.data.forEach(c => {
|
||||||
const tiers = { 1: 'Mostrador', 2: 'Taller', 3: 'Mayoreo' };
|
const tiers = { 1: 'Mostrador', 2: 'Taller', 3: 'Mayoreo' };
|
||||||
html += `<div class="ac-item" onclick='POS.selectCustomer(${JSON.stringify(c).replace(/'/g, "'")})'>
|
html += `<div style="padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--color-border);cursor:pointer;transition:var(--transition-fast);" onmouseover="this.style.background='var(--color-primary-muted)'" onmouseout="this.style.background=''" onclick='POS.selectCustomer(${JSON.stringify(c).replace(/'/g, "'")})'>
|
||||||
<div>${c.name}</div>
|
<div style="font-weight:var(--font-weight-semibold);">${c.name}</div>
|
||||||
<div class="ac-meta">${c.rfc || ''} | ${c.phone || ''} | ${tiers[c.price_tier] || 'P1'} | Credito: ${fmt(c.credit_balance)}/${fmt(c.credit_limit)}</div>
|
<div style="font-size:var(--text-caption);color:var(--color-text-muted);">${c.rfc || ''} | ${c.phone || ''} | ${tiers[c.price_tier] || 'P1'} | Credito: ${fmt(c.credit_balance)}/${fmt(c.credit_limit)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
ac.innerHTML = html;
|
ac.innerHTML = html;
|
||||||
@@ -377,7 +424,7 @@ const POS = (() => {
|
|||||||
`Credito: ${fmt(customer.credit_balance)} / ${fmt(customer.credit_limit)}`;
|
`Credito: ${fmt(customer.credit_balance)} / ${fmt(customer.credit_limit)}`;
|
||||||
document.getElementById('customerSelected').style.display = '';
|
document.getElementById('customerSelected').style.display = '';
|
||||||
|
|
||||||
// Show vehicle info
|
// Show vehicle info banner
|
||||||
if (customer.vehicle_info && customer.vehicle_info.length > 0) {
|
if (customer.vehicle_info && customer.vehicle_info.length > 0) {
|
||||||
const v = customer.vehicle_info[0];
|
const v = customer.vehicle_info[0];
|
||||||
document.getElementById('vehicleInfo').textContent =
|
document.getElementById('vehicleInfo').textContent =
|
||||||
@@ -385,7 +432,7 @@ const POS = (() => {
|
|||||||
document.getElementById('vehicleBanner').classList.add('visible');
|
document.getElementById('vehicleBanner').classList.add('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch full customer detail to get recent purchase info
|
// Fetch full customer detail
|
||||||
try {
|
try {
|
||||||
const detail = await api(`/pos/api/customers/${customer.id}`);
|
const detail = await api(`/pos/api/customers/${customer.id}`);
|
||||||
if (detail.recent_purchases && detail.recent_purchases.length > 0) {
|
if (detail.recent_purchases && detail.recent_purchases.length > 0) {
|
||||||
@@ -393,18 +440,19 @@ const POS = (() => {
|
|||||||
const daysAgo = Math.floor((Date.now() - new Date(last.created_at).getTime()) / 86400000);
|
const daysAgo = Math.floor((Date.now() - new Date(last.created_at).getTime()) / 86400000);
|
||||||
const daysText = daysAgo === 0 ? 'hoy' : daysAgo === 1 ? 'hace 1 dia' : `hace ${daysAgo} dias`;
|
const daysText = daysAgo === 0 ? 'hoy' : daysAgo === 1 ? 'hace 1 dia' : `hace ${daysAgo} dias`;
|
||||||
document.getElementById('lastPurchaseInfo').textContent =
|
document.getElementById('lastPurchaseInfo').textContent =
|
||||||
`Ultima compra: ${fmt(last.total)} ${daysText}`;
|
`${fmt(last.total)} ${daysText}`;
|
||||||
document.getElementById('vehicleBanner').classList.add('visible');
|
document.getElementById('vehicleBanner').classList.add('visible');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Could not fetch customer detail:', e);
|
console.warn('Could not fetch customer detail:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-apply prices based on customer tier
|
// Update CFDI hint
|
||||||
cart.forEach(item => {
|
const cfdiHint = document.getElementById('cfdiHint');
|
||||||
// Fetch updated price for this customer tier (would need to re-query)
|
if (cfdiHint && customer.rfc) {
|
||||||
// For now, prices stay as-is (they were set at add time)
|
cfdiHint.textContent = `RFC: ${customer.rfc}`;
|
||||||
});
|
document.getElementById('cfdiCheck').checked = !!customer.rfc;
|
||||||
|
}
|
||||||
|
|
||||||
renderCart();
|
renderCart();
|
||||||
}
|
}
|
||||||
@@ -412,20 +460,25 @@ const POS = (() => {
|
|||||||
function clearCustomer() {
|
function clearCustomer() {
|
||||||
currentCustomer = null;
|
currentCustomer = null;
|
||||||
document.getElementById('customerSelected').style.display = 'none';
|
document.getElementById('customerSelected').style.display = 'none';
|
||||||
document.getElementById('customerSearchWrap').querySelector('input').style.display = '';
|
const searchInput = document.getElementById('customerSearch');
|
||||||
document.getElementById('customerSearchWrap').querySelector('input').value = '';
|
if (searchInput) {
|
||||||
|
searchInput.style.display = '';
|
||||||
|
searchInput.value = '';
|
||||||
|
}
|
||||||
document.getElementById('vehicleBanner').classList.remove('visible');
|
document.getElementById('vehicleBanner').classList.remove('visible');
|
||||||
|
const cfdiHint = document.getElementById('cfdiHint');
|
||||||
|
if (cfdiHint) cfdiHint.textContent = '';
|
||||||
renderCart();
|
renderCart();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── New Customer Modal ──────────────
|
// ─── New Customer Modal ──────────────
|
||||||
function showNewCustomerModal() {
|
function showNewCustomerModal() {
|
||||||
document.getElementById('newCustomerModal').classList.add('active');
|
document.getElementById('newCustomerModal').classList.add('open');
|
||||||
document.getElementById('ncName').focus();
|
setTimeout(() => document.getElementById('ncName').focus(), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeNewCustomerModal() {
|
function closeNewCustomerModal() {
|
||||||
document.getElementById('newCustomerModal').classList.remove('active');
|
document.getElementById('newCustomerModal').classList.remove('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveNewCustomer() {
|
async function saveNewCustomer() {
|
||||||
@@ -462,7 +515,6 @@ const POS = (() => {
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Select the new customer
|
|
||||||
selectCustomer({
|
selectCustomer({
|
||||||
id: result.id,
|
id: result.id,
|
||||||
name: body.name,
|
name: body.name,
|
||||||
@@ -475,6 +527,7 @@ const POS = (() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
closeNewCustomerModal();
|
closeNewCustomerModal();
|
||||||
|
showToast('Cliente creado');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Error al crear cliente: ' + e.message);
|
alert('Error al crear cliente: ' + e.message);
|
||||||
}
|
}
|
||||||
@@ -482,40 +535,88 @@ const POS = (() => {
|
|||||||
|
|
||||||
// ─── Payment ─────────────────────────
|
// ─── Payment ─────────────────────────
|
||||||
function checkout() {
|
function checkout() {
|
||||||
if (cart.length === 0) { alert('Carrito vacio'); return; }
|
if (cart.length === 0) { showToast('Carrito vacio'); return; }
|
||||||
if (!currentRegister) { alert('No hay caja abierta. Abra una caja primero.'); return; }
|
if (!currentRegister) { alert('No hay caja abierta. Abra una caja primero.'); return; }
|
||||||
|
|
||||||
paymentMethod = 'efectivo';
|
paymentMethod = 'efectivo';
|
||||||
const total = getTotal();
|
const total = getTotal();
|
||||||
|
|
||||||
|
// Populate modal
|
||||||
document.getElementById('modalTotal').textContent = fmt(total);
|
document.getElementById('modalTotal').textContent = fmt(total);
|
||||||
document.getElementById('cashReceived').value = '';
|
document.getElementById('modalItemCount').textContent = `${getItemCount()} productos`;
|
||||||
document.getElementById('changeDisplay').textContent = 'Cambio: $0.00';
|
document.getElementById('modalCustomerName').textContent =
|
||||||
document.getElementById('changeDisplay').className = 'change-display positive';
|
currentCustomer ? currentCustomer.name : 'Publico General';
|
||||||
document.getElementById('paymentRef').value = '';
|
|
||||||
|
|
||||||
// Reset payment method buttons
|
// Reset inputs
|
||||||
document.querySelectorAll('.pm-btn').forEach(b => b.classList.remove('active'));
|
document.getElementById('cashReceived').value = '';
|
||||||
document.querySelector('.pm-btn[data-method="efectivo"]').classList.add('active');
|
document.getElementById('changeDisplay').textContent = '$0.00';
|
||||||
|
document.getElementById('changeDisplay').className = 'cambio-amount positive';
|
||||||
|
const payRef = document.getElementById('paymentRef');
|
||||||
|
if (payRef) payRef.value = '';
|
||||||
|
|
||||||
|
// Populate quick amounts
|
||||||
|
const quickContainer = document.getElementById('quickAmounts');
|
||||||
|
if (quickContainer) {
|
||||||
|
const rounded = Math.ceil(total);
|
||||||
|
const amounts = [rounded, Math.ceil(total / 100) * 100, Math.ceil(total / 500) * 500, Math.ceil(total / 1000) * 1000];
|
||||||
|
const unique = [...new Set(amounts)].slice(0, 4);
|
||||||
|
quickContainer.innerHTML = unique.map(a =>
|
||||||
|
`<button class="quick-btn" onclick="document.getElementById('cashReceived').value=${a};POS.updateChange();">${fmt(a)}</button>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ref amount
|
||||||
|
const refAmount = document.getElementById('refAmount');
|
||||||
|
if (refAmount) refAmount.value = fmt(total);
|
||||||
|
|
||||||
|
// Reset payment tabs
|
||||||
|
document.querySelectorAll('.pago-tab').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelector('.pago-tab[data-method="efectivo"]').classList.add('active');
|
||||||
|
document.getElementById('cashPayment').classList.add('active');
|
||||||
document.getElementById('cashPayment').style.display = '';
|
document.getElementById('cashPayment').style.display = '';
|
||||||
|
document.getElementById('refPayment').classList.remove('active');
|
||||||
document.getElementById('refPayment').style.display = 'none';
|
document.getElementById('refPayment').style.display = 'none';
|
||||||
|
document.getElementById('mixedPayment').classList.remove('active');
|
||||||
document.getElementById('mixedPayment').style.display = 'none';
|
document.getElementById('mixedPayment').style.display = 'none';
|
||||||
|
|
||||||
document.getElementById('paymentModal').classList.add('active');
|
// Reset confirm button
|
||||||
|
const confirmBtn = document.getElementById('btnConfirmPayment');
|
||||||
|
confirmBtn.disabled = false;
|
||||||
|
confirmBtn.textContent = `Confirmar Pago — ${fmt(total)}`;
|
||||||
|
|
||||||
|
document.getElementById('paymentModal').classList.add('open');
|
||||||
setTimeout(() => document.getElementById('cashReceived').focus(), 100);
|
setTimeout(() => document.getElementById('cashReceived').focus(), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPaymentMethod(method, btn) {
|
function selectPaymentMethod(method, btn) {
|
||||||
paymentMethod = method;
|
paymentMethod = method;
|
||||||
document.querySelectorAll('.pm-btn').forEach(b => b.classList.remove('active'));
|
|
||||||
btn.classList.add('active');
|
|
||||||
|
|
||||||
document.getElementById('cashPayment').style.display = method === 'efectivo' ? '' : 'none';
|
// Update tabs
|
||||||
document.getElementById('refPayment').style.display = (method === 'transferencia' || method === 'tarjeta') ? '' : 'none';
|
document.querySelectorAll('.pago-tab').forEach(t => t.classList.remove('active'));
|
||||||
document.getElementById('mixedPayment').style.display = method === 'mixto' ? '' : 'none';
|
if (btn) btn.classList.add('active');
|
||||||
|
|
||||||
|
// Show/hide tab content
|
||||||
|
const tabs = {
|
||||||
|
efectivo: 'cashPayment',
|
||||||
|
transferencia: 'refPayment',
|
||||||
|
tarjeta: 'refPayment',
|
||||||
|
mixto: 'mixedPayment',
|
||||||
|
};
|
||||||
|
|
||||||
|
['cashPayment', 'refPayment', 'mixedPayment'].forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) {
|
||||||
|
const isActive = el.id === tabs[method];
|
||||||
|
el.classList.toggle('active', isActive);
|
||||||
|
el.style.display = isActive ? '' : 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (method === 'efectivo') document.getElementById('cashReceived').focus();
|
if (method === 'efectivo') document.getElementById('cashReceived').focus();
|
||||||
if (method === 'transferencia' || method === 'tarjeta') document.getElementById('paymentRef').focus();
|
if (method === 'transferencia' || method === 'tarjeta') {
|
||||||
|
const ref = document.getElementById('paymentRef');
|
||||||
|
if (ref) ref.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateChange() {
|
function updateChange() {
|
||||||
@@ -523,8 +624,13 @@ const POS = (() => {
|
|||||||
const received = parseFloat(document.getElementById('cashReceived').value) || 0;
|
const received = parseFloat(document.getElementById('cashReceived').value) || 0;
|
||||||
const change = received - total;
|
const change = received - total;
|
||||||
const el = document.getElementById('changeDisplay');
|
const el = document.getElementById('changeDisplay');
|
||||||
el.textContent = `Cambio: ${fmt(Math.abs(change))}`;
|
if (change >= 0) {
|
||||||
el.className = 'change-display ' + (change >= 0 ? 'positive' : 'negative');
|
el.textContent = fmt(change);
|
||||||
|
el.className = 'cambio-amount positive';
|
||||||
|
} else {
|
||||||
|
el.textContent = '-' + fmt(Math.abs(change));
|
||||||
|
el.className = 'cambio-amount negative';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMixedTotal() {
|
function updateMixedTotal() {
|
||||||
@@ -534,13 +640,13 @@ const POS = (() => {
|
|||||||
sum += parseFloat(input.value) || 0;
|
sum += parseFloat(input.value) || 0;
|
||||||
});
|
});
|
||||||
const remaining = total - sum;
|
const remaining = total - sum;
|
||||||
document.getElementById('mixedRemaining').textContent =
|
const el = document.getElementById('mixedRemaining');
|
||||||
remaining > 0 ? `Faltante: ${fmt(remaining)}` : `Cubierto (${fmt(sum)})`;
|
el.textContent = remaining > 0 ? `Faltante: ${fmt(remaining)}` : `Cubierto (${fmt(sum)})`;
|
||||||
document.getElementById('mixedRemaining').style.color = remaining > 0 ? '#c62828' : '#2e7d32';
|
el.style.color = remaining > 0 ? 'var(--color-error)' : 'var(--color-success)';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePaymentModal() {
|
function closePaymentModal() {
|
||||||
document.getElementById('paymentModal').classList.remove('active');
|
document.getElementById('paymentModal').classList.remove('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmPayment() {
|
async function confirmPayment() {
|
||||||
@@ -584,10 +690,12 @@ const POS = (() => {
|
|||||||
amount_paid: amountPaid,
|
amount_paid: amountPaid,
|
||||||
payment_details: paymentDetails,
|
payment_details: paymentDetails,
|
||||||
reference: reference,
|
reference: reference,
|
||||||
|
generate_cfdi: document.getElementById('cfdiCheck').checked,
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('btnConfirmPayment').disabled = true;
|
const confirmBtn = document.getElementById('btnConfirmPayment');
|
||||||
document.getElementById('btnConfirmPayment').textContent = 'Procesando...';
|
confirmBtn.disabled = true;
|
||||||
|
confirmBtn.textContent = 'Procesando...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sale = await api('/pos/api/sales', {
|
const sale = await api('/pos/api/sales', {
|
||||||
@@ -596,6 +704,7 @@ const POS = (() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
lastSaleId = sale.id;
|
lastSaleId = sale.id;
|
||||||
|
lastSaleData = sale;
|
||||||
closePaymentModal();
|
closePaymentModal();
|
||||||
showTicket(sale);
|
showTicket(sale);
|
||||||
|
|
||||||
@@ -605,11 +714,12 @@ const POS = (() => {
|
|||||||
clearCustomer();
|
clearCustomer();
|
||||||
renderCart();
|
renderCart();
|
||||||
|
|
||||||
|
showToast(`Venta #${sale.id} completada`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Error al procesar venta: ' + e.message);
|
alert('Error al procesar venta: ' + e.message);
|
||||||
} finally {
|
} finally {
|
||||||
document.getElementById('btnConfirmPayment').disabled = false;
|
confirmBtn.disabled = false;
|
||||||
document.getElementById('btnConfirmPayment').textContent = 'Confirmar Pago';
|
confirmBtn.textContent = 'Confirmar Pago';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,7 +733,7 @@ const POS = (() => {
|
|||||||
const available = (currentCustomer.credit_limit || 0) - (currentCustomer.credit_balance || 0);
|
const available = (currentCustomer.credit_limit || 0) - (currentCustomer.credit_balance || 0);
|
||||||
|
|
||||||
if (currentCustomer.credit_limit > 0 && total > available) {
|
if (currentCustomer.credit_limit > 0 && total > available) {
|
||||||
if (!confirm(`Credito insuficiente. Disponible: ${fmt(available)}, Total: ${fmt(total)}. Continuar de todas formas?`)) {
|
if (!confirm(`Credito insuficiente. Disponible: ${fmt(available)}, Total: ${fmt(total)}. Continuar?`)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -650,6 +760,7 @@ const POS = (() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
lastSaleId = sale.id;
|
lastSaleId = sale.id;
|
||||||
|
lastSaleData = sale;
|
||||||
showTicket(sale);
|
showTicket(sale);
|
||||||
cart = [];
|
cart = [];
|
||||||
selectedRow = -1;
|
selectedRow = -1;
|
||||||
@@ -662,7 +773,7 @@ const POS = (() => {
|
|||||||
|
|
||||||
// ─── Quotation ───────────────────────
|
// ─── Quotation ───────────────────────
|
||||||
async function saveQuotation() {
|
async function saveQuotation() {
|
||||||
if (cart.length === 0) { alert('Carrito vacio'); return; }
|
if (cart.length === 0) { showToast('Carrito vacio'); return; }
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
items: cart.map(item => ({
|
items: cart.map(item => ({
|
||||||
@@ -680,7 +791,7 @@ const POS = (() => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
alert(`Cotizacion #${result.id} guardada. Total: ${fmt(result.total)}\nValida hasta: ${result.valid_until}`);
|
showToast(`Cotizacion #${result.id} guardada. Total: ${fmt(result.total)}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Error: ' + e.message);
|
alert('Error: ' + e.message);
|
||||||
}
|
}
|
||||||
@@ -718,13 +829,7 @@ const POS = (() => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
alert(
|
showToast(`Apartado #${result.id} creado. Restante: ${fmt(result.remaining)}`);
|
||||||
`Apartado #${result.id} creado.\n` +
|
|
||||||
`Total: ${fmt(result.total)}\n` +
|
|
||||||
`Anticipo: ${fmt(result.amount_paid)}\n` +
|
|
||||||
`Restante: ${fmt(result.remaining)}\n` +
|
|
||||||
`Vence: ${result.expires_at}`
|
|
||||||
);
|
|
||||||
cart = [];
|
cart = [];
|
||||||
selectedRow = -1;
|
selectedRow = -1;
|
||||||
clearCustomer();
|
clearCustomer();
|
||||||
@@ -736,64 +841,108 @@ const POS = (() => {
|
|||||||
|
|
||||||
// ─── Ticket ──────────────────────────
|
// ─── Ticket ──────────────────────────
|
||||||
function showTicket(sale) {
|
function showTicket(sale) {
|
||||||
const lines = [];
|
const dateStr = new Date(sale.created_at).toLocaleString('es-MX', {
|
||||||
lines.push('========================================');
|
year: 'numeric', month: 'short', day: 'numeric',
|
||||||
lines.push(' NEXUS POS');
|
hour: '2-digit', minute: '2-digit'
|
||||||
lines.push('========================================');
|
|
||||||
lines.push(`Venta #${sale.id}`);
|
|
||||||
lines.push(`Fecha: ${new Date(sale.created_at).toLocaleString('es-MX')}`);
|
|
||||||
if (currentCustomer) {
|
|
||||||
lines.push(`Cliente: ${currentCustomer.name}`);
|
|
||||||
if (currentCustomer.rfc) lines.push(`RFC: ${currentCustomer.rfc}`);
|
|
||||||
} else {
|
|
||||||
lines.push('Cliente: Publico General');
|
|
||||||
}
|
|
||||||
lines.push('----------------------------------------');
|
|
||||||
|
|
||||||
(sale.items || []).forEach(item => {
|
|
||||||
lines.push(`${item.name}`);
|
|
||||||
let line = ` ${item.quantity} x ${fmt(item.unit_price)}`;
|
|
||||||
if (item.discount_pct > 0) line += ` (-${item.discount_pct}%)`;
|
|
||||||
line += ` ${fmt(item.subtotal)}`;
|
|
||||||
lines.push(line);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
lines.push('----------------------------------------');
|
const customerName = currentCustomer ? currentCustomer.name : 'Publico General';
|
||||||
lines.push(`Subtotal: ${fmt(sale.subtotal).padStart(12)}`);
|
const customerRfc = currentCustomer && currentCustomer.rfc ? currentCustomer.rfc : '';
|
||||||
if (sale.discount_total > 0) {
|
|
||||||
lines.push(`Descuento: -${fmt(sale.discount_total).padStart(12)}`);
|
|
||||||
}
|
|
||||||
lines.push(`IVA: ${fmt(sale.tax_total).padStart(12)}`);
|
|
||||||
lines.push('========================================');
|
|
||||||
lines.push(`TOTAL: ${fmt(sale.total).padStart(12)}`);
|
|
||||||
lines.push('========================================');
|
|
||||||
|
|
||||||
if (sale.payment_method === 'efectivo') {
|
let itemsHtml = '';
|
||||||
lines.push(`Efectivo: ${fmt(sale.amount_paid).padStart(12)}`);
|
(sale.items || []).forEach(item => {
|
||||||
lines.push(`Cambio: ${fmt(sale.change_given).padStart(12)}`);
|
const itemTotal = (item.unit_price * item.quantity * (1 - (item.discount_pct || 0) / 100));
|
||||||
} else {
|
itemsHtml += `
|
||||||
lines.push(`Pago: ${sale.payment_method}`);
|
<div class="item-line-wide">
|
||||||
}
|
<span class="qty">${item.quantity}</span>
|
||||||
|
<span class="name">${item.name || ''}</span>
|
||||||
|
<span class="price">${fmt(item.unit_price)}</span>
|
||||||
|
<span class="subtotal">${fmt(item.subtotal || itemTotal)}</span>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
lines.push('');
|
const ticketHtml = `
|
||||||
lines.push(' Gracias por su compra!');
|
<div class="store-name">NEXUS AUTOPARTS</div>
|
||||||
lines.push('');
|
<div class="store-tagline">Tu conexion con las refacciones</div>
|
||||||
|
<div class="store-info">
|
||||||
|
Sucursal: ${currentRegister ? currentRegister.branch_name || '' : ''}<br>
|
||||||
|
RFC: NAU210315XX1
|
||||||
|
</div>
|
||||||
|
<hr class="divider-double">
|
||||||
|
<div class="folio-line">
|
||||||
|
<span>VENTA: V-${sale.id}</span>
|
||||||
|
<span>${dateStr}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ticket-row" style="font-size: 9px; color: #555; margin-bottom: 4px;">
|
||||||
|
<span>Cliente: ${customerName}</span>
|
||||||
|
${customerRfc ? `<span>RFC: ${customerRfc}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
<hr class="divider">
|
||||||
|
<div class="item-line-wide" style="font-weight: bold; font-size: 9px; color: #555; text-transform: uppercase;">
|
||||||
|
<span class="qty">Cant</span>
|
||||||
|
<span class="name">Descripcion</span>
|
||||||
|
<span class="price">P. Unit</span>
|
||||||
|
<span class="subtotal">Importe</span>
|
||||||
|
</div>
|
||||||
|
<hr class="divider" style="margin: 2px 0;">
|
||||||
|
${itemsHtml}
|
||||||
|
<hr class="divider-double">
|
||||||
|
<div class="total-section">
|
||||||
|
<div class="total-line">
|
||||||
|
<span>Subtotal:</span><span>${fmt(sale.subtotal)}</span>
|
||||||
|
</div>
|
||||||
|
${sale.discount_total > 0 ? `<div class="total-line"><span>Descuento:</span><span>-${fmt(sale.discount_total)}</span></div>` : ''}
|
||||||
|
<div class="total-line">
|
||||||
|
<span>IVA 16%:</span><span>${fmt(sale.tax_total)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="total-line grand">
|
||||||
|
<span>TOTAL:</span><span>${fmt(sale.total)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="divider">
|
||||||
|
<div class="payment-section">
|
||||||
|
<div class="ticket-row">
|
||||||
|
<span>Forma de pago:</span><span>${sale.payment_method || paymentMethod}</span>
|
||||||
|
</div>
|
||||||
|
${sale.payment_method === 'efectivo' ? `
|
||||||
|
<div class="ticket-row">
|
||||||
|
<span>Recibido:</span><span>${fmt(sale.amount_paid)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ticket-row" style="font-weight: bold;">
|
||||||
|
<span>Cambio:</span><span>${fmt(sale.change_given || 0)}</span>
|
||||||
|
</div>` : ''}
|
||||||
|
</div>
|
||||||
|
<hr class="divider">
|
||||||
|
<div class="footer-section">
|
||||||
|
<div class="thanks">Gracias por su compra!</div>
|
||||||
|
<div>Conserve su ticket como comprobante.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
document.getElementById('ticketPreview').textContent = lines.join('\n');
|
// Set both print area and preview
|
||||||
document.getElementById('ticketModal').classList.add('active');
|
const printArea = document.getElementById('ticketContent');
|
||||||
|
if (printArea) printArea.innerHTML = ticketHtml;
|
||||||
|
const preview = document.getElementById('ticketPreviewContent');
|
||||||
|
if (preview) preview.innerHTML = ticketHtml;
|
||||||
|
|
||||||
|
document.getElementById('ticketModal').classList.add('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeTicketModal() {
|
function closeTicketModal() {
|
||||||
document.getElementById('ticketModal').classList.remove('active');
|
document.getElementById('ticketModal').classList.remove('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
function printTicket() {
|
function printTicket() {
|
||||||
|
// Make print area visible for @media print
|
||||||
|
const area = document.getElementById('ticketPrintArea');
|
||||||
|
if (area) area.style.display = 'block';
|
||||||
window.print();
|
window.print();
|
||||||
|
setTimeout(() => { if (area) area.style.display = 'none'; }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Last Sale ───────────────────────
|
// ─── Last Sale ───────────────────────
|
||||||
async function showLastSale() {
|
async function showLastSale() {
|
||||||
if (!lastSaleId) { alert('No hay venta reciente'); return; }
|
if (!lastSaleId) { showToast('No hay venta reciente'); return; }
|
||||||
try {
|
try {
|
||||||
const sale = await api(`/pos/api/sales/${lastSaleId}`);
|
const sale = await api(`/pos/api/sales/${lastSaleId}`);
|
||||||
showTicket(sale);
|
showTicket(sale);
|
||||||
@@ -804,15 +953,12 @@ const POS = (() => {
|
|||||||
|
|
||||||
// ─── Drawer ──────────────────────────
|
// ─── Drawer ──────────────────────────
|
||||||
function openDrawer() {
|
function openDrawer() {
|
||||||
// Cash drawer open command (ESC/POS compatible)
|
showToast('Comando enviado al cajon de efectivo.');
|
||||||
// In a real implementation, this would send the command to the printer
|
|
||||||
alert('Comando enviado al cajon de efectivo.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Keyboard Shortcuts ──────────────
|
// ─── Keyboard Shortcuts ──────────────
|
||||||
function setupKeyboard() {
|
function setupKeyboard() {
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
// Don't intercept when typing in inputs
|
|
||||||
const tag = e.target.tagName;
|
const tag = e.target.tagName;
|
||||||
const inInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
const inInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
||||||
|
|
||||||
@@ -845,12 +991,20 @@ const POS = (() => {
|
|||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (document.getElementById('paymentModal').classList.contains('active')) {
|
if (document.getElementById('paymentModal').classList.contains('open')) {
|
||||||
closePaymentModal();
|
closePaymentModal();
|
||||||
} else if (document.getElementById('newCustomerModal').classList.contains('active')) {
|
} else if (document.getElementById('newCustomerModal').classList.contains('open')) {
|
||||||
closeNewCustomerModal();
|
closeNewCustomerModal();
|
||||||
} else if (document.getElementById('ticketModal').classList.contains('active')) {
|
} else if (document.getElementById('ticketModal').classList.contains('open')) {
|
||||||
closeTicketModal();
|
closeTicketModal();
|
||||||
|
} else if (document.querySelector('.confirm-overlay.active')) {
|
||||||
|
// Close cancel modal
|
||||||
|
const overlay = document.getElementById('overlay-cancelar-venta');
|
||||||
|
const dialog = document.getElementById('modal-cancelar-venta');
|
||||||
|
if (overlay && overlay.classList.contains('active')) {
|
||||||
|
overlay.classList.remove('active');
|
||||||
|
dialog.classList.remove('active');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
hideSearchResults();
|
hideSearchResults();
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user