feat(pos): add Cut Z (close register) UI flow

- Add 'Corte Z' button in secondary actions panel
- Add modal showing register summary before closing:
  - opening amount, total sales, cash sales, change given
  - cash movements in/out, cancellations, expected cash
  - payment method breakdown and movement detail list
- loadCutX() fetches current register summary (read-only)
- confirmCutZ() calls POST /pos/api/register/cut-z with counted amount
- Auto-fills closing amount with expected cash
- Shows toast with difference after closing
- Resets register state to 'Sin caja abierta' after close
- Bump pos.css and pos.js cache-bust to v=3
This commit is contained in:
2026-05-18 06:59:18 +00:00
parent e38148e8d5
commit d9741b21f6
2 changed files with 105 additions and 2 deletions

View File

@@ -162,6 +162,78 @@ const POS = (() => {
}
}
// ─── Cut X / Z (Close Register) ──────
function showCutZModal() {
document.getElementById('cutZModal').classList.add('open');
document.getElementById('cutZResult').innerHTML = '';
loadCutX();
}
function closeCutZModal() {
document.getElementById('cutZModal').classList.remove('open');
document.getElementById('cutZResult').innerHTML = '';
}
async function loadCutX() {
const el = document.getElementById('cutZSummary');
try {
const data = await api('/pos/api/register/cut-x');
if (data.error) {
el.innerHTML = '<p style="color:var(--color-error);">' + data.error + '</p>';
return;
}
let html = '<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);font-size:var(--text-body-sm);">';
html += '<div><span style="color:var(--color-text-muted);">Efectivo inicial</span><br/><strong>$' + fmt(data.opening_amount) + '</strong></div>';
html += '<div><span style="color:var(--color-text-muted);">Ventas totales</span><br/><strong>$' + fmt(data.total_sales) + '</strong> (' + data.total_sales_count + ')</div>';
html += '<div><span style="color:var(--color-text-muted);">Efectivo en ventas</span><br/><strong>$' + fmt(data.cash_from_sales) + '</strong></div>';
html += '<div><span style="color:var(--color-text-muted);">Cambio entregado</span><br/><strong>-$' + fmt(data.change_given) + '</strong></div>';
html += '<div><span style="color:var(--color-text-muted);">Entradas de efectivo</span><br/><strong>+$' + fmt(data.cash_movements_in) + '</strong></div>';
html += '<div><span style="color:var(--color-text-muted);">Salidas de efectivo</span><br/><strong>-$' + fmt(data.cash_movements_out) + '</strong></div>';
html += '<div><span style="color:var(--color-text-muted);">Cancelaciones</span><br/><strong>' + data.cancelled_count + ' ($' + fmt(data.cancelled_amount) + ')</strong></div>';
html += '<div><span style="color:var(--color-text-muted);">Efectivo esperado</span><br/><strong style="color:var(--color-accent);font-size:1.1em;">$' + fmt(data.expected_cash) + '</strong></div>';
html += '</div>';
if (data.sales_by_method && Object.keys(data.sales_by_method).length) {
html += '<div style="margin-top:var(--space-3);padding-top:var(--space-3);border-top:1px solid var(--color-border);">';
html += '<span style="color:var(--color-text-muted);font-size:var(--text-caption);">Por metodo de pago:</span><br/>';
for (const [method, info] of Object.entries(data.sales_by_method)) {
html += '<span style="font-size:var(--text-body-sm);margin-right:var(--space-3);">' + method + ': $' + fmt(info.amount) + ' (' + info.count + ')</span>';
}
html += '</div>';
}
if (data.movement_detail && data.movement_detail.length) {
html += '<div style="margin-top:var(--space-3);padding-top:var(--space-3);border-top:1px solid var(--color-border);">';
html += '<span style="color:var(--color-text-muted);font-size:var(--text-caption);">Movimientos de caja:</span><br/>';
data.movement_detail.forEach(function(m) {
html += '<div style="font-size:var(--text-body-sm);">' + m.type + ' $' + fmt(m.amount) + ' — ' + (m.reason || '') + '</div>';
});
html += '</div>';
}
el.innerHTML = html;
document.getElementById('cutZClosingAmount').value = data.expected_cash.toFixed(2);
} catch (e) {
el.innerHTML = '<p style="color:var(--color-error);">Error cargando resumen</p>';
}
}
async function confirmCutZ() {
const amount = parseFloat(document.getElementById('cutZClosingAmount').value) || 0;
try {
const data = await api('/pos/api/register/cut-z', {
method: 'POST',
body: JSON.stringify({ closing_amount: amount })
});
if (data.error) {
document.getElementById('cutZResult').innerHTML = '<span style="color:var(--color-error);">' + data.error + '</span>';
return;
}
const diffColor = data.difference > 0 ? 'var(--color-success)' : (data.difference < 0 ? 'var(--color-error)' : 'var(--color-text-muted)');
document.getElementById('cutZResult').innerHTML = '<span style="color:var(--color-success);">Caja cerrada correctamente</span>';
document.getElementById('registerInfo').innerHTML = '<span style="color:var(--color-text-muted);" onclick="POS.showOpenRegisterModal()">Sin caja abierta</span>';
currentRegister = null;
closeCutZModal();
showToast('Corte Z completado. Diferencia: $' + data.difference.toFixed(2));
} catch (e) {
document.getElementById('cutZResult').innerHTML = '<span style="color:var(--color-error);">Error de red</span>';
}
}
// ─── Cart ────────────────────────────
function addToCart(item) {
const existing = cart.find(c => c.inventory_id === item.inventory_id);
@@ -1195,5 +1267,6 @@ const POS = (() => {
showTicket, closeTicketModal, printTicket,
connectThermal, thermalPrint,
showOpenRegisterModal, closeOpenRegisterModal, openRegister,
showCutZModal, closeCutZModal, loadCutX, confirmCutZ,
};
})();