SISTEMA OPERATIVO · Sincronización segura activa.
Gestión de reservas

Cada cama, a la vista.

Nueva reserva

Ingresá los importes ya pagados. Son totales acumulados, no pagos adicionales. Efectivo sin recargo; tarjeta con 10%.

Total due—
Paid toward stay—
Outstanding balance—

Recibo

Room

For shared rooms, capacity is the number of beds. For private rooms, capacity is the number of guests.

Iniciar sesión

Elegí tu nombre e ingresá tu PIN personal.

NAME + PIN ACCESS · SECURE BUILD 2026-10-03-S1

Configurar Owner

Primera configuración. Creá el acceso privado del Owner.

`);w.document.close();w.focus();w.print()};$('reportpdf').onclick=()=>{const help={en:'Choose Save as PDF in the print destination.',es:'Seleccioná Guardar como PDF como destino de impresión.',pt:'Escolha Salvar como PDF no destino de impressão.'};toast(help[language]||help.en);$('reportprint').click()}; document.querySelectorAll('[data-report-card]').forEach(b=>b.onclick=()=>{let key=b.dataset.reportCard;if(['current','pending'].includes(key)){let rows=state.reservations.filter(b=>outstandingBalance(b)>0&&(key==='current'?b.start<=today()&&!['out','cancelled'].includes(b.status):b.start>today()&&b.status!=='cancelled'));detailDialog(ui(key),balanceRecords(rows));return}if(key==='expenses'){detailDialog(ui(key),expenseTable(expenses));return}let rows=key==='undated'?undated:key==='collected'?entries.filter(e=>e.kind!=='refund'):key==='refunds'?entries.filter(e=>e.kind==='refund'):key==='fees'?entries.filter(e=>e.method==='fee'):key==='platform'?entries.filter(e=>e.kind==='payout'):key==='cash'?entries.filter(e=>e.method==='cash'):key==='card'?entries.filter(e=>['card','fee'].includes(e.method)):entries;detailDialog(ui(key),transactionTable(rows)+(key==='net'?expenseTable(expenses):''))});document.querySelectorAll('[data-source-detail]').forEach(b=>b.onclick=()=>detailDialog(t(b.dataset.sourceDetail),balanceRecords(sourceGroups.get(b.dataset.sourceDetail).reservations)));document.querySelectorAll('[data-detail-open]').forEach(b=>b.onclick=()=>openBooking(b.dataset.detailOpen));document.querySelectorAll('[data-expense-void]').forEach(b=>b.onclick=()=>voidExpense(b.dataset.expenseVoid))} function balanceRecords(rows){return `
${rows.map(b=>``).join('')||``}
${ui('guest')}${ui('dates')}${ui('place')}${ui('total')}${ui('paid')}${ui('balance')}
${esc(b.guest)}${displayDate(b.start)} → ${displayDate(b.end)}${esc(placeLabel(b.room,b.bed))}${b.total==null?t('Sin confirmar'):money(b.volunteer?0:b.total)}${money(b.paid||0)}${money(outstandingBalance(b))}
${ui('none')}
`} function transactionTable(rows){return `
${rows.map(e=>``).join('')||``}
${ui('date')}${ui('kind')}${ui('guest')}${ui('method')}${ui('amount')}
${esc(displayDate(e.date||'—'))}${transactionLabel(e)}${esc(e.guest||'')}${ui(e.method==='legacy'?'other':e.method)}${money(e.amount)}${state.reservations.some(b=>b.id===e.reservationId)?``:''}
${ui('none')}
`} function expenseTable(rows){return `
${rows.map(e=>``).join('')||``}
${ui('date')}${ui('description')}${ui('category')}${ui('method')}${ui('amount')}
${esc(displayDate(e.date))}${esc(e.description)}${e.voided?' · '+ui('voided'):''}${esc(uiWords[e.category]?ui(e.category):e.category)}${ui(e.method)}${money(e.amount)}${!e.voided?``:''}
${ui('none')}
`} function accountingForm(kind){if(!['owner','manager'].includes(actor().role))return;let d=document.createElement('dialog');d.className='guest-contact-dialog';let id=crypto.randomUUID();d.innerHTML=`

${ui(kind==='expense'?'addExpense':'payouts')}

