Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!doctype html>
- <html lang="es">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no" />
- <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; media-src *;">
- <title>Calculadora Vuelto</title>
- <style>
- :root{--bg:#f6f8fa;--card:#fff;--muted:#6b7280;--accent:#0f62fe;--danger:#ef4444;--success:#16a34a;--btn-h:56px}*{box-sizing:border-box}body{margin:0;font-family:Inter,system-ui,-apple-system,"Segoe UI",Roboto,Arial;background:linear-gradient(180deg,var(--bg),#eef2ff 120px);padding:12px;color:#111827;-webkit-tap-highlight-color:transparent;user-select:none}.app{max-width:920px;margin:0 auto}.card{background:var(--card);border-radius:12px;padding:12px;box-shadow:0 6px 18px rgba(17,24,39,0.06);margin-bottom:12px}.label{display:block;font-size:.92rem;color:var(--muted);margin-bottom:6px}.input{width:100%;padding:10px 12px;border-radius:8px;border:1px solid #e6e9ef;font-size:1rem;margin-bottom:8px;user-select:text}.row{display:flex;gap:8px;flex-wrap:wrap}button{display:inline-flex;align-items:center;justify-content:center;background:var(--accent);color:white;border:none;padding:10px 12px;border-radius:10px;font-weight:700;cursor:pointer;touch-action:manipulation;min-height:44px}button:active{transform:scale(0.95);transition:transform 0.1s}button.secondary{background:#6b7280}button.ghost{background:transparent;color:var(--accent);border:1px solid #dbeafe}button.voice-btn{background:#ef4444;color:white;font-size:1.1rem}button.voice-btn.listening{background:#16a34a;animation:pulse 1s infinite}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.7}}.small{font-size:.86rem;color:var(--muted)}.display{height:56px;border-radius:8px;border:1px solid #e6e9ef;padding:10px;font-size:1.2rem;display:flex;align-items:center;justify-content:flex-end;background:#fff}.kb{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:8px}.kb button{height:var(--btn-h);font-size:1.05rem;border-radius:8px}.kb .muted-btn{background:#f3f4f6;color:#111827;border:none}.big-op{width:100%;height:56px;border-radius:10px;font-size:1.25rem;font-weight:900}.hidden{display:none}.small-note{font-size:.88rem;color:var(--muted);margin-top:6px}.breakdown{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}.chip{background:#f3f4f6;padding:8px 10px;border-radius:999px;font-weight:700}.footer{font-size:.82rem;color:var(--muted);text-align:center;margin-top:8px}.bill-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}.bill{padding:10px 12px;border-radius:8px;font-weight:800;min-width:72px;text-align:center;touch-action:manipulation;cursor:pointer;color:white;box-shadow:0 4px 10px rgba(2,6,23,0.08);min-height:50px;display:flex;align-items:center;justify-content:center;font-size:1.05rem}.bill:active{transform:scale(0.95);transition:transform 0.1s}.bill-50{background:#6d28d9}.bill-100{background:#16a34a}.bill-200{background:#b45309}.bill-500{background:#075985}.bill-1000{background:#0891b2}.bill-2000{background:#374151}.bill-10000{background:#b45309;color:#111827}.bill-20000{background:#92400e;color:#111827}.quick-amounts{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}.quick-amt{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;font-weight:800;padding:12px 16px;border-radius:10px;min-height:56px;flex:1 1 30%;font-size:1.1rem;box-shadow:0 4px 12px rgba(102,126,234,0.3);border:none}.quick-amt:active{transform:scale(0.95)}.quick-amt.frequent{background:linear-gradient(135deg,#f093fb 0%,#f5576c 100%);position:relative}.quick-amt.frequent::after{content:'⭐';position:absolute;top:4px;right:6px;font-size:0.8rem}.btn-calc{background:var(--danger)!important;padding:14px 18px!important;font-size:1.15rem!important;border-radius:12px!important;box-shadow:0 8px 20px rgba(239,68,68,0.18);flex:1;min-height:56px!important}.kbOverlay{position:fixed;left:0;right:0;bottom:0;background:#fff;padding:12px;border-top-left-radius:12px;border-top-right-radius:12px;box-shadow:0 -20px 40px rgba(2,6,23,0.12);z-index:9999}.kbOverlay .display{height:56px;border-radius:8px;border:1px solid #e6e9ef;padding:10px;font-size:1.25rem;display:flex;align-items:center;justify-content:flex-end;background:#fff}.kbOverlay .row-under-display{display:flex;align-items:center;justify-content:space-between;margin-top:8px;gap:12px}.kbOverlay .kb-del{width:68px;height:48px;border-radius:8px;border:none;background:#f3f4f6;color:#111827;font-weight:800;font-size:1.1rem}.kbOverlay .kb-ok{height:54px;min-width:140px;border-radius:10px;background:var(--danger);color:white;font-weight:900;font-size:1.12rem;box-shadow:0 8px 20px rgba(239,68,68,0.12)}.kbOverlay .kb-close{height:52px;min-width:110px;border-radius:10px;background:#111827;color:white;font-weight:800;font-size:1rem}.kbOverlay .kb-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}.kbOverlay .kb-grid button{height:56px;border-radius:8px;font-size:1.05rem}.kbOverlay .left-slot{flex:0 0 72px}.kbOverlay .center-slot{flex:1;display:flex;justify-content:center}.kbOverlay .right-slot{flex:0 0 120px;display:flex;justify-content:flex-end}@media(min-width:900px){.bill{min-width:96px}}#miniCalcResult{background:#fff;padding:10px;border-radius:8px;box-shadow:0 6px 18px rgba(17,24,39,0.06);margin-bottom:8px}#miniCalcResult .note{font-size:.9rem;color:var(--muted);margin-bottom:6px}.freq-badge{display:inline-block;background:#ef4444;color:white;border-radius:12px;padding:2px 6px;font-size:0.7rem;margin-left:4px;font-weight:800}.voice-status-error{background:#fee;border:1px solid #ef4444;padding:8px;border-radius:6px;margin-top:6px}.voice-status-success{background:#f0fdf4;border:1px solid #22c55e;padding:8px;border-radius:6px;margin-top:6px}
- </style>
- </head>
- <body>
- <div class="app">
- <div class="card">
- <h2 style="margin:0 0 8px 0">💰 Calculadora de Vuelto</h2>
- <div class="small-note">
- <span id="platformInfo" style="color:var(--accent);font-weight:700">Cargando...</span>
- </div>
- <label class="label" for="totalCompra">Monto a cobrar</label>
- <input id="totalCompra" class="input" type="number" min="0" step="1" value="11623">
- <label class="label" for="montoPagado">Monto que pagó</label>
- <input id="montoPagado" class="input" type="number" min="0" step="1" value="12000">
- <button id="voiceBtn" class="voice-btn" style="width:100%;margin-bottom:8px">🎤 Decir monto</button>
- <div id="voiceStatus" class="small-note" style="text-align:center;margin-bottom:8px"></div>
- <div id="frequentAmounts" class="hidden">
- <div class="small" style="margin-bottom:6px;font-weight:700">⭐ Más usados:</div>
- <div id="frequentBtns" class="quick-amounts"></div>
- </div>
- <div class="small" style="margin-top:10px;margin-bottom:6px;font-weight:700">⚡ Montos rápidos:</div>
- <div class="quick-amounts">
- <button class="quick-amt" data-quick="500">$500</button>
- <button class="quick-amt" data-quick="1000">$1K</button>
- <button class="quick-amt" data-quick="2000">$2K</button>
- <button class="quick-amt" data-quick="5000">$5K</button>
- <button class="quick-amt" data-quick="10000">$10K</button>
- <button class="quick-amt" data-quick="20000">$20K</button>
- </div>
- <div style="display:flex;align-items:center;gap:8px;margin-top:10px">
- <div class="modeToggle">
- <div class="small" style="margin-right:6px;font-weight:700">Modo</div>
- <button id="modeAdd" class="ghost">Sumar</button>
- <button id="modeSet" class="ghost">Poner</button>
- </div>
- <button id="tecladoRapidoBtn" class="ghost">Teclado</button>
- </div>
- <div class="bill-row" id="billRow"></div>
- <div class="row" style="margin-top:10px">
- <button id="calcularBtn" class="btn-calc">Calcular vuelto</button>
- <button id="resetStock" class="secondary">Reset bolsillo</button>
- <button id="toggleMini" class="ghost">Mini-calc</button>
- <button id="showOptA" class="ghost">Opción A</button>
- </div>
- </div>
- <div class="card" id="panel-result" style="display:none">
- <strong>Resultado</strong>
- <div id="summary" class="small-note"></div>
- <div id="noteRound" class="small-note"></div>
- <div id="optionBWrapper" class="option-block hidden" style="margin-top:10px;padding:10px;border:1px solid #e6e9ef">
- <div id="optionBHeader" style="font-weight:800"></div>
- <div id="optionBBreakdown" class="small-note"></div>
- <div style="display:flex;gap:8px;margin-top:10px">
- <button id="useB" class="secondary">Descontar</button>
- <button id="loadBmini" class="ghost">Mini-calc</button>
- </div>
- <div id="optionBmessage" class="small-note" style="margin-top:8px"></div>
- </div>
- <div id="optionAWrapper" class="option-block hidden" style="margin-top:10px;padding:10px;border:1px solid #e6e9ef"></div>
- <div id="miniCalcResult" class="hidden"></div>
- <div id="miniWrapper" class="option-block hidden" style="margin-top:12px">
- <div style="display:flex;gap:8px;align-items:center">
- <div style="flex:1">
- <div id="miniDisplay" class="display">0</div>
- <div id="miniMsg" class="small-note"></div>
- </div>
- <div style="width:220px;display:flex;flex-direction:column;gap:8px">
- <button id="opMinus" class="big-op" style="background:#ef4444;color:white">−</button>
- <button id="opEquals" class="big-op" style="background:#0f62fe;color:white">=</button>
- </div>
- </div>
- <div style="margin-top:10px">
- <button id="toggleMore" class="ghost">Más ops</button>
- <div id="moreOps" class="hidden" style="margin-top:8px">
- <div style="display:flex;gap:8px">
- <button id="opPlus" class="muted-btn">+</button>
- <button id="opMul" class="muted-btn">×</button>
- <button id="opDiv" class="muted-btn">÷</button>
- <button id="miniClear" class="secondary">C</button>
- </div>
- <div class="kb">
- <button class="muted-btn" data-key="1">1</button>
- <button class="muted-btn" data-key="2">2</button>
- <button class="muted-btn" data-key="3">3</button>
- <button class="muted-btn" data-key="4">4</button>
- <button class="muted-btn" data-key="5">5</button>
- <button class="muted-btn" data-key="6">6</button>
- <button class="muted-btn" data-key="7">7</button>
- <button class="muted-btn" data-key="8">8</button>
- <button class="muted-btn" data-key="9">9</button>
- <button class="muted-btn" data-key="C">C</button>
- <button class="muted-btn" data-key="0">0</button>
- <button class="muted-btn" data-key="DEL">←</button>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="card">
- <div id="toggleStock" style="cursor:pointer;color:var(--accent);font-weight:700">Mi Bolsillo</div>
- <div id="panel-stock" class="hidden" style="margin-top:8px">
- <div id="stockInputs"></div>
- </div>
- </div>
- <div class="card">
- <button id="clearHistory" class="secondary" style="width:100%">🗑️ Limpiar historial</button>
- <div class="small-note" style="text-align:center;margin-top:6px">
- Registros: <strong id="historyCount">0</strong>
- </div>
- </div>
- </div>
- <script type="text/javascript" src="cordova.js"></script>
- <script>
- (function(){const DENOMS=[20000,10000,2000,1000,500,200,100,50];const BILL_BUTTONS=[50,100,200,500,1000,2000,10000,20000];let isCordovaApp=false;let useCordovaVoice=false;const totalCompra=document.getElementById('totalCompra');const montoPagado=document.getElementById('montoPagado');const calcularBtn=document.getElementById('calcularBtn');const resetStock=document.getElementById('resetStock');const panelResult=document.getElementById('panel-result');const summary=document.getElementById('summary');const noteRound=document.getElementById('noteRound');const optionBWrapper=document.getElementById('optionBWrapper');const optionBHeader=document.getElementById('optionBHeader');const optionBBreakdown=document.getElementById('optionBBreakdown');const optionBmessage=document.getElementById('optionBmessage');const useB=document.getElementById('useB');const loadBmini=document.getElementById('loadBmini');const optionAWrapper=document.getElementById('optionAWrapper');const showOptA=document.getElementById('showOptA');const miniWrapper=document.getElementById('miniWrapper');const miniDisplay=document.getElementById('miniDisplay');const miniMsg=document.getElementById('miniMsg');const opMinus=document.getElementById('opMinus');const opEquals=document.getElementById('opEquals');const toggleMore=document.getElementById('toggleMore');const moreOps=document.getElementById('moreOps');const miniClear=document.getElementById('miniClear');const kbButtons=document.querySelectorAll('.kb button[data-key]');const toggleStock=document.getElementById('toggleStock');const panelStock=document.getElementById('panel-stock');const stockInputs=document.getElementById('stockInputs');const billRow=document.getElementById('billRow');const modeAddBtn=document.getElementById('modeAdd');const modeSetBtn=document.getElementById('modeSet');const tecladoRapidoBtn=document.getElementById('tecladoRapidoBtn');const voiceBtn=document.getElementById('voiceBtn');const voiceStatus=document.getElementById('voiceStatus');const frequentAmounts=document.getElementById('frequentAmounts');const frequentBtns=document.getElementById('frequentBtns');const clearHistory=document.getElementById('clearHistory');const historyCount=document.getElementById('historyCount');const platformInfo=document.getElementById('platformInfo');const STORAGE_KEY_BOLSILLO='miBolsillo_v1';const STORAGE_KEY_HISTORY='payHistory_v1';const defaultStock={};DENOMS.forEach(d=>defaultStock[d]=(d>=10000?0:10));document.addEventListener('deviceready',onDeviceReady,false);function onDeviceReady(){console.log('📱 Cordova listo');isCordovaApp=true;if(window.plugins&&window.plugins.speechRecognition){useCordovaVoice=true;platformInfo.textContent='📱 App Nativa';console.log('🎤 Plugin Cordova OK')}else{platformInfo.textContent='📱 App (sin voz)'}if(navigator.splashscreen){navigator.splashscreen.hide()}}setTimeout(()=>{if(!isCordovaApp){platformInfo.textContent='🌐 Navegador'}},500);function vibrate(pattern=20){if(navigator.vibrate){navigator.vibrate(pattern)}}function saveStockToLocal(){try{const s=getStock();localStorage.setItem(STORAGE_KEY_BOLSILLO,JSON.stringify(s))}catch(e){console.warn('no save',e)}}function loadStockFromLocal(){try{const raw=localStorage.getItem(STORAGE_KEY_BOLSILLO);if(!raw)return null;const parsed=JSON.parse(raw);for(const d of DENOMS)if(typeof parsed[d]==='undefined')parsed[d]=0;return parsed}catch(e){return null}}function saveMontoToHistory(monto){try{const history=JSON.parse(localStorage.getItem(STORAGE_KEY_HISTORY)||'{}');history[monto]=(history[monto]||0)+1;localStorage.setItem(STORAGE_KEY_HISTORY,JSON.stringify(history));updateHistoryUI()}catch(e){}}function getFrequentMontos(limit=3){try{const history=JSON.parse(localStorage.getItem(STORAGE_KEY_HISTORY)||'{}');return Object.entries(history).sort((a,b)=>b[1]-a[1]).slice(0,limit).map(([monto,count])=>({monto:Number(monto),count}))}catch(e){return[]}}function updateHistoryUI(){const frequent=getFrequentMontos(3);const history=JSON.parse(localStorage.getItem(STORAGE_KEY_HISTORY)||'{}');const totalOps=Object.values(history).reduce((a,b)=>a+b,0);historyCount.textContent=totalOps;if(frequent.length>0){frequentBtns.innerHTML='';frequent.forEach(({monto,count})=>{const btn=document.createElement('button');btn.className='quick-amt frequent';btn.innerHTML=`$${monto.toLocaleString('es-AR')}<span class="freq-badge">${count}</span>`;btn.addEventListener('click',()=>{vibrate(30);setMontoPagado(monto)});frequentBtns.appendChild(btn)});frequentAmounts.classList.remove('hidden')}else{frequentAmounts.classList.add('hidden')}}clearHistory.addEventListener('click',()=>{if(confirm('¿Borrar historial?')){localStorage.removeItem(STORAGE_KEY_HISTORY);updateHistoryUI();vibrate([30,50,30])}});function iniciarReconocimientoVoz(){if(useCordovaVoice){iniciarVozCordova()}else{iniciarVozWeb()}}function iniciarVozCordova(){console.log('🎤 Cordova');const options={language:'es-AR',matches:5,showPopup:false,showPartial:false,prompt:''};voiceBtn.classList.add('listening');voiceBtn.textContent='⏺️ HABLÁ';voiceBtn.style.fontSize='1.4rem';voiceStatus.innerHTML='<strong style="font-size:1.3rem;color:#16a34a">🔴 ESCUCHANDO...</strong>';voiceStatus.className='small-note voice-status-success';vibrate([100,30,100]);window.plugins.speechRecognition.startListening(function(matches){console.log('🎤 Cordova:',matches);if(matches&&matches.length>0){const transcript=matches[0].toLowerCase();vibrate([50,50,50,50,50]);const numero=convertirVozANumero(transcript);if(numero>0){montoPagado.value=numero;montoPagado.style.background='#dcfce7';montoPagado.style.transform='scale(1.05)';voiceStatus.innerHTML=`<strong style="font-size:1.4rem;color:#16a34a">✅ $${numero.toLocaleString('es-AR')}</strong>`;voiceStatus.className='small-note voice-status-success';vibrate([200,100,200]);setTimeout(()=>{montoPagado.style.background='';montoPagado.style.transform='';voiceStatus.textContent='';voiceStatus.className='small-note'},3000)}else{voiceStatus.innerHTML=`<strong style="color:#ef4444">❌ "${transcript}"</strong>`;voiceStatus.className='small-note voice-status-error';vibrate([150,50,150,50,150])}}voiceBtn.classList.remove('listening');voiceBtn.textContent='🎤 Decir monto';voiceBtn.style.fontSize=''},function(error){console.error('❌ Cordova:',error);let mensaje='❌ Error';if(error==='No match'){mensaje='❌ No escuché';vibrate(100)}else{vibrate(200)}voiceStatus.innerHTML=`<strong>${mensaje}</strong>`;voiceStatus.className='small-note voice-status-error';voiceBtn.classList.remove('listening');voiceBtn.textContent='🎤 Decir monto';voiceBtn.style.fontSize=''},options)}function iniciarVozWeb(){const SpeechRecognition=window.SpeechRecognition||window.webkitSpeechRecognition;if(!SpeechRecognition){voiceStatus.innerHTML='❌ No soportado';voiceStatus.className='small-note voice-status-error';voiceBtn.disabled=true;return}if(navigator.permissions){navigator.permissions.query({name:'microphone'}).then((permissionStatus)=>{if(permissionStatus.state==='denied'){voiceStatus.innerHTML='❌ Bloqueado<br><small>Activá permisos</small>';voiceStatus.className='small-note voice-status-error';return}iniciarReconocimientoWeb()}).catch(()=>{iniciarReconocimientoWeb()})}else{iniciarReconocimientoWeb()}}function iniciarReconocimientoWeb(){const SpeechRecognition=window.SpeechRecognition||window.webkitSpeechRecognition;const recognition=new SpeechRecognition();recognition.lang='es-AR';recognition.continuous=false;recognition.interimResults=false;recognition.maxAlternatives=3;recognition.onstart=()=>{voiceBtn.classList.add('listening');voiceBtn.textContent='⏺️ HABLÁ';voiceBtn.style.fontSize='1.4rem';voiceStatus.innerHTML='<strong style="font-size:1.3rem;color:#16a34a">🔴 ESCUCHANDO...</strong>';vibrate([100,30,100])};recognition.onresult=(event)=>{const transcript=event.results[0][0].transcript.toLowerCase();vibrate([50,50,50,50,50]);const numero=convertirVozANumero(transcript);if(numero>0){montoPagado.value=numero;montoPagado.style.background='#dcfce7';montoPagado.style.transform='scale(1.05)';voiceStatus.innerHTML=`<strong style="font-size:1.4rem;color:#16a34a">✅ $${numero.toLocaleString('es-AR')}</strong>`;voiceStatus.className='small-note voice-status-success';vibrate([200,100,200]);setTimeout(()=>{montoPagado.style.background='';montoPagado.style.transform='';voiceStatus.textContent=''},3000)}else{voiceStatus.innerHTML=`<strong style="color:#ef4444">❌ "${transcript}"</strong>`;voiceStatus.className='small-note voice-status-error';vibrate([150,50,150,50,150])}};recognition.onerror=(event)=>{let mensaje='❌ Error';if(event.error==='no-speech'){mensaje='❌ No escuché';vibrate([200,100,200])}else if(event.error==='not-allowed'){mensaje='❌ Bloqueado';vibrate(300)}else{vibrate(300)}voiceStatus.innerHTML=`<strong>${mensaje}</strong>`;voiceStatus.className='small-note voice-status-error'};recognition.onend=()=>{voiceBtn.classList.remove('listening');voiceBtn.textContent='🎤 Decir monto';voiceBtn.style.fontSize=''};try{recognition.start()}catch(e){voiceStatus.innerHTML='❌ Error inicio';voiceStatus.className='small-note voice-status-error'}}function convertirVozANumero(texto){texto=texto.toLowerCase().trim();const numeroDirecto=texto.match(/\d+/g);if(numeroDirecto){return Number(numeroDirecto.join(''))}const montos={'cincuenta':50,'cien':100,'doscientos':200,'quinientos':500,'mil':1000,'un mil':1000,'dos mil':2000,'tres mil':3000,'cuatro mil':4000,'cinco mil':5000,'seis mil':6000,'siete mil':7000,'ocho mil':8000,'nueve mil':9000,'diez mil':10000,'veinte mil':20000,'treinta mil':30000,'cincuenta mil':50000,'luca':1000,'una luca':1000,'dos lucas':2000,'tres lucas':3000,'cinco lucas':5000,'diez lucas':10000};const sorted=Object.entries(montos).sort((a,b)=>b[0].length-a[0].length);for(let[palabra,valor]of sorted){if(texto.includes(palabra)){return valor}}return 0}voiceBtn.addEventListener('click',()=>{vibrate(30);iniciarReconocimientoVoz()});document.querySelectorAll('.quick-amt[data-quick]').forEach(btn=>{btn.addEventListener('click',()=>{vibrate(30);const monto=Number(btn.dataset.quick);setMontoPagado(monto)})});function renderStock(stock){stockInputs.innerHTML='';for(const d of DENOMS){const row=document.createElement('div');row.style.display='flex';row.style.justifyContent='space-between';row.style.marginBottom='6px';row.innerHTML=`<div style="font-weight:700">$${d}</div><input data-denom="${d}" type="number" min="0" value="${stock[d]}" style="width:90px;padding:6px;border-radius:6px;border:1px solid #e6e9ef">`;stockInputs.appendChild(row)}stockInputs.querySelectorAll('input[data-denom]').forEach(i=>{i.addEventListener('input',()=>{if(Number(i.value)<0)i.value=0;saveStockToLocal()})})}function getStock(){const inputs=stockInputs.querySelectorAll('input[data-denom]');const s={};inputs.forEach(i=>s[Number(i.dataset.denom)]=Number(i.value)||0);return s}function setStockDefaults(){renderStock(defaultStock);saveStockToLocal()}resetStock.addEventListener('click',()=>{vibrate(50);setStockDefaults();optionBmessage.textContent=''});const saved=loadStockFromLocal();if(saved)renderStock(saved);else setStockDefaults();toggleStock.addEventListener('click',()=>{vibrate(20);panelStock.classList.toggle('hidden')});function roundOptionsBy50(v){const r=v%50;if(r===0)return{exact:true,down:v,up:v,downDiff:0,upDiff:0};const down=v-r;return{exact:false,down,up:down+50,downDiff:r,upDiff:50-r}}function computeBestChangeWithStock(target,stock){if(target===0)return{ok:true,bills:{},totalBills:0};const items=[];for(const denom of DENOMS){let cnt=stock[denom]||0;let power=1;while(cnt>0){const use=Math.min(power,cnt);items.push({value:denom*use,pieces:use,denom});cnt-=use;power<<=1}}const maxSearch=Math.min(target+Math.max(...DENOMS)*5,100000);const INF=1e9;const dp=new Uint32Array(maxSearch+1);for(let i=0;i<=maxSearch;i++)dp[i]=0xffffffff;dp[0]=0;const parentIdx=new Int32Array(maxSearch+1);parentIdx.fill(-1);const parentPrev=new Int32Array(maxSearch+1);parentPrev.fill(-1);for(let idx=0;idx<items.length;idx++){const it=items[idx],val=it.value,pieces=it.pieces;for(let s=maxSearch;s>=val;s--){const prev=s-val;if(dp[prev]!==0xffffffff){const cand=dp[prev]+pieces;if(cand<dp[s]){dp[s]=cand;parentIdx[s]=idx;parentPrev[s]=prev}}}}if(target<=maxSearch&&dp[target]!==0xffffffff){return{ok:true,totalBills:dp[target],bills:reconstructSolution(target,items,parentIdx,parentPrev)}}let bestOver=-1,bestOverBills=INF;for(let s=target+1;s<=maxSearch;s++){if(dp[s]!==0xffffffff){const over=s-target;if(bestOver===-1||over<(bestOver-target)||(over===(bestOver-target)&&dp[s]<bestOverBills)){bestOver=s;bestOverBills=dp[s]}}}let bestUnder=-1,bestUnderBills=INF;for(let s=Math.min(target-1,maxSearch);s>=0;s--){if(dp[s]!==0xffffffff){if(bestUnder===-1||s>bestUnder||(s===bestUnder&&dp[s]<bestUnderBills)){bestUnder=s;bestUnderBills=dp[s]}}}const res={ok:false};if(bestOver!==-1)res.over={amount:bestOver,bills:reconstructSolution(bestOver,items,parentIdx,parentPrev),totalBills:bestOverBills};if(bestUnder!==-1)res.under={amount:bestUnder,bills:reconstructSolution(bestUnder,items,parentIdx,parentPrev),totalBills:bestUnderBills};return res}function reconstructSolution(sum,items,parentIdx,parentPrev){const counts={};let s=sum;while(s>0&&parentIdx[s]!==-1){const idx=parentIdx[s],prev=parentPrev[s],it=items[idx];counts[it.denom]=(counts[it.denom]||0)+it.pieces;s=prev}const ordered={};for(const d of DENOMS)if(counts[d])ordered[d]=counts[d];return ordered}function renderBills(b){if(!b||Object.keys(b).length===0)return'<div class="small-note">(sin billetes)</div>';let h='<div class="breakdown">';for(const d of DENOMS)if(b[d])h+=`<div class="chip">$${d} × ${b[d]}</div>`;h+='</div>';return h}function applyBillsToStock(bills){if(!bills||Object.keys(bills).length===0)return{ok:true};const inputs=stockInputs.querySelectorAll('input[data-denom]');const stock={};inputs.forEach(i=>stock[Number(i.dataset.denom)]=Number(i.value)||0);for(const dStr of Object.keys(bills)){const d=Number(dStr),need=bills[d]||0;if((stock[d]||0)<need)return{ok:false,msg:`Falta $${d}`}}inputs.forEach(i=>{const d=Number(i.dataset.denom);i.value=(Number(i.value)||0)-(bills[d]||0)});saveStockToLocal();return{ok:true}}let lastBamount=0,lastBbills={},lastAamount=0,lastAbills={};let lastMiniBills={};calcularBtn.addEventListener('click',()=>{vibrate(50);panelResult.style.display='block';summary.textContent='';noteRound.textContent='';optionBWrapper.classList.add('hidden');optionAWrapper.classList.add('hidden');optionBmessage.textContent='';lastBamount=0;lastBbills={};lastAamount=0;lastAbills={};const total=Math.trunc(Number(totalCompra.value)||0);const pago=Math.trunc(Number(montoPagado.value)||0);if(pago>0)saveMontoToHistory(pago);if(isNaN(total)||isNaN(pago)||total<0||pago<0){summary.innerHTML=`<div style="color:#b91c1c">Montos inválidos</div>`;return}const cambio=pago-total;if(cambio<0){summary.innerHTML=`<div style="color:#b91c1c">Falta $${Math.abs(cambio)}</div>`;vibrate(200);return}if(cambio===0){summary.innerHTML=`<div style="color:#16a34a">Pago exacto</div>`;vibrate([50,100,50]);return}summary.innerHTML=`<div>Vuelto: <strong>$${cambio}</strong></div>`;const roundInfo=roundOptionsBy50(cambio);if(roundInfo.exact){noteRound.textContent='Múltiplo de $50 ✓'}else{noteRound.textContent=`No es múltiplo de $50. Redondear a: $${roundInfo.down} o $${roundInfo.up}`}const stock=getStock();const exact=computeBestChangeWithStock(cambio,stock);if(exact.ok){optionBHeader.textContent=`💰 Billetes para dar $${cambio}`;optionBBreakdown.innerHTML=renderBills(exact.bills);optionBWrapper.classList.remove('hidden');lastBamount=cambio;lastBbills=exact.bills;useB.onclick=()=>{vibrate(30);const r=applyBillsToStock(exact.bills);if(!r.ok){optionBmessage.textContent='❌ '+r.msg;optionBmessage.style.color='#b91c1c';vibrate(200);return}optionBmessage.textContent=`✅ Billetes descontados de tu bolsillo`;optionBmessage.style.color='#064e3b';vibrate([50,100,50])};loadBmini.onclick=()=>{vibrate(30);loadNumberToMini(cambio);openMini()};optionBmessage.textContent='✅ Vuelto exacto - Usá estos billetes';optionBmessage.style.color='#064e3b';vibrate([30,50,30])}else{optionBmessage.textContent='❌ No tenés billetes para dar el vuelto exacto';optionBmessage.style.color='#b91c1c'}if(!roundInfo.exact){const downRes=computeBestChangeWithStock(roundInfo.down,stock);if(downRes.ok){lastAamount=roundInfo.down;lastAbills=downRes.bills;optionAWrapper.innerHTML=`<div style="font-weight:800">Opción alternativa — $${roundInfo.down} (cliente pierde $${roundInfo.downDiff})</div><div class="small-note">${renderBills(downRes.bills)}</div><div style="display:flex;gap:8px;margin-top:10px"><button class="secondary useA">Descontar</button><button class="ghost loadAmini">Mini-calc</button></div><div id="optionAmessage" class="small-note" style="margin-top:8px"></div>`;const useA=optionAWrapper.querySelector('.useA');const loadAmini=optionAWrapper.querySelector('.loadAmini');const optionAmessage=optionAWrapper.querySelector('#optionAmessage');useA.onclick=()=>{vibrate(30);const r=applyBillsToStock(downRes.bills);if(!r.ok){optionAmessage.textContent='❌ '+r.msg;optionAmessage.style.color='#b91c1c';vibrate(200);return}optionAmessage.textContent=`✅ Opción de $${roundInfo.down} aplicada`;optionAmessage.style.color='#064e3b';vibrate([50,100,50])};loadAmini.onclick=()=>{vibrate(30);loadNumberToMini(roundInfo.down);openMini()};optionAWrapper.classList.remove('hidden');optionAWrapper.style.display='block'}const upRes=computeBestChangeWithStock(roundInfo.up,stock);if(upRes.ok && !exact.ok){lastBamount=roundInfo.up;lastBbills=upRes.bills;optionBHeader.textContent=`Opción redondeo — $${roundInfo.up} (das $${roundInfo.upDiff} más al cliente)`;optionBBreakdown.innerHTML=renderBills(upRes.bills);optionBWrapper.classList.remove('hidden');useB.onclick=()=>{vibrate(30);const r=applyBillsToStock(upRes.bills);if(!r.ok){optionBmessage.textContent=r.msg;optionBmessage.style.color='#b91c1c';vibrate(200);return}optionBmessage.textContent=`✅ Redondeado`;optionBmessage.style.color='#064e3b';vibrate([50,100,50])};loadBmini.onclick=()=>{vibrate(30);loadNumberToMini(lastBamount);openMini()}}}panelResult.scrollIntoView({behavior:'smooth'})});showOptA.addEventListener('click',()=>{vibrate(20);if(optionAWrapper.classList.contains('hidden')){optionAWrapper.classList.remove('hidden');optionAWrapper.style.display='block';optionAWrapper.scrollIntoView({behavior:'smooth'})}});let displayStr='0';let left=null;let op=null;let startingNew=false;function updateDisplay(){const n=Number(displayStr||'0');miniDisplay.textContent=n.toLocaleString('de-AR')}function openMini(){miniWrapper.classList.remove('hidden');miniMsg.textContent=''}function loadNumberToMini(n){displayStr=String(Math.trunc(Number(n)||0));updateDisplay();left=null;op=null;startingNew=false;miniMsg.textContent=`Cargado: $${Math.trunc(Number(n)||0)}`}let lastOpSymbol=null;function operatorPressed(opSymbol){vibrate(30);lastOpSymbol=opSymbol;if(left===null){left=Math.trunc(Number(displayStr)||0)}else if(op!==null&&!startingNew){const right=Math.trunc(Number(displayStr)||0);left=compute(left,op,right);displayStr=String(Math.trunc(left));updateDisplay()}op=opSymbol;startingNew=true;moreOps.classList.remove('hidden');openMini();miniMsg.textContent=`Op "${opSymbol}"`}function compute(a,o,b){if(o==='+')return a+b;if(o==='-')return a-b;if(o==='*')return a*b;if(o==='/')return b===0?0:Math.floor(a/b);return 0}opMinus.addEventListener('click',()=>operatorPressed('-'));document.getElementById('opPlus')?.addEventListener('click',()=>operatorPressed('+'));document.getElementById('opMul')?.addEventListener('click',()=>operatorPressed('*'));document.getElementById('opDiv')?.addEventListener('click',()=>operatorPressed('/'));opEquals.addEventListener('click',()=>{vibrate(50);const right=Math.trunc(Number(displayStr)||0);if(op===null||left===null){miniMsg.textContent='Falta op';return}const res=compute(left,op,right);displayStr=String(Math.trunc(res));updateDisplay();miniMsg.textContent=`= $${Math.trunc(res)}`;left=res;op=null;startingNew=true;if(lastOpSymbol==='-'){showMiniBills(Math.trunc(res))}else{document.getElementById('miniCalcResult').classList.add('hidden')}lastOpSymbol=null});kbButtons.forEach(btn=>{btn.addEventListener('click',()=>{vibrate(20);const k=btn.dataset.key;if(k==='C'){displayStr='0';left=null;op=null;startingNew=false;updateDisplay();miniMsg.textContent='';return}if(k==='DEL'){if(displayStr.length<=1)displayStr='0';else displayStr=displayStr.slice(0,-1);updateDisplay();return}if(startingNew){displayStr=k;startingNew=false}else{displayStr=(displayStr==='0')?k:(displayStr+k)}updateDisplay()})});toggleMore.addEventListener('click',()=>{vibrate(20);moreOps.classList.toggle('hidden')});miniClear.addEventListener('click',()=>{vibrate(30);displayStr='0';left=null;op=null;startingNew=false;updateDisplay();miniMsg.textContent='';document.getElementById('miniCalcResult').classList.add('hidden')});document.getElementById('toggleMini').addEventListener('click',()=>{vibrate(20);if(miniWrapper.classList.contains('hidden'))openMini();else miniWrapper.classList.add('hidden')});loadBmini.addEventListener('click',()=>{if(lastBamount){vibrate(30);loadNumberToMini(lastBamount);openMini();showMiniBills(lastBamount)}else{if(lastBamount===0)openMini()}});function renderBillButtons(){billRow.innerHTML='';const colorClassMap={50:'bill-50',100:'bill-100',200:'bill-200',500:'bill-500',1000:'bill-1000',2000:'bill-2000',10000:'bill-10000',20000:'bill-20000'};for(const b of BILL_BUTTONS){const btn=document.createElement('div');btn.className='bill '+(colorClassMap[b]||'');btn.innerText=`$${b}`;btn.dataset.val=b;btn.addEventListener('click',()=>{vibrate(30);onBillClick(Number(btn.dataset.val))});billRow.appendChild(btn)}}renderBillButtons();let quickMode='add';function setModeVisual(m){vibrate(20);quickMode=m;if(m==='add'){modeAddBtn.style.background='#0f62fe';modeAddBtn.style.color='#fff';modeSetBtn.style.background='transparent';modeSetBtn.style.color='var(--accent)'}else{modeSetBtn.style.background='#0f62fe';modeSetBtn.style.color='#fff';modeAddBtn.style.background='transparent';modeAddBtn.style.color='var(--accent)'}}setModeVisual('add');modeAddBtn.addEventListener('click',()=>setModeVisual('add'));modeSetBtn.addEventListener('click',()=>setModeVisual('set'));function getMontoPagado(){return Math.trunc(Number(montoPagado.value)||0)}function setMontoPagado(v){montoPagado.value=Math.max(0,Math.trunc(Number(v)||0));montoPagado.style.transition='background 0.15s';montoPagado.style.background='#fef9c3';setTimeout(()=>montoPagado.style.background='',220)}function onBillClick(value){if(quickMode==='add'){setMontoPagado(getMontoPagado()+value)}else{setMontoPagado(value)}}let tecladoOverlay=null;tecladoRapidoBtn.addEventListener('click',()=>{vibrate(30);if(tecladoOverlay){tecladoOverlay.remove();tecladoOverlay=null;return}tecladoOverlay=document.createElement('div');tecladoOverlay.className='kbOverlay';tecladoOverlay.innerHTML=`<div style="display:flex;justify-content:space-between;align-items:center"><div style="font-weight:800">Teclado</div></div><div class="display" id="kbDisplay">${getMontoPagado().toLocaleString('de-AR')}</div><div class="row-under-display"><div class="left-slot"><button id="kb-close" class="kb-close">Cerrar</button></div><div class="center-slot"><button id="kb-ok" class="kb-ok">OK</button></div><div class="right-slot"><button id="kb-del" class="kb-del">←</button></div></div><div class="kb-grid" id="kbGrid"><button data-key="1" class="ghost">1</button><button data-key="2" class="ghost">2</button><button data-key="3" class="ghost">3</button><button data-key="4" class="ghost">4</button><button data-key="5" class="ghost">5</button><button data-key="6" class="ghost">6</button><button data-key="7" class="ghost">7</button><button data-key="8" class="ghost">8</button><button data-key="9" class="ghost">9</button><button data-key="0" class="ghost">0</button><button data-key="00" class="ghost">00</button><button data-key="CLR" class="ghost">C</button></div>`;document.body.appendChild(tecladoOverlay);const kbDisplay=tecladoOverlay.querySelector('#kbDisplay');const kbGridButtons=tecladoOverlay.querySelectorAll('#kbGrid button[data-key]');const kbDel=tecladoOverlay.querySelector('#kb-del');const kbOk=tecladoOverlay.querySelector('#kb-ok');const kbClose=tecladoOverlay.querySelector('#kb-close');let kbStr=String(getMontoPagado()||'0');function updateKb(){kbDisplay.innerText=Number(kbStr||'0').toLocaleString('de-AR')}updateKb();kbGridButtons.forEach(b=>{b.addEventListener('click',()=>{vibrate(20);const k=b.dataset.key;if(k==='CLR'){kbStr='0';updateKb();return}if(k==='00'){kbStr=kbStr==='0'?'0':(kbStr+'00');updateKb();return}if(k==='0'){kbStr=kbStr==='0'?'0':(kbStr+'0');updateKb();return}kbStr=kbStr==='0'?k:(kbStr+k);updateKb()})});kbDel.addEventListener('click',()=>{vibrate(20);if(kbStr.length<=1)kbStr='0';else kbStr=kbStr.slice(0,-1);updateKb()});kbOk.addEventListener('click',()=>{vibrate(50);setMontoPagado(Number(kbStr));tecladoOverlay.remove();tecladoOverlay=null});kbClose.addEventListener('click',()=>{vibrate(30);tecladoOverlay.remove();tecladoOverlay=null})});panelResult.style.display='none';miniWrapper.classList.add('hidden');moreOps.classList.add('hidden');updateDisplay();const miniCalcResult=document.getElementById('miniCalcResult');function showMiniBills(amount){if(typeof amount!=='number')amount=Math.trunc(Number(amount)||0);if(amount<=0){miniCalcResult.innerHTML=`<div class="note">$0</div>`;miniCalcResult.classList.add('hidden');lastMiniBills={};return}const stock=getStock();const res=computeBestChangeWithStock(amount,stock);let html=`<div class="note">Vuelto: <strong>$${amount}</strong></div>`;if(res.ok){lastMiniBills=res.bills;html+=`<div class="small-note">Billetes:</div>`+renderBills(res.bills);html+=`<button id="descontarMiniBills" class="secondary" style="width:100%;margin-top:10px">Descontar</button>`;html+=`<div id="miniDescontarMsg" class="small-note" style="margin-top:6px"></div>`}else{lastMiniBills={};html+=`<div class="small-note">Sin billetes</div>`;if(res.over){html+=`<div style="margin-top:6px;font-weight:800">Más: $${res.over.amount}</div>`+renderBills(res.over.bills)}if(res.under){html+=`<div style="margin-top:6px;font-weight:800">Menos: $${res.under.amount}</div>`+renderBills(res.under.bills)}}miniCalcResult.innerHTML=html;miniCalcResult.classList.remove('hidden');const descontarBtn=document.getElementById('descontarMiniBills');if(descontarBtn){descontarBtn.addEventListener('click',()=>{vibrate(50);const r=applyBillsToStock(lastMiniBills);const msg=document.getElementById('miniDescontarMsg');if(!r.ok){msg.textContent=r.msg;msg.style.color='#b91c1c';vibrate(200)}else{msg.textContent=`✓ OK ($${amount})`;msg.style.color='#064e3b';vibrate([50,100,50])}})}openMini()}const originalLoadNumberToMini=loadNumberToMini;loadNumberToMini=function(n){originalLoadNumberToMini(n);showMiniBills(Math.trunc(Number(n)||0))};updateHistoryUI()})();
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment