Bloqueantes POS: modal-pago, ticket-termico, banner-cliente, fkeys-footer, columnas-costo-margen Componentes nuevos: calculadora-cambio, panel-deslizante, badge-cfdi, arbol-colapsable, tarjeta-metrica, grafica-barras, selector-periodo, etiqueta-codigo-barras Estados: estado-vacio, banner-offline, modal-confirmacion Páginas actualizadas: facturación, contabilidad, dashboard, configuración, reportes
404 lines
13 KiB
HTML
404 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es" data-theme="industrial">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Nexus Autoparts — Calculadora de Cambio</title>
|
|
<link rel="stylesheet" href="../tokens/tokens.css">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: var(--font-body); background: var(--color-bg-base);
|
|
color: var(--color-text-primary); padding: var(--space-8);
|
|
transition: var(--transition-normal);
|
|
}
|
|
.theme-switcher {
|
|
position: sticky; top: 0; z-index: calc(var(--z-modal) + 10);
|
|
display: flex; gap: var(--space-2);
|
|
padding: var(--space-3) var(--space-4);
|
|
background: var(--color-bg-elevated);
|
|
border-bottom: 1px solid var(--color-border);
|
|
margin: calc(-1 * var(--space-8)); margin-bottom: var(--space-8);
|
|
}
|
|
.theme-switcher button {
|
|
padding: var(--space-2) var(--space-4);
|
|
border: 1px solid var(--color-border);
|
|
background: var(--color-bg-base); color: var(--color-text-primary);
|
|
border-radius: var(--radius-md); cursor: pointer;
|
|
font-family: var(--font-body); font-size: var(--text-body-sm);
|
|
transition: var(--transition-fast);
|
|
}
|
|
.theme-switcher button.active {
|
|
background: var(--color-primary); color: var(--color-text-inverse);
|
|
border-color: var(--color-primary);
|
|
}
|
|
h1 { font-family: var(--font-heading); font-size: var(--text-h2);
|
|
font-weight: var(--heading-weight-primary); margin-bottom: var(--space-2);
|
|
color: var(--color-text-accent); }
|
|
.subtitle { color: var(--color-text-muted); font-size: var(--text-body); margin-bottom: var(--space-8); }
|
|
section { margin-bottom: var(--space-10); }
|
|
section > h2 {
|
|
font-family: var(--font-heading); font-size: var(--text-h4);
|
|
font-weight: var(--heading-weight-secondary); color: var(--color-text-secondary);
|
|
margin-bottom: var(--space-4); padding-bottom: var(--space-2);
|
|
border-bottom: 1px solid var(--color-border);
|
|
}
|
|
|
|
/* Calculator Container */
|
|
.calc-container {
|
|
max-width: 520px;
|
|
margin: 0 auto;
|
|
background: var(--color-bg-elevated);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--space-8);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
/* Total display */
|
|
.total-display {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
padding: var(--space-4) var(--space-5);
|
|
background: var(--color-surface-1);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
margin-bottom: var(--space-6);
|
|
}
|
|
.total-display .label {
|
|
font-size: var(--text-body);
|
|
color: var(--color-text-muted);
|
|
font-weight: var(--font-weight-semibold);
|
|
text-transform: uppercase;
|
|
letter-spacing: var(--tracking-wider);
|
|
}
|
|
.total-display .amount {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-h3);
|
|
font-weight: var(--font-weight-bold);
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
/* Input area */
|
|
.input-group {
|
|
margin-bottom: var(--space-5);
|
|
}
|
|
.input-group label {
|
|
display: block;
|
|
font-size: var(--text-label);
|
|
color: var(--color-text-secondary);
|
|
font-weight: var(--font-weight-semibold);
|
|
text-transform: uppercase;
|
|
letter-spacing: var(--tracking-wide);
|
|
margin-bottom: var(--space-2);
|
|
}
|
|
.input-group .input-wrapper {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.input-group .currency-sign {
|
|
position: absolute;
|
|
left: var(--space-4);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-h2);
|
|
color: var(--color-text-muted);
|
|
pointer-events: none;
|
|
font-weight: var(--font-weight-bold);
|
|
}
|
|
.input-group input {
|
|
width: 100%;
|
|
padding: var(--space-4) var(--space-4) var(--space-4) var(--space-10);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-h2);
|
|
font-weight: var(--font-weight-bold);
|
|
background: var(--color-bg-base);
|
|
color: var(--color-text-primary);
|
|
border: 2px solid var(--color-border-strong);
|
|
border-radius: var(--radius-md);
|
|
outline: none;
|
|
transition: var(--transition-fast);
|
|
text-align: right;
|
|
}
|
|
.input-group input:focus {
|
|
border-color: var(--color-border-focus);
|
|
box-shadow: var(--shadow-focus);
|
|
}
|
|
.input-group input::placeholder {
|
|
color: var(--color-text-disabled);
|
|
font-weight: var(--font-weight-regular);
|
|
}
|
|
|
|
/* Quick buttons */
|
|
.quick-buttons {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
margin-bottom: var(--space-6);
|
|
flex-wrap: wrap;
|
|
}
|
|
.quick-btn {
|
|
flex: 1;
|
|
min-width: 80px;
|
|
padding: var(--space-3) var(--space-3);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-body);
|
|
font-weight: var(--font-weight-semibold);
|
|
background: var(--color-surface-2);
|
|
color: var(--color-text-primary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
cursor: pointer;
|
|
transition: var(--transition-fast);
|
|
text-align: center;
|
|
}
|
|
.quick-btn:hover {
|
|
background: var(--color-primary-muted);
|
|
border-color: var(--color-border-accent);
|
|
color: var(--color-text-accent);
|
|
}
|
|
.quick-btn:active {
|
|
transform: scale(0.96);
|
|
}
|
|
.quick-btn.exact {
|
|
font-family: var(--font-body);
|
|
background: var(--btn-secondary-bg);
|
|
color: var(--btn-secondary-text);
|
|
border-color: var(--btn-secondary-border);
|
|
}
|
|
.quick-btn.exact:hover {
|
|
background: var(--btn-secondary-bg-hover);
|
|
}
|
|
|
|
/* Change display */
|
|
.change-display {
|
|
padding: var(--space-5) var(--space-6);
|
|
border-radius: var(--radius-lg);
|
|
text-align: center;
|
|
transition: var(--transition-normal);
|
|
border: 2px solid transparent;
|
|
}
|
|
.change-display .change-label {
|
|
font-size: var(--text-body-sm);
|
|
text-transform: uppercase;
|
|
letter-spacing: var(--tracking-wider);
|
|
font-weight: var(--font-weight-semibold);
|
|
margin-bottom: var(--space-1);
|
|
}
|
|
.change-display .change-amount {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-h1);
|
|
font-weight: var(--font-weight-bold);
|
|
line-height: var(--leading-h1);
|
|
}
|
|
.change-display.positive {
|
|
background: var(--color-success-light);
|
|
border-color: var(--color-success);
|
|
}
|
|
.change-display.positive .change-label {
|
|
color: var(--color-success-dark);
|
|
}
|
|
.change-display.positive .change-amount {
|
|
color: var(--color-success-dark);
|
|
}
|
|
.change-display.negative {
|
|
background: var(--color-error-light);
|
|
border-color: var(--color-error);
|
|
}
|
|
.change-display.negative .change-label {
|
|
color: var(--color-error-dark);
|
|
}
|
|
.change-display.negative .change-amount {
|
|
color: var(--color-error-dark);
|
|
}
|
|
.change-display.zero {
|
|
background: var(--color-surface-2);
|
|
border-color: var(--color-border);
|
|
}
|
|
.change-display.zero .change-label {
|
|
color: var(--color-text-muted);
|
|
}
|
|
.change-display.zero .change-amount {
|
|
color: var(--color-text-muted);
|
|
}
|
|
.change-display.empty {
|
|
background: var(--color-surface-1);
|
|
border-color: var(--color-border);
|
|
}
|
|
.change-display.empty .change-label {
|
|
color: var(--color-text-disabled);
|
|
}
|
|
.change-display.empty .change-amount {
|
|
color: var(--color-text-disabled);
|
|
font-size: var(--text-h3);
|
|
}
|
|
|
|
/* Denomination breakdown */
|
|
.breakdown {
|
|
margin-top: var(--space-5);
|
|
padding: var(--space-4);
|
|
background: var(--color-surface-1);
|
|
border-radius: var(--radius-md);
|
|
border: 1px solid var(--color-border);
|
|
display: none;
|
|
}
|
|
.breakdown.visible { display: block; }
|
|
.breakdown-title {
|
|
font-size: var(--text-caption);
|
|
color: var(--color-text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: var(--tracking-widest);
|
|
margin-bottom: var(--space-3);
|
|
}
|
|
.breakdown-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: var(--space-1) 0;
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-body-sm);
|
|
color: var(--color-text-secondary);
|
|
}
|
|
.breakdown-row .denom { color: var(--color-text-muted); }
|
|
.breakdown-row .count { color: var(--color-text-accent); font-weight: var(--font-weight-semibold); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="theme-switcher">
|
|
<button class="active" onclick="document.documentElement.dataset.theme='industrial'; this.classList.add('active'); this.nextElementSibling.classList.remove('active')">🔧 Industrial</button>
|
|
<button onclick="document.documentElement.dataset.theme='modern'; this.classList.add('active'); this.previousElementSibling.classList.remove('active')">⚡ Moderno</button>
|
|
</div>
|
|
|
|
<h1>Calculadora de Cambio</h1>
|
|
<p class="subtitle">Calcula el cambio a devolver al cliente en tiempo real</p>
|
|
|
|
<section>
|
|
<div class="calc-container">
|
|
<!-- Total -->
|
|
<div class="total-display">
|
|
<span class="label">Total a cobrar</span>
|
|
<span class="amount" id="totalAmount">$4,828.50</span>
|
|
</div>
|
|
|
|
<!-- Input -->
|
|
<div class="input-group">
|
|
<label for="received">Monto Recibido</label>
|
|
<div class="input-wrapper">
|
|
<span class="currency-sign">$</span>
|
|
<input type="text" id="received" placeholder="0.00" autocomplete="off" inputmode="decimal">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick buttons -->
|
|
<div class="quick-buttons">
|
|
<button class="quick-btn" onclick="setAmount(500)">$500</button>
|
|
<button class="quick-btn" onclick="setAmount(1000)">$1,000</button>
|
|
<button class="quick-btn" onclick="setAmount(2000)">$2,000</button>
|
|
<button class="quick-btn" onclick="setAmount(5000)">$5,000</button>
|
|
<button class="quick-btn exact" onclick="setExact()">Exacto</button>
|
|
</div>
|
|
|
|
<!-- Change display -->
|
|
<div class="change-display empty" id="changeDisplay">
|
|
<div class="change-label">Cambio</div>
|
|
<div class="change-amount" id="changeAmount">Ingrese monto</div>
|
|
</div>
|
|
|
|
<!-- Breakdown -->
|
|
<div class="breakdown" id="breakdown">
|
|
<div class="breakdown-title">Desglose sugerido</div>
|
|
<div id="breakdownRows"></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<script>
|
|
const TOTAL = 4828.50;
|
|
const input = document.getElementById('received');
|
|
const changeDisplay = document.getElementById('changeDisplay');
|
|
const changeAmount = document.getElementById('changeAmount');
|
|
const breakdown = document.getElementById('breakdown');
|
|
const breakdownRows = document.getElementById('breakdownRows');
|
|
|
|
function formatCurrency(n) {
|
|
return '$' + n.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
}
|
|
|
|
function setAmount(val) {
|
|
input.value = val.toLocaleString('es-MX');
|
|
calculate();
|
|
input.focus();
|
|
}
|
|
|
|
function setExact() {
|
|
input.value = TOTAL.toLocaleString('es-MX');
|
|
calculate();
|
|
input.focus();
|
|
}
|
|
|
|
function parseInput(str) {
|
|
if (!str) return NaN;
|
|
// Remove commas and spaces
|
|
let cleaned = str.replace(/[,\s]/g, '');
|
|
return parseFloat(cleaned);
|
|
}
|
|
|
|
function getBreakdown(amount) {
|
|
const denoms = [1000, 500, 200, 100, 50, 20, 10, 5, 2, 1, 0.50];
|
|
const denomLabels = ['$1,000', '$500', '$200', '$100', '$50', '$20', '$10', '$5', '$2', '$1', '$0.50'];
|
|
let remaining = Math.round(amount * 100) / 100;
|
|
const result = [];
|
|
for (let i = 0; i < denoms.length; i++) {
|
|
const count = Math.floor(remaining / denoms[i]);
|
|
if (count > 0) {
|
|
result.push({ denom: denomLabels[i], count: count });
|
|
remaining = Math.round((remaining - count * denoms[i]) * 100) / 100;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function calculate() {
|
|
const received = parseInput(input.value);
|
|
|
|
if (isNaN(received) || input.value.trim() === '') {
|
|
changeDisplay.className = 'change-display empty';
|
|
changeAmount.textContent = 'Ingrese monto';
|
|
breakdown.classList.remove('visible');
|
|
return;
|
|
}
|
|
|
|
const change = Math.round((received - TOTAL) * 100) / 100;
|
|
|
|
if (change > 0) {
|
|
changeDisplay.className = 'change-display positive';
|
|
changeAmount.textContent = formatCurrency(change);
|
|
// Show breakdown
|
|
const rows = getBreakdown(change);
|
|
breakdownRows.innerHTML = rows.map(r =>
|
|
`<div class="breakdown-row"><span class="denom">${r.denom}</span><span class="count">× ${r.count}</span></div>`
|
|
).join('');
|
|
breakdown.classList.add('visible');
|
|
} else if (change < 0) {
|
|
changeDisplay.className = 'change-display negative';
|
|
changeAmount.textContent = '-' + formatCurrency(Math.abs(change));
|
|
breakdown.classList.remove('visible');
|
|
} else {
|
|
changeDisplay.className = 'change-display zero';
|
|
changeAmount.textContent = formatCurrency(0);
|
|
breakdown.classList.remove('visible');
|
|
}
|
|
}
|
|
|
|
input.addEventListener('input', calculate);
|
|
input.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
input.value = '';
|
|
calculate();
|
|
}
|
|
});
|
|
|
|
// Auto-focus on load
|
|
input.focus();
|
|
</script>
|
|
</body>
|
|
</html> |