'array', 'sanitize_callback' => 'dayslot_sanitize_settings', ) ); } ); // Look of all Dayslot admin pages (settings, setup wizard, help, Pro badges). add_action( 'admin_enqueue_scripts', function ( $hook ) { if ( false !== strpos( (string) $hook, 'dayslot' ) ) { wp_enqueue_style( 'dys-admin', dayslot_asset( 'assets/admin.css' ), array(), DAYSLOT_VERSION ); } } ); add_action( 'admin_enqueue_scripts', function ( $hook ) { if ( false === strpos( (string) $hook, 'dayslot-settings' ) ) { return; } wp_enqueue_script( 'dys-admin-settings', dayslot_asset( 'assets/admin-settings.js' ), array( 'jquery', 'jquery-ui-sortable' ), DAYSLOT_VERSION, true ); wp_add_inline_script( 'dys-admin-settings', 'window.dayslotSettings = ' . wp_json_encode( array( 'presets' => dayslot_style_presets(), 'defaults' => dayslot_style_defaults(), 'i18n' => array( 'confirmClear' => __( 'Remove all rows?', 'dayslot' ), 'copied' => __( 'Copied', 'dayslot' ), ), ) ) . ';', 'before' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only picks which tab assets to load. $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general'; if ( 'appearance' === $tab ) { wp_enqueue_style( 'dys-picker', dayslot_asset( 'assets/picker.css' ), array(), DAYSLOT_VERSION ); wp_enqueue_script( 'dys-picker', dayslot_asset( 'assets/picker.js' ), array(), DAYSLOT_VERSION, true ); wp_add_inline_script( 'dys-picker', 'window.dayslotData = ' . wp_json_encode( dayslot_preview_data() ) . ';', 'before' ); } do_action( 'dayslot_settings_enqueue', $tab ); } ); /** * Picker data for the live preview (fake times, nothing booked). */ function dayslot_preview_data() { $data = dayslot_picker_data(); if ( count( $data['modes'] ) < 2 ) { $data['modes'][] = array( 'id' => 'pickup', 'label' => __( 'Pickup', 'dayslot' ), 'note' => __( 'free', 'dayslot' ), ); } $data['autoSelect'] = false; $date = dayslot_today()->modify( '+2 days' )->format( 'Y-m-d' ); $groups = array(); foreach ( dayslot_variants() as $v ) { $opts = array(); foreach ( array_slice( dayslot_variant_slots( $v ), 0, 'exact' === $v['type'] ? 40 : 6 ) as $i => $key ) { $opts[] = array( 'key' => $key, 'label' => dayslot_format_slot( $key ), 'available' => 1 !== $i, 'left' => 2 === $i ? 1 : null, ); } $price = dayslot_variant_price( $v, $date ); $groups[] = array( 'variant' => $v['id'], 'type' => $v['type'], 'label' => $v['label'], 'window' => dayslot_variant_window( $v ), 'price' => $price, 'priceText' => dayslot_money( $price ), 'options' => $opts, ); } $data['demo'] = $groups; $data['previewDate'] = $date; return $data; } /** * Which settings keys live on which tab. */ function dayslot_settings_tabs() { $tabs = array( 'general' => array( 'title' => __( 'General', 'dayslot' ), 'keys' => array( 'max_per_day', 'min_days', 'max_days', 'closed_weekdays', 'blocked_dates', 'holidays', 'holiday_country', 'statuses', 'notify_customer' ), ), 'delivery' => array( 'title' => __( 'Delivery options & prices', 'dayslot' ), 'keys' => array( 'variants', 'slot_prep_minutes', 'blocked_slots', 'shipping_title', 'hide_other_shipping', 'free_above', 'free_always', 'small_below', 'small_fee' ), ), 'checkout' => array( 'title' => __( 'Checkout', 'dayslot' ), 'keys' => array( 'placement', 'required', 'auto_select', 'hide_virtual', 'method_label', 'label', 'time_label', 'delivery_text', 'required_message', 'unavailable_message', 'slot_required_message', 'slot_full_message', 'full_times', 'full_contact', 'cart_note', 'product_note', 'earliest_text', 'cart_date_text', 'add_to_calendar', 'app_fields' ), ), 'appearance' => array( 'title' => __( 'Appearance', 'dayslot' ), 'keys' => array( 'style' ), ), 'calendars' => array( 'title' => __( 'Calendar sync', 'dayslot' ), 'keys' => array(), ), ); // Add-ons add tabs with a title, keys, a render callback( $settings ) and a sanitize callback( $input, $out ). return apply_filters( 'dayslot_settings_tabs', $tabs ); } /** * The settings sections and their tabs (only tabs that exist; tabs of add-ons not listed go to "More"). */ function dayslot_settings_groups( $tabs ) { $groups = apply_filters( 'dayslot_settings_groups', array( 'delivery' => array( 'title' => __( 'Delivery', 'dayslot' ), 'tabs' => array( 'general', 'delivery', 'pickup', 'special', 'express', 'zones', 'rules' ), ), 'checkout' => array( 'title' => __( 'Checkout', 'dayslot' ), 'tabs' => array( 'checkout', 'appearance', 'fields', 'checkout_design' ), ), 'customers' => array( 'title' => __( 'Customers', 'dayslot' ), 'tabs' => array( 'customers', 'after', 'emails', 'account_design', 'widget', 'quote' ), ), 'calendar' => array( 'title' => __( 'Calendar', 'dayslot' ), 'tabs' => array( 'calendars' ), ), 'more' => array( 'title' => __( 'More', 'dayslot' ), 'tabs' => array( 'features', 'texts' ), ), ) ); $listed = array(); foreach ( $groups as $gid => $group ) { $groups[ $gid ]['tabs'] = array_values( array_intersect( $group['tabs'], array_keys( $tabs ) ) ); $listed = array_merge( $listed, $groups[ $gid ]['tabs'] ); } $groups['more']['tabs'] = array_merge( $groups['more']['tabs'], array_values( array_diff( array_keys( $tabs ), $listed ) ) ); return array_filter( $groups, function ( $g ) { return ! empty( $g['tabs'] ); } ); } /** * Clean what comes from the form. Only the submitted tab's keys change; the others are kept. * Called without "_tab" (e.g. update_option from code) it keeps the values as they are. */ function dayslot_sanitize_settings( $input ) { $input = is_array( $input ) ? $input : array(); $existing = get_option( 'dayslot_settings', array() ); $out = is_array( $existing ) ? $existing : array(); $tabs = dayslot_settings_tabs(); if ( ! isset( $input['_tab'] ) || ! isset( $tabs[ $input['_tab'] ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- only checks where the save comes from; options.php checks the nonce. if ( isset( $_POST['option_page'] ) ) { return $out; // A settings form always names its tab: anything else is not saved. } // Saved by the plugin's own code (calendar blocking, the setup wizard, tests): known settings only. unset( $input['_tab'] ); return array_merge( $out, array_intersect_key( $input, $out + dayslot_defaults() ) ); } $tab = $input['_tab']; if ( ! empty( $tabs[ $tab ]['sanitize'] ) && is_callable( $tabs[ $tab ]['sanitize'] ) ) { $out = call_user_func( $tabs[ $tab ]['sanitize'], $input, $out ); } $keys = isset( $tabs[ $tab ]['keys'] ) ? $tabs[ $tab ]['keys'] : array(); $yes = function ( $key ) use ( $input ) { return ( ! empty( $input[ $key ] ) && 'no' !== $input[ $key ] ) ? 'yes' : 'no'; }; $text = function ( $key ) use ( $input, &$out ) { $value = isset( $input[ $key ] ) ? sanitize_text_field( $input[ $key ] ) : ''; if ( '' === $value ) { unset( $out[ $key ] ); // Fall back to the default text. return null; } return $value; }; foreach ( $keys as $key ) { switch ( $key ) { case 'max_per_day': case 'min_days': case 'slot_prep_minutes': $out[ $key ] = isset( $input[ $key ] ) ? absint( $input[ $key ] ) : 0; break; case 'max_days': $out[ $key ] = isset( $input[ $key ] ) ? min( 730, max( 1, absint( $input[ $key ] ) ) ) : 365; break; case 'closed_weekdays': $out[ $key ] = array(); foreach ( isset( $input[ $key ] ) ? (array) $input[ $key ] : array() as $day ) { if ( absint( $day ) <= 6 ) { $out[ $key ][] = absint( $day ); } } $out[ $key ] = array_values( array_unique( $out[ $key ] ) ); break; case 'blocked_dates': case 'holidays': $out[ $key ] = implode( "\n", dayslot_parse_date_list( isset( $input[ $key ] ) ? sanitize_textarea_field( $input[ $key ] ) : '' ) ); break; case 'blocked_slots': $out[ $key ] = isset( $input[ $key ] ) ? sanitize_textarea_field( $input[ $key ] ) : ''; break; case 'holiday_country': $out[ $key ] = isset( $input[ $key ] ) && 'PL' === $input[ $key ] ? 'PL' : ( isset( $input[ $key ] ) ? strtoupper( substr( sanitize_key( $input[ $key ] ), 0, 2 ) ) : '' ); break; case 'free_above': case 'small_below': case 'small_fee': $out[ $key ] = isset( $input[ $key ] ) ? max( 0, (float) wc_format_decimal( (string) $input[ $key ] ) ) : 0; break; case 'full_times': $out[ $key ] = isset( $input[ $key ] ) && in_array( $input[ $key ], array( 'show', 'hide', 'contact' ), true ) ? $input[ $key ] : 'contact'; break; case 'full_contact': case 'earliest_text': case 'cart_date_text': $value = $text( $key ); if ( null !== $value ) { $out[ $key ] = $value; } break; case 'placement': $out[ $key ] = isset( $input[ $key ] ) && in_array( $input[ $key ], array( 'before_address', 'after_address', 'before_notes', 'after_notes' ), true ) ? $input[ $key ] : 'after_address'; break; case 'required': case 'free_always': case 'cart_note': case 'product_note': case 'add_to_calendar': case 'app_fields': case 'auto_select': case 'hide_virtual': case 'notify_customer': case 'hide_other_shipping': $out[ $key ] = $yes( $key ); break; case 'method_label': case 'label': case 'time_label': case 'delivery_text': case 'shipping_title': case 'required_message': case 'unavailable_message': case 'slot_required_message': case 'slot_full_message': $value = $text( $key ); if ( null !== $value ) { $out[ $key ] = $value; } break; case 'statuses': $valid = array_map( function ( $s ) { return substr( $s, 3 ); }, array_keys( wc_get_order_statuses() ) ); $out[ $key ] = array(); foreach ( isset( $input[ $key ] ) ? (array) $input[ $key ] : array() as $status ) { $status = sanitize_key( $status ); if ( in_array( $status, $valid, true ) ) { $out[ $key ][] = $status; } } break; case 'variants': $out[ $key ] = dayslot_sanitize_variants( isset( $input[ $key ] ) ? (array) $input[ $key ] : array() ); break; case 'style': $out[ $key ] = dayslot_sanitize_style( isset( $input[ $key ] ) ? $input[ $key ] : array() ); break; } } if ( isset( $out['max_days'], $out['min_days'] ) && $out['max_days'] < $out['min_days'] ) { $out['max_days'] = $out['min_days']; } unset( $out['_tab'] ); return $out; } /** * Clean the price table rows. */ function dayslot_sanitize_variants( $rows ) { $out = array(); $used = array(); $cost = function ( $v ) { return '' === trim( (string) $v ) ? '' : max( 0, (float) wc_format_decimal( (string) $v ) ); }; foreach ( $rows as $row ) { $row = is_array( $row ) ? $row : array(); $type = isset( $row['type'] ) && in_array( $row['type'], array( 'interval', 'exact', 'night' ), true ) ? $row['type'] : ''; $from = isset( $row['from'] ) ? sanitize_text_field( $row['from'] ) : ''; $to = isset( $row['to'] ) ? sanitize_text_field( $row['to'] ) : ''; if ( ! $type || ! dayslot_valid_time( $from ) || ! dayslot_valid_time( $to ) ) { continue; } if ( 'night' !== $type && dayslot_minutes( $to ) <= dayslot_minutes( $from ) ) { continue; } $label = isset( $row['label'] ) ? sanitize_text_field( $row['label'] ) : ''; $id = isset( $row['id'] ) ? sanitize_key( $row['id'] ) : ''; if ( '' === $id ) { $id = sanitize_key( sanitize_title( $label ? $label : $type ) ); } $base = $id ? $id : $type; $n = 2; while ( '' === $id || isset( $used[ $id ] ) ) { $id = $base . '-' . ( $n++ ); } $used[ $id ] = true; $every = isset( $row['every'] ) ? absint( $row['every'] ) : 0; $out[] = array( 'id' => $id, 'type' => $type, 'label' => $label ? $label : ucfirst( $type ), 'note' => isset( $row['note'] ) ? sanitize_text_field( $row['note'] ) : '', 'from' => $from, 'to' => $to, 'length' => 'interval' === $type ? max( 5, $every ? $every : 120 ) : 0, 'step' => 'interval' === $type ? 0 : max( 5, $every ? $every : 15 ), 'limit' => isset( $row['limit'] ) ? absint( $row['limit'] ) : 0, 'price_weekday' => (float) $cost( isset( $row['price_weekday'] ) ? $row['price_weekday'] : 0 ), 'price_sunday' => (float) $cost( isset( $row['price_sunday'] ) ? $row['price_sunday'] : 0 ), 'price_holiday' => $cost( isset( $row['price_holiday'] ) ? $row['price_holiday'] : '' ), 'enabled' => empty( $row['enabled'] ) ? 'no' : 'yes', ); } return $out; } /** * Weekday names keyed 0 (Sunday) … 6, starting with the site's first weekday. */ function dayslot_weekday_names( $short = false ) { $words = dayslot_calendar_words(); $start = (int) get_option( 'start_of_week', 1 ); $out = array(); for ( $i = 0; $i < 7; $i++ ) { $num = ( $start + $i ) % 7; $out[ $num ] = $short ? $words['daysShort'][ $num ] : $words['days'][ $num ]; } return $out; } /** * Settings page. */ function dayslot_render_settings_page() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } $tabs = dayslot_settings_tabs(); $tab = isset( $_GET['tab'] ) && isset( $tabs[ sanitize_key( wp_unslash( $_GET['tab'] ) ) ] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $s = dayslot_get_settings(); ?>

