format( 't' ); $out = array(); for ( $d = 0; $d < $days; $d++ ) { $date = $first->modify( "+{$d} days" )->format( 'Y-m-d' ); if ( ! empty( self::slots( $service_id, $employee_id, $date ) ) ) { $out[] = $date; } } return $out; } /** * Free start times for a local date. * @return array [ ['time' => 'HH:MM', 'employees' => [ids]] ] */ public static function slots( $service_id, $employee_id, $date, array $opts = array() ) { $service = Repository::service( $service_id ); if ( ! $service || 'active' !== $service['status'] ) { return array(); } $settings = Settings::all(); $tz = wp_timezone(); try { $day = new \DateTimeImmutable( $date . ' 00:00:00', $tz ); } catch ( \Exception $e ) { return array(); } if ( $day->format( 'Y-m-d' ) !== $date ) { return array(); } $now = time(); $earliest = $now + (int) $settings['min_notice'] * 60; $latest_day = ( new \DateTimeImmutable( 'today', $tz ) )->modify( '+' . (int) $settings['max_advance_days'] . ' days' ); if ( empty( $opts['ignore_limits'] ) && ( $day > $latest_day || $day->getTimestamp() + DAY_IN_SECONDS < $now ) ) { return array(); } $employees = $employee_id ? array( (int) $employee_id ) : Repository::employee_ids_for_service( $service_id ); $duration = (int) $service['duration'] * 60; $before = (int) $service['buffer_before'] * 60; $after = (int) $service['buffer_after'] * 60; $step = max( 5, (int) $settings['time_slot_step'] ) * 60; $dow = (int) $day->format( 'N' ); $result = array(); foreach ( $employees as $eid ) { if ( ! Repository::employee_offers( $eid, $service_id ) || Repository::is_day_off( $eid, $date ) ) { continue; } $windows = Repository::working_windows( $eid, $dow ); if ( empty( $windows ) ) { continue; } $busy = self::busy_intervals( $eid, $day ); foreach ( $windows as $w ) { $w_start = ( new \DateTimeImmutable( $date . ' ' . $w['start_time'], $tz ) )->getTimestamp(); $w_end = ( new \DateTimeImmutable( $date . ' ' . $w['end_time'], $tz ) )->getTimestamp(); for ( $s = $w_start; $s + $duration <= $w_end; $s += $step ) { if ( empty( $opts['ignore_limits'] ) && $s < $earliest ) { continue; } $e = $s + $duration; if ( self::fits( $s - $before, $e + $after, $busy ) ) { $result[ wp_date( 'H:i', $s, $tz ) ][] = $eid; } } } } ksort( $result ); $out = array(); foreach ( $result as $time => $ids ) { $out[] = array( 'time' => $time, 'employees' => array_values( array_unique( $ids ) ) ); } return apply_filters( 'bookzenix_available_slots', $out, $service_id, $employee_id, $date ); } /** True when [from, to) overlaps none of the busy intervals. */ private static function fits( $from, $to, array $busy ) { foreach ( $busy as $b ) { if ( $from < $b['to'] && $to > $b['from'] ) { return false; } } return true; } /** Busy intervals (incl. buffers) for an employee around a local day. */ private static function busy_intervals( $employee_id, \DateTimeImmutable $day ) { global $wpdb; $from = gmdate( 'Y-m-d H:i:s', $day->getTimestamp() - DAY_IN_SECONDS ); $to = gmdate( 'Y-m-d H:i:s', $day->getTimestamp() + 2 * DAY_IN_SECONDS ); $a = Plugin::table( 'appointments' ); $s = Plugin::table( 'services' ); $status_in = "'" . implode( "','", self::BLOCKING ) . "'"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $rows = $wpdb->get_results( $wpdb->prepare( "SELECT a.start_utc, a.end_utc, s.buffer_before, s.buffer_after FROM {$a} a LEFT JOIN {$s} s ON s.id = a.service_id WHERE a.employee_id = %d AND a.status IN ({$status_in}) AND a.start_utc < %s AND a.end_utc > %s", $employee_id, $to, $from ), ARRAY_A ); $busy = array(); foreach ( (array) $rows as $r ) { $st = strtotime( $r['start_utc'] . ' UTC' ); $en = strtotime( $r['end_utc'] . ' UTC' ); $busy[] = array( 'from' => $st - (int) $r['buffer_before'] * 60, 'to' => $en + (int) $r['buffer_after'] * 60 ); } return apply_filters( 'bookzenix_busy_intervals', $busy, $employee_id, $day ); } /** Final server-side check right before writing. @return int|false employee id */ public static function reserve_employee( $service_id, $employee_id, $date, $time, array $opts = array() ) { foreach ( self::slots( $service_id, $employee_id, $date, $opts ) as $slot ) { if ( $slot['time'] === $time ) { return (int) $slot['employees'][0]; } } return false; } }