${kind==='payout'?ui('payoutNote'):''}

${kind==='expense'?``:``}
`;document.body.append(d);d.querySelector('[type=button]').onclick=()=>d.close();d.onclose=()=>d.remove();d.querySelector('form').onsubmit=async e=>{e.preventDefault();let submit=d.querySelector('[type=submit]');submit.disabled=true;try{await syncQueue;let fresh=await apiRead();state=fresh.state;serverRevision=Number(fresh.revision||0);let data=Object.fromEntries(new FormData(e.target));let response=await apiFetch('/api/accounting',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({...data,action:kind,id,revision:serverRevision})});serverRevision=response.revision;let check=await apiRead();state=check.state;serverRevision=Number(check.revision||0);if(!(kind==='expense'?state.accounting?.expenses:state.accounting?.entries)?.some(x=>x.id===id))throw new Error('SAVE_NOT_VERIFIED');d.close();toast(ui('saved'));accountingView()}catch(err){d.querySelector('.error').textContent=ui('failed')+' ['+(err.payload?.error||err.message)+']';submit.disabled=false}};d.showModal()} async function voidExpense(id){if(!['owner','manager'].includes(actor().role))return;let reason=prompt(ui('reason'));if(!reason?.trim())return;try{await syncQueue;let fresh=await apiRead();state=fresh.state;serverRevision=Number(fresh.revision||0);let response=await apiFetch('/api/accounting',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'voidExpense',id,reason,revision:serverRevision})});serverRevision=response.revision;accountingView()}catch(e){toast(ui('failed')+' ['+(e.payload?.error||e.message)+']')}} async function bootD1(){booted=true;try{await loadAuthUsers();await authGate()}catch(e){console.error('Secure startup failed',e);await openLogin()}} // Operations 3.0.6: live room availability within Reservations. let reservationSection='list',availabilityRoom=null,availabilityFrom=today(),availabilityTo=add(today(),14),availabilityOnlyFree=false; const availabilityWords={list:['Reservation list','Lista de reservas','Lista de reservas'],rooms:['Room Availability','Disponibilidad de habitaciones','Disponibilidade de quartos'],from:['Arrival / calendar start','Llegada / inicio del calendario','Chegada / início do calendário'],to:['Departure / calendar end','Salida / fin del calendario','Saída / fim do calendário'],apply:['View dates','Ver fechas','Ver datas'],free:['Available','Disponible','Disponível'],booked:['Booked','Reservada','Reservado'],closed:['Unavailable room','Habitación no disponible','Quarto indisponível'],whole:['Available for the entire stay','Disponible para toda la estadía','Disponível para toda a estadia'],only:['Show only rooms with space for the entire stay','Mostrar solo habitaciones con lugar para toda la estadía','Mostrar apenas quartos com espaço para toda a estadia'],bookings:['Bookings and stay dates','Reservas y fechas de estadía','Reservas e datas de estadia'],none:['No bookings in or after this period.','No hay reservas en este período ni posteriores.','Não há reservas neste período ou posteriores.'],hint:['Click a room to see its beds and bookings. Departure day is available for the next arrival.','Elegí una habitación para ver sus camas y reservas. El día de salida queda disponible para la próxima llegada.','Escolha um quarto para ver suas camas e reservas. O dia da saída fica disponível para a próxima chegada.'],invalid:['Choose valid dates, with departure after arrival (up to 90 nights).','Elegí fechas válidas, con salida posterior a llegada (hasta 90 noches).','Escolha datas válidas, com saída após a chegada (até 90 noites).'],previous:['Previous 3 days','3 días anteriores','3 dias anteriores'],next:['Next 3 days','3 días siguientes','Próximos 3 dias'],show:['View room','Ver habitación','Ver quarto'],conflict:['Overlapping reservation','Reserva superpuesta','Reserva sobreposta']}; function av(key){return availabilityWords[key][{en:0,es:1,pt:2}[language]??0]} function reservationTabs(){return `
`} function bindReservationTabs(){ $('reservation-list-tab').onclick=()=>{reservationSection='list';reservations()};$('reservation-rooms-tab').onclick=()=>{reservationSection='rooms';reservations()};} function availabilityBookings(r,bed,from,to){return state.reservations.filter(b=>b.room===r.id&&active(b)&&(!r.bunks||String(b.bed)===String(bed))&&b.startfrom)} function roomFreeUnits(r,from,to){return availableRoom(r)?units(r).filter(bed=>!availabilityBookings(r,bed,from,to).length):[]} function bookingConflictDetails(b,id){const r=room(b.room);let conflicts=state.reservations.filter(x=>x.id!==id&&x.room===b.room&&active(x)&&(!r?.bunks||String(x.bed)===String(b.bed))&&x.startb.start);return conflicts.map(x=>`${av('conflict')}: ${x.guest} · ${displayDate(x.start)} → ${displayDate(x.end)}`).join(' | ')} function roomAvailability(){ const rooms=state.rooms.filter(r=>Number(r.capacity)>0),selected=rooms.find(r=>r.id===availabilityRoom),days=[];for(let d=availabilityFrom;d!availabilityOnlyFree||roomFreeUnits(r,availabilityFrom,availabilityTo).length); $('content').innerHTML=reservationTabs()+`