$group ) { if ( in_array( $tab, $group['tabs'], true ) ) { $current = $gid; } } ?> 1 ) : ?>

$row ) : ?>

'', 'type' => 'interval', 'label' => '', 'note' => '', 'from' => '08:00', 'to' => '20:00', 'length' => 120, 'step' => 15, 'limit' => 1, 'price_weekday' => '', 'price_sunday' => '', 'price_holiday' => '', 'enabled' => 'yes', ) ); $name = 'dayslot_settings[variants][' . $i . ']'; $every = 'interval' === $row['type'] ? $row['length'] : ( (int) $row['step'] > 0 ? $row['step'] : 15 ); $cur = get_woocommerce_currency_symbol(); $money = function ( $v ) { return '' === (string) $v ? '' : wc_format_localized_price( $v ); }; $types = array( 'interval' => __( 'Time windows (e.g. 10:00–12:00)', 'dayslot' ), 'exact' => __( 'Exact time', 'dayslot' ), 'night' => __( 'Night — exact time over midnight', 'dayslot' ), ); $uid = 'dysv-' . $i; $field = function ( $label, $html, $hint = '', $css_class = '' ) { echo ''; }; ?>
', '', 'dys-wide' ); $options = ''; foreach ( $types as $value => $label ) { $options .= ''; } $field( __( 'Kind', 'dayslot' ), '', '', 'dys-wide' ); $field( __( 'Text under the name (optional)', 'dayslot' ), '', '', 'dys-full' ); $field( __( 'From', 'dayslot' ), '' ); $field( __( 'To', 'dayslot' ), '', '', 'dys-to' ); echo ''; echo ''; /* translators: %s: currency symbol */ $field( sprintf( __( 'Price Mon–Sat (%s)', 'dayslot' ), $cur ), '' ); /* translators: %s: currency symbol */ $field( sprintf( __( 'Price Sunday (%s)', 'dayslot' ), $cur ), '' ); /* translators: %s: currency symbol */ $field( sprintf( __( 'Price holiday (%s)', 'dayslot' ), $cur ), '' ); ?>

