= 2025 ) { $out[] = $year . '-12-24'; // Christmas Eve is a public holiday since 2025. } foreach ( array( 0, 1, 49, 60 ) as $offset ) { // Easter, Easter Monday, Pentecost, Corpus Christi. $out[] = $easter->modify( '+' . $offset . ' days' )->format( 'Y-m-d' ); } } return apply_filters( 'dayslot_country_holidays', $out, $country, $year ); } /** * All holidays (own list + automatic country holidays for this and next year). */ function dayslot_holidays() { static $memo = array(); $s = dayslot_get_settings(); $key = $s['holidays'] . '|' . $s['holiday_country']; if ( isset( $memo[ $key ] ) ) { return $memo[ $key ]; } $list = dayslot_parse_date_list( $s['holidays'] ); if ( $s['holiday_country'] ) { $year = (int) dayslot_today()->format( 'Y' ); $list = array_merge( $list, dayslot_country_holidays( $s['holiday_country'], $year ), dayslot_country_holidays( $s['holiday_country'], $year + 1 ) ); } $memo[ $key ] = array_values( array_unique( $list ) ); return $memo[ $key ]; } /** * 'weekday' (Monday–Saturday), 'sunday' or 'holiday'. */ function dayslot_day_type( $date ) { if ( in_array( $date, dayslot_holidays(), true ) ) { return 'holiday'; } $dt = dayslot_parse_date( $date ); return ( $dt && '0' === $dt->format( 'w' ) ) ? 'sunday' : 'weekday'; } function dayslot_day_type_label( $type ) { $labels = array( 'weekday' => __( 'Mon–Sat', 'dayslot' ), 'sunday' => __( 'Sunday', 'dayslot' ), 'holiday' => __( 'Holiday', 'dayslot' ), ); return isset( $labels[ $type ] ) ? $labels[ $type ] : $type; } // -------------------------------------------------------------------------- // Variants // -------------------------------------------------------------------------- /** * Configured variants keyed by id (only enabled ones unless $all). */ function dayslot_variants( $all = false ) { $out = array(); foreach ( (array) dayslot_get_settings()['variants'] as $v ) { if ( empty( $v['id'] ) || empty( $v['type'] ) ) { continue; } $v = wp_parse_args( $v, array( 'label' => '', 'note' => '', 'from' => '08:00', 'to' => '20:00', 'length' => 120, 'step' => 15, 'limit' => 0, 'price_weekday' => 0, 'price_sunday' => 0, 'price_holiday' => '', 'enabled' => 'yes', ) ); if ( $all || 'yes' === $v['enabled'] ) { $out[ $v['id'] ] = $v; } } return $out; } function dayslot_variant( $id ) { $all = dayslot_variants( true ); return isset( $all[ $id ] ) ? $all[ $id ] : null; } /** * Price of a variant on a date (by day type). An empty holiday price means "same as Sunday". */ function dayslot_variant_price( $variant, $date ) { $type = dayslot_day_type( $date ); if ( 'holiday' === $type ) { $price = '' === (string) $variant['price_holiday'] ? $variant['price_sunday'] : $variant['price_holiday']; } elseif ( 'sunday' === $type ) { $price = $variant['price_sunday']; } else { $price = $variant['price_weekday']; } return (float) apply_filters( 'dayslot_variant_price', (float) $price, $variant, $date, $type ); } /** * Slot keys a variant offers: windows "HH:MM-HH:MM", exact times "HH:MM", or the night window. */ function dayslot_variant_slots( $variant ) { $from = dayslot_minutes( $variant['from'] ); $to = dayslot_minutes( $variant['to'] ); $out = array(); switch ( $variant['type'] ) { case 'interval': $len = max( 5, (int) $variant['length'] ); for ( $t = $from, $n = 0; $t + $len <= $to && $n < 96; $t += $len, $n++ ) { $out[] = dayslot_time_from_minutes( $t ) . '-' . dayslot_time_from_minutes( $t + $len ); } break; case 'exact': $step = max( 5, (int) $variant['step'] ); for ( $t = $from, $n = 0; $t <= $to && $n < 288; $t += $step, $n++ ) { $out[] = dayslot_time_from_minutes( $t ); } break; case 'night': $step = (int) $variant['step'] > 0 ? max( 5, (int) $variant['step'] ) : 15; $end = $to <= $from ? $to + 1440 : $to; for ( $t = $from, $n = 0; $t <= $end && $n < 288; $t += $step, $n++ ) { $out[] = dayslot_time_from_minutes( $t ); } break; } return $out; } /** * The night window key of a night variant, e.g. "21:00-08:00" (its shared limit). */ function dayslot_night_key( $variant ) { return $variant['from'] . '-' . $variant['to']; } /** * Is this time of a night variant after midnight (so on the next calendar day)? */ function dayslot_night_next_day( $variant, $slot ) { return $variant && 'night' === $variant['type'] && dayslot_valid_time( $slot ) && dayslot_minutes( $slot ) < dayslot_minutes( $variant['from'] ); } /** * Which variant does a slot key belong to (for old orders without a stored variant)? */ function dayslot_variant_for_slot( $slot, $variant_id = '' ) { if ( $variant_id && dayslot_variant( $variant_id ) ) { return dayslot_variant( $variant_id ); } foreach ( dayslot_variants( true ) as $v ) { if ( in_array( $slot, dayslot_variant_slots( $v ), true ) || ( 'night' === $v['type'] && dayslot_night_key( $v ) === $slot ) ) { return $v; } } return null; } /** * All interval windows, used to find the window an exact time falls into: array( key => array( from, to ) ). */ function dayslot_interval_windows() { static $memo = null; static $sig = null; $now = md5( wp_json_encode( dayslot_get_settings()['variants'] ) ); if ( null === $memo || $sig !== $now ) { $sig = $now; $memo = array(); foreach ( dayslot_variants() as $v ) { if ( 'interval' !== $v['type'] ) { continue; } foreach ( dayslot_variant_slots( $v ) as $key ) { list( $a, $b ) = explode( '-', $key ); $memo[ $key ] = array( dayslot_minutes( $a ), dayslot_minutes( $b ) ); } } } return $memo; } /** * Keys a booking takes a place in: the slot itself and, for an exact time, the window around it. */ function dayslot_capacity_keys( $slot, $variant_id = '' ) { $keys = array( $slot ); $variant = $variant_id ? dayslot_variant( $variant_id ) : null; if ( $variant && 'night' === $variant['type'] ) { // Every night time takes a place in the night (shared limit, can be blocked as a whole). $keys[] = dayslot_night_key( $variant ); return array_values( array_unique( $keys ) ); } if ( dayslot_valid_time( $slot ) ) { $t = dayslot_minutes( $slot ); foreach ( dayslot_interval_windows() as $key => $range ) { if ( $t >= $range[0] && $t < $range[1] ) { $keys[] = $key; break; } } } elseif ( preg_match( '/^(\d{2}:\d{2})-(\d{2}:\d{2})$/', $slot, $m ) && ! isset( dayslot_interval_windows()[ $slot ] ) && dayslot_minutes( $m[2] ) > dayslot_minutes( $m[1] ) ) { // A custom window (e.g. a calendar entry 10:30-11:30) takes a place in the window of its start time. $t = dayslot_minutes( $m[1] ); foreach ( dayslot_interval_windows() as $key => $range ) { if ( $t >= $range[0] && $t < $range[1] ) { $keys[] = $key; break; } } } return $keys; } /** * Blocked slots from settings: array( 'Y-m-d' or 'sat' => array( slot keys ) ). */ function dayslot_blocked_slots() { $out = array(); foreach ( preg_split( '/\r\n|\r|\n/', (string) dayslot_get_settings()['blocked_slots'] ) as $line ) { $line = trim( preg_replace( '/\s+/', ' ', $line ) ); if ( preg_match( '/^(\S+) (\d{2}:\d{2}(?:\s*-\s*\d{2}:\d{2})?)$/', $line, $m ) ) { $out[ strtolower( $m[1] ) ][] = str_replace( ' ', '', $m[2] ); } } return $out; } /** * Is a slot blocked on a date? A blocked window also blocks the exact times inside it. */ function dayslot_slot_is_blocked( $date, $slot, $blocked = null, $variant_id = '' ) { $dt = dayslot_parse_date( $date ); $blocked = null === $blocked ? dayslot_blocked_slots() : $blocked; $list = array_merge( isset( $blocked[ $date ] ) ? $blocked[ $date ] : array(), $dt && isset( $blocked[ strtolower( $dt->format( 'D' ) ) ] ) ? $blocked[ strtolower( $dt->format( 'D' ) ) ] : array() ); if ( ! $list ) { return false; } foreach ( dayslot_capacity_keys( $slot, $variant_id ) as $key ) { if ( in_array( $key, $list, true ) ) { return true; } } return false; } /** * Start of a slot on a date as DateTimeImmutable (site time zone). */ function dayslot_slot_start( $date, $slot ) { $from = substr( (string) $slot, 0, 5 ); return dayslot_valid_time( $from ) ? DateTimeImmutable::createFromFormat( 'Y-m-d H:i|', $date . ' ' . $from, wp_timezone() ) : null; } /** * The earliest start a customer can still choose: this minute + "Preparation time" * (at 19:00 with 120 minutes, 21:00 can still be chosen). */ function dayslot_notice_cutoff() { $now = current_datetime(); $now = $now->setTime( (int) $now->format( 'G' ), (int) $now->format( 'i' ) ); return $now->modify( '+' . max( 0, (int) dayslot_get_settings()['slot_prep_minutes'] ) . ' minutes' ); } /** * Start and end time of a slot: array( 'HH:MM', 'HH:MM' ) or null. Exact times last one hour. */ function dayslot_slot_range( $slot ) { if ( preg_match( '/^(\d{2}:\d{2})-(\d{2}:\d{2})$/', (string) $slot, $m ) ) { return array( $m[1], $m[2] ); } if ( dayslot_valid_time( $slot ) ) { return array( $slot, dayslot_time_from_minutes( min( 1439, dayslot_minutes( $slot ) + 60 ) ) ); } return null; } /** * The hours of a kind of delivery for its card, e.g. "08:00 – 20:00": windows from the first start to the last end. */ function dayslot_variant_window( $variant ) { if ( 'interval' !== $variant['type'] ) { return dayslot_format_slot( $variant['from'] . '-' . $variant['to'] ); } $slots = dayslot_variant_slots( $variant ); $first = $slots ? dayslot_slot_range( reset( $slots ) ) : null; $last = $slots ? dayslot_slot_range( end( $slots ) ) : null; return $first && $last ? dayslot_format_slot( $first[0] . '-' . $last[1] ) : ''; } /** * Time options for a date, grouped by variant, with price and availability. * * @return array[] Each group: array( 'variant', 'type', 'label', 'price', 'options' => array( array( 'key', 'label', 'available', 'left' ) ) ). */ function dayslot_time_options( $date, $ctx = array() ) { $ctx = dayslot_ctx( $ctx ); if ( ! dayslot_parse_date( $date ) ) { return array(); } $groups = array(); if ( 'delivery' === $ctx['mode'] ) { $occ = dayslot_occupancy( $date, $ctx ); $used = $occ['slots']; $cutoff = dayslot_notice_cutoff(); $blocked = dayslot_blocked_slots(); $windows = dayslot_variants(); $limits = array(); foreach ( $windows as $v ) { if ( 'interval' === $v['type'] ) { foreach ( dayslot_variant_slots( $v ) as $key ) { $limits[ $key ] = (int) $v['limit']; } } } foreach ( dayslot_variants() as $v ) { $options = array(); foreach ( dayslot_variant_slots( $v ) as $key ) { $start = dayslot_slot_start( $date, $key ); if ( $start && dayslot_night_next_day( $v, $key ) ) { $start = $start->modify( '+1 day' ); } if ( ! $start || $start < $cutoff || dayslot_slot_is_blocked( $date, $key, $blocked, $v['id'] ) ) { continue; } $left = null; if ( 'night' === $v['type'] ) { if ( (int) $v['limit'] > 0 ) { $night = dayslot_night_key( $v ); $left = max( 0, (int) $v['limit'] - ( isset( $used[ $night ] ) ? $used[ $night ] : 0 ) ); } } elseif ( 'exact' === $v['type'] ) { foreach ( dayslot_capacity_keys( $key, $v['id'] ) as $cap ) { $limit = $cap === $key ? (int) $v['limit'] : ( isset( $limits[ $cap ] ) ? $limits[ $cap ] : 0 ); if ( $limit > 0 ) { $room = max( 0, $limit - ( isset( $used[ $cap ] ) ? $used[ $cap ] : 0 ) ); $left = null === $left ? $room : min( $left, $room ); } } } elseif ( (int) $v['limit'] > 0 ) { $left = max( 0, (int) $v['limit'] - ( isset( $used[ $key ] ) ? $used[ $key ] : 0 ) ); } $options[] = array( 'key' => $key, /* translators: %s: time after midnight, e.g. 01:30 */ 'label' => dayslot_night_next_day( $v, $key ) ? sprintf( __( '%s (after midnight)', 'dayslot' ), $key ) : dayslot_format_slot( $key ), 'available' => null === $left || $left > 0, 'left' => $left, ); } if ( ! $options ) { continue; } $price = dayslot_variant_price( $v, $date ); $groups[] = array( 'variant' => $v['id'], 'type' => $v['type'], 'label' => $v['label'], 'window' => dayslot_variant_window( $v ), 'note' => isset( $v['note'] ) ? (string) $v['note'] : '', 'price' => $price, 'priceText' => dayslot_money( $price ), 'options' => $options, ); } } return apply_filters( 'dayslot_time_options', $groups, $date, $ctx ); } /** * Find a chosen option: array( group, option ) or null. */ function dayslot_find_option( $date, $slot, $variant, $ctx ) { foreach ( dayslot_time_options( $date, $ctx ) as $group ) { if ( $variant && $group['variant'] !== $variant ) { continue; } foreach ( $group['options'] as $option ) { if ( $option['key'] === $slot ) { return array( $group, $option ); } } } return null; } /** * Price of the delivery for a choice, split into parts: array( array( 'label', 'amount', 'type' ) ). * The Pro add-on adds the distance surcharge. */ function dayslot_price_parts( $choice, $package = array() ) { $parts = array(); $times = dayslot_feature_on( 'times' ); if ( 'delivery' === $choice['mode'] && dayslot_parse_date( $choice['date'] ) && ( $choice['slot'] || ! $times ) ) { // Without the time step the first option of the price table sets the price. $all = dayslot_variants(); // A kind that isn't in the price table (Pro "As soon as possible") prices itself. $own = $choice['variant'] && ! dayslot_variant( $choice['variant'] ); $variant = $times ? ( $own ? null : dayslot_variant_for_slot( $choice['slot'], $choice['variant'] ) ) : reset( $all ); if ( $variant ) { $parts[] = array( 'type' => 'base', 'label' => $variant['label'] . ', ' . dayslot_day_type_label( dayslot_day_type( $choice['date'] ) ), 'amount' => dayslot_variant_price( $variant, $choice['date'] ), ); } } return apply_filters( 'dayslot_price_parts', $parts, $choice, $package ); }