'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(); ?>
| $name ) : ?> | |
| $name ) : ?> | |
|
$label ) : ?>
|
|
| __( 'Show them greyed out ("full")', 'dayslot' ), 'hide' => __( 'Hide them', 'dayslot' ), 'contact' => __( 'Hide them and show a message with the booked times', 'dayslot' ), ); foreach ( $full_choices as $value => $label ) : ?> | |
|
|
|
__( '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 ) :
?>