' . esc_html__( 'Booking is not available right now. Please contact support.', 'woodoo' ) . '

'; return; } // Upcoming bookings for this user $upcoming = []; if ( $partner_id ) { $now = gmdate( 'Y-m-d H:i:s' ); $upcoming = $api->search_read( 'calendar.event', [ [ 'partner_ids', 'in', [ $partner_id ] ], [ 'start', '>=', $now ], [ 'active', '=', true ], ], [ 'id', 'name', 'start', 'stop', 'location', 'description', 'videocall_location' ], 20, 0, 'start asc' ); } include WOODOO_DIR . 'templates/myaccount-calendar.php'; } // ── Slot Calculation ────────────────────────────────────────────────── /** * Returns available booking slots for a given date (Y-m-d). * Filters out already-booked times from Odoo calendar. * * @return array List of ['start' => 'Y-m-d H:i', 'end' => 'Y-m-d H:i'] */ public static function get_available_slots( string $date ): array { $available_days = (array) get_option( 'woodoo_available_days', [ 1, 2, 3, 4, 5 ] ); $from = get_option( 'woodoo_available_from', '09:00' ); $to = get_option( 'woodoo_available_to', '17:00' ); $duration = (int) get_option( 'woodoo_meeting_duration', 30 ); $ts = strtotime( $date ); $day_of_w = (int) gmdate( 'N', $ts ); // 1=Mon … 7=Sun if ( ! in_array( $day_of_w, array_map( 'intval', $available_days ), true ) ) { return []; } // Build all slots for the day $slots = []; $from_ts = strtotime( $date . ' ' . $from . ':00' ); $to_ts = strtotime( $date . ' ' . $to . ':00' ); $step = $duration * 60; for ( $start = $from_ts; $start + $step <= $to_ts; $start += $step ) { $slots[] = [ 'start' => gmdate( 'Y-m-d H:i', $start ), 'end' => gmdate( 'Y-m-d H:i', $start + $step ), 'start_ts' => $start, 'end_ts' => $start + $step, ]; } // Fetch existing events from Odoo for this day $api = woodoo_api(); if ( ! $api ) return $slots; $day_start = $date . ' 00:00:00'; $day_end = $date . ' 23:59:59'; $cache_key = 'woodoo_booked_' . $date; $booked = get_transient( $cache_key ); if ( false === $booked ) { $booked = $api->search_read( 'calendar.event', [ [ 'start', '>=', $day_start ], [ 'start', '<=', $day_end ], [ 'active', '=', true ], ], [ 'id', 'start', 'stop' ], 100 ); set_transient( $cache_key, $booked, 120 ); } // Remove slots that overlap with existing events $available = array_filter( $slots, function ( $slot ) use ( $booked ) { foreach ( $booked as $event ) { $e_start = strtotime( $event['start'] ); $e_end = strtotime( $event['stop'] ); // Overlap: slot_start < e_end AND slot_end > e_start if ( $slot['start_ts'] < $e_end && $slot['end_ts'] > $e_start ) { return false; } } return true; } ); // Strip internal timestamps before returning return array_values( array_map( fn($s) => [ 'start' => $s['start'], 'end' => $s['end'], ], $available ) ); } // ── AJAX: Get Slots ─────────────────────────────────────────────────── public static function ajax_get_slots(): void { check_ajax_referer( 'woodoo_calendar', 'nonce' ); if ( ! is_user_logged_in() ) wp_send_json_error( 'Not logged in', 401 ); $date = sanitize_text_field( wp_unslash( $_POST['date'] ?? '' ) ); if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) { wp_send_json_error( 'Invalid date format' ); } // Don't allow past dates if ( strtotime( $date ) < strtotime( 'today' ) ) { wp_send_json_success( [] ); } wp_send_json_success( self::get_available_slots( $date ) ); } // ── AJAX: Book Slot ─────────────────────────────────────────────────── public static function ajax_book_slot(): void { check_ajax_referer( 'woodoo_calendar', 'nonce' ); if ( ! is_user_logged_in() ) wp_send_json_error( 'Not logged in', 401 ); $start = sanitize_text_field( wp_unslash( $_POST['start'] ?? '' ) ); $end = sanitize_text_field( wp_unslash( $_POST['end'] ?? '' ) ); $notes = sanitize_textarea_field( wp_unslash( $_POST['notes'] ?? '' ) ); if ( ! preg_match( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $start ) || ! preg_match( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $end ) ) { wp_send_json_error( 'Invalid date/time format' ); } if ( strtotime( $start ) < time() ) { wp_send_json_error( __( 'Cannot book a slot in the past.', 'woodoo' ) ); } $user_id = get_current_user_id(); $user = get_userdata( $user_id ); $api = woodoo_api(); if ( ! $api ) wp_send_json_error( 'API not configured' ); // Find or create Odoo partner for the customer $partner_id = (int) get_user_meta( $user_id, 'woodoo_odoo_partner_id', true ); if ( ! $partner_id ) { $partner_id = $api->find_or_create_partner( $user->user_email, $user->display_name ); if ( $partner_id ) { update_user_meta( $user_id, 'woodoo_odoo_partner_id', $partner_id ); } } if ( ! $partner_id ) { wp_send_json_error( __( 'Could not find your Odoo record.', 'woodoo' ) ); } // Get the Odoo user (organizer) – the API user $me_uid = $api->authenticate(); // Find res.users for this uid to get partner_id of organizer $organizer_users = $api->search_read( 'res.users', [ [ 'id', '=', $me_uid ] ], [ 'partner_id' ], 1 ); $organizer_partner_id = $organizer_users[0]['partner_id'][0] ?? null; $title = get_option( 'woodoo_meeting_title_prefix', 'Meeting via WooDoo' ) . ' – ' . $user->display_name; $partner_ids = array_filter( [ $partner_id, $organizer_partner_id ] ); $event_vals = [ 'name' => $title, 'start' => $start . ':00', 'stop' => $end . ':00', 'description' => $notes ?: false, 'partner_ids' => array_map( fn($id) => [ 4, $id, 0 ], $partner_ids ), 'privacy' => 'confidential', ]; $event_id = $api->create( 'calendar.event', $event_vals ); if ( ! $event_id ) { wp_send_json_error( __( 'Could not create meeting in Odoo. Please try again.', 'woodoo' ) ); } // Invalidate booked slots cache for this date $date = substr( $start, 0, 10 ); delete_transient( 'woodoo_booked_' . $date ); wp_send_json_success( [ 'event_id' => $event_id, 'start' => $start, 'end' => $end, 'message' => __( 'Meeting booked! You will receive a confirmation from Odoo.', 'woodoo' ), ] ); } // ── AJAX: Get User's Meetings ───────────────────────────────────────── public static function ajax_get_meetings(): void { check_ajax_referer( 'woodoo_calendar', 'nonce' ); if ( ! is_user_logged_in() ) wp_send_json_error( 'Not logged in', 401 ); $user_id = get_current_user_id(); $partner_id = (int) get_user_meta( $user_id, 'woodoo_odoo_partner_id', true ); if ( ! $partner_id ) wp_send_json_success( [] ); $api = woodoo_api(); if ( ! $api ) wp_send_json_error( 'API not configured' ); $now = gmdate( 'Y-m-d H:i:s' ); $meetings = $api->search_read( 'calendar.event', [ [ 'partner_ids', 'in', [ $partner_id ] ], [ 'start', '>=', $now ], [ 'active', '=', true ], ], [ 'id', 'name', 'start', 'stop', 'location', 'videocall_location' ], 20, 0, 'start asc' ); wp_send_json_success( $meetings ); } // ── AJAX: Cancel Meeting ────────────────────────────────────────────── public static function ajax_cancel_meeting(): void { check_ajax_referer( 'woodoo_calendar', 'nonce' ); if ( ! is_user_logged_in() ) wp_send_json_error( 'Not logged in', 401 ); $event_id = absint( $_POST['event_id'] ?? 0 ); $user_id = get_current_user_id(); $partner_id = (int) get_user_meta( $user_id, 'woodoo_odoo_partner_id', true ); if ( ! $event_id || ! $partner_id ) wp_send_json_error( 'Invalid request' ); $api = woodoo_api(); if ( ! $api ) wp_send_json_error( 'API not configured' ); // Verify the event belongs to this user $events = $api->search( 'calendar.event', [ [ 'id', '=', $event_id ], [ 'partner_ids', 'in', [ $partner_id ] ] ], 1 ); if ( empty( $events ) ) { wp_send_json_error( __( 'Meeting not found.', 'woodoo' ) ); } // Unlink the customer partner from the event (don't delete the event) $api->write( 'calendar.event', [ $event_id ], [ 'partner_ids' => [ [ 3, $partner_id, 0 ] ] ] ); wp_send_json_success( [ 'message' => __( 'Meeting cancelled.', 'woodoo' ) ] ); } }