✏️ 正在编辑: booking-ajax.php
路径:
/home/h359620/public_html/wp-content/themes/datis/inc/includes/consultation/booking-ajax.php
提示:
您可以编辑任何文件(包括二进制文件),但请注意不当修改可能导致文件损坏。
<?php if (!defined('ABSPATH')) exit; if (!class_exists('Datis_Consultation_Booking_Ajax')) { class Datis_Consultation_Booking_Ajax { /** @var self|null */ private static $instance = null; /** Nonce action used in wp_localize_script */ private $nonce_action = 'datis_consult_booking'; /** Taxonomy for consultation categories */ private $taxonomy = 'consultation-categories'; /** Schedule CPT */ private $schedule_cpt = 'consult_schedule'; /** Booking CPT */ private $booking_cpt = 'consultation_booking'; /** Optional debug (set true temporarily for troubleshooting) */ private $debug = false; private function __construct() { $this->register_hooks(); } private function register_hooks() { add_action('wp_ajax_datis_consult_get_categories', array($this, 'ajax_get_categories')); add_action('wp_ajax_nopriv_datis_consult_get_categories', array($this, 'ajax_get_categories')); add_action('wp_ajax_datis_consult_get_default_schedule', array($this, 'ajax_get_default_schedule')); add_action('wp_ajax_nopriv_datis_consult_get_default_schedule', array($this, 'ajax_get_default_schedule')); add_action('wp_ajax_datis_consult_get_available_dates', array($this, 'ajax_get_available_dates')); add_action('wp_ajax_nopriv_datis_consult_get_available_dates', array($this, 'ajax_get_available_dates')); add_action('wp_ajax_datis_consult_get_times', array($this, 'ajax_get_times')); add_action('wp_ajax_nopriv_datis_consult_get_times', array($this, 'ajax_get_times')); add_action('wp_ajax_datis_consult_create_booking', array($this, 'ajax_create_booking')); add_action('wp_ajax_nopriv_datis_consult_create_booking', array($this, 'ajax_create_booking')); } private function verify_request() { if (!isset($_POST['nonce'])) { wp_send_json_error(array('message' => 'Missing nonce'), 403); } $nonce = sanitize_text_field(wp_unslash($_POST['nonce'])); if (!wp_verify_nonce($nonce, $this->nonce_action)) { wp_send_json_error(array('message' => 'Invalid nonce'), 403); } } public function ajax_get_categories() { $this->verify_request(); $terms = get_terms(array( 'taxonomy' => $this->taxonomy, 'hide_empty' => false, )); if (is_wp_error($terms)) { wp_send_json_error(array('message' => $terms->get_error_message()), 500); } $out = array(); foreach ($terms as $t) { $out[] = array( 'id' => (int) $t->term_id, 'name' => (string) $t->name, ); } wp_send_json_success(array('terms' => $out)); } public function ajax_get_default_schedule() { $this->verify_request(); $ids = get_posts(array( 'post_type' => $this->schedule_cpt, 'post_status' => 'publish', 'posts_per_page' => 1, 'orderby' => 'ID', 'order' => 'ASC', 'fields' => 'ids', )); $sid = !empty($ids[0]) ? absint($ids[0]) : 0; wp_send_json_success(array('schedule_id' => $sid)); } public function ajax_get_available_dates() { $this->verify_request(); $schedule_id = isset($_POST['schedule_id']) ? absint($_POST['schedule_id']) : 0; if (!$schedule_id) { wp_send_json_error(array('message' => 'Missing schedule_id'), 400); } $days_ahead = 30; $dates = $this->build_available_dates($schedule_id, $days_ahead); wp_send_json_success(array( 'schedule_id' => $schedule_id, 'dates' => $dates, )); } public function ajax_get_times() { $this->verify_request(); $schedule_id = isset($_POST['schedule_id']) ? absint($_POST['schedule_id']) : 0; $term_id = isset($_POST['term_id']) ? absint($_POST['term_id']) : 0; // currently not used for slot logic $date = isset($_POST['date']) ? sanitize_text_field(wp_unslash($_POST['date'])) : ''; if (!$schedule_id || empty($date)) { wp_send_json_error(array('message' => 'Missing schedule_id or date'), 400); } $times = $this->get_day_slots($schedule_id, $date); wp_send_json_success(array( 'schedule_id' => $schedule_id, 'term_id' => $term_id, 'date' => $date, 'times' => $times, )); } public function ajax_create_booking() { $this->verify_request(); if (!is_user_logged_in()) { wp_send_json_error([ 'message' => 'You must be logged in to book.', 'user_id' => isset($_POST['user_id']) ? absint($_POST['user_id']) : 0, ], 403); } $current_user_id = get_current_user_id(); $schedule_id = isset($_POST['schedule_id']) ? absint($_POST['schedule_id']) : 0; $term_id = isset($_POST['term_id']) ? absint($_POST['term_id']) : 0; $full_name = isset($_POST['full_name']) ? sanitize_text_field(wp_unslash($_POST['full_name'])) : ''; $mobile = isset($_POST['mobile']) ? sanitize_text_field(wp_unslash($_POST['mobile'])) : ''; $date = isset($_POST['date']) ? sanitize_text_field(wp_unslash($_POST['date'])) : ''; $time = isset($_POST['time']) ? sanitize_text_field(wp_unslash($_POST['time'])) : ''; // (Optional) reCAPTCHA token (left as-is) $recaptcha_token = isset($_POST['recaptcha']) ? sanitize_text_field(wp_unslash($_POST['recaptcha'])) : ''; $secret = ''; // e.g. datis_get_option('consult_recaptcha_secret', ''); if (!empty($secret)) { if (empty($recaptcha_token)) { wp_send_json_error(['message' => 'reCAPTCHA is required.'], 400); } $resp = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', [ 'timeout' => 10, 'body' => [ 'secret' => $secret, 'response' => $recaptcha_token, 'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '', ], ]); if (is_wp_error($resp)) { wp_send_json_error(['message' => 'reCAPTCHA verification failed.'], 500); } $body = wp_remote_retrieve_body($resp); $json = json_decode($body, true); if (empty($json['success'])) { wp_send_json_error(['message' => 'reCAPTCHA verification failed.'], 403); } } if (!$schedule_id || !$term_id || !$full_name || !$mobile || !$date || !$time) { wp_send_json_error(array('message' => 'Missing fields'), 400); } if (!$this->is_ymd($date) || !$this->is_hhmm($time)) { wp_send_json_error(array('message' => 'Invalid date or time format'), 400); } // Ensure the requested slot is still available $slots = $this->get_day_slots($schedule_id, $date); $is_ok = false; foreach ($slots as $s) { if (!empty($s['time']) && $s['time'] === $time) { $is_ok = true; break; } } if (!$is_ok) { wp_send_json_error(array('message' => 'Slot not available'), 409); } $post_id = wp_insert_post(array( 'post_type' => $this->booking_cpt, 'post_status' => 'publish', 'post_title' => sprintf('%s - %s %s', $full_name, $date, $time), ), true); if (is_wp_error($post_id) || !$post_id) { wp_send_json_error(array('message' => 'Could not create booking'), 500); } update_post_meta($post_id, '_datis_cb_schedule_id', $schedule_id); update_post_meta($post_id, '_datis_cb_full_name', $full_name); update_post_meta($post_id, '_datis_cb_mobile', $mobile); update_post_meta($post_id, '_datis_cb_date', $date); // Gregorian YYYY-MM-DD update_post_meta($post_id, '_datis_cb_time', $time); // HH:MM update_post_meta($post_id, '_datis_cb_status', 'confirmed'); update_post_meta($post_id, '_datis_cb_user_id', $current_user_id); wp_set_object_terms($post_id, array($term_id), $this->taxonomy, false); wp_send_json_success(array( 'booking_id' => (int) $post_id, 'message' => 'Booking created', )); } /** * Safely get meta as array (supports serialized string). */ private function get_meta_array($post_id, $key): array { $v = get_post_meta($post_id, $key, true); if (is_string($v)) { $maybe = maybe_unserialize($v); if (is_array($maybe)) return $maybe; return []; } return is_array($v) ? $v : []; } /** * Convert 'YYYY-MM-DD' to day slug: sat/sun/mon/tue/wed/thu/fri * Uses WordPress timezone to avoid server/DB timezone mismatches. */ private function day_key_from_date(string $date): string { if (!$this->is_ymd($date)) return ''; $tz = function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone(get_option('timezone_string') ?: 'UTC'); $dt = \DateTimeImmutable::createFromFormat('Y-m-d', $date, $tz); if (!$dt) { try { $dt = new \DateTimeImmutable($date, $tz); } catch (\Exception $e) { return ''; } } return strtolower($dt->format('D')); // sat sun mon tue wed thu fri } /* ============================================================ * Slot / Schedule logic * ============================================================ */ private function build_available_dates($schedule_id, $days_ahead) { $dates = array(); $tz = function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone(get_option('timezone_string') ?: 'UTC'); $today = new \DateTimeImmutable('now', $tz); for ($i = 0; $i < (int) $days_ahead; $i++) { $d = $today->modify('+' . $i . ' day')->format('Y-m-d'); $slots = $this->get_day_slots($schedule_id, $d); if (!empty($slots)) { $dates[] = $d; } } return $dates; } private function get_day_slots($schedule_id, $date) { if (!$this->is_ymd($date)) return array(); $enabled_raw = get_post_meta($schedule_id, '_datis_cs_enabled', true); $enabled = ($enabled_raw === 1 || $enabled_raw === '1' || $enabled_raw === 'on' || $enabled_raw === true); if (!$enabled) return array(); $duration = (int) get_post_meta($schedule_id, '_datis_cs_slot_duration', true); if ($duration <= 0) $duration = 30; $cap_default = (int) get_post_meta($schedule_id, '_datis_cs_capacity_default', true); if ($cap_default <= 0) $cap_default = 1; // support serialized meta $working_days = $this->get_meta_array($schedule_id, '_datis_cs_working_days'); $weekly_ranges = $this->get_meta_array($schedule_id, '_datis_cs_weekly_ranges'); $overrides = $this->get_meta_array($schedule_id, '_datis_cs_overrides'); $dayKey = $this->day_key_from_date($date); if (!$dayKey) return array(); // Find override for exact date $ov = $this->find_override($overrides, $date); // Closed day override if ($ov && (($ov['type'] ?? '') === 'closed')) { return array(); } // Base daily capacity (cap_day) $cap_day = $cap_default; if ($ov && isset($ov['cap_day']) && $ov['cap_day'] !== '') { $cap_day = max(1, (int) $ov['cap_day']); } // Resolve ranges $ranges = array(); if ($ov && (($ov['type'] ?? '') === 'open_custom')) { // Custom open hours for a specific date $st = $ov['start'] ?? ''; $en = $ov['end'] ?? ''; if ($this->is_hhmm($st) && $this->is_hhmm($en)) { $ranges[] = array('start' => $st, 'end' => $en, 'cap' => ($ov['cap'] ?? '')); } } else { // Weekly defaults require day to be active if (!$this->is_working_day($working_days, $dayKey)) { return array(); } // day-aware weekly ranges: day-specific overrides replace 'all' $ranges = $this->ranges_for_day($weekly_ranges, $dayKey); } if (empty($ranges)) return array(); $out = array(); foreach ($ranges as $r) { $st = $r['start'] ?? ''; $en = $r['end'] ?? ''; if (!$this->is_hhmm($st) || !$this->is_hhmm($en)) continue; // Slot capacity (range cap overrides daily cap) $cap_slot = $cap_day; if (isset($r['cap']) && $r['cap'] !== '' && $r['cap'] !== null) { $cap_slot = max(1, (int) $r['cap']); } // capacity_only: your meta stores it in cap_day if ($ov && (($ov['type'] ?? '') === 'capacity_only') && isset($ov['cap_day']) && $ov['cap_day'] !== '') { $cap_slot = max(1, (int) $ov['cap_day']); } $slots = $this->build_time_slots($st, $en, $duration); foreach ($slots as $t) { $used = $this->count_bookings_for_slot($schedule_id, $date, $t); if ($used < $cap_slot) { $out[] = array( 'time' => $t, 'label' => $this->time_label($t), 'cap' => $cap_slot, 'used' => $used, 'left' => ($cap_slot - $used), ); } } } return $out; } private function find_override($overrides, $date) { foreach ($overrides as $row) { if (!empty($row['date']) && $row['date'] === $date) return $row; } return null; } private function is_working_day($working_days, $dayKey) { if (isset($working_days[$dayKey])) { $v = $working_days[$dayKey]; return ($v === 1 || $v === '1' || $v === true || $v === 'on'); } return in_array($dayKey, $working_days, true); } /** * weekly_ranges supports: * A) Old keyed-by-day: ['thu'=>[...], 'sat'=>[...]] * B) New list day-aware: [ ['day'=>'all','start'=>'08:00','end'=>'17:00'], ['day'=>'thu','start'=>'08:00','end'=>'12:00'] ... ] * * Rule: * - If day-specific exists => ONLY those ranges apply (override 'all') * - Else => use 'all' ranges (or missing day treated as 'all') */ private function ranges_for_day($weekly_ranges, $dayKey) { // A) keyed-by-day legacy format if (isset($weekly_ranges[$dayKey]) && is_array($weekly_ranges[$dayKey])) { return $this->normalize_ranges_list($weekly_ranges[$dayKey]); } // B) list day-aware format if (!is_array($weekly_ranges)) return array(); $specific = array(); $all = array(); foreach ($weekly_ranges as $r) { if (!is_array($r)) continue; $day = isset($r['day']) ? sanitize_key((string)$r['day']) : 'all'; if ($day === '') $day = 'all'; if ($day === $dayKey) { $specific[] = $r; } elseif ($day === 'all') { $all[] = $r; } } // If specific exists, it replaces all if (!empty($specific)) { return $this->normalize_ranges_list($specific); } return $this->normalize_ranges_list($all); } /** * Normalize list of range rows to: * [ ['start'=>'HH:MM','end'=>'HH:MM','cap'=>...], ... ] */ private function normalize_ranges_list($list) { $out = array(); if (!is_array($list)) return $out; foreach ($list as $r) { if (!is_array($r)) continue; $st = $r['start'] ?? ''; $en = $r['end'] ?? ''; if ($this->is_hhmm($st) && $this->is_hhmm($en)) { $out[] = array( 'start' => $st, 'end' => $en, 'cap' => $r['cap'] ?? '', ); } } return $out; } private function build_time_slots($start, $end, $duration_min) { $out = array(); $start = (string) $start; $end = (string) $end; $utc = new \DateTimeZone('UTC'); $sdt = \DateTimeImmutable::createFromFormat('Y-m-d H:i', '1970-01-01 ' . $start, $utc); $edt = \DateTimeImmutable::createFromFormat('Y-m-d H:i', '1970-01-01 ' . $end, $utc); if (!$sdt || !$edt) return $out; $s = $sdt->getTimestamp(); $e = $edt->getTimestamp(); if ($e <= $s) return $out; $step = max(5, (int) $duration_min) * 60; for ($t = $s; $t + $step <= $e; $t += $step) { $out[] = gmdate('H:i', $t); } return $out; } private function count_bookings_for_slot($schedule_id, $date, $time) { $q = new WP_Query(array( 'post_type' => $this->booking_cpt, 'post_status' => array('publish', 'pending', 'draft'), 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => false, 'meta_query' => array( array('key' => '_datis_cb_schedule_id', 'value' => $schedule_id, 'compare' => '=', 'type' => 'NUMERIC'), array('key' => '_datis_cb_date', 'value' => $date, 'compare' => '='), array('key' => '_datis_cb_time', 'value' => $time, 'compare' => '='), array('key' => '_datis_cb_status', 'value' => 'cancelled', 'compare' => '!='), ), )); return (int) $q->found_posts; } private function time_label($hhmm) { $h = (int) substr($hhmm, 0, 2); if ($h < 12) return 'صبح'; if ($h < 17) return 'ظهر'; return 'عصر'; } private function is_ymd($v) { return (is_string($v) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $v)); } private function is_hhmm($v) { return (is_string($v) && preg_match('/^\d{2}:\d{2}$/', $v)); } public function get_booking_display_data($post_id) { if (get_post_type($post_id) !== $this->booking_cpt) return []; $full_name = get_post_meta($post_id, '_datis_cb_full_name', true); $mobile = get_post_meta($post_id, '_datis_cb_mobile', true); $date_g = get_post_meta($post_id, '_datis_cb_date', true); // Gregorian $time = get_post_meta($post_id, '_datis_cb_time', true); $status = get_post_meta($post_id, '_datis_cb_status', true); $schedule = get_post_meta($post_id, '_datis_cb_schedule_id', true); $date_shamsi = $date_g; if ($date_g && function_exists('jdate')) { $date_shamsi = jdate('Y/m/d', strtotime($date_g)); } return [ 'full_name' => $full_name, 'mobile' => $mobile, 'date_gregorian' => $date_g, 'date_shamsi' => $date_shamsi, 'time' => $time, 'status' => $status, 'schedule_id' => $schedule, ]; } public static function get_instance() { if (is_null(self::$instance)) self::$instance = new self(); return self::$instance; } } Datis_Consultation_Booking_Ajax::get_instance(); }
💾 保存文件
← 返回文件管理器