✏️ 正在编辑: schedule-meta.php
路径:
/home/h359620/public_html/wp-content/themes/datis/inc/includes/schedules/schedule-meta.php
提示:
您可以编辑任何文件(包括二进制文件),但请注意不当修改可能导致文件损坏。
<?php if (!defined('ABSPATH')) exit; if (!class_exists('Datis_Consultation_Schedule_Meta')) { final class Datis_Consultation_Schedule_Meta { private static $instance = null; private string $pt = 'consult_schedule'; // Field IDs / meta keys (IMPORTANT: keep unchanged) private string $enabled = '_datis_cs_enabled'; private string $slot_duration = '_datis_cs_slot_duration'; private string $capacity_default = '_datis_cs_capacity_default'; private string $working_days = '_datis_cs_working_days'; private string $weekly_ranges = '_datis_cs_weekly_ranges'; private string $overrides = '_datis_cs_overrides'; public static function get_instance(): self { return self::$instance ?? (self::$instance = new self()); } private function __construct() { add_action('add_meta_boxes', [$this, 'register_metabox']); add_action('save_post_' . $this->pt, [$this, 'save_meta'], 10, 2); // Admin-only UI helpers: tabs + repeaters + sortable + override visibility add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); add_action('admin_footer', [$this, 'print_admin_inline_js'], 99); } /* ============================================================ * Metabox registration * ============================================================ */ public function register_metabox(): void { if (!post_type_exists($this->pt)) return; add_meta_box( 'datis_cs_schedule_box', esc_html__('Consultation Schedule', 'datis'), [$this, 'render_metabox'], $this->pt, 'normal', 'high' ); } /* ============================================================ * Render metabox * ============================================================ */ public function render_metabox(\WP_Post $post): void { wp_nonce_field('datis_cs_schedule_save', 'datis_cs_schedule_nonce'); // Load stored values $enabled = get_post_meta($post->ID, $this->enabled, true); $enabled = ($enabled === '' || $enabled === null) ? '1' : (string)$enabled; $enabled = !empty($enabled) ? '1' : '0'; $slot_duration = (int) get_post_meta($post->ID, $this->slot_duration, true); $slot_duration = $slot_duration > 0 ? $slot_duration : 30; $capacity_default = (int) get_post_meta($post->ID, $this->capacity_default, true); $capacity_default = $capacity_default > 0 ? $capacity_default : 1; $working_days = get_post_meta($post->ID, $this->working_days, true); if (is_string($working_days)) { $maybe = maybe_unserialize($working_days); if (is_array($maybe)) $working_days = $maybe; } if (!is_array($working_days) || empty($working_days)) { $working_days = ['sat','sun','mon','tue','wed','thu']; } $weekly_ranges = get_post_meta($post->ID, $this->weekly_ranges, true); if (is_string($weekly_ranges)) { $maybe = maybe_unserialize($weekly_ranges); if (is_array($maybe)) $weekly_ranges = $maybe; } if (!is_array($weekly_ranges)) $weekly_ranges = []; $overrides = get_post_meta($post->ID, $this->overrides, true); if (is_string($overrides)) { $maybe = maybe_unserialize($overrides); if (is_array($maybe)) $overrides = $maybe; } if (!is_array($overrides)) $overrides = []; // Day options (kept same slugs as original) $days_options = [ 'sat' => esc_html__('Sat', 'datis'), 'sun' => esc_html__('Sun', 'datis'), 'mon' => esc_html__('Mon', 'datis'), 'tue' => esc_html__('Tue', 'datis'), 'wed' => esc_html__('Wed', 'datis'), 'thu' => esc_html__('Thu', 'datis'), 'fri' => esc_html__('Fri', 'datis'), ]; // Weekly range day selector options (adds "all") $weekly_day_options = ['all' => esc_html__('All working days', 'datis')] + $days_options; ?> <style> .datis-cs-wrap { margin-top: 10px; } .datis-cs-tabs { display:flex; gap:10px; margin: 12px 0; } .datis-cs-tab-btn { border: 1px solid #dcdcde; background:#fff; padding:8px 12px; border-radius:6px; cursor:pointer; user-select:none; } .datis-cs-tab-btn.is-active { background:#f6f7f7; font-weight:700; } .datis-cs-panel { display:none; border:1px solid #dcdcde; background:#fff; padding:12px; border-radius:8px; } .datis-cs-panel.is-active { display:block; } .datis-grid { display:grid; grid-template-columns: 220px 1fr; gap:10px 12px; align-items:start; } .datis-grid label { font-weight:600; } .datis-small { font-size: 12px; color:#646970; margin-top: 6px; } .datis-repeater-wrap { margin-top: 14px; } .datis-repeater-item { border: 1px solid #dcdcde; background:#fff; padding: 12px; margin: 10px 0; border-radius: 6px; } .datis-repeater-head { display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom: 10px; } .datis-repeater-head strong { font-size: 13px; } .datis-remove-item { color:#b32d2e; text-decoration:none; } .datis-add-item { margin-top: 10px; } .datis-inline { display:inline-flex; gap:14px; align-items:center; flex-wrap:wrap; } .datis-handle { cursor: move; user-select:none; color:#646970; font-weight:700; } </style> <div class="datis-cs-wrap"> <div class="datis-cs-tabs" role="tablist"> <button type="button" class="datis-cs-tab-btn is-active" data-tab="weekly"> <?php echo esc_html__('Weekly Defaults', 'datis'); ?> </button> <button type="button" class="datis-cs-tab-btn" data-tab="overrides"> <?php echo esc_html__('Custom Days / Holidays', 'datis'); ?> </button> </div> <!-- Weekly Defaults --> <div class="datis-cs-panel is-active" data-panel="weekly"> <div class="datis-grid"> <label for="<?php echo esc_attr($this->enabled); ?>"><?php echo esc_html__('Enable this schedule', 'datis'); ?></label> <div> <label> <input type="checkbox" id="<?php echo esc_attr($this->enabled); ?>" name="<?php echo esc_attr($this->enabled); ?>" value="1" <?php checked($enabled, '1'); ?> /> <?php echo esc_html__('Enabled', 'datis'); ?> </label> </div> <label for="<?php echo esc_attr($this->slot_duration); ?>"><?php echo esc_html__('Slot Duration (minutes)', 'datis'); ?></label> <div> <input type="number" min="5" step="5" id="<?php echo esc_attr($this->slot_duration); ?>" name="<?php echo esc_attr($this->slot_duration); ?>" value="<?php echo esc_attr((string)$slot_duration); ?>" style="max-width:120px;" /> <div class="datis-small"><?php echo esc_html__('Minimum: 5 minutes.', 'datis'); ?></div> </div> <label for="<?php echo esc_attr($this->capacity_default); ?>"><?php echo esc_html__('Default Capacity (per slot)', 'datis'); ?></label> <div> <input type="number" min="1" step="1" id="<?php echo esc_attr($this->capacity_default); ?>" name="<?php echo esc_attr($this->capacity_default); ?>" value="<?php echo esc_attr((string)$capacity_default); ?>" style="max-width:120px;" /> <div class="datis-small"><?php echo esc_html__('Minimum: 1.', 'datis'); ?></div> </div> <label><?php echo esc_html__('Working Days', 'datis'); ?></label> <div class="datis-inline"> <?php foreach ($days_options as $k => $label) : ?> <label> <input type="checkbox" name="<?php echo esc_attr($this->working_days); ?>[]" value="<?php echo esc_attr($k); ?>" <?php checked(in_array($k, $working_days, true)); ?> /> <?php echo esc_html($label); ?> </label> <?php endforeach; ?> <div class="datis-small" style="flex-basis:100%;"> <?php echo esc_html__('Select the days that accept bookings.', 'datis'); ?> </div> </div> </div> <hr /> <h4 style="margin: 10px 0 0;"><?php echo esc_html__('Default Weekly Time Ranges', 'datis'); ?></h4> <p class="datis-small"> <?php echo esc_html__('Create time ranges per day (e.g., Thu 08:00–12:00). Use "All working days" to apply to every enabled working day.', 'datis'); ?> </p> <div class="datis-repeater-wrap" data-repeater="cs_weekly_ranges"> <div class="datis-repeater-items datis-sortable"> <?php $i = 0; foreach ($weekly_ranges as $row) : if (!is_array($row)) continue; // Day key (backward compatible: missing => "all") $day = isset($row['day']) ? sanitize_key((string)$row['day']) : 'all'; $day = array_key_exists($day, $weekly_day_options) ? $day : 'all'; $start = isset($row['start']) ? (string)$row['start'] : ''; $end = isset($row['end']) ? (string)$row['end'] : ''; $cap = isset($row['cap']) ? (string)$row['cap'] : ''; ?> <div class="datis-repeater-item" data-index="<?php echo esc_attr($i); ?>"> <div class="datis-repeater-head"> <strong> <span class="datis-handle">☰</span> <?php echo esc_html(sprintf(__('Weekly Range #%d', 'datis'), $i + 1)); ?> </strong> <a href="#" class="datis-remove-item"><?php echo esc_html__('Remove', 'datis'); ?></a> </div> <div class="datis-grid"> <label><?php echo esc_html__('Day', 'datis'); ?></label> <div> <select name="<?php echo esc_attr($this->weekly_ranges); ?>[<?php echo esc_attr($i); ?>][day]"> <?php foreach ($weekly_day_options as $key => $lbl) : ?> <option value="<?php echo esc_attr($key); ?>" <?php selected($day, $key); ?>> <?php echo esc_html($lbl); ?> </option> <?php endforeach; ?> </select> <div class="datis-small"><?php echo esc_html__('Choose a weekday to override hours for that day.', 'datis'); ?></div> </div> <label><?php echo esc_html__('Start Time', 'datis'); ?></label> <div> <input type="time" name="<?php echo esc_attr($this->weekly_ranges); ?>[<?php echo esc_attr($i); ?>][start]" value="<?php echo esc_attr($start); ?>" step="60" /> </div> <label><?php echo esc_html__('End Time', 'datis'); ?></label> <div> <input type="time" name="<?php echo esc_attr($this->weekly_ranges); ?>[<?php echo esc_attr($i); ?>][end]" value="<?php echo esc_attr($end); ?>" step="60" /> </div> <label><?php echo esc_html__('Capacity Override (optional)', 'datis'); ?></label> <div> <input type="number" min="1" step="1" name="<?php echo esc_attr($this->weekly_ranges); ?>[<?php echo esc_attr($i); ?>][cap]" value="<?php echo esc_attr($cap); ?>" style="max-width:120px;" /> <div class="datis-small"><?php echo esc_html__('Leave empty to use Default Capacity.', 'datis'); ?></div> </div> </div> </div> <?php $i++; endforeach; ?> </div> <button type="button" class="button datis-add-item" data-add="cs_weekly_ranges"> <?php echo esc_html__('Add Range', 'datis'); ?> </button> <script type="text/template" id="tmpl-datis-cs-weekly-range-row"> <div class="datis-repeater-item" data-index="{{index}}"> <div class="datis-repeater-head"> <strong> <span class="datis-handle">☰</span> <?php echo esc_html__('Weekly Range #', 'datis'); ?>{{num}} </strong> <a href="#" class="datis-remove-item"><?php echo esc_html__('Remove', 'datis'); ?></a> </div> <div class="datis-grid"> <label><?php echo esc_html__('Day', 'datis'); ?></label> <div> <select name="<?php echo esc_attr($this->weekly_ranges); ?>[{{index}}][day]"> <?php foreach ($weekly_day_options as $key => $lbl) : ?> <option value="<?php echo esc_attr($key); ?>" <?php selected($key, 'all'); ?>> <?php echo esc_html($lbl); ?> </option> <?php endforeach; ?> </select> <div class="datis-small"><?php echo esc_html__('Choose a weekday to override hours for that day.', 'datis'); ?></div> </div> <label><?php echo esc_html__('Start Time', 'datis'); ?></label> <div> <input type="time" name="<?php echo esc_attr($this->weekly_ranges); ?>[{{index}}][start]" value="" step="60" /> </div> <label><?php echo esc_html__('End Time', 'datis'); ?></label> <div> <input type="time" name="<?php echo esc_attr($this->weekly_ranges); ?>[{{index}}][end]" value="" step="60" /> </div> <label><?php echo esc_html__('Capacity Override (optional)', 'datis'); ?></label> <div> <input type="number" min="1" step="1" name="<?php echo esc_attr($this->weekly_ranges); ?>[{{index}}][cap]" value="" style="max-width:120px;" /> <div class="datis-small"><?php echo esc_html__('Leave empty to use Default Capacity.', 'datis'); ?></div> </div> </div> </div> </script> </div> </div> <!-- Overrides --> <div class="datis-cs-panel" data-panel="overrides"> <h4 style="margin: 0;"><?php echo esc_html__('Specific Date Overrides', 'datis'); ?></h4> <p class="datis-small"><?php echo esc_html__('Specific dates: holiday, custom hours, or capacity changes.', 'datis'); ?></p> <div class="datis-repeater-wrap" data-repeater="cs_overrides"> <div class="datis-repeater-items datis-sortable"> <?php $i = 0; foreach ($overrides as $row) : if (!is_array($row)) continue; $date = isset($row['date']) ? (string)$row['date'] : ''; $type = isset($row['type']) ? (string)$row['type'] : 'closed'; $type = in_array($type, ['closed','open_custom','capacity_only'], true) ? $type : 'closed'; $start = isset($row['start']) ? (string)$row['start'] : ''; $end = isset($row['end']) ? (string)$row['end'] : ''; $cap = isset($row['cap']) ? (string)$row['cap'] : ''; $capd = isset($row['cap_day']) ? (string)$row['cap_day'] : ''; ?> <div class="datis-repeater-item cmb-repeatable-grouping" data-index="<?php echo esc_attr($i); ?>"> <div class="datis-repeater-head"> <strong> <span class="datis-handle">☰</span> <?php echo esc_html(sprintf(__('Override #%d', 'datis'), $i + 1)); ?> </strong> <a href="#" class="datis-remove-item"><?php echo esc_html__('Remove', 'datis'); ?></a> </div> <div class="datis-grid"> <label><?php echo esc_html__('Date', 'datis'); ?></label> <div> <input type="text" class="datis-cs-date" name="<?php echo esc_attr($this->overrides); ?>[<?php echo esc_attr($i); ?>][date]" value="<?php echo esc_attr($date); ?>" placeholder="YYYY-MM-DD" autocomplete="off" readonly="readonly" style="max-width:260px;" /> <div class="datis-small"><?php echo esc_html__('UI can be Jalali/Gregorian; stored value is always Gregorian YYYY-MM-DD.', 'datis'); ?></div> </div> <label><?php echo esc_html__('Type', 'datis'); ?></label> <div> <select name="<?php echo esc_attr($this->overrides); ?>[<?php echo esc_attr($i); ?>][type]" class="datis-cs-ov-type" > <option value="closed" <?php selected($type, 'closed'); ?>><?php echo esc_html__('Closed (holiday)', 'datis'); ?></option> <option value="open_custom" <?php selected($type, 'open_custom'); ?>><?php echo esc_html__('Open with custom hours', 'datis'); ?></option> <option value="capacity_only" <?php selected($type, 'capacity_only'); ?>><?php echo esc_html__('Change capacity only', 'datis'); ?></option> </select> </div> <label><?php echo esc_html__('Start Time', 'datis'); ?></label> <div> <input type="time" step="300" class="datis-cs-ov-start" name="<?php echo esc_attr($this->overrides); ?>[<?php echo esc_attr($i); ?>][start]" value="<?php echo esc_attr($start); ?>" /> </div> <label><?php echo esc_html__('End Time', 'datis'); ?></label> <div> <input type="time" step="300" class="datis-cs-ov-end" name="<?php echo esc_attr($this->overrides); ?>[<?php echo esc_attr($i); ?>][end]" value="<?php echo esc_attr($end); ?>" /> </div> <label><?php echo esc_html__('Capacity Override (optional)', 'datis'); ?></label> <div> <input type="number" min="1" step="1" class="datis-cs-ov-cap" name="<?php echo esc_attr($this->overrides); ?>[<?php echo esc_attr($i); ?>][cap]" value="<?php echo esc_attr($cap); ?>" style="max-width:120px;" /> </div> <label><?php echo esc_html__('Capacity (for this date)', 'datis'); ?></label> <div> <input type="number" min="1" step="1" class="datis-cs-ov-capday" name="<?php echo esc_attr($this->overrides); ?>[<?php echo esc_attr($i); ?>][cap_day]" value="<?php echo esc_attr($capd); ?>" style="max-width:120px;" /> </div> </div> </div> <?php $i++; endforeach; ?> </div> <button type="button" class="button datis-add-item" data-add="cs_overrides"> <?php echo esc_html__('Add Override', 'datis'); ?> </button> <script type="text/template" id="tmpl-datis-cs-override-row"> <div class="datis-repeater-item cmb-repeatable-grouping" data-index="{{index}}"> <div class="datis-repeater-head"> <strong> <span class="datis-handle">☰</span> <?php echo esc_html__('Override #', 'datis'); ?>{{num}} </strong> <a href="#" class="datis-remove-item"><?php echo esc_html__('Remove', 'datis'); ?></a> </div> <div class="datis-grid"> <label><?php echo esc_html__('Date', 'datis'); ?></label> <div> <input type="text" class="datis-cs-date" name="<?php echo esc_attr($this->overrides); ?>[{{index}}][date]" value="" placeholder="YYYY-MM-DD" autocomplete="off" readonly="readonly" style="max-width:260px;" /> <div class="datis-small"><?php echo esc_html__('UI can be Jalali/Gregorian; stored value is always Gregorian YYYY-MM-DD.', 'datis'); ?></div> </div> <label><?php echo esc_html__('Type', 'datis'); ?></label> <div> <select name="<?php echo esc_attr($this->overrides); ?>[{{index}}][type]" class="datis-cs-ov-type"> <option value="closed" selected><?php echo esc_html__('Closed (holiday)', 'datis'); ?></option> <option value="open_custom"><?php echo esc_html__('Open with custom hours', 'datis'); ?></option> <option value="capacity_only"><?php echo esc_html__('Change capacity only', 'datis'); ?></option> </select> </div> <label><?php echo esc_html__('Start Time', 'datis'); ?></label> <div> <input type="time" step="300" class="datis-cs-ov-start" name="<?php echo esc_attr($this->overrides); ?>[{{index}}][start]" value="" /> </div> <label><?php echo esc_html__('End Time', 'datis'); ?></label> <div> <input type="time" step="300" class="datis-cs-ov-end" name="<?php echo esc_attr($this->overrides); ?>[{{index}}][end]" value="" /> </div> <label><?php echo esc_html__('Capacity Override (optional)', 'datis'); ?></label> <div> <input type="number" min="1" step="1" class="datis-cs-ov-cap" name="<?php echo esc_attr($this->overrides); ?>[{{index}}][cap]" value="" style="max-width:120px;" /> </div> <label><?php echo esc_html__('Capacity (for this date)', 'datis'); ?></label> <div> <input type="number" min="1" step="1" class="datis-cs-ov-capday" name="<?php echo esc_attr($this->overrides); ?>[{{index}}][cap_day]" value="" style="max-width:120px;" /> </div> </div> </div> </script> </div> </div> </div> <?php } /* ============================================================ * Save handler * ============================================================ */ public function save_meta(int $post_id, \WP_Post $post): void { if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if (wp_is_post_revision($post_id)) return; if (empty($_POST['datis_cs_schedule_nonce']) || !wp_verify_nonce( sanitize_text_field(wp_unslash($_POST['datis_cs_schedule_nonce'])), 'datis_cs_schedule_save' )) { return; } if (!current_user_can('edit_post', $post_id)) return; // Enabled $en = !empty($_POST[$this->enabled]) ? '1' : '0'; update_post_meta($post_id, $this->enabled, $en); // Slot duration (min 5) $dur = isset($_POST[$this->slot_duration]) ? max(5, absint(wp_unslash($_POST[$this->slot_duration]))) : 30; update_post_meta($post_id, $this->slot_duration, (string)$dur); // Default capacity (min 1) $cap = isset($_POST[$this->capacity_default]) ? max(1, absint(wp_unslash($_POST[$this->capacity_default]))) : 1; update_post_meta($post_id, $this->capacity_default, (string)$cap); // Working days $days = isset($_POST[$this->working_days]) ? $_POST[$this->working_days] : []; $days = $this->sanitize_working_days($days); update_post_meta($post_id, $this->working_days, $days); // Weekly ranges repeater (day-aware; backward compatible) $wr = isset($_POST[$this->weekly_ranges]) ? $_POST[$this->weekly_ranges] : []; $wr = $this->sanitize_weekly_ranges($wr); update_post_meta($post_id, $this->weekly_ranges, $wr); // Overrides repeater $ov = isset($_POST[$this->overrides]) ? $_POST[$this->overrides] : []; $ov = $this->sanitize_overrides($ov); update_post_meta($post_id, $this->overrides, $ov); } /* ============================================================ * Sanitizers * ============================================================ */ private function sanitize_working_days($value): array { if (!is_array($value)) return []; $allowed = ['sat','sun','mon','tue','wed','thu','fri']; $out = []; foreach ($value as $v) { $v = sanitize_key((string)$v); if (in_array($v, $allowed, true)) $out[] = $v; } return array_values(array_unique($out)); } private function sanitize_time_hhmm(string $v): string { $v = trim($v); return preg_match('/^\d{2}:\d{2}$/', $v) ? $v : ''; } private function sanitize_ymd(string $v): string { $v = trim($v); return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : ''; } private function sanitize_optional_int($v, int $min = 1): string { if ($v === '' || $v === null) return ''; return (string) max($min, absint($v)); } /** * Weekly ranges are day-aware: * - row['day'] can be: all|sat|sun|mon|tue|wed|thu|fri * - Backward compatibility: if day is missing, it is treated as "all". * * Storage meta_key is unchanged: _datis_cs_weekly_ranges */ private function sanitize_weekly_ranges($value): array { if (!is_array($value)) return []; $allowed_days = ['all','sat','sun','mon','tue','wed','thu','fri']; $out = []; foreach ($value as $row) { if (!is_array($row)) continue; $day = isset($row['day']) ? sanitize_key((string)$row['day']) : 'all'; $day = in_array($day, $allowed_days, true) ? $day : 'all'; $start = $this->sanitize_time_hhmm((string)($row['start'] ?? '')); $end = $this->sanitize_time_hhmm((string)($row['end'] ?? '')); $cap = $this->sanitize_optional_int($row['cap'] ?? '', 1); // Skip empty rows if ($start === '' && $end === '' && $cap === '') continue; $out[] = [ 'day' => $day, 'start' => $start, 'end' => $end, 'cap' => $cap, ]; } return $out; } private function sanitize_overrides($value): array { if (!is_array($value)) return []; $allowed_types = ['closed','open_custom','capacity_only']; $out = []; foreach ($value as $row) { if (!is_array($row)) continue; $date = $this->sanitize_ymd((string)($row['date'] ?? '')); $type = sanitize_key((string)($row['type'] ?? 'closed')); $type = in_array($type, $allowed_types, true) ? $type : 'closed'; $start = $this->sanitize_time_hhmm((string)($row['start'] ?? '')); $end = $this->sanitize_time_hhmm((string)($row['end'] ?? '')); $cap = $this->sanitize_optional_int($row['cap'] ?? '', 1); $capd = $this->sanitize_optional_int($row['cap_day'] ?? '', 1); if ($date === '') continue; if ($type === 'closed') { $out[] = [ 'date' => $date, 'type' => $type, 'start' => '', 'end' => '', 'cap' => '', 'cap_day' => '', ]; continue; } if ($type === 'capacity_only') { $out[] = [ 'date' => $date, 'type' => $type, 'start' => '', 'end' => '', 'cap' => '', 'cap_day' => $capd !== '' ? $capd : '', ]; continue; } // open_custom $out[] = [ 'date' => $date, 'type' => $type, 'start' => $start, 'end' => $end, 'cap' => $cap, 'cap_day' => '', ]; } return $out; } /* ============================================================ * ✅ Effective schedule helpers (THIS FIXES YOUR ISSUE) * ============================================================ */ /** * Convert a Gregorian date (Y-m-d) to weekday key (sat..fri). * * @param string $ymd Gregorian date in Y-m-d * @return string sat|sun|mon|tue|wed|thu|fri or empty string on invalid */ public static function weekday_key_from_ymd(string $ymd): string { $ymd = trim($ymd); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $ymd)) return ''; $ts = strtotime($ymd . ' 00:00:00'); if (!$ts) return ''; // PHP: w => 0 (Sun) ... 6 (Sat) $w = (int) date('w', $ts); // Map to your slugs return match ($w) { 6 => 'sat', 0 => 'sun', 1 => 'mon', 2 => 'tue', 3 => 'wed', 4 => 'thu', 5 => 'fri', default => '', }; } /** * ✅ Core fix: * Return effective weekly ranges for a weekday. * * Rule: * - If there is at least one range with day == $weekday, ONLY use those. * - Otherwise, use ranges with day == 'all' (backward-compatible default). */ public static function get_effective_weekly_ranges_for_weekday(array $weekly_ranges, string $weekday): array { $weekday = sanitize_key($weekday); $allowed = ['sat','sun','mon','tue','wed','thu','fri']; if (!in_array($weekday, $allowed, true)) return []; $specific = []; $all = []; foreach ($weekly_ranges as $row) { if (!is_array($row)) continue; $day = isset($row['day']) ? sanitize_key((string)$row['day']) : 'all'; // backward compat $start = isset($row['start']) ? trim((string)$row['start']) : ''; $end = isset($row['end']) ? trim((string)$row['end']) : ''; $cap = isset($row['cap']) ? trim((string)$row['cap']) : ''; if ($start === '' || $end === '') continue; $norm = [ 'day' => $day ?: 'all', 'start' => $start, 'end' => $end, 'cap' => $cap, ]; if ($norm['day'] === $weekday) { $specific[] = $norm; } elseif ($norm['day'] === 'all' || $norm['day'] === '') { $all[] = $norm; } } // ✅ precedence: specific day overrides "all" return !empty($specific) ? $specific : $all; } /** * Find an override row by date (Y-m-d). * Returns normalized override row or null. */ public static function find_override_for_date(array $overrides, string $ymd): ?array { $ymd = trim($ymd); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $ymd)) return null; foreach ($overrides as $row) { if (!is_array($row)) continue; $date = isset($row['date']) ? (string)$row['date'] : ''; if ($date !== $ymd) continue; $type = isset($row['type']) ? (string)$row['type'] : 'closed'; $type = in_array($type, ['closed','open_custom','capacity_only'], true) ? $type : 'closed'; return [ 'date' => $ymd, 'type' => $type, 'start' => (string)($row['start'] ?? ''), 'end' => (string)($row['end'] ?? ''), 'cap' => (string)($row['cap'] ?? ''), 'cap_day' => (string)($row['cap_day'] ?? ''), ]; } return null; } /** * Build the effective schedule for a specific date. * * Output: * - status: closed|open * - ranges: array of [start,end,cap(optional)] * - capacity_default: int * - capacity_for_date: optional (if capacity_only) */ public function get_effective_schedule_for_date(int $post_id, string $ymd): array { $enabled = get_post_meta($post_id, $this->enabled, true); $enabled = !empty($enabled); $slot_duration = (int) get_post_meta($post_id, $this->slot_duration, true); $slot_duration = $slot_duration > 0 ? $slot_duration : 30; $capacity_default = (int) get_post_meta($post_id, $this->capacity_default, true); $capacity_default = $capacity_default > 0 ? $capacity_default : 1; $working_days = get_post_meta($post_id, $this->working_days, true); if (is_string($working_days)) { $maybe = maybe_unserialize($working_days); if (is_array($maybe)) $working_days = $maybe; } if (!is_array($working_days) || empty($working_days)) { $working_days = ['sat','sun','mon','tue','wed','thu']; } $weekly_ranges = get_post_meta($post_id, $this->weekly_ranges, true); if (is_string($weekly_ranges)) { $maybe = maybe_unserialize($weekly_ranges); if (is_array($maybe)) $weekly_ranges = $maybe; } if (!is_array($weekly_ranges)) $weekly_ranges = []; $overrides = get_post_meta($post_id, $this->overrides, true); if (is_string($overrides)) { $maybe = maybe_unserialize($overrides); if (is_array($maybe)) $overrides = $maybe; } if (!is_array($overrides)) $overrides = []; if (!$enabled) { return [ 'status' => 'closed', 'reason' => 'disabled', 'slot_duration' => $slot_duration, 'capacity_default' => $capacity_default, 'ranges' => [], ]; } $weekday = self::weekday_key_from_ymd($ymd); if ($weekday === '' || !in_array($weekday, $working_days, true)) { return [ 'status' => 'closed', 'reason' => 'not_working_day', 'slot_duration' => $slot_duration, 'capacity_default' => $capacity_default, 'ranges' => [], ]; } // ✅ Date override has higher priority than weekly ranges $ov = self::find_override_for_date($overrides, $ymd); if ($ov) { if ($ov['type'] === 'closed') { return [ 'status' => 'closed', 'reason' => 'override_closed', 'slot_duration' => $slot_duration, 'capacity_default' => $capacity_default, 'ranges' => [], ]; } if ($ov['type'] === 'capacity_only') { $cap_day = (int) ($ov['cap_day'] !== '' ? $ov['cap_day'] : 0); return [ 'status' => 'open', 'reason' => 'override_capacity_only', 'slot_duration' => $slot_duration, 'capacity_default' => $capacity_default, 'capacity_for_date' => ($cap_day > 0 ? $cap_day : $capacity_default), // ranges come from weekly effective ranges 'ranges' => self::get_effective_weekly_ranges_for_weekday($weekly_ranges, $weekday), ]; } // open_custom return [ 'status' => 'open', 'reason' => 'override_open_custom', 'slot_duration' => $slot_duration, 'capacity_default' => $capacity_default, 'ranges' => [[ 'start' => $ov['start'], 'end' => $ov['end'], 'cap' => $ov['cap'], ]], ]; } // ✅ Weekly effective ranges (FIX: day-specific overrides 'all') $ranges = self::get_effective_weekly_ranges_for_weekday($weekly_ranges, $weekday); return [ 'status' => !empty($ranges) ? 'open' : 'closed', 'reason' => !empty($ranges) ? 'weekly' : 'no_ranges', 'slot_duration' => $slot_duration, 'capacity_default' => $capacity_default, 'ranges' => $ranges, ]; } /* ============================================================ * Admin assets + inline JS (tabs + repeaters + visibility) * ============================================================ */ public function enqueue_admin_assets(string $hook): void { if (!in_array($hook, ['post.php', 'post-new.php'], true)) return; $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen || ($screen->post_type ?? '') !== $this->pt) return; wp_enqueue_script('jquery'); wp_enqueue_script('jquery-ui-sortable'); // Date picker libs are expected to be enqueued by your admin stack: // moment + moment-jalaali + mds.bs.datetimepicker.js } public function print_admin_inline_js(): void { if (!is_admin()) return; $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen || ($screen->post_type ?? '') !== $this->pt) return; $weekly_key = esc_js($this->weekly_ranges); $ov_key = esc_js($this->overrides); ?> <script> (function ($) { "use strict"; /* ----------------------------- * Tabs * ----------------------------- */ function initTabs() { $(document).off("click.datisCSTabs", ".datis-cs-tab-btn"); $(document).on("click.datisCSTabs", ".datis-cs-tab-btn", function () { var tab = ($(this).data("tab") || "").toString(); if (!tab) return; $(".datis-cs-tab-btn").removeClass("is-active"); $(this).addClass("is-active"); $(".datis-cs-panel").removeClass("is-active"); $('.datis-cs-panel[data-panel="' + tab + '"]').addClass("is-active"); // Let external date script re-init date pickers for visible fields $(document).trigger("datis_cs_reinit", [$(document)]); }); } /* ----------------------------- * Override visibility (no .cmb-row) * ----------------------------- */ function getFieldContainer($input) { var $grid = $input.closest(".datis-grid"); if (!$grid.length) return $input.closest("div"); // Each field value lives in the "value column" div; we hide that div. return $input.closest("div"); } function applyOverrideVisibility($group) { var type = ($group.find(".datis-cs-ov-type").val() || "closed").toString(); var $startField = getFieldContainer($group.find(".datis-cs-ov-start")); var $endField = getFieldContainer($group.find(".datis-cs-ov-end")); var $capField = getFieldContainer($group.find(".datis-cs-ov-cap")); var $capdField = getFieldContainer($group.find(".datis-cs-ov-capday")); if (type === "closed") { $startField.hide(); $endField.hide(); $capField.hide(); $capdField.hide(); return; } if (type === "open_custom") { $startField.show(); $endField.show(); $capField.show(); $capdField.hide(); return; } if (type === "capacity_only") { $startField.hide(); $endField.hide(); $capField.hide(); $capdField.show(); return; } $startField.hide(); $endField.hide(); $capField.hide(); $capdField.hide(); } function initOverrideRows($context) { $context = $context || $(document); $context.find(".cmb-repeatable-grouping, .datis-repeater-item").each(function () { var $group = $(this); if ($group.find(".datis-cs-ov-type").length) { applyOverrideVisibility($group); $group.off("change.datisCS").on("change.datisCS", ".datis-cs-ov-type", function () { applyOverrideVisibility($group); }); } }); } /* ----------------------------- * Reindexing (metaKey[index][field]) * ----------------------------- */ function reindexRepeater($wrap, metaKey) { var $items = $wrap.find(".datis-repeater-items").first(); $items.find(".datis-repeater-item").each(function(i){ var $item = $(this); $item.attr("data-index", i); $item.find("input, select, textarea").each(function(){ var $el = $(this); var name = ($el.attr("name") || ""); if (!name) return; // Replace only the first [index] part: metaKey[123]... var re = new RegExp("^" + metaKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\[\\d+\\]"); name = name.replace(re, metaKey + "[" + i + "]"); $el.attr("name", name); }); }); } function addRepeaterRow(type) { var $wrap = $('[data-repeater="'+type+'"]'); if (!$wrap.length) return; var $items = $wrap.find(".datis-repeater-items").first(); var index = $items.find(".datis-repeater-item").length; var tmplId = ""; var metaKey = ""; if (type === "cs_weekly_ranges") { tmplId = "#tmpl-datis-cs-weekly-range-row"; metaKey = "<?php echo $weekly_key; ?>"; } if (type === "cs_overrides") { tmplId = "#tmpl-datis-cs-override-row"; metaKey = "<?php echo $ov_key; ?>"; } var tmpl = $(tmplId).html(); if (!tmpl) return; var html = tmpl .replaceAll("{{index}}", String(index)) .replaceAll("{{num}}", String(index + 1)); var $row = $(html); $items.append($row); if (metaKey) reindexRepeater($wrap, metaKey); // Re-bind UI behaviors for the new row initOverrideRows($row); // Let external date picker script re-init inside this row $(document).trigger("datis_cs_reinit", [$row]); } function bindRepeaters() { $(document).off("click.datisCSAdd", ".datis-add-item"); $(document).on("click.datisCSAdd", ".datis-add-item", function(e){ e.preventDefault(); var type = ($(this).data("add") || "").toString(); if (!type) return; addRepeaterRow(type); }); $(document).off("click.datisCSRemove", ".datis-remove-item"); $(document).on("click.datisCSRemove", ".datis-remove-item", function(e){ e.preventDefault(); var $wrap = $(this).closest(".datis-repeater-wrap"); var type = ($wrap.data("repeater") || "").toString(); $(this).closest(".datis-repeater-item").remove(); if (type === "cs_weekly_ranges") reindexRepeater($wrap, "<?php echo $weekly_key; ?>"); if (type === "cs_overrides") reindexRepeater($wrap, "<?php echo $ov_key; ?>"); initOverrideRows($wrap); $(document).trigger("datis_cs_reinit", [$wrap]); }); $(".datis-sortable").each(function(){ var $items = $(this); if ($items.data("datisSortable")) return; $items.data("datisSortable", true); $items.sortable({ handle: ".datis-handle", stop: function(){ var $wrap = $items.closest(".datis-repeater-wrap"); var type = ($wrap.data("repeater") || "").toString(); if (type === "cs_weekly_ranges") reindexRepeater($wrap, "<?php echo $weekly_key; ?>"); if (type === "cs_overrides") reindexRepeater($wrap, "<?php echo $ov_key; ?>"); initOverrideRows($wrap); $(document).trigger("datis_cs_reinit", [$wrap]); } }); }); } $(function(){ initTabs(); bindRepeaters(); initOverrideRows($(document)); // If another script triggers this event, re-init inside that context $(document).on("datis_cs_reinit", function(e, $ctx){ initOverrideRows($ctx && $ctx.length ? $ctx : $(document)); }); }); })(jQuery); </script> <?php } } Datis_Consultation_Schedule_Meta::get_instance(); }
💾 保存文件
← 返回文件管理器