$args Optional query arguments. * @return string */ public static function get_admin_url(array $args = []): string { $url = admin_url('admin.php?page=' . self::PAGE_SLUG); if (!empty($args)) { $url = add_query_arg($args, $url); } return $url; } /** * Post types treated as plugin bookings (current CPT plus legacy `booking`). * * @return list */ public static function get_booking_post_types(): array { return ['bizdexul_booking', 'booking']; } /** * Whether a post is a plugin booking (current or legacy post type). */ public static function is_booking_post(int $post_id): bool { if ($post_id <= 0) { return false; } return in_array((string) get_post_type($post_id), self::get_booking_post_types(), true); } /** * Hook admin menus and early-init bulk action handlers. * * Runs only in admin context (class is instantiated from {@see Admin_Manager}). * * @return void */ public function __construct() { add_action('admin_menu', [$this, 'add_admin_menus'], 9); add_action('admin_init', [$this, 'handle_booking_bulk_actions']); add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']); } /** * Enqueue booking entries admin assets. * * @param string $hook Current admin page hook. * @return void */ public function enqueue_scripts(string $hook): void { if (strpos($hook, self::PAGE_SLUG) === false && strpos($hook, self::VIEW_ENTRY_PAGE_SLUG) === false) { return; } wp_register_style( 'bizdex-uplisting-booking-entries', BIZDEX_UPLISTING_PLUGIN_URL . 'assets/css/booking-entries-management.css', [], BIZDEX_UPLISTING_VERSION ); wp_enqueue_style('bizdex-uplisting-booking-entries'); FrontendManager::ensure_dom_assets(); wp_register_script( 'bizdex-uplisting-admin-js', BIZDEX_UPLISTING_PLUGIN_URL . 'assets/js/admin.js', ['bizdex-uplisting-dom'], BIZDEX_UPLISTING_VERSION, true ); wp_enqueue_script('bizdex-uplisting-admin-js'); } /** * Register visible and hidden admin submenu pages for bookings. * * Pages: * - `bizdex-uplisting-bookings` — list view ({@see render_booking_page()}) * - `bizdex-uplisting-view-booking-entry` — single entry (hidden menu) * * Requires `manage_options` capability. * * @return void */ public function add_admin_menus(): void { add_menu_page( __('All Bookings', 'advanced-connector-for-uplisting'), __('Uplisting Connector', 'advanced-connector-for-uplisting'), 'manage_options', self::PAGE_SLUG, [$this, 'render_booking_page'], 'dashicons-list-view', 26 ); add_submenu_page( self::PAGE_SLUG, __('All Bookings', 'advanced-connector-for-uplisting'), __('All Bookings', 'advanced-connector-for-uplisting'), 'manage_options', self::PAGE_SLUG, [$this, 'render_booking_page'] ); // View Booking Entry (Hidden) add_submenu_page( null, 'View Booking Entry', 'View Entry', 'manage_options', self::VIEW_ENTRY_PAGE_SLUG, [$this, 'render_single_booking_entry'] ); } /** * Render the single booking detail admin screen. * * Handles POST actions (add note, update payment status, resend emails) * when nonce {@see self::ENTRY_ACTION_NONCE} verifies. Loads * `templates/admin/view-booking-entry.php` with `$booking` in scope. * * Query arg: `entry_id` (booking post ID). * * @return void */ public function render_single_booking_entry(): void { if (!current_user_can('manage_options')) { wp_die( esc_html__('You are not allowed to perform this action.', 'advanced-connector-for-uplisting'), '', ['response' => 403] ); } if (Request::has('post', 'action')) { Request::require_admin_nonce(self::ENTRY_ACTION_NONCE); } $entry_id = Request::absint('get', 'entry_id'); if (!$entry_id || !self::is_booking_post($entry_id)) { echo '

' . esc_html__('Booking not found', 'advanced-connector-for-uplisting') . '

'; return; } if (Request::has('post', 'action')) { $action = Request::key('post', 'action'); if (!in_array($action, ['add_note', 'update_status', 'resend_emails'], true)) { $action = ''; } switch ($action) { case 'add_note': $note = Request::textarea('post', 'note'); if ($note !== '') { $this->add_note($entry_id, $note); echo '

' . esc_html__('Note added successfully.', 'advanced-connector-for-uplisting') . '

'; } break; case 'update_status': $new_status = Request::key('post', 'new_status'); $allowed = array_keys(ReservationService::get_booking_payment_statuses()); if (in_array($new_status, $allowed, true)) { update_post_meta($entry_id, bizdex_uplisting_meta_key('_booking_payment_status'), $new_status); $this->add_note($entry_id, 'Status changed to: ' . ucfirst($new_status)); echo '

' . esc_html__('Status updated successfully.', 'advanced-connector-for-uplisting') . '

'; } break; case 'resend_emails': $sent = ReservationService::instance()->send_booking_emails($entry_id); if ($sent) { $this->add_note($entry_id, 'Confirmation emails resent'); echo '

' . esc_html__('Emails sent successfully.', 'advanced-connector-for-uplisting') . '

'; } else { echo '

' . esc_html__('Failed to send one or more emails. Check mail configuration.', 'advanced-connector-for-uplisting') . '

'; } break; } } // Get booking data via BookingDetails model $booking = new BookingDetails($entry_id); $entry_id = $booking->get_id(); $property_id = $booking->get_property_id(); $property_title = get_the_title($property_id); $property_url = get_permalink($property_id); $booking_statuses = ReservationService::get_booking_payment_statuses(); $list_base = self::get_admin_url(); $delete_url = wp_nonce_url( self::get_admin_url(['action' => 'delete', 'entry_id' => $entry_id]), self::DELETE_NONCE ); $this->load_view('view-booking-entry', [ 'entry_id' => $entry_id, 'booking' => $booking, 'property_title' => $property_title, 'property_url' => $property_url, 'booking_statuses' => $booking_statuses, 'list_base' => $list_base, 'delete_url' => $delete_url, ]); } /** * CSV export is not available in Advanced Connector for Uplisting. * * @return void */ public function handle_export_bookings(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to export bookings.', 'advanced-connector-for-uplisting')); } wp_die(esc_html__('CSV export is not available in this version of Advanced Connector for Uplisting.', 'advanced-connector-for-uplisting')); } /** * Process delete and bulk status actions from the bookings list table. * * Triggered on `admin_init` when `page=bizdex-uplisting-bookings`. * Single delete uses GET + nonce {@see self::DELETE_NONCE}. * Bulk actions use POST + nonce {@see self::BULK_NONCE_ACTION} and `booking_ids[]`. * Redirects back with `msg` query arg for admin notices. * * @return void */ public function handle_booking_bulk_actions(): void { if (!is_admin()) { return; } $wants_delete = Request::has('get', 'action') && Request::has('get', 'entry_id') && Request::has('get', '_wpnonce'); $wants_bulk = Request::has('post', 'booking_ids'); if (!$wants_delete && !$wants_bulk) { return; } if (!current_user_can('manage_options')) { wp_die( esc_html__('You are not allowed to perform this action.', 'advanced-connector-for-uplisting'), '', ['response' => 403] ); } if ($wants_delete) { Request::require_admin_nonce(self::DELETE_NONCE); if (Request::key('get', 'page') !== self::PAGE_SLUG || Request::key('get', 'action') !== 'delete') { return; } $entry_id = Request::absint('get', 'entry_id'); if (self::is_booking_post($entry_id) && wp_delete_post($entry_id, true)) { wp_safe_redirect(self::get_admin_url(['msg' => 'deleted'])); exit; } wp_safe_redirect(self::get_admin_url(['msg' => 'delete_failed'])); exit; } Request::require_admin_nonce(self::BULK_NONCE_ACTION, self::BULK_NONCE_FIELD); if (Request::key('get', 'page') !== self::PAGE_SLUG) { return; } $allowed_bulk = ['delete', 'mark_completed', 'mark_pending', 'mark_cancelled']; $action = Request::key('post', 'action'); if (!in_array($action, $allowed_bulk, true)) { $action = Request::key('post', 'action2'); } if (!in_array($action, $allowed_bulk, true)) { $action = ''; } if ($action === '') { return; } $booking_ids = []; foreach (Request::int_list('post', 'booking_ids') as $id) { if (self::is_booking_post($id)) { $booking_ids[] = $id; } } $booking_ids = array_values(array_unique($booking_ids)); $count = 0; switch ($action) { case 'delete': foreach ($booking_ids as $id) { wp_delete_post($id, true); $count++; } wp_safe_redirect(self::get_admin_url(['msg' => 'bulk_deleted', 'count' => $count])); exit; case 'mark_completed': foreach ($booking_ids as $id) { update_post_meta($id, bizdex_uplisting_meta_key('_booking_payment_status'), 'completed'); $count++; } wp_safe_redirect(self::get_admin_url(['msg' => 'bulk_completed', 'count' => $count])); exit; case 'mark_pending': foreach ($booking_ids as $id) { update_post_meta($id, bizdex_uplisting_meta_key('_booking_payment_status'), 'pending'); $count++; } wp_safe_redirect(self::get_admin_url(['msg' => 'bulk_pending', 'count' => $count])); exit; case 'mark_cancelled': foreach ($booking_ids as $id) { update_post_meta($id, bizdex_uplisting_meta_key('_booking_payment_status'), 'cancelled'); $count++; } wp_safe_redirect(self::get_admin_url(['msg' => 'bulk_cancelled', 'count' => $count])); exit; } } /** * Append a timestamped admin note to a booking. * * Notes are stored in `_booking_notes` as an array of * `{ note, user, date }` objects. Does not deduplicate. * * @param int $booking_id Booking CPT post ID. * @param string $note Sanitized note body (caller should sanitize). * @return void */ public function add_note(int $booking_id, string $note): void { if (!self::is_booking_post($booking_id)) { return; } $booking = new BookingDetails($booking_id); $notes = $booking->get_meta('notes', []); if (!is_array($notes)) { $notes = []; } $current_user = wp_get_current_user(); $notes[] = [ 'note' => $note, 'user' => (string) $current_user->display_name, 'date' => current_time('mysql') ]; update_post_meta($booking_id, bizdex_uplisting_meta_key('_booking_notes'), $notes); } /** * Fetch admin notes for a booking via {@see BookingDetails}. * * @since 1.0.0 * * @param int $booking_id Booking CPT post ID. * @return array Newest-first. */ public function get_notes(int $booking_id): array { $booking = new BookingDetails($booking_id); $notes = $booking->get_notes(); return is_array($notes) ? $notes : []; } /** * Render the paginated bookings list admin page. * * Displays flash messages from bulk/single actions (`msg` query arg), * computes payment status counts for filter tabs, and includes * `templates/admin/booking-page.php` which hosts {@see Booking_List_Table}. * * @since 1.0.0 * * @return void */ public function render_booking_page(): void { if (!current_user_can('manage_options')) { return; } // Show success/delete notifications $msg = Request::key_in_list( 'get', 'msg', ['deleted', 'delete_failed', 'bulk_deleted', 'bulk_completed', 'bulk_pending', 'bulk_cancelled'], '' ); if ($msg !== '') { $count = Request::absint('get', 'count'); switch ($msg) { case 'deleted': echo '

' . esc_html__('Booking entry deleted successfully.', 'advanced-connector-for-uplisting') . '

'; break; case 'delete_failed': echo '

' . esc_html__('The booking could not be deleted.', 'advanced-connector-for-uplisting') . '

'; break; case 'bulk_deleted': echo '

' . esc_html((string) $count) . ' entries deleted successfully.

'; break; case 'bulk_completed': echo '

' . esc_html((string) $count) . ' entries marked as completed.

'; break; case 'bulk_pending': echo '

' . esc_html((string) $count) . ' entries marked as pending.

'; break; case 'bulk_cancelled': echo '

' . esc_html((string) $count) . ' entries marked as cancelled.

'; break; } } // Get status counts $all_bookings = new \WP_Query([ 'post_type' => self::get_booking_post_types(), 'posts_per_page' => -1, 'post_status' => 'any', ]); $status_counts = ['all' => (int) $all_bookings->found_posts]; foreach (array_keys(ReservationService::get_booking_payment_statuses()) as $status_key) { $status_counts[$status_key] = 0; } foreach ($all_bookings->posts as $booking_post) { $booking = new BookingDetails($booking_post); $status = $booking->get_payment_status() ?: 'enquiry'; if (isset($status_counts[$status])) { $status_counts[$status]++; } } $booking_statuses = ReservationService::get_booking_payment_statuses(); $status_icons = []; foreach (array_keys($booking_statuses) as $status_key) { $status_icons[$status_key] = ReservationService::get_booking_payment_status_icon($status_key); } $properties = get_posts([ 'post_type' => 'bizdexul_property', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC', 'post_status' => 'publish', ]); $status_filter = Request::key_in_list( 'get', 'status', array_merge(['all'], array_keys($booking_statuses)), 'all' ); $property_filter = Request::absint('get', 'property_id'); $date_filter = Request::key_in_list( 'get', 'date_filter', ['today', 'yesterday', 'last_7_days', 'last_30_days', 'this_month'], '' ); $search = Request::text('request', 's'); if (!class_exists('WP_List_Table')) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } $table = new BookingListTable(); $table->status_counts = $status_counts; $table->prepare_items(); $list_base = self::get_admin_url(); $page_slug = self::PAGE_SLUG; $this->load_view('booking-page', [ 'status_counts' => $status_counts, 'booking_statuses' => $booking_statuses, 'status_icons' => $status_icons, 'properties' => $properties, 'status_filter' => $status_filter, 'property_filter' => $property_filter, 'date_filter' => $date_filter, 'search' => $search, 'list_base' => $list_base, 'page_slug' => $page_slug, 'table' => $table, ]); } /** * Include an admin template with a view-model object (no extract()). * * @since 1.0.0 * * @param string $view Basename of the file in `templates/admin/`. * @param array $args Assigned to `$vm` (and `$view_args` for older templates). * @return void */ private function load_view(string $view, array $args = []): void { if (!current_user_can('manage_options')) { return; } $view_file = BIZDEX_UPLISTING_PLUGIN_DIR . 'templates/admin/' . $view . '.php'; if (!file_exists($view_file)) { return; } $vm = (object) $args; $view_args = $args; include $view_file; } }