__( 'Before the address', 'dayslot' ), 'after_address' => __( 'After the address (recommended)', 'dayslot' ), 'before_notes' => __( 'Before the order notes', 'dayslot' ), 'after_notes' => __( 'After the order notes', 'dayslot' ), ); ?> array( __( 'Step 1 title', 'dayslot' ), dayslot_defaults()['method_label'] ), 'delivery_text' => array( __( 'Button: delivery', 'dayslot' ), dayslot_defaults()['delivery_text'] ), 'label' => array( __( 'Step 2 title (date)', 'dayslot' ), dayslot_defaults()['label'] ), 'time_label' => array( __( 'Step 3 title (time)', 'dayslot' ), dayslot_defaults()['time_label'] ), 'required_message' => array( __( 'Message: date missing', 'dayslot' ), dayslot_defaults()['required_message'] ), 'unavailable_message' => array( __( 'Message: date not available', 'dayslot' ), dayslot_defaults()['unavailable_message'] ), 'slot_required_message' => array( __( 'Message: time missing', 'dayslot' ), dayslot_defaults()['slot_required_message'] ), 'slot_full_message' => array( __( 'Message: time not available', 'dayslot' ), dayslot_defaults()['slot_full_message'] ), ); foreach ( $texts as $key => $info ) : ?> __( 'Main colour (selected day, active button)', 'dayslot' ), 'on_primary' => __( 'Text on the main colour', 'dayslot' ), 'bg' => __( 'Background', 'dayslot' ), 'text' => __( 'Text', 'dayslot' ), 'border' => __( 'Borders', 'dayslot' ), 'busy' => __( 'Unavailable days', 'dayslot' ), 'fee' => __( 'Mark of days with a higher price', 'dayslot' ), ); ?>

