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(
'',
$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();
}
);