format( 'Y-m-d' ); $parts = array(); $warn = false; foreach ( dayslot_modes() as $mode => $label ) { $reason = dayslot_day_closed_reason( $today, array( 'mode' => $mode ) ); $name = '' . esc_html( $label ) . ''; if ( '' === $reason ) { /* translators: %s: delivery way, e.g. Delivery */ $parts[] = sprintf( __( '%s: open', 'dayslot' ), $name ); } else { $warn = true; /* translators: 1: delivery way, e.g. Delivery, 2: why today is closed */ $parts[] = sprintf( __( '%1$s: closed. %2$s', 'dayslot' ), $name, esc_html( $reason ) ); } } if ( in_array( wp_timezone_string(), array( 'UTC', '+00:00' ), true ) ) { $warn = true; $parts[] = esc_html__( 'The site time zone is UTC: if your shop is elsewhere, set your city in Settings → General → Timezone.', 'dayslot' ); } printf( '

%2$s %3$s

', $warn ? 'notice-warning' : 'notice-info', /* translators: %s: current time, e.g. 19:17 */ esc_html( sprintf( __( 'Today at checkout (now %s):', 'dayslot' ), current_datetime()->format( 'H:i' ) ) ), wp_kses( implode( ' · ', $parts ), array( 'strong' => array() ) ) ); } add_action( 'admin_enqueue_scripts', function ( $hook ) { if ( 'toplevel_page_dayslot' !== $hook ) { return; } $s = dayslot_get_settings(); $variants = array(); foreach ( dayslot_variants( true ) as $v ) { $variants[] = array( 'id' => $v['id'], 'type' => $v['type'], 'label' => $v['label'], 'slots' => dayslot_variant_slots( $v ), ); } $statuses = array(); foreach ( wc_get_order_statuses() as $key => $name ) { $statuses[ substr( $key, 3 ) ] = $name; } wp_enqueue_style( 'dys-admin-calendar', dayslot_asset( 'assets/admin-calendar.css' ), array(), DAYSLOT_VERSION ); wp_enqueue_script( 'dys-admin-calendar', dayslot_asset( 'assets/admin-calendar.js' ), array(), DAYSLOT_VERSION, true ); wp_add_inline_script( 'dys-admin-calendar', 'window.dayslotCal = ' . wp_json_encode( array( 'ajax' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'dayslot_calendar' ), 'today' => dayslot_today()->format( 'Y-m-d' ), 'firstDay' => (int) get_option( 'start_of_week', 1 ), 'months' => dayslot_calendar_words()['months'], 'dateMonths' => dayslot_calendar_words()['dateMonths'], 'days' => dayslot_calendar_words()['days'], 'daysShort' => dayslot_calendar_words()['daysShort'], 'variants' => $variants, 'modes' => dayslot_modes(), 'statuses' => $statuses, 'notify' => 'yes' === $s['notify_customer'], 'canBlock' => current_user_can( 'manage_woocommerce' ), 'dayLimit' => (int) $s['max_per_day'], 'hours' => dayslot_calendar_hours(), 'pro' => dayslot_is_pro(), 'i18n' => array( 'month' => __( 'Month', 'dayslot' ), 'week' => __( 'Week', 'dayslot' ), 'day' => __( 'Day', 'dayslot' ), 'today' => __( 'Today', 'dayslot' ), 'prev' => __( 'Previous', 'dayslot' ), 'next' => __( 'Next', 'dayslot' ), 'openOrder' => __( 'Open order', 'dayslot' ), 'close' => __( 'Close', 'dayslot' ), 'change' => __( 'Change date / time', 'dayslot' ), 'save' => __( 'Save', 'dayslot' ), 'cancel' => __( 'Cancel', 'dayslot' ), 'notifyQ' => __( 'Notify the customer by email?', 'dayslot' ), 'saveNotify' => __( 'Save and email the customer', 'dayslot' ), 'saveQuiet' => __( 'Save without email', 'dayslot' ), /* translators: 1: order number, 2: new date and time */ 'moveTo' => __( 'Move %1$s to %2$s?', 'dayslot' ), 'blockDay' => __( 'Block day', 'dayslot' ), 'unblockDay' => __( 'Unblock day', 'dayslot' ), 'block' => __( 'Block', 'dayslot' ), 'unblock' => __( 'Unblock', 'dayslot' ), 'blocked' => __( 'Blocked', 'dayslot' ), /* translators: %d: number of blocked time windows */ 'blockedTimes' => __( '%d times blocked', 'dayslot' ), 'unblockTimes' => __( 'Unblock the times', 'dayslot' ), 'closed' => __( 'Closed', 'dayslot' ), 'full' => __( 'Full', 'dayslot' ), 'delivery' => __( 'Delivery', 'dayslot' ), 'pickup' => __( 'Pickup', 'dayslot' ), 'customer' => __( 'Customer', 'dayslot' ), 'phone' => __( 'Phone', 'dayslot' ), 'address' => __( 'Address', 'dayslot' ), 'items' => __( 'Products', 'dayslot' ), 'time' => __( 'Time', 'dayslot' ), 'date' => __( 'Date', 'dayslot' ), 'shipping' => __( 'Delivery price', 'dayslot' ), 'total' => __( 'Order total', 'dayslot' ), 'status' => __( 'Status', 'dayslot' ), 'note' => __( 'Note', 'dayslot' ), 'noEvents' => __( 'Nothing planned.', 'dayslot' ), 'error' => __( 'Something went wrong. Please reload the page.', 'dayslot' ), 'windows' => __( 'Time windows', 'dayslot' ), 'orders' => __( 'orders', 'dayslot' ), 'allDay' => __( 'No time', 'dayslot' ), 'legendOrder' => __( 'Delivery', 'dayslot' ), 'legendPick' => __( 'Pickup', 'dayslot' ), 'legendEntry' => __( 'Own entry', 'dayslot' ), 'legendExt' => __( 'External calendar', 'dayslot' ), 'dragHint' => __( 'Tip: drag an order to another day or time.', 'dayslot' ), 'filterMode' => __( 'Delivery or pickup', 'dayslot' ), 'allModes' => __( 'Delivery and pickup', 'dayslot' ), 'allStatuses' => __( 'All statuses', 'dayslot' ), 'allProducts' => __( 'All products', 'dayslot' ), ), ) ) . ';', 'before' ); do_action( 'dayslot_calendar_enqueue' ); } ); /** * Hours shown in week/day views: from the earliest option to the latest (at least 8–22). */ function dayslot_calendar_hours() { $from = 8; $to = 22; foreach ( dayslot_variants() as $v ) { $a = intdiv( dayslot_minutes( $v['from'] ), 60 ); $b = (int) ceil( dayslot_minutes( $v['to'] ) / 60 ); if ( 'night' === $v['type'] ) { $b = 24; } $from = min( $from, $a ); $to = max( $to, $b ); } return array( $from, min( 24, $to ) ); } /** * Check the AJAX request. */ function dayslot_calendar_check( $cap = 'edit_shop_orders' ) { if ( ! current_user_can( $cap ) || ! check_ajax_referer( 'dayslot_calendar', 'nonce', false ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'dayslot' ) ), 403 ); } } /** * One order as a calendar event. */ function dayslot_order_event( $order ) { $c = dayslot_order_choice( $order ); $range = dayslot_slot_range( $c['slot'] ); $night = 'delivery' === $c['mode'] ? dayslot_variant_for_slot( $c['slot'], $c['variant'] ) : null; if ( dayslot_night_next_day( $night, $c['slot'] ) ) { // After midnight: shown with the evening it belongs to, in the "no time" row, marked as night. $range = null; } $address = $order->has_shipping_address() ? array( $order->get_shipping_address_1(), $order->get_shipping_address_2(), trim( $order->get_shipping_postcode() . ' ' . $order->get_shipping_city() ) ) : array( $order->get_billing_address_1(), $order->get_billing_address_2(), trim( $order->get_billing_postcode() . ' ' . $order->get_billing_city() ) ); $items = array(); $products = array(); foreach ( $order->get_items() as $item ) { $items[] = $item->get_quantity() . '× ' . $item->get_name(); $products[] = $item->get_name(); } $details = array(); foreach ( dayslot_order_summary_rows( $order ) as $key => $row ) { if ( ! in_array( $key, array( 'dayslot_event_date', 'dayslot_time_slot', 'dayslot_mode' ), true ) ) { $details[] = array( $row['label'], $row['value'] ); } } $modes = dayslot_modes(); return apply_filters( 'dayslot_order_event', array( 'kind' => 'order', 'id' => $order->get_id(), 'number' => '#' . $order->get_order_number(), 'title' => ( $night && 'night' === $night['type'] ? '☾ ' . ( $range ? '' : $c['slot'] . ' ' ) : '' ) . trim( $order->get_formatted_billing_full_name() ), 'date' => $c['date'], 'slot' => $c['slot'], 'variant' => $c['variant'], 'start' => $range ? $range[0] : '', 'end' => $range ? $range[1] : '', 'mode' => $c['mode'], 'modeText' => isset( $modes[ $c['mode'] ] ) ? $modes[ $c['mode'] ] : $c['mode'], 'status' => $order->get_status(), 'phone' => $order->get_billing_phone(), 'email' => $order->get_billing_email(), 'address' => 'pickup' === $c['mode'] ? '' : implode( ', ', array_filter( $address ) ), 'items' => $items, 'products' => array_values( array_unique( $products ) ), 'shipping' => html_entity_decode( wp_strip_all_tags( wc_price( (float) $order->get_shipping_total(), array( 'currency' => $order->get_currency() ) ) ), ENT_QUOTES, 'UTF-8' ), 'total' => html_entity_decode( wp_strip_all_tags( wc_price( (float) $order->get_total(), array( 'currency' => $order->get_currency() ) ) ), ENT_QUOTES, 'UTF-8' ), 'details' => $details, 'note' => $order->get_customer_note(), 'editUrl' => $order->get_edit_order_url(), 'movable' => true, ), $order ); } /** * Events + day info between two dates. */ add_action( 'wp_ajax_dayslot_cal_events', function () { dayslot_calendar_check(); $start = isset( $_GET['start'] ) ? sanitize_text_field( wp_unslash( $_GET['start'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- checked in dayslot_calendar_check(). $end = isset( $_GET['end'] ) ? sanitize_text_field( wp_unslash( $_GET['end'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! dayslot_parse_date( $start ) || ! dayslot_parse_date( $end ) || $end < $start ) { wp_send_json_error( array( 'message' => 'Bad range' ), 400 ); } if ( dayslot_parse_date( $start )->diff( dayslot_parse_date( $end ) )->days > 62 ) { $end = dayslot_parse_date( $start )->modify( '+62 days' )->format( 'Y-m-d' ); } $statuses = array_unique( array_merge( (array) dayslot_get_settings()['statuses'], array( 'pending', 'on-hold', 'processing', 'completed' ) ) ); $events = array(); foreach ( dayslot_booking_rows( $start, $end, $statuses ) as $row ) { if ( 'order' !== $row['kind'] ) { continue; } $order = wc_get_order( $row['id'] ); if ( $order ) { $events[] = dayslot_order_event( $order ); } } $events = apply_filters( 'dayslot_calendar_events', $events, $start, $end ); $s = dayslot_get_settings(); $blocked = dayslot_blocked_dates(); $bslots = dayslot_blocked_slots(); $closed = array_map( 'intval', (array) $s['closed_weekdays'] ); $occ = dayslot_aggregate_rows( dayslot_booking_rows( $start, $end ) ); $special = (array) apply_filters( 'dayslot_special_days', array() ); $days = array(); $last = dayslot_parse_date( $end ); for ( $d = dayslot_parse_date( $start ), $i = 0; $d <= $last && $i < 70; $d = $d->modify( '+1 day' ), $i++ ) { $key = $d->format( 'Y-m-d' ); $days[ $key ] = array( 'count' => isset( $occ[ $key ]['delivery'] ) ? $occ[ $key ]['delivery']['total'] : 0, 'slots' => isset( $occ[ $key ]['delivery'] ) ? $occ[ $key ]['delivery']['slots'] : (object) array(), 'blocked' => in_array( $key, $blocked, true ), 'closed' => in_array( (int) $d->format( 'w' ), $closed, true ), 'blockedSlots' => isset( $bslots[ $key ] ) ? $bslots[ $key ] : array(), 'limit' => dayslot_day_limit( $key ), 'special' => isset( $special[ $key ] ) ? $special[ $key ] : '', 'dayType' => dayslot_day_type( $key ), ); } // Days customers can't choose at checkout although the shop didn't close them: say why under the day. $today = dayslot_today()->format( 'Y-m-d' ); foreach ( dayslot_modes() as $mode => $label ) { $off = array_flip( dayslot_unavailable_dates( array( 'mode' => $mode, 'check_all' => true, ) ) ); foreach ( $days as $key => $info ) { if ( $key >= $today && isset( $off[ $key ] ) && ! $info['blocked'] && ! $info['closed'] ) { $why = dayslot_day_closed_reason( $key, array( 'mode' => $mode ) ); if ( '' !== $why ) { $days[ $key ]['why'][] = ( 'pickup' === $mode ? '🏠 ' : '🚚 ' ) . $why; } } } } $days = apply_filters( 'dayslot_calendar_days', $days, $start, $end ); $windows = array(); foreach ( dayslot_variants() as $v ) { if ( 'night' === $v['type'] ) { // One row for the whole night: its limit is shared by every time in it. $windows[] = array( 'slot' => dayslot_night_key( $v ), 'label' => $v['label'] . ' ' . dayslot_format_slot( dayslot_night_key( $v ) ), 'limit' => (int) $v['limit'], ); } elseif ( 'interval' === $v['type'] ) { foreach ( dayslot_variant_slots( $v ) as $slot ) { $windows[] = array( 'slot' => $slot, 'limit' => (int) $v['limit'], ); } } } wp_send_json_success( array( 'events' => array_values( $events ), 'days' => $days, 'windows' => $windows, ) ); } ); /** * Move an order (drag & drop or card). */ add_action( 'wp_ajax_dayslot_cal_move', function () { dayslot_calendar_check(); // phpcs:disable WordPress.Security.NonceVerification.Missing -- checked in dayslot_calendar_check(). $id = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0; $order = $id ? wc_get_order( $id ) : null; if ( ! $order ) { wp_send_json_error( array( 'message' => __( 'Order not found.', 'dayslot' ) ), 404 ); } $choice = dayslot_order_choice( $order ); $choice['date'] = isset( $_POST['date'] ) ? sanitize_text_field( wp_unslash( $_POST['date'] ) ) : $choice['date']; $choice['slot'] = isset( $_POST['slot'] ) ? sanitize_text_field( wp_unslash( $_POST['slot'] ) ) : $choice['slot']; $choice['variant'] = isset( $_POST['variant'] ) ? sanitize_key( wp_unslash( $_POST['variant'] ) ) : $choice['variant']; $notify = ! empty( $_POST['notify'] ); // phpcs:enable if ( ! dayslot_parse_date( $choice['date'] ) || ( $choice['slot'] && ! dayslot_valid_slot_key( $choice['slot'] ) ) ) { wp_send_json_error( array( 'message' => __( 'Please choose a valid date and time.', 'dayslot' ) ), 400 ); } dayslot_admin_change_order( $order, $choice, $notify ); wp_send_json_success( array( 'event' => dayslot_order_event( wc_get_order( $id ) ) ) ); } ); /** * Block or unblock a day ($slot empty) or one time window; $only_slots = unblock only the day's time windows. * Unblocking a day also unblocks the time windows blocked on that date. */ function dayslot_toggle_block( $date, $slot = '', $only_slots = false ) { $only = $only_slots; $s = dayslot_get_settings(); // Time windows blocked on this very date (not the weekly ones like "sat 10:00-12:00"). $without_date = function () use ( $s, $date ) { $lines = array_filter( array_map( 'trim', preg_split( '/\r\n|\r|\n/', (string) $s['blocked_slots'] ) ) ); return implode( "\n", array_filter( $lines, function ( $line ) use ( $date ) { return 0 !== strpos( $line, $date . ' ' ); } ) ); }; if ( $only ) { dayslot_update_settings( array( 'blocked_slots' => $without_date() ) ); } elseif ( $slot ) { $lines = array_filter( array_map( 'trim', preg_split( '/\r\n|\r|\n/', (string) $s['blocked_slots'] ) ) ); $line = $date . ' ' . $slot; $lines = in_array( $line, $lines, true ) ? array_diff( $lines, array( $line ) ) : array_merge( $lines, array( $line ) ); dayslot_update_settings( array( 'blocked_slots' => implode( "\n", $lines ) ) ); } else { $dates = dayslot_blocked_dates(); $unblock = in_array( $date, $dates, true ); $dates = $unblock ? array_diff( $dates, array( $date ) ) : array_merge( $dates, array( $date ) ); sort( $dates ); $changes = array( 'blocked_dates' => implode( "\n", $dates ) ); if ( $unblock ) { // "Unblock day" frees the whole day, its blocked time windows too. $changes['blocked_slots'] = $without_date(); } dayslot_update_settings( $changes ); } } /** * Block / unblock a day or a time window. */ add_action( 'wp_ajax_dayslot_cal_block', function () { dayslot_calendar_check( 'manage_woocommerce' ); // phpcs:disable WordPress.Security.NonceVerification.Missing -- checked in dayslot_calendar_check(). $date = isset( $_POST['date'] ) ? sanitize_text_field( wp_unslash( $_POST['date'] ) ) : ''; $slot = isset( $_POST['slot'] ) ? sanitize_text_field( wp_unslash( $_POST['slot'] ) ) : ''; $only = ! empty( $_POST['clear_slots'] ); // Only unblock the day's time windows. // phpcs:enable if ( ! dayslot_parse_date( $date ) || ( $slot && ! dayslot_valid_slot_key( $slot ) ) ) { wp_send_json_error( array( 'message' => 'Bad date' ), 400 ); } dayslot_toggle_block( $date, $slot, $only ); wp_send_json_success(); } );