$preset ) : ?>

$label ) : ?>

array( __( 'Rounded corners — everything (px)', 'dayslot' ), 10, false ), 'radius_btn' => array( __( 'Buttons: delivery / pickup, kinds, times', 'dayslot' ), '', true ), 'radius_day' => array( __( 'Calendar days', 'dayslot' ), '', true ), 'radius_card' => array( __( 'Boxes: calendar, time list', 'dayslot' ), '', true ), 'radius_input' => array( __( 'Fields and drop-downs', 'dayslot' ), '', true ), 'font_size' => array( __( 'Font size (px)', 'dayslot' ), '', false ), 'gap' => array( __( 'Spacing (px)', 'dayslot' ), 12, false ), ); foreach ( $sizes as $key => $info ) : ?>


__( 'Switch', 'dayslot' ), 'cards' => __( 'Cards with icons', 'dayslot' ), 'buttons' => __( 'Plain buttons', 'dayslot' ), ) as $value => $label ) : ?>


__( 'Calendar', 'dayslot' ), 'select' => __( 'Drop-down list of free dates', 'dayslot' ), ) as $value => $label ) : ?>


__( 'List', 'dayslot' ), 'grid' => __( 'Grid of buttons', 'dayslot' ), 'select' => __( 'Drop-down', 'dayslot' ), ) as $value => $label ) : ?>