${av('rooms')}

${av('hint')}

${av('free')}${av('booked')}${av('closed')}
${shown.map(r=>{let free=roomFreeUnits(r,availabilityFrom,availabilityTo);return ``}).join('')||`

${av('closed')}

`}
${selected?availabilityTimeline(selected,days):''}
`; bindReservationTabs();$('availability-only').onchange=e=>{availabilityOnlyFree=e.target.checked;roomAvailability()};$('availability-apply').onclick=()=>{const from=$('availability-from').value,to=$('availability-to').value;if(!from||!to||to<=from||nights(from,to)>90)return toast(av('invalid'));availabilityFrom=from;availabilityTo=to;roomAvailability()};for(const [id,step]of [['availability-prev',-1],['availability-next',1]])$(id).onclick=()=>preserveAvailabilityScroll('.availability-scroll',()=>{availabilityFrom=add(availabilityFrom,3*step);availabilityTo=add(availabilityTo,3*step);roomAvailability()});document.querySelectorAll('[data-availability-room]').forEach(el=>el.onclick=()=>{availabilityRoom=el.dataset.availabilityRoom;roomAvailability()});document.querySelectorAll('[data-detail-open]').forEach(el=>el.onclick=()=>openBooking(el.dataset.detailOpen));document.querySelectorAll('[data-availability-booking]').forEach(el=>el.onclick=()=>openBooking(el.dataset.availabilityBooking));document.querySelectorAll('[data-availability-free]').forEach(el=>el.onclick=()=>{const r=room(availabilityRoom);if(r&&availableRoom(r))openBooking(null,r.id,el.dataset.bed,el.dataset.availabilityFree)}); } function availabilityTimeline(r,days){const bookings=state.reservations.filter(b=>b.room===r.id&&b.status!=='cancelled'&&b.end>availabilityFrom&&(b.status!=='out'||b.starta.start.localeCompare(b.start));return `

${esc(t(r.name))} · ${displayDate(availabilityFrom)} → ${displayDate(availabilityTo)}

${days.map(d=>``).join('')}${units(r).map(bed=>`${days.map(day=>{const found=state.reservations.filter(b=>b.room===r.id&&(!r.bunks||String(b.bed)===String(bed))&&bookedForDay(b,day));return ``}).join('')}`).join('')}
${r.bunks?t('Cama'):t('Habitación completa')}${displayDate(d)}
${r.bunks?t('Cama')+' '+bed:t('Habitación completa')}${found.length?found.map(b=>``).join(''):availableRoom(r)?``:av('closed')}

${av('bookings')}

