Plugin Directory

Changeset 3448790


Ignore:
Timestamp:
01/28/2026 02:33:42 PM (2 months ago)
Author:
creavi
Message:

Added Minimum Time Before Booking option — per service lead-time setting

Location:
creavi-booking-service
Files:
34 added
7 edited

Legend:

Unmodified
Added
Removed
  • creavi-booking-service/trunk/assets/js/booking.js

    r3445660 r3448790  
    276276    }
    277277
    278         // -----------------------------
     278    // -----------------------------
    279279    // Dynamic availability helpers
    280280    // -----------------------------
     
    331331    }
    332332
     333    function creavibcGetLeadMinutes(instance) {
     334        const v = parseInt(instance?.MIN_TIME_BEFORE_BOOKING_MIN || 0, 10);
     335        return Number.isFinite(v) ? Math.max(0, v) : 0;
     336    }
     337
     338    function creavibcGetPickerZone(instance, userTz) {
     339        const tzMode = instance?.TIMEZONE_MODE || 'localized';
     340        const adminTz = instance?.ADMIN_TIMEZONE || 'UTC';
     341        return (tzMode === 'locked') ? adminTz : userTz;
     342    }
     343
    333344
    334345    function initInlineCalendar(serviceId) {
     
    341352        const instance = window.CREAVIBC_INSTANCES?.[serviceId];
    342353        if (!instance) return;
     354
     355
     356        const userTz = window.CREAVIBC_USER_TIMEZONE || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
     357        const { DateTime } = luxon;
     358
     359        const leadMin = creavibcGetLeadMinutes(instance);
     360        const pickerZone = creavibcGetPickerZone(instance, userTz);
     361
     362        // Earliest selectable day (based on lead time)
     363        const earliestISO = DateTime.now().setZone(pickerZone).plus({ minutes: leadMin }).toISODate();
     364
     365
     366
    343367
    344368        // Sort AVAILABLE_DATES and prefer the first date >= today
     
    361385        dates = dates.slice().sort();
    362386
    363         const todayStr = new Date().toISOString().slice(0, 10);
    364         const firstEnabled = dates.find(d => d >= todayStr) || dates[0] || null;
    365 
    366 
     387       
     388        //const todayStr = new Date().toISOString().slice(0, 10);
     389        //const firstEnabled = dates.find(d => d >= todayStr) || dates[0] || null;
     390
     391        const firstEnabled = dates.find(d => d >= earliestISO) || dates[0] || null;
    367392
    368393        const fp = flatpickr(calendarEl, {
    369394            inline: true,
    370             minDate: "today",
     395            //minDate: "today",
     396            minDate: earliestISO,
    371397            dateFormat: "Y-m-d",
    372398            enable: dates,
     
    440466    }
    441467
    442     /*
    443     function renderTimeSlotsForDate(dateStr, serviceId, popup) {
    444         const container = popup.querySelector('.creavibc-time-slots');
    445 
    446         // Smooth clearing (fade old out)
    447         container.classList.add('fade-out');
    448         setTimeout(() => {
    449             container.innerHTML = '';
    450             container.classList.remove('fade-out');
    451 
    452             const instance = window.CREAVIBC_INSTANCES?.[serviceId];
    453             if (!instance) return;
    454 
    455             const weekday = new Date(dateStr).toLocaleDateString('en-US', { weekday: 'long' }).toLowerCase();
    456             const slots = instance.WEEKDAY_SLOTS?.[weekday] || [];
    457             const duration = parseInt(instance.SERVICE_DURATION || 30, 10);
    458             const isLocalized = instance.TIMEZONE_MODE === 'localized';
    459 
    460 
    461             const { DateTime } = luxon;
    462 
    463             const userTz  = window.CREAVIBC_USER_TIMEZONE || 'UTC';
    464             const adminTz = instance.ADMIN_TIMEZONE || 'UTC';
    465 
    466             const tzMode = instance.TIMEZONE_MODE || 'localized';
    467 
    468             // "Now" should be compared in the SAME timezone as slotStart.
    469             const now = (tzMode === 'locked')
    470             ? DateTime.now().setZone(adminTz)
    471             : DateTime.now().setZone(userTz);
    472 
    473             // Identify "today" in the same comparison zone
    474             const todayISO = now.toISODate();
    475 
    476 
    477 
    478             if (!slots.length) {
    479                 container.innerHTML = '<p>No slots available for this date.</p>';
    480                 return;
    481             }
    482 
    483             fetchBookedSlots(dateStr, serviceId).then(bookedSlots => {
    484                 slots.sort().forEach((start, i) => {
    485 
    486                     // Build slot start in the comparison timezone
    487                     let slotStart;
    488 
    489                     if (tzMode === 'locked') {
    490                     // Slot times are provider/admin timezone
    491                     slotStart = DateTime.fromISO(`${dateStr}T${start}`, { zone: adminTz });
    492                     } else {
    493                     // Slot times are displayed/treated as user's timezone
    494                     // We must interpret the selected date as user-date and compare in userTz.
    495                     // BUT your slot list "start" is based on admin times. So we convert admin->user for comparison.
    496                     const adminStart = DateTime.fromISO(`${dateStr}T${start}`, { zone: adminTz });
    497                     slotStart = adminStart.setZone(userTz);
    498                     }
    499 
    500                     // Is this slot in the past (only matters for same-day)
    501                     const isPast = (slotStart.toISODate() === todayISO) && (slotStart < now);
    502 
    503 
    504                     let displayStart = start;
    505                     let displayEnd = calculateEnd(start, duration);
    506 
    507                     if (isLocalized && window.luxon) {
    508                         const { DateTime } = luxon;
    509                         const adminZoned = DateTime.fromISO(`${dateStr}T${start}`, { zone: instance.ADMIN_TIMEZONE });
    510                         const userZonedStart = adminZoned.setZone(window.CREAVIBC_USER_TIMEZONE);
    511                         const userZonedEnd = userZonedStart.plus({ minutes: duration });
    512 
    513                         displayStart = userZonedStart.toFormat('HH:mm');
    514                         displayEnd = userZonedEnd.toFormat('HH:mm');
    515                     }
    516 
    517                     const btn = document.createElement('button');
    518                     btn.type = 'button';
    519                     btn.className = 'creavibc-slot-btn';
    520                     btn.textContent = displayStart;
    521                     btn.dataset.time = start;
    522 
    523                     btn.dataset.displayStart = displayStart;
    524                     btn.dataset.displayEnd   = displayEnd;
    525 
    526                     if (bookedSlots.includes(start)) {
    527                         btn.classList.add('creavibc-slot-disabled');
    528                         btn.disabled = true;
    529                     } else {
    530                         btn.addEventListener('click', () => {
    531                             popup.querySelectorAll('.creavibc-slot-btn').forEach(b => b.classList.remove('selected'));
    532                             btn.classList.add('selected');
    533 
    534                             const tzMode = instance.TIMEZONE_MODE || 'localized';
    535                             const adminTimezone = instance.ADMIN_TIMEZONE;
    536                             const userTimezone = window.CREAVIBC_USER_TIMEZONE;
    537                             const showTz = (tzMode === 'locked') ? adminTimezone : userTimezone;
    538                             popup.querySelector('.creavibc-summary-time-text').innerHTML = `${displayStart} – ${displayEnd} (${showTz})`;
    539 
    540                             popup.querySelector('.creavibc-summary-time').classList.remove('time-hidden');
    541                             validateNextButton(serviceId);
    542                         });
    543                     }
    544 
    545                     container.appendChild(btn);
    546 
    547                     // Animate each with stagger
    548                     setTimeout(() => btn.classList.add('show'), i * 20);
    549                 });
    550 
    551 
    552                 // Auto-select the first enabled slot (after fade-out/stagger are done)
    553                 // Auto-select the first enabled slot (if any)
    554                 setTimeout(() => {
    555                     const firstEnabled = container.querySelector('.creavibc-slot-btn:not(.creavibc-slot-disabled)');
    556                     if (firstEnabled) {
    557                         firstEnabled.click(); // triggers your existing selection logic + validates Next
    558                     }
    559                 }, 100);
    560 
    561 
    562 
    563 
    564 
    565             });
    566         }, 200); // allow fade-out delay
    567     }
    568     */
    569 
    570    
     468    /*   
    571469    function renderTimeSlotsForDate(dateStr, serviceId, popup) {
    572470        const container = popup.querySelector('.creavibc-time-slots');
     
    689587
    690588        }, 200); // allow fade-out delay
    691     }
     589    } */
     590
     591    function renderTimeSlotsForDate(dateStr, serviceId, popup) {
     592        const container = popup.querySelector('.creavibc-time-slots');
     593
     594        // Smooth clearing (fade old out)
     595        container.classList.add('fade-out');
     596        setTimeout(() => {
     597            container.innerHTML = '';
     598            container.classList.remove('fade-out');
     599
     600            const sid = String(serviceId);
     601            const instance = window.CREAVIBC_INSTANCES?.[sid];
     602            if (!instance || !window.luxon) return;
     603
     604            const { DateTime } = luxon;
     605
     606            // Safer weekday derivation (matches keys: monday, tuesday...)
     607            const weekday = DateTime.fromISO(dateStr).toFormat('cccc').toLowerCase();
     608
     609            const slots = (instance.WEEKDAY_SLOTS?.[weekday] || []).slice();
     610            const duration = parseInt(instance.SERVICE_DURATION || 30, 10);
     611            const tzMode = instance.TIMEZONE_MODE || 'localized';
     612            const isLocalized = tzMode === 'localized';
     613
     614            const userTz =
     615                window.CREAVIBC_USER_TIMEZONE ||
     616                Intl.DateTimeFormat().resolvedOptions().timeZone ||
     617                'UTC';
     618
     619            const adminTz = instance.ADMIN_TIMEZONE || 'UTC';
     620
     621            // Compare in the correct "now" timezone context
     622            const now = (tzMode === 'locked')
     623                ? DateTime.now().setZone(adminTz)
     624                : DateTime.now().setZone(userTz);
     625
     626            // Lead time cutoff (minutes)
     627            const leadMinRaw = parseInt(instance.MIN_TIME_BEFORE_BOOKING_MIN || 0, 10);
     628            const leadMin = Number.isFinite(leadMinRaw) ? Math.max(0, leadMinRaw) : 0;
     629            const cutoff = now.plus({ minutes: leadMin });
     630
     631            if (!slots.length) {
     632                container.innerHTML = '<p>No slots available for this date.</p>';
     633                return;
     634            }
     635
     636            fetchBookedSlots(dateStr, serviceId).then(bookedSlots => {
     637                const booked = Array.isArray(bookedSlots) ? bookedSlots : [];
     638
     639                slots.sort().forEach((start, i) => {
     640                    // Build slot start in correct timezone for comparison
     641                    let slotStart;
     642
     643                    if (tzMode === 'locked') {
     644                        // Slot times are provider/admin timezone
     645                        slotStart = DateTime.fromISO(`${dateStr}T${start}`, { zone: adminTz });
     646                    } else {
     647                        // Slot list is based on admin timezone; compare in user timezone
     648                        const adminStart = DateTime.fromISO(`${dateStr}T${start}`, { zone: adminTz });
     649                        slotStart = adminStart.setZone(userTz);
     650                    }
     651
     652                    // Disable slots earlier than "now + lead time"
     653                    const isBeforeCutoff = slotStart < cutoff;
     654
     655                    // Display values
     656                    let displayStart = start;
     657                    let displayEnd = calculateEnd(start, duration);
     658
     659                    // Localized display: show in user's timezone
     660                    if (isLocalized) {
     661                        const adminZoned = DateTime.fromISO(`${dateStr}T${start}`, { zone: adminTz });
     662                        const userZonedStart = adminZoned.setZone(userTz);
     663                        const userZonedEnd = userZonedStart.plus({ minutes: duration });
     664
     665                        displayStart = userZonedStart.toFormat('HH:mm');
     666                        displayEnd = userZonedEnd.toFormat('HH:mm');
     667                    }
     668
     669                    const btn = document.createElement('button');
     670                    btn.type = 'button';
     671                    btn.className = 'creavibc-slot-btn';
     672                    btn.textContent = displayStart;
     673                    btn.dataset.time = start;
     674
     675                    btn.dataset.displayStart = displayStart;
     676                    btn.dataset.displayEnd = displayEnd;
     677
     678                    const isBooked = booked.includes(start);
     679
     680                    if (isBooked || isBeforeCutoff) {
     681                        btn.classList.add('creavibc-slot-disabled');
     682                        btn.disabled = true;
     683
     684                        // Optional tooltip:
     685                        // if (isBeforeCutoff) btn.title = 'This time is not available yet';
     686                    } else {
     687                        btn.addEventListener('click', () => {
     688                            popup.querySelectorAll('.creavibc-slot-btn').forEach(b => b.classList.remove('selected'));
     689                            btn.classList.add('selected');
     690
     691                            const showTz = (tzMode === 'locked') ? adminTz : userTz;
     692
     693                            popup.querySelector('.creavibc-summary-time-text').innerHTML =
     694                                `${displayStart} – ${displayEnd} (${showTz})`;
     695
     696                            popup.querySelector('.creavibc-summary-time').classList.remove('time-hidden');
     697                            validateNextButton(serviceId);
     698                        });
     699                    }
     700
     701                    container.appendChild(btn);
     702
     703                    // Animate each with stagger
     704                    setTimeout(() => btn.classList.add('show'), i * 20);
     705                });
     706
     707                // Auto-select the first enabled slot (if any)
     708                setTimeout(() => {
     709                    const firstEnabled = container.querySelector('.creavibc-slot-btn:not(.creavibc-slot-disabled)');
     710                    if (firstEnabled) {
     711                        firstEnabled.click();
     712                    }
     713                }, 100);
     714            });
     715
     716        }, 200); // allow fade-out delay
     717    }
     718   
    692719
    693720
  • creavi-booking-service/trunk/creavi-booking-service.php

    r3445660 r3448790  
    11<?php
    22/**
    3  * Plugin Name: Booking Calendar
    4  * Description: A simple service booking system with popup UI.
     3 * Plugin Name: Appointment Booking Calendar
     4 * Description: Easy appointment booking system. Create services, manage availability, and accept bookings with a simple booking calendar.
    55 * Text Domain: creavi-booking-service
    6  * Version: 1.1.5
     6 * Version: 1.1.6
    77 * Author: Creavi
    88 * License: GPL2
     
    1515define('CREAVIBC_PLUGIN_URL', plugin_dir_url(__FILE__));
    1616define('CREAVIBC_PLUGIN_PATH', plugin_dir_path(__FILE__));
    17 define('CREAVIBC_VERSION', '1.1.5');
     17define('CREAVIBC_VERSION', '1.1.6');
    1818
    1919require_once CREAVIBC_PLUGIN_DIR . 'includes/deactivation-feedback.php';
  • creavi-booking-service/trunk/includes/functions.php

    r3445660 r3448790  
    8181        $thankyou_text = __( "Thank you for booking\nSee you soon!", 'creavi-booking-service' );
    8282    }
     83
     84    $min_time_value = (int) get_post_meta( $post->ID, '_creavibc_min_time_value', true );
     85    $min_time_value = max(0, $min_time_value);
     86
     87    $min_time_unit = (string) get_post_meta( $post->ID, '_creavibc_min_time_unit', true );
     88    $min_time_unit = in_array($min_time_unit, ['minutes','hours','days'], true) ? $min_time_unit : 'hours';
     89
     90    // Convert to minutes for JS (easy to calculate)
     91    $min_time_minutes = $min_time_value;
     92    if ($min_time_unit === 'hours') {
     93        $min_time_minutes = $min_time_value * 60;
     94    } elseif ($min_time_unit === 'days') {
     95        $min_time_minutes = $min_time_value * 1440;
     96    }
     97
    8398
    8499
     
    98113    'MONTHS_AHEAD'        => $months_ahead,           // 1..12
    99114    'EXCLUDED_DATES'      => $excluded,               // "YYYY-MM-DD,YYYY-MM-DD"
     115    'MIN_TIME_BEFORE_BOOKING_MIN' => (int) $min_time_minutes,
     116   
    100117    'GCAL_BLOCK_LIVE'     => $gcal_block_live ? 1 : 0,
    101118    ];
  • creavi-booking-service/trunk/includes/meta-boxes.php

    r3442505 r3448790  
    122122    $months_ahead = max(1, min(12, $months_ahead ?: 1));
    123123
    124     // Canonical excluded field name + meta key (do not change!)
     124    // Canonical excluded field name + meta key (do not change!)
    125125    $excluded_value  = (string) get_post_meta($post->ID, '_creavibc_excluded_booking_days', true);
    126126    $excluded_array  = array_filter(array_map('trim', explode(',', $excluded_value)));
    127127    $excluded_string = esc_attr(implode(',', $excluded_array));
    128128
     129    // Minimum time before booking (value + unit)
     130    $min_time_value = (int) get_post_meta( $post->ID, '_creavibc_min_time_value', true );
     131    $min_time_value = $min_time_value >= 0 ? $min_time_value : 0;
     132
     133    $min_time_unit = (string) get_post_meta( $post->ID, '_creavibc_min_time_unit', true );
     134    $min_time_unit = in_array( $min_time_unit, ['minutes','hours','days'], true ) ? $min_time_unit : 'hours';
     135
     136
    129137    wp_nonce_field('creavibc_save_days', 'creavibc_days_nonce');
    130138
    131139    $is_dynamic = ($mode === 'dynamic');
    132140    ?>
     141
     142    <div class="creavibc-field">
     143        <label><strong><?php esc_html_e('Minimum time before booking', 'creavi-booking-service'); ?></strong></label>
     144        <div class="creavibc-help">
     145            <?php esc_html_e('Defines how long before the appointment booking is allowed.', 'creavi-booking-service'); ?>
     146        </div>
     147
     148        <div style="display:flex; gap:8px; align-items:center; max-width:320px;">
     149            <input
     150                type="number"
     151                min="0"
     152                step="1"
     153                name="creavibc_min_time_value"
     154                value="<?php echo esc_attr($min_time_value); ?>"
     155                class="creavibc-input"
     156                style="width:120px;"
     157            >
     158            <select
     159                name="creavibc_min_time_unit"
     160                class="creavibc-select"
     161                style="width:160px;"
     162            >
     163                <option value="minutes" <?php selected($min_time_unit, 'minutes'); ?>><?php esc_html_e('Minutes', 'creavi-booking-service'); ?></option>
     164                <option value="hours" <?php selected($min_time_unit, 'hours'); ?>><?php esc_html_e('Hours', 'creavi-booking-service'); ?></option>
     165                <option value="days" <?php selected($min_time_unit, 'days'); ?>><?php esc_html_e('Days', 'creavi-booking-service'); ?></option>
     166            </select>
     167        </div>
     168    </div>
     169
    133170
    134171    <div class="creavibc-tabs" data-default="<?php echo esc_attr($mode); ?>">
     
    197234                        <div class="creavibc-help"><?php esc_html_e('Pick one or multiple dates to block. Stored as comma-separated YYYY-MM-DD.', 'creavi-booking-service'); ?></div>
    198235
    199                         <!-- Keep the SAME ID + NAME so flatpickr init & POST save match -->
     236                        <!-- Keep the SAME ID + NAME so flatpickr init & POST save match -->
    200237                        <input
    201238                            type="text"
     
    689726    if ( $is_connected ) {
    690727
    691         // ✅ Best source of truth: Google userinfo (email behind the token)
     728        // Google userinfo (email behind the token)
    692729        $connected_email = '';
    693730
     
    747784             * NOTE:
    748785             * We pass the CURRENT WP user initiating OAuth (so Google consent is done by the admin who clicks Connect).
    749              * Your backend stores wp_user_id for that user, then your plugin maps it back to connection_id later.
     786             * backend stores wp_user_id for that user, then plugin maps it back to connection_id later.
    750787             */
    751788            $connect_url = add_query_arg(
  • creavi-booking-service/trunk/includes/render-booking-inline.php

    r3445660 r3448790  
    6565    }
    6666
     67    $min_time_value = (int) get_post_meta( $post->ID, '_creavibc_min_time_value', true );
     68    $min_time_value = max(0, $min_time_value);
     69
     70    $min_time_unit = (string) get_post_meta( $post->ID, '_creavibc_min_time_unit', true );
     71    $min_time_unit = in_array($min_time_unit, ['minutes','hours','days'], true) ? $min_time_unit : 'hours';
     72
     73    // Convert to minutes for JS (easy to calculate)
     74    $min_time_minutes = $min_time_value;
     75    if ($min_time_unit === 'hours') {
     76        $min_time_minutes = $min_time_value * 60;
     77    } elseif ($min_time_unit === 'days') {
     78        $min_time_minutes = $min_time_value * 1440;
     79    }
    6780
    6881
     
    8295        'EXCLUDED_DATES'      => $excluded,               // "YYYY-MM-DD,YYYY-MM-DD"
    8396        'GCAL_BLOCK_LIVE'     => $gcal_block_live ? 1 : 0,
     97
     98        'MIN_TIME_BEFORE_BOOKING_MIN' => (int) $min_time_minutes,
    8499    ];
    85100    ?>
  • creavi-booking-service/trunk/includes/save-service.php

    r3441012 r3448790  
    5353            // (Do nothing)
    5454        }
     55
     56        // 5) Minimum time before booking (value + unit)
     57        if ( isset($_POST['creavibc_min_time_value'], $_POST['creavibc_min_time_unit']) ) {
     58
     59            $value = max( 0, (int) wp_unslash( $_POST['creavibc_min_time_value'] ) );
     60            $unit  = sanitize_text_field( wp_unslash( $_POST['creavibc_min_time_unit'] ) );
     61
     62            if ( ! in_array( $unit, ['minutes','hours','days'], true ) ) {
     63                $unit = 'hours';
     64            }
     65
     66            update_post_meta( $post_id, '_creavibc_min_time_value', $value );
     67            update_post_meta( $post_id, '_creavibc_min_time_unit', $unit );
     68        }
     69
    5570    }
    5671
  • creavi-booking-service/trunk/readme.txt

    r3445660 r3448790  
    33Tags: appointments, booking, booking calendar, bookings, scheduling
    44Requires at least: 6.0 
    5 Tested up to: 6.
     5Tested up to: 6.7
    66Requires PHP: 7.4 
    7 Stable tag: 1.1.5
     7Stable tag: 1.1.6
    88License: GPLv2 or later 
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    1616**Appointment Booking Calendar**
    1717
    18 **The #1 Booking Plugin for Your Website**
    1918Booking Calendar is the ultimate all-in-one plugin to add professional bookings and appointments directly to your WordPress website.
    2019Built natively for WordPress, it’s designed to make online bookings simple, fast, and intuitive - both for you and your clients.
     
    8685
    8786== Changelog ==
     87
     88= 1.1.6 =
     89* Added Minimum Time Before Booking option - per service lead-time setting.
    8890
    8991= 1.1.5 =
Note: See TracChangeset for help on using the changeset viewer.