'window', 'type' => 'interval', 'label' => __( '2-hour window', 'dayslot' ), 'note' => '', 'from' => '08:00', 'to' => '20:00', 'length' => 120, 'step' => 0, 'limit' => 2, 'price_weekday' => 20, 'price_sunday' => 50, 'price_holiday' => '', 'enabled' => 'yes', ), array( 'id' => 'exact', 'type' => 'exact', 'label' => __( 'Exact time', 'dayslot' ), 'note' => '', 'from' => '08:00', 'to' => '21:00', 'length' => 0, 'step' => 15, 'limit' => 0, 'price_weekday' => 50, 'price_sunday' => 70, 'price_holiday' => '', 'enabled' => 'yes', ), array( 'id' => 'night', 'type' => 'night', 'label' => __( 'Night', 'dayslot' ), 'note' => '', 'from' => '21:00', 'to' => '08:00', 'length' => 0, 'step' => 15, 'limit' => 1, 'price_weekday' => 70, 'price_sunday' => 100, 'price_holiday' => '', 'enabled' => 'yes', ), ); } /** * Default settings (translated). Add-ons add theirs with the "dayslot_default_settings" filter. */ function dayslot_defaults() { return apply_filters( 'dayslot_default_settings', array( // General. 'max_per_day' => 5, 'min_days' => 1, 'max_days' => 365, 'closed_weekdays' => array(), 'blocked_dates' => '', 'holidays' => '', 'holiday_country' => '', 'required' => 'yes', 'auto_select' => 'no', 'hide_virtual' => 'yes', 'statuses' => array( 'processing', 'on-hold', 'completed' ), 'notify_customer' => 'yes', // Delivery options. 'variants' => dayslot_default_variants(), 'slot_prep_minutes' => 60, 'blocked_slots' => '', 'shipping_title' => __( 'Delivery', 'dayslot' ), 'hide_other_shipping' => 'yes', 'free_above' => 0, 'free_always' => 'no', 'small_below' => 0, 'small_fee' => 0, // Checkout texts and placement. 'placement' => 'after_address', 'method_label' => __( 'Delivery method', 'dayslot' ), 'label' => __( 'Delivery date', 'dayslot' ), 'time_label' => __( 'Delivery time', 'dayslot' ), 'delivery_text' => __( 'Delivery', 'dayslot' ), 'required_message' => __( 'Please choose the delivery date.', 'dayslot' ), 'unavailable_message' => __( 'Sorry, this date is not available. Please choose another one.', 'dayslot' ), 'slot_required_message' => __( 'Please choose the delivery time.', 'dayslot' ), 'slot_full_message' => __( 'Sorry, this time is no longer available. Please choose another one.', 'dayslot' ), 'full_times' => 'contact', /* translators: {times}: booked times, e.g. 10:00–12:00 */ 'full_contact' => __( 'Already booked: {times}. Want delivery at that time? Please contact us.', 'dayslot' ), // Appearance. 'style' => array(), ) ); } /** * Plugin settings merged with defaults. */ function dayslot_get_settings() { static $defaults = array(); static $cache = null; static $raw = null; static $locale = null; static $busy = false; $saved = get_option( 'dayslot_settings', array() ); $here = determine_locale(); if ( null !== $cache && $saved === $raw && $here === $locale ) { return $cache; } // Defaults per language: multilingual shops switch the language during a page. if ( ! isset( $defaults[ $here ] ) ) { $defaults[ $here ] = dayslot_defaults(); } $saved = is_array( $saved ) ? $saved : array(); if ( $busy ) { return wp_parse_args( $saved, $defaults[ $here ] ); } $busy = true; $cache = wp_parse_args( (array) apply_filters( 'dayslot_saved_settings', $saved ), $defaults[ $here ] ); $busy = false; $raw = get_option( 'dayslot_settings', array() ); $locale = $here; return $cache; } /** * Save settings (merging into what is stored). */ function dayslot_update_settings( $changes ) { $settings = get_option( 'dayslot_settings', array() ); $settings = is_array( $settings ) ? $settings : array(); update_option( 'dayslot_settings', array_merge( $settings, $changes ), true ); } /** * Normalise a context array. */ function dayslot_ctx( $ctx = array() ) { $ctx = wp_parse_args( is_array( $ctx ) ? $ctx : array(), array( 'mode' => 'delivery', 'location' => '', 'exclude_order' => 0, 'items' => null, ) ); if ( 'pickup' !== $ctx['mode'] ) { $ctx['mode'] = 'delivery'; if ( ! dayslot_branches() ) { $ctx['location'] = ''; } } return $ctx; } /** * Branches (Pro pickup points chosen first): every point also delivers, with its own hours and places. */ function dayslot_branches() { return (bool) apply_filters( 'dayslot_branches', false ); } /** * Capacity pool key of a context: "delivery" or "pickup:". */ function dayslot_pool_key( $ctx ) { if ( 'pickup' === $ctx['mode'] ) { return 'pickup:' . $ctx['location']; } return dayslot_branches() && '' !== (string) $ctx['location'] ? 'delivery:' . $ctx['location'] : 'delivery'; } /** * Parse a Y-m-d string into a DateTimeImmutable in the site time zone, or null. */ function dayslot_parse_date( $date ) { if ( ! is_string( $date ) || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) { return null; } $dt = DateTimeImmutable::createFromFormat( '!Y-m-d', $date, wp_timezone() ); return ( $dt && $dt->format( 'Y-m-d' ) === $date ) ? $dt : null; } /** * Today at midnight, site time zone. */ function dayslot_today() { return current_datetime()->setTime( 0, 0 ); } /** * Dates from a textarea (one per line or separated by commas): array of Y-m-d. */ function dayslot_parse_date_list( $text ) { $dates = array(); foreach ( preg_split( '/[\s,;]+/', (string) $text ) as $line ) { $line = trim( $line ); if ( dayslot_parse_date( $line ) ) { $dates[] = $line; } } return array_values( array_unique( $dates ) ); } /** * Manually blocked dates (array of Y-m-d). */ function dayslot_blocked_dates() { return dayslot_parse_date_list( dayslot_get_settings()['blocked_dates'] ); } /** * Month and weekday names in the plugin's own translations, so the calendar always speaks the same language as * the rest of the plugin (WordPress's own names follow the WordPress language, which may differ). * Weekdays are keyed 0 = Sunday … 6 = Saturday; "dateMonths" are for a day inside a date ("%d October" → * "22 października" in Polish). */ function dayslot_calendar_words() { return array( 'months' => array( __( 'January', 'dayslot' ), __( 'February', 'dayslot' ), __( 'March', 'dayslot' ), __( 'April', 'dayslot' ), __( 'May', 'dayslot' ), __( 'June', 'dayslot' ), __( 'July', 'dayslot' ), __( 'August', 'dayslot' ), __( 'September', 'dayslot' ), __( 'October', 'dayslot' ), __( 'November', 'dayslot' ), __( 'December', 'dayslot' ) ), /* translators: %d: day of the month; the month as used inside a date */ 'dateMonths' => array( __( '%d January', 'dayslot' ), __( '%d February', 'dayslot' ), __( '%d March', 'dayslot' ), __( '%d April', 'dayslot' ), __( '%d May', 'dayslot' ), __( '%d June', 'dayslot' ), __( '%d July', 'dayslot' ), __( '%d August', 'dayslot' ), __( '%d September', 'dayslot' ), __( '%d October', 'dayslot' ), __( '%d November', 'dayslot' ), __( '%d December', 'dayslot' ) ), 'days' => array( __( 'Sunday', 'dayslot' ), __( 'Monday', 'dayslot' ), __( 'Tuesday', 'dayslot' ), __( 'Wednesday', 'dayslot' ), __( 'Thursday', 'dayslot' ), __( 'Friday', 'dayslot' ), __( 'Saturday', 'dayslot' ) ), 'daysShort' => array( __( 'Sun', 'dayslot' ), __( 'Mon', 'dayslot' ), __( 'Tue', 'dayslot' ), __( 'Wed', 'dayslot' ), __( 'Thu', 'dayslot' ), __( 'Fri', 'dayslot' ), __( 'Sat', 'dayslot' ) ), ); } /** * Human-readable date in the site language and date format. */ function dayslot_format_date( $date ) { $dt = dayslot_parse_date( $date ); if ( ! $dt ) { return ''; } // The site's date format, with month and weekday names from the plugin's translations // ("j F" becomes "22 października" in Polish, not "22 październik"). $words = dayslot_calendar_words(); $month = (int) $dt->format( 'n' ) - 1; $wday = (int) $dt->format( 'w' ); $marks = array(); // Languages where the month changes inside a date ("25 października", "25. Oktober") get that form also // with a "month day, year" format: "F j, Y" would give "Październik 25, 2026". $in_date = str_replace( '%d', $dt->format( 'j' ), $words['dateMonths'][ $month ] ); $changes = '%d ' . $words['months'][ $month ] !== $words['dateMonths'][ $month ]; $fmt = preg_replace_callback( '/\\\\.|F jS?,?|j F|[FMlD]/', function ( $m ) use ( &$marks, $dt, $words, $month, $wday, $in_date, $changes ) { if ( '\\' === $m[0][0] ) { return $m[0]; } $values = array( 'j F' => $in_date, 'F' => $words['months'][ $month ], 'M' => mb_substr( $words['months'][ $month ], 0, 3 ), 'l' => $words['days'][ $wday ], 'D' => $words['daysShort'][ $wday ], ); if ( ! isset( $values[ $m[0] ] ) ) { // Formats like F j, F jS and "F j," (with a comma). $values[ $m[0] ] = $changes ? $in_date : $words['months'][ $month ] . ' ' . $dt->format( false !== strpos( $m[0], 'S' ) ? 'jS' : 'j' ) . ( ',' === substr( $m[0], -1 ) ? ',' : '' ); } $key = '{' . count( $marks ) . '}'; $marks[ $key ] = $values[ $m[0] ]; return '\\' . implode( '\\', str_split( $key ) ); }, (string) get_option( 'date_format', 'j F Y' ) ); return strtr( $dt->format( $fmt ), $marks ); } /** * Validate "HH:MM". */ function dayslot_valid_time( $time ) { return is_string( $time ) && (bool) preg_match( '/^([01]\d|2[0-3]):[0-5]\d$/', $time ); } /** * Valid slot key: "HH:MM-HH:MM" (a window) or "HH:MM" (an exact time). */ function dayslot_valid_slot_key( $key ) { if ( ! is_string( $key ) ) { return false; } $parts = explode( '-', $key ); if ( 1 === count( $parts ) ) { return dayslot_valid_time( $parts[0] ); } return 2 === count( $parts ) && dayslot_valid_time( $parts[0] ) && dayslot_valid_time( $parts[1] ); } /** * "10:00-12:00" → "10:00 – 12:00", "10:30" → "10:30". */ function dayslot_format_slot( $key ) { $parts = explode( '-', (string) $key ); return 2 === count( $parts ) ? $parts[0] . ' – ' . $parts[1] : (string) $key; } /** * Date plus time, e.g. "24 października 2026, 10:00 – 12:00". */ function dayslot_format_date_slot( $date, $slot ) { $out = dayslot_format_date( $date ); if ( $slot ) { $out .= ', ' . dayslot_format_slot( $slot ); } return $out; } /** * Minutes since midnight for "HH:MM". */ function dayslot_minutes( $time ) { $p = explode( ':', (string) $time ); return 2 === count( $p ) ? (int) $p[0] * 60 + (int) $p[1] : 0; } /** * "HH:MM" for minutes since midnight (wraps at 24h). */ function dayslot_time_from_minutes( $minutes ) { $minutes = ( (int) $minutes % 1440 + 1440 ) % 1440; return sprintf( '%02d:%02d', intdiv( $minutes, 60 ), $minutes % 60 ); } /** * Plain-text money, e.g. "20 zł". */ function dayslot_money( $amount ) { return html_entity_decode( wp_strip_all_tags( wc_price( $amount ) ), ENT_QUOTES, 'UTF-8' ); } /** * Earliest bookable day offset (days from today) for a context. */ function dayslot_min_days( $ctx = array() ) { return max( 0, (int) apply_filters( 'dayslot_min_days', (int) dayslot_get_settings()['min_days'], dayslot_ctx( $ctx ) ) ); } /** * Daily order limit for a context (0 = unlimited). */ function dayslot_daily_limit( $ctx = array() ) { return max( 0, (int) apply_filters( 'dayslot_daily_limit', (int) dayslot_get_settings()['max_per_day'], dayslot_ctx( $ctx ) ) ); } /** * Daily limit on one date (Pro special dates can have their own). */ function dayslot_day_limit( $date, $ctx = array() ) { $ctx = dayslot_ctx( $ctx ); return max( 0, (int) apply_filters( 'dayslot_day_limit', dayslot_daily_limit( $ctx ), $date, $ctx ) ); } /** * Delivery modes the customer can choose from: array( 'delivery' => label, ... ). * The Pro add-on adds pickup. */ function dayslot_modes() { return apply_filters( 'dayslot_modes', array( 'delivery' => dayslot_get_settings()['delivery_text'] ) ); } // -------------------------------------------------------------------------- // Cart / order products // -------------------------------------------------------------------------- /** * Products in the current cart. * * @return WC_Product[] */ function dayslot_cart_products() { $out = array(); if ( function_exists( 'WC' ) && WC()->cart ) { foreach ( WC()->cart->get_cart() as $item ) { if ( ! empty( $item['data'] ) && is_a( $item['data'], 'WC_Product' ) ) { $out[] = $item['data']; } } } return $out; } /** * Products in an order. * * @return WC_Product[] */ function dayslot_order_products( $order ) { $out = array(); foreach ( $order->get_items() as $item ) { $product = is_callable( array( $item, 'get_product' ) ) ? $item->get_product() : null; if ( $product ) { $out[] = $product; } } return $out; } /** * Do these products need a delivery date at all? */ function dayslot_products_need_date( $products ) { if ( ! $products ) { return true; } $needs = true; if ( 'yes' === dayslot_get_settings()['hide_virtual'] ) { $needs = false; foreach ( $products as $product ) { if ( ! $product->is_virtual() ) { $needs = true; break; } } } return (bool) apply_filters( 'dayslot_products_need_date', $needs, $products ); } function dayslot_cart_needs_date() { return dayslot_products_need_date( dayslot_cart_products() ); } function dayslot_order_needs_date( $order ) { return dayslot_products_need_date( dayslot_order_products( $order ) ); } // -------------------------------------------------------------------------- // The customer's choice (session) — used for the shipping price and live recalculation // -------------------------------------------------------------------------- /** * Clean a choice array (POST, Store API, AJAX, session). */ function dayslot_read_choice( $src ) { $src = is_array( $src ) ? $src : array(); $get = function ( $key ) use ( $src ) { return isset( $src[ $key ] ) && is_scalar( $src[ $key ] ) ? sanitize_text_field( wp_unslash( (string) $src[ $key ] ) ) : ''; }; $mode = 'pickup' === $get( 'mode' ) ? 'pickup' : 'delivery'; $date = $get( 'date' ); $slot = $get( 'slot' ); $choice = array( 'date' => dayslot_parse_date( $date ) ? $date : '', 'slot' => dayslot_valid_slot_key( $slot ) ? $slot : '', 'variant' => sanitize_key( $get( 'variant' ) ), 'mode' => $mode, 'location' => 'pickup' === $mode || dayslot_branches() ? sanitize_key( $get( 'location' ) ) : '', ); if ( '' === $choice['date'] && '' !== $date ) { $choice['date'] = 'invalid'; } return apply_filters( 'dayslot_read_choice', $choice, $src ); } function dayslot_empty_choice() { return array( 'date' => '', 'slot' => '', 'variant' => '', 'mode' => 'delivery', 'location' => '', ); } function dayslot_session_get() { $data = ( function_exists( 'WC' ) && WC()->session ) ? WC()->session->get( 'dayslot_choice' ) : null; return wp_parse_args( is_array( $data ) ? $data : array(), dayslot_empty_choice() ); } function dayslot_session_set( $choice ) { if ( ! function_exists( 'WC' ) || ! WC()->session ) { return; } if ( 'invalid' === $choice['date'] ) { $choice['date'] = ''; } WC()->session->set( 'dayslot_choice', $choice ); } /** * Context for a customer choice + products. */ function dayslot_ctx_for_choice( $choice, $products = null, $exclude_order = 0 ) { return dayslot_ctx( array( 'mode' => $choice['mode'], 'location' => $choice['location'], 'items' => $products, 'exclude_order' => $exclude_order, ) ); } /** * Store a choice on an order (does not call save()). */ function dayslot_save_order_values( $order, $choice ) { $choice = wp_parse_args( $choice, dayslot_empty_choice() ); if ( ! dayslot_parse_date( $choice['date'] ) ) { return; } $order->update_meta_data( DAYSLOT_META, $choice['date'] ); if ( dayslot_valid_slot_key( $choice['slot'] ) ) { $order->update_meta_data( DAYSLOT_SLOT_META, $choice['slot'] ); } else { $order->delete_meta_data( DAYSLOT_SLOT_META ); } if ( $choice['variant'] ) { $order->update_meta_data( DAYSLOT_VARIANT_META, $choice['variant'] ); } else { $order->delete_meta_data( DAYSLOT_VARIANT_META ); } if ( 'pickup' === $choice['mode'] ) { $order->update_meta_data( DAYSLOT_MODE_META, 'pickup' ); $order->update_meta_data( DAYSLOT_LOCATION_META, sanitize_key( $choice['location'] ) ); } else { $order->delete_meta_data( DAYSLOT_MODE_META ); if ( dayslot_branches() && $choice['location'] ) { $order->update_meta_data( DAYSLOT_LOCATION_META, sanitize_key( $choice['location'] ) ); } else { $order->delete_meta_data( DAYSLOT_LOCATION_META ); } } do_action( 'dayslot_order_values_saved', $order, $choice ); } /** * The choice stored on an order. */ function dayslot_order_choice( $order ) { $mode = 'pickup' === $order->get_meta( DAYSLOT_MODE_META ) ? 'pickup' : 'delivery'; return array( 'date' => (string) $order->get_meta( DAYSLOT_META ), 'slot' => (string) $order->get_meta( DAYSLOT_SLOT_META ), 'variant' => (string) $order->get_meta( DAYSLOT_VARIANT_META ), 'mode' => $mode, 'location' => 'pickup' === $mode || dayslot_branches() ? (string) $order->get_meta( DAYSLOT_LOCATION_META ) : '', ); } /** * Simple per-IP rate limit. Returns false when $max hits within $window seconds were exceeded. */ function dayslot_rate_limit( $bucket, $max, $window = HOUR_IN_SECONDS ) { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $key = 'dayslot_rl_' . md5( $bucket . '|' . $ip ); $hit = (int) get_transient( $key ); if ( $hit >= $max ) { return false; } set_transient( $key, $hit + 1, $window ); return true; } /** * Tags and attributes of form controls built in code (admin), for wp_kses() before they are printed. */ function dayslot_form_tags() { $common = array( 'class' => true, 'id' => true, 'name' => true, 'value' => true, 'style' => true, 'aria-label' => true, 'data-*' => true, ); return array( 'input' => array_merge( $common, array( 'type' => true, 'placeholder' => true, 'required' => true, 'min' => true, 'max' => true, 'step' => true, 'inputmode' => true, 'checked' => true, 'disabled' => true, ) ), 'select' => $common, 'option' => array( 'value' => true, 'selected' => true, 'disabled' => true, ), 'optgroup' => array( 'label' => true ), 'textarea' => array_merge( $common, array( 'rows' => true ) ), 'span' => $common, 'small' => $common, ); }