Plugin Directory

Changeset 3739702


Ignore:
Timestamp:
10/11/2026 03:38:26 PM (13 days ago)
Author:
pabloaronnabookingfacil
Message:

Release 1.3.23 - Manual booking delayed stays, interactive unit cards and capacity calculations

Location:
bookingfacil-reservas
Files:
16 edited
1 copied

Legend:

Unmodified
Added
Removed
  • bookingfacil-reservas/tags/1.3.23/assets/css/admin/admin-reservations.css

    r3719967 r3739702  
    403403        flex-direction: column;
    404404    }
     405
    405406    .wp-booking-manual-col-left,
    406407    .wp-booking-manual-col-right {
    … …  
    516517}
    517518
     519/* Aviso de carga atrasada (check-in anterior a hoy) */
     520.wp-booking-quote-past-notice {
     521    padding: 8px 12px;
     522    margin-bottom: 8px;
     523    background: rgba(245, 158, 11, 0.1);
     524    border: 1px solid #f59e0b;
     525    border-radius: 6px;
     526    color: #fcd34d;
     527    font-size: 12px;
     528}
     529
    518530.wp-booking-quote-card-success {
    519531    background: rgba(16, 185, 129, 0.08);
    … …  
    9901002        grid-template-columns: 1fr;
    9911003    }
     1004
    9921005    .wp-booking-occ-doc-row {
    9931006        flex-direction: column;
    9941007    }
     1008
    9951009    .wp-booking-occ-doctype-select {
    9961010        width: 100% !important;
    9971011    }
    9981012}
     1013
     1014/* Selector de unidades en tarjetas (Nueva Reserva en Mostrador) */
     1015.wp-booking-unit-cards { display: grid; gap: 8px; margin-top: 6px; }
     1016.wp-booking-unit-card {
     1017    padding: 10px 12px; border: 1px solid #334155; border-radius: 8px; background: rgba(15, 23, 42, 0.5);
     1018    cursor: pointer; display: flex; flex-direction: column; gap: 4px; transition: border-color .15s, background .15s;
     1019}
     1020.wp-booking-unit-card:hover, .wp-booking-unit-card:focus { border-color: #64748b; outline: none; }
     1021.wp-booking-unit-card.is-selected { border-color: #38bdf8; background: rgba(56, 189, 248, 0.08); box-shadow: 0 0 0 1px #38bdf8; }
     1022.wp-booking-unit-card.is-unavailable { opacity: .85; }
     1023.wp-booking-unit-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
     1024.wp-booking-unit-card-head small { color: #94a3b8; }
     1025.wp-booking-unit-card-badge { font-size: 12px; font-weight: 600; }
     1026.wp-booking-unit-card-badge.ok { color: #34d399; }
     1027.wp-booking-unit-card-badge.no { color: #f87171; }
     1028.wp-booking-unit-card-price { font-size: 13px; font-weight: 600; }
     1029.wp-booking-unit-card-reason { font-size: 12px; color: #cbd5e1; line-height: 1.4; }
     1030.wp-booking-unit-card-hint { font-size: 11px; color: #94a3b8; }
     1031.wp-booking-unit-card-hint.warn { color: #fcd34d; }
     1032
     1033/* Aviso de reserva forzada (sin disponibilidad validada) */
     1034.wp-booking-quote-override-warning {
     1035    padding: 10px 12px; margin-bottom: 8px; background: rgba(245, 158, 11, 0.1); border: 1px solid #f59e0b;
     1036    border-radius: 6px; color: #fcd34d; font-size: 12px; line-height: 1.5;
     1037}
     1038.wp-booking-quote-override-warning p { margin: 6px 0 0; }
  • bookingfacil-reservas/tags/1.3.23/assets/js/admin/admin-accommodations.js

    r3692007 r3739702  
    309309    const children = parseInt(document.getElementById('wp-booking-prop-children')?.value, 10) || 0;
    310310    const totalGuests = adults + children;
     311    const totalEl = document.getElementById('wp-booking-prop-capacity-total');
     312    if (totalEl) {
     313      totalEl.textContent = `Capacidad total: ${totalGuests} persona${totalGuests !== 1 ? 's' : ''}` +
     314        (adults > 0 ? ` (hasta ${adults} adulto${adults !== 1 ? 's' : ''}${children > 0 ? `; ${children} plaza${children !== 1 ? 's' : ''} solo para niños` : ''})` : '');
     315    }
    311316    const highlight = document.getElementById('wp-booking-prop-highlight')?.value?.trim();
    312317
  • bookingfacil-reservas/tags/1.3.23/assets/js/admin/admin-reservations.js

    r3737447 r3739702  
    8686  let cachedGuestsList = [];
    8787  let currentManualQuote = null;
     88  let currentManualForced = false;
     89  let manualUnitStatus = {};
     90  const manualUnitCards = document.getElementById('wp-booking-manual-unit-cards');
    8891  let currentManualGrandTotal = 0;
    8992  let manualQuoteDebounce = null;
    … …  
    23812384    }).then(data => {
    23822385      const searchResults = data && data.success && Array.isArray(data.results) ? data.results : [];
     2386      const unavailableList = data && Array.isArray(data.unavailable_units) ? data.unavailable_units : [];
    23832387      const currentSelectedUnitId = manualUnitSelect ? manualUnitSelect.value : '';
    23842388      let hasAvailableSelected = false;
     2389
     2390      // Estado por unidad: disponible (con precio) o no disponible (con motivo)
     2391      manualUnitStatus = {};
     2392      allUnits.forEach(u => {
     2393        const match = searchResults.find(r => Number(r.id) === Number(u.id));
     2394        const why = unavailableList.find(x => Number(x.id) === Number(u.id));
     2395        if (match && match.available && match.total_price !== null) {
     2396          manualUnitStatus[u.id] = { available: true, price: Number(match.total_price) };
     2397        } else {
     2398          manualUnitStatus[u.id] = {
     2399            available: false,
     2400            reason: why || { code: 'unknown', overridable: false, message: 'No disponible para las fechas seleccionadas.' }
     2401          };
     2402        }
     2403      });
    23852404
    23862405      if (manualUnitSelect) {
    23872406        manualUnitSelect.innerHTML = '';
    23882407        allUnits.forEach(u => {
    2389           const match = searchResults.find(r => Number(r.id) === Number(u.id));
     2408          const st = manualUnitStatus[u.id];
    23902409          const opt = document.createElement('option');
    23912410          opt.value = u.id;
    2392           const uCapacity = u.capacity ? u.capacity.adults : u.adultsCapacity || 2;
    2393 
    2394           if (match && match.available && match.total_price !== null) {
    2395             opt.textContent = `🟢 ${u.name} — Disponible ($${Number(match.total_price).toLocaleString('es-AR')} total)`;
    2396             if (Number(u.id) === Number(currentSelectedUnitId)) hasAvailableSelected = true;
    2397           } else {
    2398             opt.textContent = `🔴 ${u.name} — Sin disponibilidad (Capacidad: ${uCapacity} pers.)`;
    2399           }
     2411          opt.textContent = st.available ? `${u.name} — Disponible` : `${u.name} — ${st.reason.message}`;
    24002412          manualUnitSelect.appendChild(opt);
     2413          if (st.available && Number(u.id) === Number(currentSelectedUnitId)) hasAvailableSelected = true;
    24012414        });
    24022415
    2403         // Si la unidad actual no estaba disponible o no había seleccionada, elegir la primera disponible
     2416        // Si la unidad actual no está disponible, elegir la primera disponible (el operador puede elegir otra a mano)
    24042417        if (!hasAvailableSelected) {
    2405           const firstAvailable = searchResults.find(r => r.available && r.total_price !== null);
     2418          const firstAvailable = allUnits.find(u => manualUnitStatus[u.id].available);
    24062419          if (firstAvailable) {
    24072420            manualUnitSelect.value = firstAvailable.id;
     2421          } else if (currentSelectedUnitId && allUnits.some(u => Number(u.id) === Number(currentSelectedUnitId))) {
     2422            manualUnitSelect.value = currentSelectedUnitId;
    24082423          } else if (allUnits.length > 0) {
    24092424            manualUnitSelect.value = allUnits[0].id;
    … …  
    24132428        }
    24142429
     2430        renderManualUnitCards();
    24152431        updateManualUnitServicesAndPhysical(manualUnitSelect.value);
    24162432      }
    … …  
    24192435    }).catch(() => {
    24202436      calculateManualQuote();
     2437    });
     2438  }
     2439
     2440  function renderManualUnitCards() {
     2441    if (!manualUnitCards || !manualUnitSelect) return;
     2442    const selectedId = manualUnitSelect.value;
     2443    manualUnitCards.innerHTML = '';
     2444    allUnits.forEach(u => {
     2445      const st = manualUnitStatus[u.id];
     2446      if (!st) return;
     2447      const isSelected = Number(u.id) === Number(selectedId);
     2448      const cap = u.capacity ? u.capacity.adults : u.adultsCapacity || 2;
     2449      const capKids = u.capacity ? u.capacity.children : u.childrenCapacity || 0;
     2450      const card = document.createElement('div');
     2451      card.className = 'wp-booking-unit-card ' + (st.available ? 'is-available' : 'is-unavailable') + (isSelected ? ' is-selected' : '');
     2452      card.setAttribute('role', 'radio');
     2453      card.setAttribute('aria-checked', isSelected ? 'true' : 'false');
     2454      card.tabIndex = 0;
     2455      card.dataset.unitId = u.id;
     2456      let statusHtml;
     2457      if (st.available) {
     2458        statusHtml = `<span class="wp-booking-unit-card-badge ok">🟢 Disponible</span><span class="wp-booking-unit-card-price">$${st.price.toLocaleString('es-AR')} total</span>`;
     2459      } else {
     2460        const tag = st.reason.overridable ? 'Reservable bajo responsabilidad' : 'No reservable';
     2461        statusHtml = `<span class="wp-booking-unit-card-badge no">🔴 Sin disponibilidad</span>` +
     2462          `<span class="wp-booking-unit-card-reason">${escapeHtml(st.reason.message)}</span>` +
     2463          `<span class="wp-booking-unit-card-hint ${st.reason.overridable ? 'warn' : ''}">${tag}</span>`;
     2464      }
     2465      card.innerHTML = `<div class="wp-booking-unit-card-head"><strong>${escapeHtml(u.name)}</strong><small>👥 ${cap} ad.${capKids ? ' + ' + capKids + ' niños' : ''}</small></div>${statusHtml}`;
     2466      const choose = () => {
     2467        manualUnitSelect.value = u.id;
     2468        manualUnitSelect.dispatchEvent(new Event('change'));
     2469        renderManualUnitCards();
     2470      };
     2471      card.addEventListener('click', choose);
     2472      card.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); choose(); } });
     2473      manualUnitCards.appendChild(card);
    24212474    });
    24222475  }
    … …  
    24422495    }
    24432496
    2444     const todayStr = getLocalDateString(new Date());
    2445     if (checkin < todayStr) {
    2446       manualQuotePreview.innerHTML = '<div class="wp-booking-quote-error-msg">⚠️ La fecha de check-in no puede ser anterior a hoy.</div>';
    2447       if (manualSaveBtn) manualSaveBtn.disabled = true;
    2448       return;
    2449     }
    2450 
    24512497    if (checkin >= checkout) {
    24522498      manualQuotePreview.innerHTML = '<div class="wp-booking-quote-error-msg">⚠️ La fecha de check-out debe ser posterior al check-in.</div>';
    … …  
    24692515      children,
    24702516      quantity,
    2471       extra_services: extraServices
     2517      extra_services: extraServices,
     2518      admin_preview: true
    24722519    }).then(data => {
    24732520      if (data.success) {
    … …  
    24982545    const finalGrandTotal = baseAccommodation + extraServicesTotal;
    24992546
    2500     if (data.available) {
     2547    // Aviso (no bloqueante) para carga de reservas atrasadas
     2548    const checkinVal = manualCheckinInput ? manualCheckinInput.value : '';
     2549    const isPastCheckin = checkinVal && checkinVal < getLocalDateString(new Date());
     2550    const pastNoticeHtml = isPastCheckin
     2551      ? `<div class="wp-booking-quote-past-notice">🕓 Estás cargando una reserva con ingreso anterior a hoy (${escapeHtml(checkinVal.split('-').reverse().join('/'))}). Se registrará como carga atrasada.</div>`
     2552      : '';
     2553
     2554    const unav = data.unavailability;
     2555    const forceable = !data.available && !!unav && unav.overridable === true && data.has_full_coverage !== false;
     2556    currentManualForced = forceable;
     2557
     2558    const overrideWarningHtml = forceable ? `
     2559      <div class="wp-booking-quote-override-warning">
     2560        <strong>⚠️ Reserva fuera de la disponibilidad validada por el sistema</strong>
     2561        <p>${escapeHtml(unav.message)}</p>
     2562        <p>Podés registrar la reserva de todos modos. Es responsabilidad del operador verificar la disponibilidad real del establecimiento y asignar manualmente a los pasajeros en la reserva (unidad física, fechas o alojamiento alternativo), evitando superposiciones con otras estadías.</p>
     2563      </div>` : '';
     2564
     2565    if (data.available || forceable) {
    25012566      let extrasHtml = '';
    25022567      if (extraItems.length > 0) {
    … …  
    25082573      }
    25092574
    2510       manualQuotePreview.innerHTML = `
     2575      manualQuotePreview.innerHTML = pastNoticeHtml + overrideWarningHtml + `
    25112576        <div class="wp-booking-quote-card-success">
    25122577          <div class="wp-booking-quote-header">
    2513             <strong>✅ ¡Estadía Disponible! (${nightsCount} noche${nightsCount !== 1 ? 's' : ''})</strong>
    2514             <span class="wp-booking-badge-success">Disponible</span>
     2578            <strong>${forceable ? '⚠️ Cotización de la estadía' : '✅ ¡Estadía Disponible!'} (${nightsCount} noche${nightsCount !== 1 ? 's' : ''})</strong>
     2579            <span class="wp-booking-badge-success">${forceable ? 'Sin disponibilidad' : 'Disponible'}</span>
    25152580          </div>
    25162581          <div class="wp-booking-quote-body">
    … …  
    25302595    } else {
    25312596      currentManualGrandTotal = 0;
    2532       manualQuotePreview.innerHTML = `
     2597      const noRates = data.has_full_coverage === false;
     2598      const blockMsg = unav && unav.message ? `<br><small>${escapeHtml(unav.message)}</small>` : '';
     2599      manualQuotePreview.innerHTML = pastNoticeHtml + (!noRates && !forceable && unav ? `
     2600        <div class="wp-booking-quote-card-error">
     2601          <strong>⚠️ No se puede registrar esta reserva</strong>${blockMsg}
     2602        </div>
     2603      ` : noRates ? `
     2604        <div class="wp-booking-quote-card-error">
     2605          <strong>⚠️ No hay tarifas cargadas</strong> para todas las noches seleccionadas.${blockMsg}<br>
     2606          <small>Cargá una tarifa que cubra esas fechas en "Tarifas" para poder registrar la reserva${isPastCheckin ? ' (también aplica a fechas pasadas)' : ''}.</small>
     2607        </div>
     2608      ` : `
    25332609        <div class="wp-booking-quote-card-error">
    25342610          <strong>⚠️ Sin disponibilidad suficiente</strong> para las fechas solicitadas.<br>
    2535           <small>Capacidad disponible en este período: ${data.min_availability !== undefined ? data.min_availability : 0} unidad(es).</small>
     2611          <small>Capacidad disponible en este período: ${data.available_count !== undefined ? data.available_count : 0} unidad(es).</small>
    25362612        </div>
    2537       `;
     2613      `);
    25382614      if (manualSaveBtn) manualSaveBtn.disabled = true;
    25392615      updateManualFinancialSummary();
    … …  
    26702746        altFormat: 'd/m/Y',
    26712747        allowInput: false,
    2672         minDate: 'today',
     2748        // Sin fecha mínima: los propietarios necesitan cargar reservas atrasadas (fechas anteriores a hoy)
    26732749        onChange: function (selectedDates) {
    26742750          if (selectedDates.length > 0 && manualCheckoutFp) {
    … …  
    27332809
    27342810    if (manualCheckinFp) {
    2735       manualCheckinFp.set('minDate', 'today');
    27362811      manualCheckinFp.setDate(today, false);
    27372812    } else if (manualCheckinInput) {
    2738       manualCheckinInput.min = todayStr;
     2813      manualCheckinInput.removeAttribute('min');
    27392814      manualCheckinInput.value = todayStr;
    27402815    }
    … …  
    27872862  }
    27882863
     2864  // Si la estadía ya finalizó (carga atrasada), no tiene sentido enviar la confirmación al huésped
     2865  function isManualStayFinished() {
     2866    const out = manualCheckoutInput ? manualCheckoutInput.value : '';
     2867    return !!out && out <= getLocalDateString(new Date());
     2868  }
     2869
    27892870  if (manualGuestEmailInput && manualSendGuestEmailCheck) {
    27902871    manualGuestEmailInput.addEventListener('input', function () {
    2791       if (this.value.trim().length > 0) {
     2872      if (this.value.trim().length > 0 && !isManualStayFinished()) {
    27922873        manualSendGuestEmailCheck.checked = true;
    27932874      }
    27942875    });
    27952876  }
     2877
     2878  [manualCheckinInput, manualCheckoutInput].forEach(input => {
     2879    if (input && manualSendGuestEmailCheck) {
     2880      input.addEventListener('change', function () {
     2881        if (isManualStayFinished()) manualSendGuestEmailCheck.checked = false;
     2882      });
     2883    }
     2884  });
    27962885
    27972886  function showManualSearchResults() {
    … …  
    28932982  if (manualUnitSelect) {
    28942983    manualUnitSelect.addEventListener('change', function () {
     2984      renderManualUnitCards();
    28952985      updateManualUnitServicesAndPhysical(this.value);
    28962986      debounceCalculateManualQuote();
    … …  
    29012991    manualCheckinInput.addEventListener('change', function () {
    29022992      const todayStr = getLocalDateString(new Date());
    2903       this.min = todayStr;
    2904       if (this.value && this.value < todayStr) {
    2905         this.value = todayStr;
    2906       }
    29072993      const inVal = this.value || todayStr;
    29082994      const inDate = new Date(inVal + 'T00:00:00');
    … …  
    29883074      }
    29893075
     3076      // Carga atrasada: se permite registrar reservas con ingreso anterior a hoy, previa confirmación
    29903077      const todayStr = getLocalDateString(new Date());
    29913078      if (checkin < todayStr) {
    2992         if (feedbackManual) {
    2993           feedbackManual.className = 'wp-booking-admin-feedback error';
    2994           feedbackManual.style.display = 'block';
    2995           feedbackManual.textContent = 'La fecha de check-in no puede ser anterior al día de hoy.';
    2996         }
    2997         return;
     3079        const fmt = s => s.split('-').reverse().join('/');
     3080        const stayFinished = checkout <= todayStr;
     3081        const pastMsg = `Estás registrando una reserva con fecha de ingreso anterior a hoy:\n\n` +
     3082          `Check-in: ${fmt(checkin)}  →  Check-out: ${fmt(checkout)}\n` +
     3083          (stayFinished ? `\nLa estadía ya finalizó.` : `\nLa estadía está en curso.`) +
     3084          `\n\n¿Confirmás la carga de esta reserva atrasada?`;
     3085        if (!confirm(pastMsg)) return;
    29983086      }
    29993087
    … …  
    30053093        }
    30063094        return;
     3095      }
     3096
     3097      if (currentManualForced) {
     3098        const reasonTxt = currentManualQuote && currentManualQuote.unavailability ? currentManualQuote.unavailability.message : 'La unidad no tiene disponibilidad para el período seleccionado.';
     3099        const forceMsg = `ATENCIÓN: RESERVA SIN DISPONIBILIDAD VALIDADA\n\n` +
     3100          `El sistema indica que esta unidad no está disponible para el período seleccionado:\n• ${reasonTxt}\n\n` +
     3101          `Si continúa, la reserva se registrará de todos modos. Es responsabilidad del operador verificar la disponibilidad real del establecimiento y asignar a los pasajeros en la reserva (unidad física, fechas o alojamiento alternativo) para evitar superposiciones con otras estadías.\n\n` +
     3102          `¿Desea registrar la reserva de todos modos?`;
     3103        if (!confirm(forceMsg)) return;
    30073104      }
    30083105
    … …  
    30503147        payment_method: 'destino',
    30513148        is_admin_manual: true,
     3149        allow_unavailable: currentManualForced === true,
    30523150        send_guest_email: (manualSendGuestEmailCheck && guestEmail.length > 0) ? manualSendGuestEmailCheck.checked : false,
    30533151        status: paymentMode === 'none' ? 'pending_payment' : 'confirmed',
  • bookingfacil-reservas/tags/1.3.23/assets/js/frontend/frontend-search.js

    r3703315 r3739702  
    471471                      </div>
    472472                      <div class="wp-booking-range-unit-meta">
    473                         <span class="wp-booking-range-meta-pill">👥 Capacidad: ${parseInt(unit.capacity.adults, 10)} adultos</span>
     473                        <span class="wp-booking-range-meta-pill">👥 Hasta ${(parseInt(unit.capacity.adults, 10) || 0) + (parseInt(unit.capacity.children, 10) || 0)} personas</span>
    474474                        ${minNightsBadge}
    475475                      </div>
  • bookingfacil-reservas/tags/1.3.23/booking-facil.php

    r3738258 r3739702  
    33 * Plugin Name: BookingFacil Reservations for Accommodations
    44 * Description: Accept online reservations for accommodations and campsites. Manage rates, parcel occupancy, payments, and confirmations from WordPress.
    5  * Version:     1.3.22
     5 * Version:     1.3.23
    66 * Requires at least: 5.8
    77 * Requires PHP: 7.4
    … …  
    2020
    2121// Definir constantes del plugin
    22 define( 'BOOKING_FACIL_VERSION', '1.3.22' );
     22define( 'BOOKING_FACIL_VERSION', '1.3.23' );
    2323define( 'BOOKING_FACIL_PATH', plugin_dir_path( __FILE__ ) );
    2424define( 'BOOKING_FACIL_URL', plugin_dir_url( __FILE__ ) );
  • bookingfacil-reservas/tags/1.3.23/includes/admin-pages/reservations.php

    r3721666 r3739702  
    776776                            <div class="wp-booking-form-group wbf-style-b0473a88">
    777777                                <label for="wp-booking-manual-unit-id" class="wbf-style-16fa3e7d"><?php esc_html_e( 'Tipo de Unidad Habitacional *', 'bookingfacil-reservas' ); ?></label>
    778                                 <select id="wp-booking-manual-unit-id" required class="wbf-style-48c62ea1">
     778                                <select id="wp-booking-manual-unit-id" required class="wbf-style-48c62ea1" style="display:none;" aria-hidden="true" tabindex="-1">
    779779                                    <option value=""><?php esc_html_e( 'Seleccione una unidad...', 'bookingfacil-reservas' ); ?></option>
    780780                                </select>
     781                                <div id="wp-booking-manual-unit-cards" class="wp-booking-unit-cards" role="radiogroup" aria-label="<?php esc_attr_e( 'Tipo de unidad habitacional', 'bookingfacil-reservas' ); ?>">
     782                                    <div class="wp-booking-quote-empty-msg"><?php esc_html_e( 'Seleccioná las fechas para ver la disponibilidad de cada unidad.', 'bookingfacil-reservas' ); ?></div>
     783                                </div>
    781784                            </div>
    782785
  • bookingfacil-reservas/tags/1.3.23/includes/admin-pages/units.php

    r3692007 r3739702  
    112112                        <div class="wp-booking-form-row">
    113113                            <div class="wp-booking-form-group">
    114                                 <label for="wp-booking-prop-adults"><?php esc_html_e( 'Capacidad Adultos *', 'bookingfacil-reservas' ); ?></label>
     114                                <label for="wp-booking-prop-adults"><?php esc_html_e( 'Plazas para adultos *', 'bookingfacil-reservas' ); ?></label>
    115115                                <input type="number" id="wp-booking-prop-adults" min="1" value="2" required />
     116                                <small class="description"><?php esc_html_e( 'Los niños también pueden ocupar estas plazas.', 'bookingfacil-reservas' ); ?></small>
    116117                            </div>
    117118                            <div class="wp-booking-form-group">
    118                                 <label for="wp-booking-prop-children"><?php esc_html_e( 'Capacidad Niños', 'bookingfacil-reservas' ); ?></label>
     119                                <label for="wp-booking-prop-children"><?php esc_html_e( 'Plazas adicionales solo para niños', 'bookingfacil-reservas' ); ?></label>
    119120                                <input type="number" id="wp-booking-prop-children" min="0" value="0" />
    120                             </div>
    121                         </div>
     121                                <small class="description"><?php esc_html_e( 'Dejalo en 0 si la unidad no tiene camas o cunas extra para niños.', 'bookingfacil-reservas' ); ?></small>
     122                            </div>
     123                        </div>
     124                        <p id="wp-booking-prop-capacity-total" class="description" style="margin:-4px 0 12px;font-weight:600;"></p>
    122125
    123126                        <!-- ESPECIFICACIONES CLAVE: DORMITORIOS Y BAÑOS -->
  • bookingfacil-reservas/tags/1.3.23/readme.txt

    r3738258 r3739702  
    55Tested up to: 7.1.2
    66Requires PHP: 7.4
    7 Stable tag: 1.3.22
     7Stable tag: 1.3.23
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    8888== Changelog ==
    8989
     90= 1.3.23 =
     91* Manual Booking Creation: allows lodging managers to register delayed or ongoing bookings with check-in dates prior to today, with an intuitive confirmation prompt.
     92* Automatic Email Suppression for Past Stays: automatically disables email delivery to guests if the manual stay has already finished.
     93* Interactive Unit Availability Cards: introduced interactive visual unit cards in manual reservations displaying live rates, available physical counts, and detailed blockage/unavailability reasons.
     94* Operator Availability Override: allows operators to proceed with booking registration under manual responsibility when units lack validated online inventory.
     95* Adult & Child Capacity Definitions: clarified adult bed capacities (which children can also occupy) vs. extra child-only beds/cribs with real-time total person counters in unit settings.
     96* Public Search Capacity Display: updated range recommendations and availability chips to state "Hasta X personas" including adult and child capacities.
     97
    9098= 1.3.22 =
    9199* Frontdesk Daily Operations: added interactive confirmation alert prior to check-in registration to prevent accidental clicks.
    … …  
    252260== Upgrade Notice ==
    253261
     262= 1.3.23 =
     263* Recommended update: enhances manual booking creation with delayed/past stay support, interactive unit availability cards, forced override under operator responsibility, and clarified adult/child capacity handling.
     264
    254265= 1.3.22 =
    255266* Recommended update: enhances Frontdesk check-in workflow with interactive confirmation, automatic transfer to In-House Guests, reversible check-in, and clear visual distinction between vacant and occupied units in Housekeeping Express.
  • bookingfacil-reservas/trunk/assets/css/admin/admin-reservations.css

    r3719967 r3739702  
    403403        flex-direction: column;
    404404    }
     405
    405406    .wp-booking-manual-col-left,
    406407    .wp-booking-manual-col-right {
    … …  
    516517}
    517518
     519/* Aviso de carga atrasada (check-in anterior a hoy) */
     520.wp-booking-quote-past-notice {
     521    padding: 8px 12px;
     522    margin-bottom: 8px;
     523    background: rgba(245, 158, 11, 0.1);
     524    border: 1px solid #f59e0b;
     525    border-radius: 6px;
     526    color: #fcd34d;
     527    font-size: 12px;
     528}
     529
    518530.wp-booking-quote-card-success {
    519531    background: rgba(16, 185, 129, 0.08);
    … …  
    9901002        grid-template-columns: 1fr;
    9911003    }
     1004
    9921005    .wp-booking-occ-doc-row {
    9931006        flex-direction: column;
    9941007    }
     1008
    9951009    .wp-booking-occ-doctype-select {
    9961010        width: 100% !important;
    9971011    }
    9981012}
     1013
     1014/* Selector de unidades en tarjetas (Nueva Reserva en Mostrador) */
     1015.wp-booking-unit-cards { display: grid; gap: 8px; margin-top: 6px; }
     1016.wp-booking-unit-card {
     1017    padding: 10px 12px; border: 1px solid #334155; border-radius: 8px; background: rgba(15, 23, 42, 0.5);
     1018    cursor: pointer; display: flex; flex-direction: column; gap: 4px; transition: border-color .15s, background .15s;
     1019}
     1020.wp-booking-unit-card:hover, .wp-booking-unit-card:focus { border-color: #64748b; outline: none; }
     1021.wp-booking-unit-card.is-selected { border-color: #38bdf8; background: rgba(56, 189, 248, 0.08); box-shadow: 0 0 0 1px #38bdf8; }
     1022.wp-booking-unit-card.is-unavailable { opacity: .85; }
     1023.wp-booking-unit-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
     1024.wp-booking-unit-card-head small { color: #94a3b8; }
     1025.wp-booking-unit-card-badge { font-size: 12px; font-weight: 600; }
     1026.wp-booking-unit-card-badge.ok { color: #34d399; }
     1027.wp-booking-unit-card-badge.no { color: #f87171; }
     1028.wp-booking-unit-card-price { font-size: 13px; font-weight: 600; }
     1029.wp-booking-unit-card-reason { font-size: 12px; color: #cbd5e1; line-height: 1.4; }
     1030.wp-booking-unit-card-hint { font-size: 11px; color: #94a3b8; }
     1031.wp-booking-unit-card-hint.warn { color: #fcd34d; }
     1032
     1033/* Aviso de reserva forzada (sin disponibilidad validada) */
     1034.wp-booking-quote-override-warning {
     1035    padding: 10px 12px; margin-bottom: 8px; background: rgba(245, 158, 11, 0.1); border: 1px solid #f59e0b;
     1036    border-radius: 6px; color: #fcd34d; font-size: 12px; line-height: 1.5;
     1037}
     1038.wp-booking-quote-override-warning p { margin: 6px 0 0; }
  • bookingfacil-reservas/trunk/assets/js/admin/admin-accommodations.js

    r3692007 r3739702  
    309309    const children = parseInt(document.getElementById('wp-booking-prop-children')?.value, 10) || 0;
    310310    const totalGuests = adults + children;
     311    const totalEl = document.getElementById('wp-booking-prop-capacity-total');
     312    if (totalEl) {
     313      totalEl.textContent = `Capacidad total: ${totalGuests} persona${totalGuests !== 1 ? 's' : ''}` +
     314        (adults > 0 ? ` (hasta ${adults} adulto${adults !== 1 ? 's' : ''}${children > 0 ? `; ${children} plaza${children !== 1 ? 's' : ''} solo para niños` : ''})` : '');
     315    }
    311316    const highlight = document.getElementById('wp-booking-prop-highlight')?.value?.trim();
    312317
  • bookingfacil-reservas/trunk/assets/js/admin/admin-reservations.js

    r3737447 r3739702  
    8686  let cachedGuestsList = [];
    8787  let currentManualQuote = null;
     88  let currentManualForced = false;
     89  let manualUnitStatus = {};
     90  const manualUnitCards = document.getElementById('wp-booking-manual-unit-cards');
    8891  let currentManualGrandTotal = 0;
    8992  let manualQuoteDebounce = null;
    … …  
    23812384    }).then(data => {
    23822385      const searchResults = data && data.success && Array.isArray(data.results) ? data.results : [];
     2386      const unavailableList = data && Array.isArray(data.unavailable_units) ? data.unavailable_units : [];
    23832387      const currentSelectedUnitId = manualUnitSelect ? manualUnitSelect.value : '';
    23842388      let hasAvailableSelected = false;
     2389
     2390      // Estado por unidad: disponible (con precio) o no disponible (con motivo)
     2391      manualUnitStatus = {};
     2392      allUnits.forEach(u => {
     2393        const match = searchResults.find(r => Number(r.id) === Number(u.id));
     2394        const why = unavailableList.find(x => Number(x.id) === Number(u.id));
     2395        if (match && match.available && match.total_price !== null) {
     2396          manualUnitStatus[u.id] = { available: true, price: Number(match.total_price) };
     2397        } else {
     2398          manualUnitStatus[u.id] = {
     2399            available: false,
     2400            reason: why || { code: 'unknown', overridable: false, message: 'No disponible para las fechas seleccionadas.' }
     2401          };
     2402        }
     2403      });
    23852404
    23862405      if (manualUnitSelect) {
    23872406        manualUnitSelect.innerHTML = '';
    23882407        allUnits.forEach(u => {
    2389           const match = searchResults.find(r => Number(r.id) === Number(u.id));
     2408          const st = manualUnitStatus[u.id];
    23902409          const opt = document.createElement('option');
    23912410          opt.value = u.id;
    2392           const uCapacity = u.capacity ? u.capacity.adults : u.adultsCapacity || 2;
    2393 
    2394           if (match && match.available && match.total_price !== null) {
    2395             opt.textContent = `🟢 ${u.name} — Disponible ($${Number(match.total_price).toLocaleString('es-AR')} total)`;
    2396             if (Number(u.id) === Number(currentSelectedUnitId)) hasAvailableSelected = true;
    2397           } else {
    2398             opt.textContent = `🔴 ${u.name} — Sin disponibilidad (Capacidad: ${uCapacity} pers.)`;
    2399           }
     2411          opt.textContent = st.available ? `${u.name} — Disponible` : `${u.name} — ${st.reason.message}`;
    24002412          manualUnitSelect.appendChild(opt);
     2413          if (st.available && Number(u.id) === Number(currentSelectedUnitId)) hasAvailableSelected = true;
    24012414        });
    24022415
    2403         // Si la unidad actual no estaba disponible o no había seleccionada, elegir la primera disponible
     2416        // Si la unidad actual no está disponible, elegir la primera disponible (el operador puede elegir otra a mano)
    24042417        if (!hasAvailableSelected) {
    2405           const firstAvailable = searchResults.find(r => r.available && r.total_price !== null);
     2418          const firstAvailable = allUnits.find(u => manualUnitStatus[u.id].available);
    24062419          if (firstAvailable) {
    24072420            manualUnitSelect.value = firstAvailable.id;
     2421          } else if (currentSelectedUnitId && allUnits.some(u => Number(u.id) === Number(currentSelectedUnitId))) {
     2422            manualUnitSelect.value = currentSelectedUnitId;
    24082423          } else if (allUnits.length > 0) {
    24092424            manualUnitSelect.value = allUnits[0].id;
    … …  
    24132428        }
    24142429
     2430        renderManualUnitCards();
    24152431        updateManualUnitServicesAndPhysical(manualUnitSelect.value);
    24162432      }
    … …  
    24192435    }).catch(() => {
    24202436      calculateManualQuote();
     2437    });
     2438  }
     2439
     2440  function renderManualUnitCards() {
     2441    if (!manualUnitCards || !manualUnitSelect) return;
     2442    const selectedId = manualUnitSelect.value;
     2443    manualUnitCards.innerHTML = '';
     2444    allUnits.forEach(u => {
     2445      const st = manualUnitStatus[u.id];
     2446      if (!st) return;
     2447      const isSelected = Number(u.id) === Number(selectedId);
     2448      const cap = u.capacity ? u.capacity.adults : u.adultsCapacity || 2;
     2449      const capKids = u.capacity ? u.capacity.children : u.childrenCapacity || 0;
     2450      const card = document.createElement('div');
     2451      card.className = 'wp-booking-unit-card ' + (st.available ? 'is-available' : 'is-unavailable') + (isSelected ? ' is-selected' : '');
     2452      card.setAttribute('role', 'radio');
     2453      card.setAttribute('aria-checked', isSelected ? 'true' : 'false');
     2454      card.tabIndex = 0;
     2455      card.dataset.unitId = u.id;
     2456      let statusHtml;
     2457      if (st.available) {
     2458        statusHtml = `<span class="wp-booking-unit-card-badge ok">🟢 Disponible</span><span class="wp-booking-unit-card-price">$${st.price.toLocaleString('es-AR')} total</span>`;
     2459      } else {
     2460        const tag = st.reason.overridable ? 'Reservable bajo responsabilidad' : 'No reservable';
     2461        statusHtml = `<span class="wp-booking-unit-card-badge no">🔴 Sin disponibilidad</span>` +
     2462          `<span class="wp-booking-unit-card-reason">${escapeHtml(st.reason.message)}</span>` +
     2463          `<span class="wp-booking-unit-card-hint ${st.reason.overridable ? 'warn' : ''}">${tag}</span>`;
     2464      }
     2465      card.innerHTML = `<div class="wp-booking-unit-card-head"><strong>${escapeHtml(u.name)}</strong><small>👥 ${cap} ad.${capKids ? ' + ' + capKids + ' niños' : ''}</small></div>${statusHtml}`;
     2466      const choose = () => {
     2467        manualUnitSelect.value = u.id;
     2468        manualUnitSelect.dispatchEvent(new Event('change'));
     2469        renderManualUnitCards();
     2470      };
     2471      card.addEventListener('click', choose);
     2472      card.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); choose(); } });
     2473      manualUnitCards.appendChild(card);
    24212474    });
    24222475  }
    … …  
    24422495    }
    24432496
    2444     const todayStr = getLocalDateString(new Date());
    2445     if (checkin < todayStr) {
    2446       manualQuotePreview.innerHTML = '<div class="wp-booking-quote-error-msg">⚠️ La fecha de check-in no puede ser anterior a hoy.</div>';
    2447       if (manualSaveBtn) manualSaveBtn.disabled = true;
    2448       return;
    2449     }
    2450 
    24512497    if (checkin >= checkout) {
    24522498      manualQuotePreview.innerHTML = '<div class="wp-booking-quote-error-msg">⚠️ La fecha de check-out debe ser posterior al check-in.</div>';
    … …  
    24692515      children,
    24702516      quantity,
    2471       extra_services: extraServices
     2517      extra_services: extraServices,
     2518      admin_preview: true
    24722519    }).then(data => {
    24732520      if (data.success) {
    … …  
    24982545    const finalGrandTotal = baseAccommodation + extraServicesTotal;
    24992546
    2500     if (data.available) {
     2547    // Aviso (no bloqueante) para carga de reservas atrasadas
     2548    const checkinVal = manualCheckinInput ? manualCheckinInput.value : '';
     2549    const isPastCheckin = checkinVal && checkinVal < getLocalDateString(new Date());
     2550    const pastNoticeHtml = isPastCheckin
     2551      ? `<div class="wp-booking-quote-past-notice">🕓 Estás cargando una reserva con ingreso anterior a hoy (${escapeHtml(checkinVal.split('-').reverse().join('/'))}). Se registrará como carga atrasada.</div>`
     2552      : '';
     2553
     2554    const unav = data.unavailability;
     2555    const forceable = !data.available && !!unav && unav.overridable === true && data.has_full_coverage !== false;
     2556    currentManualForced = forceable;
     2557
     2558    const overrideWarningHtml = forceable ? `
     2559      <div class="wp-booking-quote-override-warning">
     2560        <strong>⚠️ Reserva fuera de la disponibilidad validada por el sistema</strong>
     2561        <p>${escapeHtml(unav.message)}</p>
     2562        <p>Podés registrar la reserva de todos modos. Es responsabilidad del operador verificar la disponibilidad real del establecimiento y asignar manualmente a los pasajeros en la reserva (unidad física, fechas o alojamiento alternativo), evitando superposiciones con otras estadías.</p>
     2563      </div>` : '';
     2564
     2565    if (data.available || forceable) {
    25012566      let extrasHtml = '';
    25022567      if (extraItems.length > 0) {
    … …  
    25082573      }
    25092574
    2510       manualQuotePreview.innerHTML = `
     2575      manualQuotePreview.innerHTML = pastNoticeHtml + overrideWarningHtml + `
    25112576        <div class="wp-booking-quote-card-success">
    25122577          <div class="wp-booking-quote-header">
    2513             <strong>✅ ¡Estadía Disponible! (${nightsCount} noche${nightsCount !== 1 ? 's' : ''})</strong>
    2514             <span class="wp-booking-badge-success">Disponible</span>
     2578            <strong>${forceable ? '⚠️ Cotización de la estadía' : '✅ ¡Estadía Disponible!'} (${nightsCount} noche${nightsCount !== 1 ? 's' : ''})</strong>
     2579            <span class="wp-booking-badge-success">${forceable ? 'Sin disponibilidad' : 'Disponible'}</span>
    25152580          </div>
    25162581          <div class="wp-booking-quote-body">
    … …  
    25302595    } else {
    25312596      currentManualGrandTotal = 0;
    2532       manualQuotePreview.innerHTML = `
     2597      const noRates = data.has_full_coverage === false;
     2598      const blockMsg = unav && unav.message ? `<br><small>${escapeHtml(unav.message)}</small>` : '';
     2599      manualQuotePreview.innerHTML = pastNoticeHtml + (!noRates && !forceable && unav ? `
     2600        <div class="wp-booking-quote-card-error">
     2601          <strong>⚠️ No se puede registrar esta reserva</strong>${blockMsg}
     2602        </div>
     2603      ` : noRates ? `
     2604        <div class="wp-booking-quote-card-error">
     2605          <strong>⚠️ No hay tarifas cargadas</strong> para todas las noches seleccionadas.${blockMsg}<br>
     2606          <small>Cargá una tarifa que cubra esas fechas en "Tarifas" para poder registrar la reserva${isPastCheckin ? ' (también aplica a fechas pasadas)' : ''}.</small>
     2607        </div>
     2608      ` : `
    25332609        <div class="wp-booking-quote-card-error">
    25342610          <strong>⚠️ Sin disponibilidad suficiente</strong> para las fechas solicitadas.<br>
    2535           <small>Capacidad disponible en este período: ${data.min_availability !== undefined ? data.min_availability : 0} unidad(es).</small>
     2611          <small>Capacidad disponible en este período: ${data.available_count !== undefined ? data.available_count : 0} unidad(es).</small>
    25362612        </div>
    2537       `;
     2613      `);
    25382614      if (manualSaveBtn) manualSaveBtn.disabled = true;
    25392615      updateManualFinancialSummary();
    … …  
    26702746        altFormat: 'd/m/Y',
    26712747        allowInput: false,
    2672         minDate: 'today',
     2748        // Sin fecha mínima: los propietarios necesitan cargar reservas atrasadas (fechas anteriores a hoy)
    26732749        onChange: function (selectedDates) {
    26742750          if (selectedDates.length > 0 && manualCheckoutFp) {
    … …  
    27332809
    27342810    if (manualCheckinFp) {
    2735       manualCheckinFp.set('minDate', 'today');
    27362811      manualCheckinFp.setDate(today, false);
    27372812    } else if (manualCheckinInput) {
    2738       manualCheckinInput.min = todayStr;
     2813      manualCheckinInput.removeAttribute('min');
    27392814      manualCheckinInput.value = todayStr;
    27402815    }
    … …  
    27872862  }
    27882863
     2864  // Si la estadía ya finalizó (carga atrasada), no tiene sentido enviar la confirmación al huésped
     2865  function isManualStayFinished() {
     2866    const out = manualCheckoutInput ? manualCheckoutInput.value : '';
     2867    return !!out && out <= getLocalDateString(new Date());
     2868  }
     2869
    27892870  if (manualGuestEmailInput && manualSendGuestEmailCheck) {
    27902871    manualGuestEmailInput.addEventListener('input', function () {
    2791       if (this.value.trim().length > 0) {
     2872      if (this.value.trim().length > 0 && !isManualStayFinished()) {
    27922873        manualSendGuestEmailCheck.checked = true;
    27932874      }
    27942875    });
    27952876  }
     2877
     2878  [manualCheckinInput, manualCheckoutInput].forEach(input => {
     2879    if (input && manualSendGuestEmailCheck) {
     2880      input.addEventListener('change', function () {
     2881        if (isManualStayFinished()) manualSendGuestEmailCheck.checked = false;
     2882      });
     2883    }
     2884  });
    27962885
    27972886  function showManualSearchResults() {
    … …  
    28932982  if (manualUnitSelect) {
    28942983    manualUnitSelect.addEventListener('change', function () {
     2984      renderManualUnitCards();
    28952985      updateManualUnitServicesAndPhysical(this.value);
    28962986      debounceCalculateManualQuote();
    … …  
    29012991    manualCheckinInput.addEventListener('change', function () {
    29022992      const todayStr = getLocalDateString(new Date());
    2903       this.min = todayStr;
    2904       if (this.value && this.value < todayStr) {
    2905         this.value = todayStr;
    2906       }
    29072993      const inVal = this.value || todayStr;
    29082994      const inDate = new Date(inVal + 'T00:00:00');
    … …  
    29883074      }
    29893075
     3076      // Carga atrasada: se permite registrar reservas con ingreso anterior a hoy, previa confirmación
    29903077      const todayStr = getLocalDateString(new Date());
    29913078      if (checkin < todayStr) {
    2992         if (feedbackManual) {
    2993           feedbackManual.className = 'wp-booking-admin-feedback error';
    2994           feedbackManual.style.display = 'block';
    2995           feedbackManual.textContent = 'La fecha de check-in no puede ser anterior al día de hoy.';
    2996         }
    2997         return;
     3079        const fmt = s => s.split('-').reverse().join('/');
     3080        const stayFinished = checkout <= todayStr;
     3081        const pastMsg = `Estás registrando una reserva con fecha de ingreso anterior a hoy:\n\n` +
     3082          `Check-in: ${fmt(checkin)}  →  Check-out: ${fmt(checkout)}\n` +
     3083          (stayFinished ? `\nLa estadía ya finalizó.` : `\nLa estadía está en curso.`) +
     3084          `\n\n¿Confirmás la carga de esta reserva atrasada?`;
     3085        if (!confirm(pastMsg)) return;
    29983086      }
    29993087
    … …  
    30053093        }
    30063094        return;
     3095      }
     3096
     3097      if (currentManualForced) {
     3098        const reasonTxt = currentManualQuote && currentManualQuote.unavailability ? currentManualQuote.unavailability.message : 'La unidad no tiene disponibilidad para el período seleccionado.';
     3099        const forceMsg = `ATENCIÓN: RESERVA SIN DISPONIBILIDAD VALIDADA\n\n` +
     3100          `El sistema indica que esta unidad no está disponible para el período seleccionado:\n• ${reasonTxt}\n\n` +
     3101          `Si continúa, la reserva se registrará de todos modos. Es responsabilidad del operador verificar la disponibilidad real del establecimiento y asignar a los pasajeros en la reserva (unidad física, fechas o alojamiento alternativo) para evitar superposiciones con otras estadías.\n\n` +
     3102          `¿Desea registrar la reserva de todos modos?`;
     3103        if (!confirm(forceMsg)) return;
    30073104      }
    30083105
    … …  
    30503147        payment_method: 'destino',
    30513148        is_admin_manual: true,
     3149        allow_unavailable: currentManualForced === true,
    30523150        send_guest_email: (manualSendGuestEmailCheck && guestEmail.length > 0) ? manualSendGuestEmailCheck.checked : false,
    30533151        status: paymentMode === 'none' ? 'pending_payment' : 'confirmed',
  • bookingfacil-reservas/trunk/assets/js/frontend/frontend-search.js

    r3703315 r3739702  
    471471                      </div>
    472472                      <div class="wp-booking-range-unit-meta">
    473                         <span class="wp-booking-range-meta-pill">👥 Capacidad: ${parseInt(unit.capacity.adults, 10)} adultos</span>
     473                        <span class="wp-booking-range-meta-pill">👥 Hasta ${(parseInt(unit.capacity.adults, 10) || 0) + (parseInt(unit.capacity.children, 10) || 0)} personas</span>
    474474                        ${minNightsBadge}
    475475                      </div>
  • bookingfacil-reservas/trunk/booking-facil.php

    r3738258 r3739702  
    33 * Plugin Name: BookingFacil Reservations for Accommodations
    44 * Description: Accept online reservations for accommodations and campsites. Manage rates, parcel occupancy, payments, and confirmations from WordPress.
    5  * Version:     1.3.22
     5 * Version:     1.3.23
    66 * Requires at least: 5.8
    77 * Requires PHP: 7.4
    … …  
    2020
    2121// Definir constantes del plugin
    22 define( 'BOOKING_FACIL_VERSION', '1.3.22' );
     22define( 'BOOKING_FACIL_VERSION', '1.3.23' );
    2323define( 'BOOKING_FACIL_PATH', plugin_dir_path( __FILE__ ) );
    2424define( 'BOOKING_FACIL_URL', plugin_dir_url( __FILE__ ) );
  • bookingfacil-reservas/trunk/includes/admin-pages/reservations.php

    r3721666 r3739702  
    776776                            <div class="wp-booking-form-group wbf-style-b0473a88">
    777777                                <label for="wp-booking-manual-unit-id" class="wbf-style-16fa3e7d"><?php esc_html_e( 'Tipo de Unidad Habitacional *', 'bookingfacil-reservas' ); ?></label>
    778                                 <select id="wp-booking-manual-unit-id" required class="wbf-style-48c62ea1">
     778                                <select id="wp-booking-manual-unit-id" required class="wbf-style-48c62ea1" style="display:none;" aria-hidden="true" tabindex="-1">
    779779                                    <option value=""><?php esc_html_e( 'Seleccione una unidad...', 'bookingfacil-reservas' ); ?></option>
    780780                                </select>
     781                                <div id="wp-booking-manual-unit-cards" class="wp-booking-unit-cards" role="radiogroup" aria-label="<?php esc_attr_e( 'Tipo de unidad habitacional', 'bookingfacil-reservas' ); ?>">
     782                                    <div class="wp-booking-quote-empty-msg"><?php esc_html_e( 'Seleccioná las fechas para ver la disponibilidad de cada unidad.', 'bookingfacil-reservas' ); ?></div>
     783                                </div>
    781784                            </div>
    782785
  • bookingfacil-reservas/trunk/includes/admin-pages/units.php

    r3692007 r3739702  
    112112                        <div class="wp-booking-form-row">
    113113                            <div class="wp-booking-form-group">
    114                                 <label for="wp-booking-prop-adults"><?php esc_html_e( 'Capacidad Adultos *', 'bookingfacil-reservas' ); ?></label>
     114                                <label for="wp-booking-prop-adults"><?php esc_html_e( 'Plazas para adultos *', 'bookingfacil-reservas' ); ?></label>
    115115                                <input type="number" id="wp-booking-prop-adults" min="1" value="2" required />
     116                                <small class="description"><?php esc_html_e( 'Los niños también pueden ocupar estas plazas.', 'bookingfacil-reservas' ); ?></small>
    116117                            </div>
    117118                            <div class="wp-booking-form-group">
    118                                 <label for="wp-booking-prop-children"><?php esc_html_e( 'Capacidad Niños', 'bookingfacil-reservas' ); ?></label>
     119                                <label for="wp-booking-prop-children"><?php esc_html_e( 'Plazas adicionales solo para niños', 'bookingfacil-reservas' ); ?></label>
    119120                                <input type="number" id="wp-booking-prop-children" min="0" value="0" />
    120                             </div>
    121                         </div>
     121                                <small class="description"><?php esc_html_e( 'Dejalo en 0 si la unidad no tiene camas o cunas extra para niños.', 'bookingfacil-reservas' ); ?></small>
     122                            </div>
     123                        </div>
     124                        <p id="wp-booking-prop-capacity-total" class="description" style="margin:-4px 0 12px;font-weight:600;"></p>
    122125
    123126                        <!-- ESPECIFICACIONES CLAVE: DORMITORIOS Y BAÑOS -->
  • bookingfacil-reservas/trunk/readme.txt

    r3738258 r3739702  
    55Tested up to: 7.1.2
    66Requires PHP: 7.4
    7 Stable tag: 1.3.22
     7Stable tag: 1.3.23
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    8888== Changelog ==
    8989
     90= 1.3.23 =
     91* Manual Booking Creation: allows lodging managers to register delayed or ongoing bookings with check-in dates prior to today, with an intuitive confirmation prompt.
     92* Automatic Email Suppression for Past Stays: automatically disables email delivery to guests if the manual stay has already finished.
     93* Interactive Unit Availability Cards: introduced interactive visual unit cards in manual reservations displaying live rates, available physical counts, and detailed blockage/unavailability reasons.
     94* Operator Availability Override: allows operators to proceed with booking registration under manual responsibility when units lack validated online inventory.
     95* Adult & Child Capacity Definitions: clarified adult bed capacities (which children can also occupy) vs. extra child-only beds/cribs with real-time total person counters in unit settings.
     96* Public Search Capacity Display: updated range recommendations and availability chips to state "Hasta X personas" including adult and child capacities.
     97
    9098= 1.3.22 =
    9199* Frontdesk Daily Operations: added interactive confirmation alert prior to check-in registration to prevent accidental clicks.
    … …  
    252260== Upgrade Notice ==
    253261
     262= 1.3.23 =
     263* Recommended update: enhances manual booking creation with delayed/past stay support, interactive unit availability cards, forced override under operator responsibility, and clarified adult/child capacity handling.
     264
    254265= 1.3.22 =
    255266* Recommended update: enhances Frontdesk check-in workflow with interactive confirmation, automatic transfer to In-House Guests, reversible check-in, and clear visual distinction between vacant and occupied units in Housekeeping Express.
Note: See TracChangeset for help on using the changeset viewer.