${bookings.length?recordList(bookings):`

${av('none')}

`}`} bootD1(); // Operations 2.1: shared-device refresh without interrupting active forms/dialogs. let mandiocaRemoteCheckBusy=false; async function mandiocaRemoteCheck(){ if(mandiocaRemoteCheckBusy||!booted||!authToken)return; if(document.querySelector('dialog[open]'))return; mandiocaRemoteCheckBusy=true; try{ let r=await fetch(API_STATE,{headers:{Authorization:`Bearer ${authToken}`},cache:'no-store'}); if(r.ok){ let data=await r.json(),rev=Number(data.revision||0); if(rev>serverRevision&&data.state){ state=data.state;serverRevision=rev; if(!Array.isArray(state.supplies))state.supplies=[]; if(!Array.isArray(state.staffNotes))state.staffNotes=[]; render();toast(t("Updated from another device")); } } }catch(e){}finally{mandiocaRemoteCheckBusy=false} } setInterval(mandiocaRemoteCheck,15000); document.addEventListener('visibilitychange',()=>{if(!document.hidden)mandiocaRemoteCheck()}); // Adapt the existing ISO-value controls without changing reservation/API payloads. (()=>{ const nativeValue=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value'); const message=()=>language==='es'?'Usá DD/MM/AAAA y una fecha válida.':language==='pt'?'Use DD/MM/AAAA e uma data válida.':'Use DD/MM/YYYY and a valid date.'; function enhance(input){ if(input.dataset.datePicker==='native'||input.dataset.dayFirst==='true')return; const initial=nativeValue.get.call(input);input.dataset.dayFirst='true';input.type='text';input.inputMode='numeric';input.placeholder='DD/MM/YYYY';input.autocomplete='off';input.pattern='[0-9]{2}/[0-9]{2}/[0-9]{4}';input.maxLength=10; const raw=()=>nativeValue.get.call(input),iso=()=>dateInputISO(raw()); const validate=()=>{let v=iso(),s=raw();input.setCustomValidity(s&&(!v||(input.min&&vinput.max))?message():'');}; Object.defineProperty(input,'value',{configurable:true,get:iso,set(value){nativeValue.set.call(input,displayDate(value));validate();if(picker)picker.value=iso();}}); Object.defineProperty(input,'valueAsDate',{configurable:true,get:()=>iso()?new Date(iso()+'T00:00:00Z'):null,set:d=>{input.value=d instanceof Date&&!Number.isNaN(d.getTime())?d.toISOString().slice(0,10):'';}}); Object.defineProperty(input,'valueAsNumber',{configurable:true,get:()=>iso()?Date.parse(iso()+'T00:00:00Z'):NaN,set:n=>{input.value=Number.isFinite(n)?new Date(n).toISOString().slice(0,10):'';}}); let picker=null; const wrapper=document.createElement('span');wrapper.className='mandioca-date-control';input.before(wrapper);wrapper.append(input); picker=document.createElement('input');picker.type='date';picker.dataset.datePicker='native';picker.className='mandioca-date-picker';picker.tabIndex=-1;picker.setAttribute('aria-hidden','true');wrapper.append(picker); const button=document.createElement('button');button.type='button';button.className='mandioca-date-button';button.textContent='▦';wrapper.append(button); const refresh=()=>{button.setAttribute('aria-label',language==='es'?'Elegir fecha':language==='pt'?'Escolher data':'Choose date');button.disabled=input.disabled||input.readOnly;picker.min=input.min;picker.max=input.max;picker.value=iso();}; input.value=initial;refresh(); input.addEventListener('input',()=>{const v=iso();if(v)nativeValue.set.call(input,displayDate(v));validate();refresh();},{capture:true}); input.addEventListener('change',()=>{const v=iso();if(v)nativeValue.set.call(input,displayDate(v));validate();refresh();},{capture:true}); button.onclick=()=>{refresh();if(input.disabled||input.readOnly)return;try{if(picker.showPicker)picker.showPicker();else picker.click();}catch{input.focus();}}; picker.onchange=()=>{input.value=picker.value;input.dispatchEvent(new Event('input',{bubbles:true}));input.dispatchEvent(new Event('change',{bubbles:true}));}; } const scan=()=>document.querySelectorAll('input[type="date"]:not([data-date-picker="native"])').forEach(enhance); document.addEventListener('formdata',e=>{e.target.querySelectorAll('input[data-day-first="true"][name]').forEach(input=>{if(!input.disabled)e.formData.set(input.name,input.value);});},true); document.addEventListener('reset',e=>setTimeout(()=>{e.target.querySelectorAll('input[data-day-first="true"]').forEach(input=>{input.value=displayDate(nativeValue.get.call(input));});},0),true); scan();new MutationObserver(scan).observe(document.body,{childList:true,subtree:true}); })();