--- a/computer-repair-shop/activate.php
+++ b/computer-repair-shop/activate.php
@@ -475,6 +475,41 @@
$computer_repair_customer_devices = $wpdb->prefix.'wc_cr_customer_devices';
$computer_repair_feedback_log = $wpdb->prefix.'wc_cr_feedback_log';
$computer_repair_time_logs = $wpdb->prefix . 'wc_cr_time_logs';
+
+ $computer_repair_appointments = $wpdb->prefix . 'wc_cr_appointments';
+
+ $sql = 'CREATE TABLE IF NOT EXISTS ' . $computer_repair_appointments . ' (
+ `appointment_id` BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ `appointment_number` VARCHAR(50) UNIQUE,
+ `job_id` BIGINT(20) UNSIGNED DEFAULT NULL,
+ `customer_id` BIGINT(20) UNSIGNED NOT NULL,
+ `technician_id` BIGINT(20) UNSIGNED DEFAULT NULL,
+ `appointment_type` ENUM("store", "pickup", "onsite", "ship") DEFAULT "store",
+ `appointment_date` DATE NOT NULL,
+ `appointment_time` VARCHAR(50) NOT NULL,
+ `appointment_datetime` DATETIME,
+ `duration_minutes` INT DEFAULT 60,
+ `status` ENUM("scheduled", "confirmed", "in_progress", "completed", "cancelled", "no_show", "rescheduled", "trashed") DEFAULT "scheduled",
+ `location_type` ENUM("store", "customer_address", "onsite", "shipping") DEFAULT "store",
+ `location_details` TEXT,
+ `notes` TEXT,
+ `reminder_sent` BOOLEAN DEFAULT FALSE,
+ `created_by` BIGINT(20) UNSIGNED NOT NULL,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY (`job_id`) REFERENCES ' . $wpdb->prefix . 'posts(ID) ON DELETE SET NULL,
+ FOREIGN KEY (`customer_id`) REFERENCES ' . $wpdb->prefix . 'users(ID),
+ FOREIGN KEY (`technician_id`) REFERENCES ' . $wpdb->prefix . 'users(ID),
+ FOREIGN KEY (`created_by`) REFERENCES ' . $wpdb->prefix . 'users(ID)
+ ) ' . $charset_collate . ';';
+ dbDelta($sql);
+
+ // Indexes for better performance
+ $wpdb->query('CREATE INDEX IF NOT EXISTS idx_appointment_date ON ' . $computer_repair_appointments . '(appointment_date)');
+ $wpdb->query('CREATE INDEX IF NOT EXISTS idx_appointment_status ON ' . $computer_repair_appointments . '(status)');
+ $wpdb->query('CREATE INDEX IF NOT EXISTS idx_appointment_technician ON ' . $computer_repair_appointments . '(technician_id)');
+ $wpdb->query('CREATE INDEX IF NOT EXISTS idx_appointment_customer ON ' . $computer_repair_appointments . '(customer_id)');
+ $wpdb->query('CREATE INDEX IF NOT EXISTS idx_appointment_datetime ON ' . $computer_repair_appointments . '(appointment_datetime)');
$sql = 'CREATE TABLE IF NOT EXISTS '.$computer_repair_customer_devices.'(
`device_id` bigint(20) NOT NULL AUTO_INCREMENT,
--- a/computer-repair-shop/admin_menu.php
+++ b/computer-repair-shop/admin_menu.php
@@ -32,7 +32,7 @@
__( 'Appointments', 'computer-repair-shop' ),
'delete_posts',
'wc-computer-rep-shop-appointments',
- array( $WCRB_APPOINTMENTS,'appointments_page_output' ),
+ array( $WCRB_APPOINTMENTS, 'appointments_page_output' ),
200 );
add_submenu_page(
'wc-computer-rep-shop-handle',
--- a/computer-repair-shop/computer_repair_shop.php
+++ b/computer-repair-shop/computer_repair_shop.php
@@ -3,7 +3,7 @@
Plugin Name: CRM WordPress Plugin - RepairBuddy
Plugin URI: https://www.webfulcreations.com/
Description: WordPress CRM Plugin which helps you manage your jobs, parts, services and extras better client and jobs management system.
- Version: 4.1116
+ Version: 4.1125
Author: Webful Creations
Author URI: https://www.webfulcreations.com/
License: GPLv2 or later.
@@ -14,7 +14,7 @@
Tested up to: 6.9
Requires PHP: 8.1
- @package : 4.1116
+ @package : 4.1125
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -22,7 +22,7 @@
if ( ! defined( 'DS' ) ) {
define( 'DS', '/' ); // Defining Directory seprator, not using php default Directory seprator to avoide problem in windows.
}
-define( 'WC_CR_SHOP_VERSION', '4.1116' );
+define( 'WC_CR_SHOP_VERSION', '4.1125' );
if ( ! function_exists( 'wc_language_plugin_init' ) ) :
/**
--- a/computer-repair-shop/lib/includes/classes/class-appointments-manager.php
+++ b/computer-repair-shop/lib/includes/classes/class-appointments-manager.php
@@ -0,0 +1,1333 @@
+<?php
+/**
+ * Appointments Manager
+ *
+ * Helpful class for managing appointments
+ *
+ * @package computer-repair-shop
+ * @version 4.1115
+ */
+defined( 'ABSPATH' ) || exit;
+
+if ( ! class_exists( 'WC_CR_Appointments_Manager' ) ) :
+class WC_CR_Appointments_Manager {
+ private static $instance = null;
+
+ public static function getInstance() {
+ if ( self::$instance == null ) {
+ self::$instance = new WC_CR_Appointments_Manager();
+ }
+ return self::$instance;
+ }
+
+ public function current_timestamp() {
+ return current_time( 'timestamp' );
+ }
+
+ public function current_date( $format = 'Y-m-d' ) {
+ return current_time( $format );
+ }
+
+ public function current_datetime( $format = 'mysql' ) {
+ return current_time( $format );
+ }
+
+ public function format_date( $timestamp, $format = 'Y-m-d' ) {
+ return date_i18n( $format, $timestamp );
+ }
+
+ public function date_to_timestamp( $date_string ) {
+ $wp_timezone = wp_timezone();
+ $datetime = DateTime::createFromFormat( 'Y-m-d', $date_string, $wp_timezone );
+
+ if ( $datetime === false ) {
+ $timestamp = strtotime($date_string . ' 00:00:00');
+ $gmt_offset = get_option('gmt_offset') * HOUR_IN_SECONDS;
+ return $timestamp + $gmt_offset;
+ }
+ return $datetime->getTimestamp();
+ }
+
+ public function generate_appointment_number() {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'wc_cr_appointments';
+
+ $year = date( 'Y', $this->current_timestamp() );
+
+ // Get last appointment number for this year
+ $last_number = $wpdb->get_var($wpdb->prepare(
+ "SELECT appointment_number FROM $table_name
+ WHERE appointment_number LIKE %s
+ ORDER BY appointment_id DESC LIMIT 1",
+ "APT-$year-%"
+ ));
+
+ if ( $last_number ) {
+ $parts = explode( '-', $last_number );
+ $last_seq = intval($parts[2]);
+ $new_seq = str_pad( $last_seq + 1, 4, '0', STR_PAD_LEFT );
+ } else {
+ $new_seq = '0001';
+ }
+ return "APT-$year-$new_seq";
+ }
+
+ public function get_appointment_types() {
+ $saved_enabled_options = get_option( 'wcrb_appointment_options_enabled', 'store,pickup,onsite,ship' );
+ $enabled_options_array = explode(',', $saved_enabled_options);
+ $enabled_options_array = array_map('trim', $enabled_options_array);
+
+ $types = array();
+
+ $option_keys = array( 'store', 'pickup', 'onsite', 'ship' );
+ foreach ($option_keys as $option_key) {
+ if (in_array($option_key, $enabled_options_array)) {
+ $title = get_option('wcrb_appointment_option_' . $option_key . '_title', '');
+ $description = get_option('wcrb_appointment_option_' . $option_key . '_description', '');
+
+ if ( empty( $title ) ) {
+ switch ( $option_key ) {
+ case 'store':
+ $title = __('Come by our store', 'computer-repair-shop');
+ break;
+ case 'pickup':
+ $title = __('Let us pickup your device', 'computer-repair-shop');
+ break;
+ case 'onsite':
+ $title = __('Repair on your location', 'computer-repair-shop');
+ break;
+ case 'ship':
+ $title = __('Ship device to us', 'computer-repair-shop');
+ break;
+ }
+ }
+
+ $types[$option_key] = $title;
+ }
+ }
+ if (empty($types)) {
+ $types['store'] = __('Come by our store', 'computer-repair-shop');
+ }
+ return $types;
+ }
+
+ public function get_appointment_type_descriptions() {
+ $descriptions = array();
+
+ $option_keys = array('store', 'pickup', 'onsite', 'ship');
+ foreach ($option_keys as $option_key) {
+ $description = get_option('wcrb_appointment_option_' . $option_key . '_description', '');
+ if (!empty($description)) {
+ $descriptions[$option_key] = $description;
+ }
+ }
+
+ return $descriptions;
+ }
+
+ public function get_shipping_terms() {
+ $saved_enabled_options = get_option('wcrb_appointment_options_enabled', 'store,pickup,onsite,ship');
+ $enabled_options_array = explode(',', $saved_enabled_options);
+
+ if (in_array('ship', $enabled_options_array)) {
+ return get_option('wcrb_appointment_option_ship_terms', '');
+ }
+
+ return '';
+ }
+
+ public function get_time_slot_duration() {
+ return get_option('wcrb_time_slot_duration', '30');
+ }
+
+ public function get_buffer_time() {
+ return get_option('wcrb_buffer_time', '15');
+ }
+
+ public function get_max_appointments_per_day() {
+ return get_option('wcrb_max_appointments_per_day', '20');
+ }
+
+ public function get_booking_lead_time() {
+ return get_option('wcrb_booking_lead_time', '24');
+ }
+
+ public function get_working_hours($day) {
+ $enabled = get_option('wcrb_' . $day . '_enabled', ($day == 'sunday' || $day == 'saturday') ? '0' : '1');
+
+ if ($enabled != '1') {
+ return array('enabled' => false);
+ }
+
+ return array(
+ 'enabled' => true,
+ 'start_time' => get_option('wcrb_' . $day . '_start_time', '09:00'),
+ 'end_time' => get_option('wcrb_' . $day . '_end_time', '17:00')
+ );
+ }
+
+ public function get_working_days_schedule() {
+ $days_of_week = array('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday');
+ $schedule = array();
+
+ foreach ($days_of_week as $day) {
+ $schedule[$day] = $this->get_working_hours($day);
+ }
+
+ return $schedule;
+ }
+
+ public function is_date_available($date) {
+ $date = trim($date);
+
+ $today = $this->current_date('Y-m-d');
+
+ if ( $date < $today ) {
+ return false;
+ }
+
+ $timestamp_utc = strtotime( $date . ' 00:00:00 UTC' );
+ $day_of_week = strtolower( date( 'l', $timestamp_utc ) );
+ $working_hours = $this->get_working_hours($day_of_week);
+
+ return $working_hours['enabled'];
+ }
+
+ public function get_available_time_slots($date, $technician_id = null) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'wc_cr_appointments';
+
+ $date_timestamp = $this->date_to_timestamp($date);
+
+ $day_of_week = strtolower(date('l', $date_timestamp));
+ $working_hours = $this->get_working_hours($day_of_week);
+
+ if ( ! $working_hours['enabled'] ) {
+ return array();
+ }
+
+ $slot_duration = $this->get_time_slot_duration();
+ $buffer_time = $this->get_buffer_time();
+ $max_appointments = $this->get_max_appointments_per_day();
+
+ if ( ! $this->is_date_available( $date ) ) {
+ return array();
+ }
+
+ $current_user = wp_get_current_user();
+ $is_technician = in_array( 'technician', $current_user->roles );
+
+ if ( $is_technician && empty( $technician_id ) ) {
+ $technician_id = $current_user->ID;
+ }
+
+ if ( $technician_id ) {
+ $existing_appointments = $wpdb->get_results($wpdb->prepare(
+ "SELECT appointment_time, technician_id FROM $table_name
+ WHERE appointment_date = %s
+ AND status IN ('scheduled', 'confirmed', 'in_progress')
+ AND (technician_id = %d OR technician_id IS NULL)",
+ $date,
+ $technician_id
+ ));
+ } else {
+ $existing_appointments = $wpdb->get_results($wpdb->prepare(
+ "SELECT appointment_time, technician_id FROM $table_name
+ WHERE appointment_date = %s
+ AND status IN ('scheduled', 'confirmed', 'in_progress')",
+ $date
+ ));
+ }
+
+ $appointment_count = count( $existing_appointments );
+ if ( $appointment_count >= $max_appointments ) {
+ return array();
+ }
+
+ // Generate time slots
+ $time_slots = array();
+ $current_time = strtotime($working_hours['start_time']);
+ $end_time = strtotime($working_hours['end_time']);
+
+ while ( $current_time < $end_time ) {
+ $slot_start_time = date('H:i', $current_time);
+ $slot_end_time = date('H:i', $current_time + ($slot_duration * 60));
+ $slot_display = date('g:i A', strtotime($slot_start_time)) . ' - ' . date('g:i A', strtotime($slot_end_time));
+
+ if ( $slot_end_time > date( 'H:i', $end_time ) ) {
+ break;
+ }
+
+ $is_booked = false;
+ foreach ( $existing_appointments as $appointment ) {
+ $appointment_parts = explode( ' - ', $appointment->appointment_time );
+ if (count($appointment_parts) >= 1) {
+ $appointment_start_str = trim($appointment_parts[0]);
+ $appointment_start_time = date('H:i', strtotime($appointment_start_str));
+
+ if ($slot_start_time === $appointment_start_time) {
+ $is_booked = true;
+ break;
+ }
+ }
+ }
+
+ if (!$is_booked) {
+ $time_slots[] = array(
+ 'start' => $slot_start_time,
+ 'end' => $slot_end_time,
+ 'display' => $slot_display
+ );
+ }
+
+ // Move to next slot with buffer time
+ $current_time += ($slot_duration + $buffer_time) * 60;
+ }
+
+ return $time_slots;
+ }
+
+ public function add_appointment($data) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'wc_cr_appointments';
+
+ // Generate appointment number if not provided
+ if (empty($data['appointment_number'])) {
+ $data['appointment_number'] = $this->generate_appointment_number();
+ }
+
+ // Calculate datetime if date and time are provided
+ if (!empty($data['appointment_date']) && !empty($data['appointment_time'])) {
+ $time_start = explode(' - ', $data['appointment_time'])[0];
+ $data['appointment_datetime'] = $data['appointment_date'] . ' ' . $time_start;
+ }
+
+ // Get duration from settings based on appointment type
+ if (empty($data['duration_minutes'])) {
+ $data['duration_minutes'] = $this->get_time_slot_duration();
+ }
+
+ // Set default values using WordPress timezone
+ $defaults = array(
+ 'appointment_type' => 'store',
+ 'status' => 'scheduled',
+ 'location_type' => 'store',
+ 'reminder_sent' => FALSE,
+ 'created_by' => get_current_user_id(),
+ 'created_at' => $this->current_datetime('mysql'),
+ 'updated_at' => $this->current_datetime('mysql')
+ );
+
+ $data = wp_parse_args( $data, $defaults );
+
+ $wpdb->insert( $table_name, $data );
+
+ $appointment_id = $wpdb->insert_id;
+
+ // Log to job history if job_id exists
+ $_jobid = $data['job_id'] ?? 0;
+
+ if ( $_jobid != 0 ) {
+ $appointment_msg = sprintf(
+ __( 'Appointment added %s for date %s for time %s', 'computer-repair-shop' ),
+ $data['appointment_number'],
+ $data['appointment_date'],
+ $data['appointment_time']
+ );
+
+ // Add to job history
+ if ( class_exists( 'WCRB_JOB_HISTORY_LOGS' ) ) {
+ $WCRB_JOB_HISTORY_LOGS = WCRB_JOB_HISTORY_LOGS::getInstance();
+ $history_data = array(
+ "job_id" => $_jobid,
+ "name" => $appointment_msg,
+ "type" => 'private',
+ "field" => '_wc_appointment_data',
+ "change_detail" => __('Appointment Added', 'computer-repair-shop')
+ );
+ $WCRB_JOB_HISTORY_LOGS->wc_record_job_history($history_data);
+ }
+ }
+ return $appointment_id;
+ }
+
+ /**
+ * Simple status update method
+ */
+ public function update_status($appointment_id, $new_status) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'wc_cr_appointments';
+
+ // Get appointment before update
+ $appointment = $this->get_appointment($appointment_id);
+
+ $result = $wpdb->update(
+ $table_name,
+ array(
+ 'status' => $new_status,
+ 'updated_at' => $this->current_datetime('mysql')
+ ),
+ array('appointment_id' => $appointment_id)
+ );
+
+ // Log to job history if job_id exists
+ if ($result && $appointment && $appointment->job_id && class_exists('WCRB_JOB_HISTORY_LOGS')) {
+ $WCRB_JOB_HISTORY_LOGS = WCRB_JOB_HISTORY_LOGS::getInstance();
+ $history_data = array(
+ "job_id" => $appointment->job_id,
+ "name" => sprintf(__('Appointment status updated: %s', 'computer-repair-shop'), $appointment->appointment_number),
+ "type" => 'private',
+ "field" => '_wc_appointment_data',
+ "change_detail" => sprintf(__('Status changed to: %s', 'computer-repair-shop'), $new_status)
+ );
+ $WCRB_JOB_HISTORY_LOGS->wc_record_job_history($history_data);
+ }
+
+ return $result;
+ }
+
+ public function get_appointment($appointment_id) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'wc_cr_appointments';
+
+ $appointment = $wpdb->get_row($wpdb->prepare(
+ "SELECT a.*,
+ u.ID as customer_user_id,
+ u.display_name as customer_name,
+ u.user_email as customer_email,
+ t.display_name as technician_name,
+ c.display_name as created_by_name,
+ p.post_title as job_title,
+ p.ID as job_post_id
+ FROM $table_name a
+ LEFT JOIN {$wpdb->prefix}users u ON a.customer_id = u.ID
+ LEFT JOIN {$wpdb->prefix}users t ON a.technician_id = t.ID
+ LEFT JOIN {$wpdb->prefix}users c ON a.created_by = c.ID
+ LEFT JOIN {$wpdb->prefix}posts p ON a.job_id = p.ID
+ WHERE a.appointment_id = %d",
+ $appointment_id
+ ));
+
+ if ($appointment && $appointment->customer_user_id) {
+ // Add customer meta data
+ $customer_id = $appointment->customer_user_id;
+
+ // Get first_name and last_name from user meta
+ $appointment->first_name = get_user_meta($customer_id, "first_name", true);
+ $appointment->last_name = get_user_meta($customer_id, "last_name", true);
+
+ // Get other customer meta
+ $appointment->customer_phone = get_user_meta($customer_id, "billing_phone", true);
+ $appointment->customer_company = get_user_meta($customer_id, "billing_company", true);
+ $appointment->customer_tax = get_user_meta($customer_id, "billing_tax", true);
+ $appointment->customer_address_1 = get_user_meta($customer_id, 'billing_address_1', true);
+ $appointment->customer_city = get_user_meta($customer_id, 'billing_city', true);
+ $appointment->customer_postcode = get_user_meta($customer_id, 'billing_postcode', true);
+ $appointment->customer_state = get_user_meta($customer_id, 'billing_state', true);
+ $appointment->customer_country = get_user_meta($customer_id, 'billing_country', true);
+
+ // Build full name
+ $first_name = !empty($appointment->first_name) ? $appointment->first_name : '';
+ $last_name = !empty($appointment->last_name) ? $appointment->last_name : '';
+ $appointment->customer_full_name = trim($first_name . ' ' . $last_name);
+ if (empty($appointment->customer_full_name)) {
+ $appointment->customer_full_name = $appointment->customer_name;
+ }
+ }
+
+ return $appointment;
+ }
+
+ public function get_appointments($args = array()) {
+ global $wpdb;
+
+ $defaults = array(
+ 'limit' => 20,
+ 'offset' => 0,
+ 'orderby' => 'appointment_datetime',
+ 'order' => 'ASC',
+ 'search' => '',
+ 'appointment_type' => '',
+ 'status' => '',
+ 'technician_id' => '',
+ 'customer_id' => '',
+ 'start_date' => '',
+ 'end_date' => '',
+ 'job_id' => '',
+ 'include_trashed' => false
+ );
+
+ $args = wp_parse_args($args, $defaults);
+
+ $table_name = $wpdb->prefix . 'wc_cr_appointments';
+
+ $where = array('1=1');
+ $params = array();
+
+ // Check if current user is a technician
+ $current_user = wp_get_current_user();
+ $is_technician = in_array('technician', $current_user->roles);
+
+ // If user is a technician and no specific technician filter is set,
+ // automatically filter to only show their appointments
+ if ($is_technician && empty($args['technician_id'])) {
+ $where[] = 'a.technician_id = %d';
+ $params[] = $current_user->ID;
+ } elseif (!empty($args['technician_id'])) {
+ // If technician filter is explicitly set, use it
+ $where[] = 'a.technician_id = %d';
+ $params[] = $args['technician_id'];
+ }
+
+ // Type filter - only show enabled appointment types
+ if (!empty($args['appointment_type'])) {
+ $where[] = 'a.appointment_type = %s';
+ $params[] = $args['appointment_type'];
+ } else {
+ // Filter by enabled appointment types only
+ $enabled_types = array_keys($this->get_appointment_types());
+ if (!empty($enabled_types)) {
+ $placeholders = array_fill(0, count($enabled_types), '%s');
+ $where[] = 'a.appointment_type IN (' . implode(',', $placeholders) . ')';
+ $params = array_merge($params, $enabled_types);
+ }
+ }
+
+ // Other filters...
+ if (!empty($args['status'])) {
+ $where[] = 'a.status = %s';
+ $params[] = $args['status'];
+ } elseif (!$args['include_trashed']) {
+ // Only exclude trashed if not explicitly including them
+ $where[] = 'a.status != "trashed"';
+ }
+
+ if (!empty($args['customer_id'])) {
+ $where[] = 'a.customer_id = %d';
+ $params[] = $args['customer_id'];
+ }
+
+ if (!empty($args['job_id'])) {
+ $where[] = 'a.job_id = %d';
+ $params[] = $args['job_id'];
+ }
+
+ if (!empty($args['start_date'])) {
+ $where[] = 'a.appointment_date >= %s';
+ $params[] = $args['start_date'];
+ }
+
+ if (!empty($args['end_date'])) {
+ $where[] = 'a.appointment_date <= %s';
+ $params[] = $args['end_date'];
+ }
+
+ if (!empty($args['search'])) {
+ $where[] = '(a.appointment_number LIKE %s OR u.display_name LIKE %s OR u.user_email LIKE %s)';
+ $search_term = '%' . $wpdb->esc_like($args['search']) . '%';
+ $params[] = $search_term;
+ $params[] = $search_term;
+ $params[] = $search_term;
+ }
+
+ $where_clause = implode(' AND ', $where);
+
+ // Get total count
+ $count_query = "SELECT COUNT(*) FROM $table_name a
+ LEFT JOIN {$wpdb->prefix}users u ON a.customer_id = u.ID
+ WHERE $where_clause";
+
+ if (!empty($params)) {
+ $count_query = $wpdb->prepare($count_query, $params);
+ }
+
+ $total = $wpdb->get_var($count_query);
+
+ // Get data
+ $query = "SELECT a.*,
+ u.display_name as customer_name,
+ u.user_email as customer_email,
+ t.display_name as technician_name,
+ p.post_title as job_title,
+ c.display_name as created_by_name
+ FROM $table_name a
+ LEFT JOIN {$wpdb->prefix}users u ON a.customer_id = u.ID
+ LEFT JOIN {$wpdb->prefix}users t ON a.technician_id = t.ID
+ LEFT JOIN {$wpdb->prefix}posts p ON a.job_id = p.ID
+ LEFT JOIN {$wpdb->prefix}users c ON a.created_by = c.ID
+ WHERE $where_clause
+ ORDER BY a.{$args['orderby']} {$args['order']}";
+
+ // Add LIMIT and OFFSET if limit > 0
+ if ($args['limit'] > 0) {
+ $query .= " LIMIT %d OFFSET %d";
+ $params[] = $args['limit'];
+ $params[] = $args['offset'];
+ }
+
+ // Prepare and execute the query
+ if (!empty($params)) {
+ $query = $wpdb->prepare($query, $params);
+ }
+
+ $appointments = $wpdb->get_results($query);
+
+ return array(
+ 'appointments' => $appointments,
+ 'total' => $total
+ );
+ }
+
+ public function get_calendar_appointments($start_date, $end_date, $technician_id = null) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'wc_cr_appointments';
+
+ $where = array('1=1');
+ $params = array();
+
+ // Date range
+ $where[] = 'a.appointment_date >= %s';
+ $params[] = $start_date;
+ $where[] = 'a.appointment_date <= %s';
+ $params[] = $end_date;
+
+ // Check if current user is a technician
+ $current_user = wp_get_current_user();
+ $is_technician = in_array('technician', $current_user->roles);
+
+ // If user is a technician and no specific technician filter is set,
+ // automatically filter to only show their appointments
+ if ($is_technician && empty($technician_id)) {
+ $where[] = 'a.technician_id = %d';
+ $params[] = $current_user->ID;
+ } elseif ($technician_id) {
+ // If technician filter is explicitly set, use it
+ $where[] = '(a.technician_id = %d OR a.technician_id IS NULL)';
+ $params[] = $technician_id;
+ }
+
+ // Only show scheduled/confirmed appointments
+ $where[] = 'a.status IN ("scheduled", "confirmed", "in_progress")';
+
+ // Only show enabled appointment types
+ $enabled_types = array_keys($this->get_appointment_types());
+ if (!empty($enabled_types)) {
+ $placeholders = array_fill(0, count($enabled_types), '%s');
+ $where[] = 'a.appointment_type IN (' . implode(',', $placeholders) . ')';
+ $params = array_merge($params, $enabled_types);
+ }
+
+ $where_clause = implode(' AND ', $where);
+
+ $query = "SELECT a.*,
+ u.display_name as customer_name,
+ u.user_email as customer_email,
+ t.display_name as technician_name,
+ p.post_title as job_title
+ FROM $table_name a
+ LEFT JOIN {$wpdb->prefix}users u ON a.customer_id = u.ID
+ LEFT JOIN {$wpdb->prefix}users t ON a.technician_id = t.ID
+ LEFT JOIN {$wpdb->prefix}posts p ON a.job_id = p.ID
+ WHERE $where_clause
+ ORDER BY a.appointment_date, a.appointment_time";
+
+ if (!empty($params)) {
+ $query = $wpdb->prepare($query, $params);
+ }
+
+ return $wpdb->get_results($query);
+ }
+
+ public function delete_appointment( $appointment_id ) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'wc_cr_appointments';
+
+ // Get appointment data before deletion
+ $appointment = $this->get_appointment($appointment_id);
+
+ $result = $wpdb->update(
+ $table_name,
+ array(
+ 'status' => 'trashed',
+ 'updated_at' => $this->current_datetime('mysql')
+ ),
+ array('appointment_id' => $appointment_id)
+ );
+
+ // Log to job history if job_id exists
+ if ($result && $appointment && $appointment->job_id && class_exists('WCRB_JOB_HISTORY_LOGS')) {
+ $appointment_msg = sprintf(
+ __( 'Appointment deleted %s for date %s for time %s', 'computer-repair-shop' ),
+ $appointment->appointment_number,
+ $appointment->appointment_date,
+ $appointment->appointment_time
+ );
+
+ $WCRB_JOB_HISTORY_LOGS = WCRB_JOB_HISTORY_LOGS::getInstance();
+ $history_data = array(
+ "job_id" => $appointment->job_id,
+ "name" => $appointment_msg,
+ "type" => 'private',
+ "field" => '_wc_appointment_data',
+ "change_detail" => __('Appointment Deleted (Moved to Trash)', 'computer-repair-shop')
+ );
+ $WCRB_JOB_HISTORY_LOGS->wc_record_job_history($history_data);
+ }
+
+ return $result;
+ }
+
+ public function get_appointment_statuses() {
+ return array(
+ 'scheduled' => __('Scheduled', 'computer-repair-shop'),
+ 'confirmed' => __('Confirmed', 'computer-repair-shop'),
+ 'in_progress' => __('In Progress', 'computer-repair-shop'),
+ 'completed' => __('Completed', 'computer-repair-shop'),
+ 'cancelled' => __('Cancelled', 'computer-repair-shop'),
+ 'no_show' => __('No Show', 'computer-repair-shop'),
+ 'rescheduled' => __('Rescheduled', 'computer-repair-shop')
+ );
+ }
+
+ public function get_appointment_type_colors() {
+ return array(
+ 'store' => '#3498db', // Blue
+ 'pickup' => '#2ecc71', // Green
+ 'onsite' => '#e74c3c', // Red
+ 'ship' => '#f39c12' // Orange
+ );
+ }
+
+ public function get_statistics($period = 'month', $technician_id = null) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'wc_cr_appointments';
+
+ $where = array('1=1');
+ $params = array();
+
+ // Check if current user is a technician
+ $current_user = wp_get_current_user();
+ $is_technician = in_array('technician', $current_user->roles);
+
+ // If user is a technician and no specific technician filter is set,
+ // automatically filter to only show their statistics
+ if ($is_technician && empty($technician_id)) {
+ $where[] = 'technician_id = %d';
+ $params[] = $current_user->ID;
+ } elseif ($technician_id) {
+ // If technician filter is explicitly set, use it
+ $where[] = 'technician_id = %d';
+ $params[] = $technician_id;
+ }
+
+ // Only include enabled appointment types
+ $enabled_types = array_keys($this->get_appointment_types());
+ if (!empty($enabled_types)) {
+ $placeholders = array_fill(0, count($enabled_types), '%s');
+ $where[] = 'appointment_type IN (' . implode(',', $placeholders) . ')';
+ $params = array_merge($params, $enabled_types);
+ }
+
+ $where_clause = implode(' AND ', $where);
+ $where_sql = $where_clause ? "WHERE $where_clause" : '';
+
+ // Get totals
+ $totals_query = "SELECT
+ COUNT(*) as total_count,
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_count,
+ SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_count,
+ SUM(CASE WHEN status = 'no_show' THEN 1 ELSE 0 END) as no_show_count
+ FROM $table_name
+ $where_sql";
+
+ if ($params) {
+ $totals_query = $wpdb->prepare($totals_query, $params);
+ }
+
+ $totals = $wpdb->get_row($totals_query);
+
+ // Get type breakdown
+ $type_query = "SELECT
+ appointment_type,
+ COUNT(*) as count
+ FROM $table_name
+ $where_sql
+ GROUP BY appointment_type";
+
+ if ($params) {
+ $type_query = $wpdb->prepare($type_query, $params);
+ }
+
+ $types = $wpdb->get_results($type_query);
+
+ // Get status breakdown
+ $status_query = "SELECT
+ status,
+ COUNT(*) as count
+ FROM $table_name
+ $where_sql
+ GROUP BY status";
+
+ if ($params) {
+ $status_query = $wpdb->prepare($status_query, $params);
+ }
+
+ $statuses = $wpdb->get_results($status_query);
+
+ return array(
+ 'totals' => $totals,
+ 'types' => $types,
+ 'statuses' => $statuses
+ );
+ }
+
+ /**
+ * Check for appointment conflicts
+ */
+ public function check_conflict($date, $time, $technician_id, $exclude_appointment_id = null) {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'wc_cr_appointments';
+
+ $time_start = explode(' - ', $time)[0];
+
+ $query = "SELECT COUNT(*) as conflict_count
+ FROM $table_name
+ WHERE appointment_date = %s
+ AND appointment_time LIKE %s
+ AND technician_id = %d
+ AND status IN ('scheduled', 'confirmed', 'in_progress')";
+
+ $params = array($date, $time_start . '%', $technician_id);
+
+ if ($exclude_appointment_id) {
+ $query .= " AND appointment_id != %d";
+ $params[] = $exclude_appointment_id;
+ }
+
+ $result = $wpdb->get_row($wpdb->prepare($query, $params));
+
+ return $result->conflict_count > 0;
+ }
+
+ /**
+ * Check if appointment type is enabled
+ */
+ public function is_appointment_type_enabled($type) {
+ $enabled_types = array_keys($this->get_appointment_types());
+ return in_array($type, $enabled_types);
+ }
+}
+endif;
+
+// Initialize the class
+function WC_CR_APPOINTMENTS_MANAGEMENT() {
+ return WC_CR_Appointments_Manager::getInstance();
+}
+
+/**
+ * Ajax Methods
+ * Add Appointment
+ * Update Appointment
+ * Get Appointment
+ */
+/**
+ * AJAX handler for getting available time slots
+ */
+add_action('wp_ajax_wcrb_get_available_time_slots', 'wcrb_get_available_time_slots_ajax');
+add_action('wp_ajax_nopriv_wcrb_get_available_time_slots', 'wcrb_get_available_time_slots_ajax');
+
+function wcrb_get_available_time_slots_ajax() {
+ // Verify nonce
+ if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'wcrb_appointment_nonce')) {
+ wp_send_json_error(array('message' => 'Security check failed'));
+ }
+
+ $appointment_manager = WC_CR_APPOINTMENTS_MANAGEMENT();
+
+ $date = isset($_POST['date']) ? sanitize_text_field($_POST['date']) : '';
+ $technician_id = isset($_POST['technician_id']) ? intval($_POST['technician_id']) : null;
+
+ if (empty( $date ) ) {
+ wp_send_json_error( array( 'message' => 'Date is required' ) );
+ }
+
+ // Validate date format
+ $date_parts = explode( '-', $date );
+ if ( count( $date_parts ) !== 3 || !checkdate($date_parts[1], $date_parts[2], $date_parts[0] ) ) {
+ wp_send_json_error(array('message' => 'Invalid date format'));
+ }
+
+ // Check if date is available for booking
+ if ( ! $appointment_manager->is_date_available( $date ) ) {
+ wp_send_json_success(array(
+ 'time_slots' => array(),
+ 'message' => 'No time slots available for this date'
+ ));
+ }
+
+ // Get available time slots
+ $time_slots = $appointment_manager->get_available_time_slots($date, $technician_id);
+
+ if (empty($time_slots)) {
+ wp_send_json_success(array(
+ 'time_slots' => array(),
+ 'message' => 'All time slots are booked for this date'
+ ));
+ }
+
+ wp_send_json_success(array(
+ 'time_slots' => $time_slots,
+ 'message' => sprintf(__('%d time slots available', 'computer-repair-shop'), count($time_slots))
+ ));
+}
+
+/**
+ * AJAX handler for adding appointment
+ */
+add_action('wp_ajax_wcrb_add_appointment', 'wcrb_add_appointment_ajax');
+add_action('wp_ajax_nopriv_wcrb_add_appointment', 'wcrb_add_appointment_ajax');
+
+function wcrb_add_appointment_ajax() {
+ // Verify nonce
+ if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'wcrb_appointment_nonce')) {
+ wp_send_json_error(array('message' => 'Security check failed'));
+ }
+
+ $appointment_manager = WC_CR_APPOINTMENTS_MANAGEMENT();
+
+ // Validate required fields
+ $required_fields = array('customer_id', 'appointment_date', 'appointment_time', 'appointment_type');
+ foreach ($required_fields as $field) {
+ if (empty($_POST[$field])) {
+ wp_send_json_error(array('message' => sprintf(__('%s is required', 'computer-repair-shop'), $field)));
+ }
+ }
+
+ // Check if appointment type is enabled
+ $appointment_type = sanitize_text_field($_POST['appointment_type']);
+ if (!$appointment_manager->is_appointment_type_enabled($appointment_type)) {
+ wp_send_json_error(array('message' => __('This appointment type is not available', 'computer-repair-shop')));
+ }
+
+ $_technician_id = !empty($_POST['technician_id']) ? intval($_POST['technician_id']) : null;
+ $_technician_id = ( empty( $_technician_id ) && isset( $_POST['technician_idadd'] ) ) ? sanitize_text_field( $_POST['technician_idadd'] ) : $_technician_id;
+
+ // Prepare data
+ $data = array(
+ 'customer_id' => intval($_POST['customer_id']),
+ 'job_id' => !empty($_POST['job_id']) ? intval($_POST['job_id']) : null,
+ 'technician_id' => $_technician_id,
+ 'appointment_type' => $appointment_type,
+ 'appointment_date' => sanitize_text_field($_POST['appointment_date']),
+ 'appointment_time' => sanitize_text_field($_POST['appointment_time']),
+ 'duration_minutes' => !empty($_POST['duration_minutes']) ? intval($_POST['duration_minutes']) : $appointment_manager->get_time_slot_duration(),
+ 'status' => !empty($_POST['status']) ? sanitize_text_field($_POST['status']) : 'scheduled',
+ 'location_details' => !empty($_POST['location_details']) ? sanitize_textarea_field($_POST['location_details']) : '',
+ 'notes' => !empty($_POST['notes']) ? sanitize_textarea_field($_POST['notes']) : '',
+ 'created_by' => get_current_user_id()
+ );
+
+ // Check for conflicts if technician is assigned
+ if (!empty($data['technician_id'])) {
+ $conflict = $appointment_manager->check_conflict(
+ $data['appointment_date'],
+ $data['appointment_time'],
+ $data['technician_id']
+ );
+
+ if ($conflict) {
+ wp_send_json_error(array('message' => __('Time slot is already booked for this technician', 'computer-repair-shop')));
+ }
+ }
+
+ // Add appointment
+ $appointment_id = $appointment_manager->add_appointment($data);
+
+ if ($appointment_id) {
+ wp_send_json_success(array(
+ 'message' => __('Appointment added successfully!', 'computer-repair-shop'),
+ 'appointment_id' => $appointment_id
+ ));
+ } else {
+ wp_send_json_error(array('message' => __('Failed to add appointment', 'computer-repair-shop')));
+ }
+}
+
+/**
+ * AJAX handler for getting appointment details with enhanced HTML view
+ */
+add_action('wp_ajax_wcrb_get_appointment_details', 'wcrb_get_appointment_details_ajax');
+add_action('wp_ajax_nopriv_wcrb_get_appointment_details', 'wcrb_get_appointment_details_ajax');
+
+function wcrb_get_appointment_details_ajax() {
+ // Verify nonce
+ if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'wcrb_appointment_nonce')) {
+ wp_send_json_error(array('message' => 'Security check failed'));
+ }
+
+ $appointment_id = isset($_POST['appointment_id']) ? intval($_POST['appointment_id']) : 0;
+
+ if (!$appointment_id) {
+ wp_send_json_error(array('message' => 'Appointment ID is required'));
+ }
+
+ $appointment_manager = WC_CR_APPOINTMENTS_MANAGEMENT();
+ $appointment = $appointment_manager->get_appointment($appointment_id);
+
+ if (!$appointment) {
+ wp_send_json_error(array('message' => 'Appointment not found'));
+ }
+
+ // Get appointment types and statuses
+ $appointment_types = $appointment_manager->get_appointment_types();
+ $appointment_statuses = $appointment_manager->get_appointment_statuses();
+ $appointment_colors = $appointment_manager->get_appointment_type_colors();
+
+ // Status badge classes
+ $status_class = array(
+ 'scheduled' => 'secondary',
+ 'confirmed' => 'primary',
+ 'in_progress' => 'info',
+ 'completed' => 'success',
+ 'cancelled' => 'danger',
+ 'no_show' => 'warning',
+ 'rescheduled' => 'warning'
+ );
+
+ // Format dates and times
+ $appointment_date_formatted = date_i18n(get_option('date_format'), strtotime($appointment->appointment_date));
+ $appointment_time_formatted = esc_html($appointment->appointment_time);
+ $created_at_formatted = date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($appointment->created_at));
+ $updated_at_formatted = date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($appointment->updated_at));
+
+ // Build customer details HTML like in wcrb_reload_customer_data
+ $customer_html = '';
+ if ($appointment->customer_user_id) {
+ $customer_html .= !empty($appointment->customer_full_name) ? esc_html($appointment->customer_full_name) . '<br>' : '';
+
+ $contact_row = '';
+ $contact_row .= !empty($appointment->customer_email) ? '<strong>E :</strong> ' . esc_html($appointment->customer_email) . ' ' : '';
+ $contact_row .= !empty($appointment->customer_phone) ? '<strong>P :</strong> ' . esc_html($appointment->customer_phone) . '' : '';
+ $customer_html .= !empty($contact_row) ? $contact_row . '<br>' : '';
+
+ $company_row = '';
+ $company_row .= !empty($appointment->customer_company) ? '<strong>' . esc_html__('Company', 'computer-repair-shop') . ' :</strong> ' . esc_html($appointment->customer_company) . ' ' : '';
+ $company_row .= !empty($appointment->customer_tax) ? '<strong>' . esc_html__('Tax ID', 'computer-repair-shop') . ' :</strong> ' . esc_html($appointment->customer_tax) . '' : '';
+ $customer_html .= !empty($company_row) ? $company_row . '<br>' : '';
+
+ // Build address
+ if (!empty($appointment->customer_address_1) || !empty($appointment->customer_city) || !empty($appointment->customer_postcode)) {
+ $customer_html .= '<strong>' . esc_html__('Address', 'computer-repair-shop') . ' :</strong> ';
+
+ $address_parts = array();
+ if (!empty($appointment->customer_address_1)) $address_parts[] = esc_html($appointment->customer_address_1);
+ if (!empty($appointment->customer_city)) $address_parts[] = esc_html($appointment->customer_city);
+ if (!empty($appointment->customer_postcode)) $address_parts[] = esc_html($appointment->customer_postcode);
+ if (!empty($appointment->customer_state)) $address_parts[] = esc_html($appointment->customer_state);
+ if (!empty($appointment->customer_country)) $address_parts[] = esc_html($appointment->customer_country);
+
+ $customer_html .= implode(', ', $address_parts);
+ }
+ }
+
+ // Build the HTML
+ $html = '
+ <div class="appointment-details-enhanced compact">
+ <div class="row g-0">
+ <!-- Appointment Number and Date -->
+ <div class="col-md-6 mb-2">
+ <div class="info-card appointment-number-card">
+ <div class="info-label">' . __('Appointment #', 'computer-repair-shop') . '</div>
+ <div class="info-value">' . esc_html($appointment->appointment_number) . '</div>
+ </div>
+ </div>
+ <div class="col-md-6 mb-2">
+ <div class="info-card date-card">
+ <div class="info-label">' . __('Date', 'computer-repair-shop') . '</div>
+ <div class="info-value">' . $appointment_date_formatted . '</div>
+ </div>
+ </div>
+
+ <!-- Time and Duration -->
+ <div class="col-md-6 mb-2">
+ <div class="info-card time-card">
+ <div class="info-label">' . __('Time', 'computer-repair-shop') . '</div>
+ <div class="info-value">' . $appointment_time_formatted . '</div>
+ </div>
+ </div>
+ <div class="col-md-6 mb-2">
+ <div class="info-card duration-card">
+ <div class="info-label">' . __('Duration', 'computer-repair-shop') . '</div>
+ <div class="info-value">' . esc_html($appointment->duration_minutes) . ' ' . __('minutes', 'computer-repair-shop') . '</div>
+ </div>
+ </div>
+
+ <!-- Type and Status -->
+ <div class="col-md-6 mb-2">
+ <div class="info-card type-card">
+ <div class="info-label">' . __('Type', 'computer-repair-shop') . '</div>
+ <div class="info-value">
+ <span class="type-badge" style="background-color: ' . esc_attr($appointment_colors[$appointment->appointment_type] ?? '#6c757d') . '">
+ ' . esc_html($appointment_types[$appointment->appointment_type] ?? $appointment->appointment_type) . '
+ </span>
+ </div>
+ </div>
+ </div>
+ <div class="col-md-6 mb-2">
+ <div class="info-card status-card">
+ <div class="info-label">' . __('Status', 'computer-repair-shop') . '</div>
+ <div class="info-value">
+ <span class="status-badge badge-' . ($status_class[$appointment->status] ?? 'secondary') . '">
+ ' . esc_html($appointment_statuses[$appointment->status] ?? $appointment->status) . '
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <!-- Customer Information -->
+ <div class="col-12 mb-2">
+ <div class="info-card customer-card">
+ <div class="info-label">' . __('Customer Details', 'computer-repair-shop') . '</div>
+ <div class="info-value">';
+
+ if (!empty($customer_html)) {
+ $html .= $customer_html;
+ } else {
+ $html .= esc_html($appointment->customer_name);
+ if (!empty($appointment->customer_email)) {
+ $html .= '<br><a href="mailto:' . esc_attr($appointment->customer_email) . '" class="text-decoration-none">' . esc_html($appointment->customer_email) . '</a>';
+ }
+ }
+
+ $html .= '
+ </div>
+ </div>
+ </div>
+
+ <!-- Technician Information -->
+ <div class="col-md-6 mb-2">
+ <div class="info-card technician-card">
+ <div class="info-label">' . __('Technician', 'computer-repair-shop') . '</div>
+ <div class="info-value">' . ($appointment->technician_name ? esc_html($appointment->technician_name) : '<span class="text-muted">' . __('Unassigned', 'computer-repair-shop') . '</span>') . '</div>
+ </div>
+ </div>';
+
+ // Job Information
+ if ($appointment->job_id && $appointment->job_title) {
+ $job_link = admin_url('post.php?post=' . $appointment->job_id . '&action=edit');
+ $html .= '
+ <div class="col-md-6 mb-2">
+ <div class="info-card job-card">
+ <div class="info-label">' . __('Job', 'computer-repair-shop') . '</div>
+ <div class="info-value">
+ <a href="' . esc_url($job_link) . '" target="_blank" class="text-decoration-none">
+ ' . esc_html(wp_trim_words($appointment->job_title, 4)) . '
+ </a>
+ </div>
+ </div>
+ </div>';
+ } else {
+ $html .= '
+ <div class="col-md-6 mb-2">
+ <div class="info-card job-card">
+ <div class="info-label">' . __('Job', 'computer-repair-shop') . '</div>
+ <div class="info-value">
+ <span class="text-muted">' . __('No Job', 'computer-repair-shop') . '</span>
+ </div>
+ </div>
+ </div>';
+ }
+
+ // Location Details (if applicable)
+ if (!empty($appointment->location_details) && in_array($appointment->appointment_type, ['pickup', 'onsite'])) {
+ $html .= '
+ <div class="col-12 mb-2">
+ <div class="info-card location-card">
+ <div class="info-label">' . __('Location Details', 'computer-repair-shop') . '</div>
+ <div class="info-value location-text">
+ ' . nl2br(esc_html($appointment->location_details)) . '
+ </div>
+ </div>
+ </div>';
+ }
+
+ // Notes
+ if (!empty($appointment->notes)) {
+ $html .= '
+ <div class="col-12 mb-2">
+ <div class="info-card notes-card">
+ <div class="info-label">' . __('Notes', 'computer-repair-shop') . '</div>
+ <div class="info-value notes-text">
+ ' . nl2br(esc_html($appointment->notes)) . '
+ </div>
+ </div>
+ </div>';
+ }
+
+ // Metadata
+ $html .= '
+ <!-- Created Information -->
+ <div class="col-md-6 mb-2">
+ <div class="info-card created-by-card">
+ <div class="info-label">' . __('Created By', 'computer-repair-shop') . '</div>
+ <div class="info-value">' . esc_html($appointment->created_by_name) . '</div>
+ </div>
+ </div>
+ <div class="col-md-6 mb-2">
+ <div class="info-card created-at-card">
+ <div class="info-label">' . __('Created At', 'computer-repair-shop') . '</div>
+ <div class="info-value">' . $created_at_formatted . '</div>
+ </div>
+ </div>';
+
+ // Updated Information (if different from created)
+ if ($appointment->created_at != $appointment->updated_at) {
+ $html .= '
+ <div class="col-md-6 mb-2">
+ <div class="info-card updated-at-card">
+ <div class="info-label">' . __('Last Updated', 'computer-repair-shop') . '</div>
+ <div class="info-value">' . $updated_at_formatted . '</div>
+ </div>
+ </div>';
+ }
+
+ $html .= '
+ </div>
+ </div>';
+
+ wp_send_json_success(array('html' => $html));
+}
+
+/**
+ * AJAX handler for updating appointment status
+ */
+add_action('wp_ajax_wcrb_update_appointment_status', 'wcrb_update_appointment_status_ajax');
+add_action('wp_ajax_nopriv_wcrb_update_appointment_status', 'wcrb_update_appointment_status_ajax');
+
+function wcrb_update_appointment_status_ajax() {
+ // Verify nonce
+ if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'wcrb_appointment_nonce')) {
+ wp_send_json_error(array('message' => 'Security check failed'));
+ }
+
+ $appointment_id = isset($_POST['appointment_id']) ? intval($_POST['appointment_id']) : 0;
+ $new_status = isset($_POST['new_status']) ? sanitize_text_field($_POST['new_status']) : '';
+
+ if (!$appointment_id) {
+ wp_send_json_error(array('message' => 'Appointment ID is required'));
+ }
+
+ if (empty($new_status)) {
+ wp_send_json_error(array('message' => 'New status is required'));
+ }
+
+ $appointment_manager = WC_CR_APPOINTMENTS_MANAGEMENT();
+
+ // Validate status
+ $valid_statuses = $appointment_manager->get_appointment_statuses();
+ if (!isset($valid_statuses[$new_status])) {
+ wp_send_json_error(array('message' => 'Invalid status'));
+ }
+
+ // Use simple status update method
+ $result = $appointment_manager->update_status($appointment_id, $new_status);
+
+ if ($result !== false) {
+ wp_send_json_success(array(
+ 'message' => __('Appointment status updated successfully!', 'computer-repair-shop'),
+ 'new_status' => $new_status,
+ 'status_label' => $valid_statuses[$new_status]
+ ));
+ } else {
+ wp_send_json_error(array('message' => __('Failed to update appointment status', 'computer-repair-shop')));
+ }
+}
+
+add_action('wp_ajax_wcrb_delete_appointment', 'wcrb_delete_appointment_ajax');
+function wcrb_delete_appointment_ajax() {
+ if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'wcrb_appointment_nonce')) {
+ wp_send_json_error(array('message' => 'Security check failed'));
+ }
+
+ $appointment_id = isset($_POST['appointment_id']) ? intval($_POST['appointment_id']) : 0;
+
+ if (!$appointment_id) {
+ wp_send_json_error(array('message' => 'Appointment ID is required'));
+ }
+
+ $appointment_manager = WC_CR_APPOINTMENTS_MANAGEMENT();
+ $result = $appointment_manager->delete_appointment($appointment_id);
+
+ if ($result) {
+ wp_send_json_success(array('message' => __('Appointment deleted successfully!', 'computer-repair-shop')));
+ } else {
+ wp_send_json_error(array('message' => __('Failed to delete appointment', 'computer-repair-shop')));
+ }
+}
+
+/**
+ * AJAX handler for getting customer jobs
+ */
+add_action('wp_ajax_wcrb_get_customer_jobs', 'wcrb_get_customer_jobs_ajax');
+add_action('wp_ajax_nopriv_wcrb_get_customer_jobs', 'wcrb_get_customer_jobs_ajax');
+
+function wcrb_get_customer_jobs_ajax() {
+ // Verify nonce
+ if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'wcrb_appointment_nonce')) {
+ wp_send_json_error(array('message' => 'Security check failed'));
+ }
+
+ $customer_id = isset($_POST['customer_id']) ? intval($_POST['customer_id']) : 0;
+
+ if (!$customer_id) {
+ wp_send_json_success(array(
+ 'jobs' => array(),
+ 'message' => 'No customer selected'
+ ));
+ }
+
+ global $wpdb;
+ $jobs_manager = WCRB_JOBS_MANAGER::getInstance();
+
+ // Get jobs for this customer
+ $jobs = $wpdb->get_results($wpdb->prepare(
+ "SELECT p.ID, p.post_title, p.post_date
+ FROM {$wpdb->prefix}posts p
+ LEFT JOIN {$wpdb->prefix}postmeta pm ON p.ID = pm.post_id
+ WHERE p.post_type = 'rep_jobs'
+ AND p.post_status != 'trash'
+ AND pm.meta_key = '_customer'
+ AND pm.meta_value = %d
+ ORDER BY p.post_date DESC
+ LIMIT 50",
+ $customer_id
+ ));
+
+ $job_options = array();
+
+ if ($jobs) {
+ foreach ($jobs as $job) {
+ $job_data = $jobs_manager->get_job_display_data($job->ID);
+ $job_number = !empty($job_data['formatted_job_number']) ? $job_data['formatted_job_number'] : '#' . $job->ID;
+ $job_title = wp_trim_words($job->post_title, 4);
+
+ // Get job status for display
+ $status_terms = wp_get_post_terms($job->ID, 'job_status');
+ $status_text = '';
+ if (!is_wp_error($status_terms) && !empty($status_terms)) {
+ $status_text = ' - ' . $status_terms[0]->name;
+ }
+
+ $job_options[] = array(
+ 'id' => $job->ID,
+ 'text' => $job_number . ' - ' . $job_title . $status_text
+ );
+ }
+ }
+
+ wp_send_json_success(array(
+ 'jobs' => $job_options,
+ 'total' => count($job_options),
+ 'message' => sprintf(__('%d jobs found', 'computer-repair-shop'), count($job_options))
+ ));
+}
No newline at end of file
--- a/computer-repair-shop/lib/includes/classes/class-appointments.php
+++ b/computer-repair-shop/lib/includes/classes/class-appointments.php
@@ -116,6 +116,7 @@
<select id="calendarFilter" class="form-select form-select-sm" style="width: auto; min-width: 180px;">
<option value="all"><?php esc_html_e( 'All Items', 'computer-repair-shop' ); ?></option>
<option value="jobs"><?php esc_html_e( 'Jobs Only', 'computer-repair-shop' ); ?></option>
+ <option value="appointments"><?php esc_html_e( 'Appointments Only', 'computer-repair-shop' ); ?></option>
<option value="estimates"><?php esc_html_e( 'Estimates Only', 'computer-repair-shop' ); ?></option>
<?php if ( $is_admin || $is_store_manager ) : ?>
<option value="my_assignments"><?php esc_html_e( 'My Assignments', 'computer-repair-shop' ); ?></option>
@@ -185,7 +186,7 @@
$ajaxurl = admin_url('admin-ajax.php');
?>
<style>
- /* Your existing styles */
+ /* Add tooltip styling */
.fc-event {
border-radius: 4px;
border: none;
@@ -204,10 +205,49 @@
.status-completed { background-color: #6f42c1 !important; }
.status-delivered { background-color: #e83e8c !important; }
.status-cancelled { background-color: #dc3545 !important; }
+
+ /* Appointment styling */
+ .appointment-event {
+ border-radius: 4px !important;
+ padding: 2px 4px !important;
+ font-size: 0.85em !important;
+ border: 1px solid rgba(0,0,0,0.1) !important;
+ }
+
+ /* Appointment type colors */
+ .appointment-type-store-visit {
+ background-color: #3498db !important;
+ border-left: 4px solid #2980b9 !important;
+ }
+ .appointment-type-pickup {
+ background-color: #2ecc71 !important;
+ border-left: 4px solid #27ae60 !important;
+ }
+ .appointment-type-onsite {
+ background-color: #e74c3c !important;
+