hero($label); ?> kpi_cards($abptb_info, $kpi); ?>
upcoming_journeys($kpi['upcoming'] ?? array()); ?> orders_summary(); ?>
display_name) ? $user->display_name : __('Admin', 'abp-transport-booking'); ?>

$group_post_id, 'rows' => array()); } $groups[$group_post_id]['rows'][] = $journey; } $group_list = array_values($groups); $total_pages = max(1, (int)ceil(count($group_list) / $per_page)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only pagination value, cast with absint() and used only as an array_slice() offset. $current_page = isset($_GET['abptb_journey_page']) ? absint($_GET['abptb_journey_page']) : 1; $current_page = max(1, min($current_page, $total_pages)); $page_offset = ($current_page - 1) * $per_page; $page_groups = array_slice($group_list, $page_offset, $per_page); $journey_before = 0; for ($gi = 0; $gi < $page_offset; $gi++) { $journey_before += count($group_list[$gi]['rows']); } $journey_on_page = 0; foreach ($page_groups as $group_page) { $journey_on_page += count($group_page['rows']); } ?>

1) { ?>
(int)$group['post_id'])); ?>
0) { ?> ' . esc_html(sprintf(__('Reserve %d', 'abp-transport-booking'), $reserve)) . ''; ?>
1) { $this->journey_pagination($current_page, $total_pages); } ?>

$page]); }; $window = 2; $items = array(); $last = 0; for ($page = 1; $page <= $total_pages; $page++) { $is_edge = (1 === $page || $total_pages === $page); if (!$is_edge && abs($page - $current_page) > $window) { continue; } if ($last && $page - $last > 1) { $items[] = '…'; } $items[] = $page === $current_page ? '' . esc_html($page) . '' : '' . esc_html($page) . ''; $last = $page; } ?> 'on_off']), 'warning'), array('fas fa-calendar-days', __('Global Date', 'abp-transport-booking'), __('Manage global dates', 'abp-transport-booking'), ABPTB_Function::build_url('global', ['global' => 'dates']), 'theme'), ); if (ABPTB_Function::on_off('additional_info')) { $actions[] = array('fas fa-list-check', __('Additional Services', 'abp-transport-booking'), __('Manage additional services', 'abp-transport-booking'), ABPTB_Function::build_url('global', ['global' => 'additional']), 'success'); } if (ABPTB_Function::on_off('client_info')) { $actions[] = array('fas fa-user', __('Client Form', 'abp-transport-booking'), __('Manage client form fields', 'abp-transport-booking'), ABPTB_Function::build_url('global', ['global' => 'client_form']), 'purple'); } $actions[] = array('fas fa-route', __('Stops Configuration', 'abp-transport-booking'), __('Manage stops / locations', 'abp-transport-booking'), ABPTB_Function::build_url('global', ['global' => 'location']), 'navy'); if (defined('ABPTB_DIR_PRO')) { if (ABPTB_Function::on_off('partial_payment')) { $actions[] = array('fas fa-hand-holding-dollar', __('Partial Payment', 'abp-transport-booking'), __('Configure partial payment', 'abp-transport-booking'), ABPTB_Function::build_url('global', ['global' => 'partial_payment']), 'success'); } if ((ABPTB_Function::on_off('seasonal') && ABPTB_Function::on_off('seasonal_global')) || (ABPTB_Function::on_off('early_bird') && ABPTB_Function::on_off('early_bird_global'))) { $actions[] = array('fas fa-percent', __('Global Discount', 'abp-transport-booking'), __('Configure global discounts', 'abp-transport-booking'), ABPTB_Function::build_url('global', ['global' => 'discount']), 'info'); } if (ABPTB_Function::on_off('cancel_request')) { $actions[] = array('fas fa-ban', __('Cancel Request', 'abp-transport-booking'), __('Manage cancellation requests', 'abp-transport-booking'), ABPTB_Function::build_url('cancel_requests'), 'warning'); } } $actions[] = array('fas fa-book-open', __('Documentation', 'abp-transport-booking'), __('Read the plugin documentation & help', 'abp-transport-booking'), 'https://demo-tb.abp-team.com/documentation/', 'info', '_blank'); if (!defined('ABPTB_DIR_PRO')) { $actions[] = array('fas fa-crown', __('Buy Pro', 'abp-transport-booking'), __('Unlock all Pro features', 'abp-transport-booking'), 'https://abp-team.com/downloads/abp-transport-booking-pro/', 'success', '_blank'); } ?>

$today_date), 10); $today_total = (int)ABPTB_Query::get_booking_query(array('order_date' => $today_date), 0, 0, true); ?>

order_table($last, false); ?>
order_table($today, false); ?>
0); ?> 0 && $dp_id > 0) ? ABPTB_Function::location_value($bp_id) . ' - ' . ABPTB_Function::location_value($dp_id) : '—'; $total = (float)($item['total'] ?? 0); $totals['total'] += $total; ?>
# (int)($item['post_id'] ?? 0))); ?> 0 ? wp_kses_post(ABPTB_WC >= 2 ? wc_price($total) : number_format_i18n((float)$total, 2)) : esc_html__('FREE', 'abp-transport-booking'); ?>
= 2 ? wc_price($totals['total']) : number_format_i18n((float)$totals['total'], 2)); ?>
__('Invalid security token or Insufficient permissions.', 'abp-transport-booking'), 'type' => 'warn'], 403); } $post_id = isset($_POST['post_id']) ? (int)$_POST['post_id'] : 0; $start_time = isset($_POST['start_time']) ? sanitize_text_field(wp_unslash($_POST['start_time'])) : ''; $direction = isset($_POST['direction']) && 'return' === $_POST['direction'] ? 'return' : 'up'; if ($post_id <= 0 || empty($start_time)) { wp_send_json_error(['msg' => __('Invalid journey.', 'abp-transport-booking'), 'type' => 'warn'], 400); } $meta = self::journey_meta($post_id, $start_time, -1, -1, $direction); $seat_type = $meta['seat_type'] ?? 'ticket'; ob_start(); ?>

0) { ?>
journey_seat_map($post_id, $start_time); ?>
journey_ticket_table($post_id, $start_time); ?>
()
journey_bookings($post_id, $start_time); ?>
$html, 'msg' => esc_html__('Journey loaded.', 'abp-transport-booking'), 'type' => 'success']); } //=============================// private function journey_ticket_table($post_id, $start_time): void { $post_infos = ABPTB_Function::get_all_meta($post_id); $ticket_infos = $post_infos['ticket_infos'] ?? array(); $filters = array('post_id' => $post_id, 'start_time' => $start_time, 'cache' => false); $sold = ABPTB_Query::get_sold_ticket($filters, true); if (empty($ticket_infos) || !is_array($ticket_infos)) { echo '

' . esc_html__('No ticket configuration found.', 'abp-transport-booking') . '

'; return; } ?> $ticket_info) { $qty = (int)($ticket_info['qty'] ?? 0); $reserve = (int)($ticket_info['reserve'] ?? 0); $sold_qty = (int)($sold[$tic_id] ?? 0); $avail = max(0, $qty - $sold_qty - $reserve); ?>

' . esc_html__('No seat plan assigned.', 'abp-transport-booking') . '

'; return; } $filters = array('post_id' => $post_id, 'start_time' => $start_time, 'cache' => false); $sold_seat = ABPTB_Query::get_sold_seat($filters, true); foreach ($sp_infos as $sp_item) { $sp_id = (int)($sp_item['id'] ?? 0); if ($sp_id <= 0) { continue; } $this->render_seat_plan($sp_id, $sold_seat); } } //=============================// private function render_seat_plan($sp_id, $sold_seat): void { $row = ABPTB_Query::get_sp($sp_id); $sp_info = !empty($row) ? current($row) : array(); if (empty($sp_info)) { return; } $others = json_decode($sp_info['others'] ?? '', true) ?: array(); $cell_width = $others['width'] ?? 40; $cell_height = $others['height'] ?? 40; $gap = $others['gap'] ?? 0; $radius = $others['radius'] ?? 0; $cols = intval($others['column'] ?? 10); $bg_image = $others['bg_image'] ?? ''; $img_url = !empty($bg_image) && $bg_image > 0 ? ABPTB_Function::get_image_url('', $bg_image) : ''; $bg_color = $others['bg_color'] ?? '#fff'; $layout = json_decode($sp_info['layout_data'] ?? '', true) ?: array(); $sp_name = $sp_info['name'] ?? ''; // Multi-row/column cells must hide their "occupied" grid slots. $hidden_cells = array(); foreach ($layout as $index => $cell) { $c_span = intval($cell['width_ratio'] ?? 1); $r_span = intval($cell['height_ratio'] ?? 1); if ($c_span > 1 || $r_span > 1) { for ($r = 0; $r < $r_span; $r++) { for ($c = 0; $c < $c_span; $c++) { if ($r === 0 && $c === 0) { continue; } $target_idx = $index + ($r * $cols) + $c; $hidden_cells[$target_idx] = true; } } } } ?>
$cell) { if (isset($hidden_cells[$index])) { continue; } $type_id = $cell['id'] ?? ''; $name = $cell['name'] ?? ''; $c_span = intval($cell['width_ratio'] ?? 1); $r_span = intval($cell['height_ratio'] ?? 1); $rotate = intval($cell['rotate'] ?? 0); $fs = $cell['fs'] ?? 12; $is_seat = ('seat' === ($cell['type'] ?? '')); $seat_type_class = $is_seat ? 'available' : ''; $seat_type_class = $is_seat && in_array($name, $sold_seat, true) ? 'sold' : $seat_type_class; $class = $is_seat ? 'sp_cell ' . $seat_type_class : 'sp_decor'; $color = $is_seat ? ABPTB_Function::ticket_color($type_id) : ABPTB_Function::decor_color($type_id); $icon_image = $is_seat ? ABPTB_Function::ticket_icon($type_id) : ABPTB_Function::decor_icon($type_id); $width = $cell_width * $c_span; $height = $cell_height * $r_span; if ($gap > 0) { $width = $c_span > 1 ? $width + ($c_span - 1) * $gap : $width; $height = $r_span > 1 ? $height + ($r_span - 1) * $gap : $height; } $style = "color: {$color}; grid-column: span {$c_span}; grid-row: span {$r_span}; width:{$width}px;height:{$height}px; border:1px solid {$color};font-size:{$fs}px;border-radius:{$radius}px;"; $image = ''; if (!empty($icon_image) && is_numeric($icon_image)) { $image = ABPTB_Function::get_image_url('', $icon_image); } ?>
" style="background-image: url('');">
$post_id, 'start_time' => $start_time, 'status' => ABPTB_Function::booking_status_sold(), 'cache' => false, )); if (empty($bookings)) { echo '

' . esc_html__('No bookings for this journey yet.', 'abp-transport-booking') . '

'; return; } ?>

= 2 && defined('WC_VERSION')) { ?>
= 2) { $wc_name = get_option('woocommerce_email_from_name'); $wc_email = get_option('woocommerce_email_from_address'); if (!empty($wc_name)) { ?>
checklist_item(__('WooCommerce Plugin', 'abp-transport-booking'), ABPTB_WC >= 2, ABPTB_WC == 1 ? 'wc_active' : 'wc_install_active'); ?> checklist_item(__('Booking Page', 'abp-transport-booking'), (bool)ABPTB_Function::get_page_by_slug('tf_booking'), 'tf_booking'); ?> checklist_item( /* translators: %s: transport label. */ sprintf(esc_html__('%s List Page', 'abp-transport-booking'), esc_html($label)), (bool)ABPTB_Function::get_page_by_slug('tf_post'), 'tf_post' ); ?> checklist_item(__('Gallery Page', 'abp-transport-booking'), (bool)ABPTB_Function::get_page_by_slug('tf_gallery'), 'tf_gallery'); ?> 1) { do_action('abptb_add_page'); do_action('abptb_add_tools'); } ?> checklist_item(__('First Transport Added', 'abp-transport-booking'), $total > 0, 'add_new'); ?>
0) { ?>
>

0 ? (int)round(($row[2] / $sum) * 100) : 0; if ($row[2] > 0 && $width < 1) { $width = 1; } ?>
0, 'tickets' => 0, 'today' => 0, 'upcoming' => array()); $agg = ABPTB_Query::get_booking_query(array('aggregate' => array('total' => 'SUM', 'qty' => 'SUM'))); if (is_array($agg)) { $data['revenue'] = (float)($agg['total'] ?? 0); $data['tickets'] = (int)($agg['qty'] ?? 0); } $data['today'] = (int)ABPTB_Query::get_booking_query(array('order_date' => current_time('Y-m-d')), 0, 0, true); $data['upcoming'] = self::journey_list(); wp_cache_set($cache_key, $data, 'abptb_dashboard'); return $data; } //=============================// private static array $slot_batch = array(); //=============================// public static function journey_list(): array { $post_ids = defined('ABPTB_ids') && !empty(ABPTB_ids) ? ABPTB_ids : ABPTB_Query::get_post_id(); if (empty($post_ids) || !is_array($post_ids)) { return array(); } $today = current_time('Y-m-d'); // Batch-load sold qty + order count for every journey of today in one query // instead of two queries per journey row. self::$slot_batch = array( 'ready' => true, 'ids' => array_values(array_filter(array_map('absint', $post_ids))), 'map' => ABPTB_Query::get_slot_aggregates($post_ids, $today), ); $journeys = array(); foreach ($post_ids as $post_id) { $post_id = (int)$post_id; if ($post_id <= 0) { continue; } $journeys = array_merge($journeys, self::journey_today($post_id, $today)); } if (count($journeys) > 1) { usort($journeys, function ($a, $b) { return strtotime($a['start_time'] ?? '') <=> strtotime($b['start_time'] ?? ''); }); } return $journeys; } //=============================// private static function journey_today($post_id, $today): array { $post_infos = ABPTB_Function::get_all_meta($post_id); if (empty($post_infos)) { return array(); } // Is today among the post's scheduled dates? $dates = ABPTB_Function::date($post_id, array(), $today); $today_is_schedule = is_array($dates) && in_array($today, $dates, true); // Keep listing transports whose trips have already started today. if (!$today_is_schedule && !self::is_schedule_day($post_infos, $today)) { return array(); } $journeys = array(); // Outbound trips: every scheduled departure time today (multi schedule = several times). $times = ABPTB_Function::time_operation($post_id, $today); if (!empty($times) && is_array($times)) { foreach ($times as $_time) { if ('' === (string)$_time) { continue; } $journeys[] = self::journey_meta($post_id, $today . ' ' . $_time, -1, -1, 'up'); } } // Return trips: separate return timetable when the transport has return active. if (self::return_enabled($post_infos)) { $return_times = ABPTB_Function::time_operation($post_id, $today, true); if (!empty($return_times) && is_array($return_times)) { foreach ($return_times as $_time) { if ('' === (string)$_time) { continue; } $journeys[] = self::journey_meta($post_id, $today . ' ' . $_time, -1, -1, 'return'); } } } return $journeys; } //=============================// private static function return_enabled($post_infos): bool { if (!ABPTB_Function::on_off('return')) { return false; } $display_return = $post_infos['display_return'] ?? 'off'; if ('on' !== $display_return) { return false; } $return_price_infos = $post_infos['return_price_infos'] ?? array(); return !empty($return_price_infos) && is_array($return_price_infos); } //=============================// private static function is_schedule_day($post_infos, $today): bool { $date_infos = $post_infos['abptb_dates'] ?? array(); if (empty($date_infos) || !is_array($date_infos)) { return false; } $date_type = $date_infos['date_type'] ?? 'periodic_date'; if ('specific_date' === $date_type) { $specific_dates = $date_infos['specific_dates'] ?? array(); if (is_array($specific_dates)) { foreach ($specific_dates as $date_item) { if (!empty($date_item) && gmdate('Y-m-d', strtotime($date_item)) === $today) { return true; } } } return false; } $start_date = $date_infos['periodic_start_date'] ?? ''; if (empty($start_date) || strtotime($today) < strtotime($start_date)) { return false; } $end_date = $date_infos['periodic_end_date'] ?? ''; $end_date = !empty($end_date) ? gmdate('Y-m-d', strtotime($end_date)) : ''; $advance_days = (ABPTB_Date_Config['advance_date_number'] ?? null) ?: 28; $calc_end = gmdate('Y-m-d', strtotime($start_date . ' +' . $advance_days . ' day')); if (!empty($end_date) && strtotime($end_date) < strtotime($calc_end)) { $calc_end = $end_date; } return in_array($today, ABPTB_Function::date_list_modify($start_date, $calc_end, $date_infos), true); } //=============================// private static function prefetched_slot($post_id, $start_time): ?array { if (empty(self::$slot_batch['ready'])) { return null; } $post_id = (int)$post_id; if (!in_array($post_id, self::$slot_batch['ids'], true)) { return null; } $timestamp = strtotime($start_time); if (!$timestamp || '00:00' === gmdate('H:i', $timestamp)) { return null; } $key = $post_id . '|' . gmdate('Y-m-d H:i', $timestamp); // The batch covers this transport and day, so a missing slot means zero orders. return self::$slot_batch['map'][$key] ?? array('sold' => 0, 'orders' => 0); } //=============================// public static function journey_meta($post_id, $start_time, $sold_qty = -1, $order_count = -1, $direction = 'up'): array { $post_id = (int)$post_id; $post_infos = ABPTB_Function::get_all_meta($post_id); $seat_type = $post_infos['seat_type'] ?? 'sp'; $seat_type = ABPTB_Function::on_off('sp') ? $seat_type : 'ticket'; $total = (int)(ABPTB_Function::get_total_qty($post_id, $post_infos)); $prefetched = self::prefetched_slot($post_id, $start_time); // Sold quantity. if ($sold_qty < 0) { $sold_qty = null !== $prefetched ? (int)$prefetched['sold'] : (int)((ABPTB_Query::get_sold_ticket(array('post_id' => $post_id, 'start_time' => $start_time, 'cache' => false), true)['total'] ?? 0)); } if ($order_count < 0) { $order_count = null !== $prefetched ? (int)$prefetched['orders'] : count(ABPTB_Query::get_booking_query(array( 'post_id' => $post_id, 'start_time' => $start_time, 'status' => ABPTB_Function::booking_status_sold(), 'cache' => false, ))); } // Reserve quantity (only meaningful for ticket-type seating). $reserve = 0; if ('ticket' === $seat_type) { $ticket_infos = $post_infos['ticket_infos'] ?? array(); if (is_array($ticket_infos)) { foreach ($ticket_infos as $ticket_info) { $reserve += (int)($ticket_info['reserve'] ?? 0); } } } $available = $total - $sold_qty - $reserve; if ($available < 0) { $available = 0; } // Route label of the trip direction (start -> end point). $is_return = 'return' === $direction; $route_infos = $is_return ? ($post_infos['return_routing_infos'] ?? array()) : ($post_infos['routing_infos'] ?? array()); $route = ''; if (!empty($route_infos) && is_array($route_infos)) { $bp = array_key_first($route_infos); $dp = array_key_last($route_infos); if ($bp !== $dp && null !== $bp && null !== $dp) { $route = ABPTB_Function::location_value($bp) . ' -> ' . ABPTB_Function::location_value($dp); } } return array( 'post_id' => $post_id, 'start_time' => $start_time, 'title' => get_the_title($post_id), 'seat_type' => $seat_type, 'direction' => $is_return ? 'return' : 'up', 'route' => $route, 'started' => strtotime($start_time) <= current_time('timestamp'), 'sold' => (int)$sold_qty, 'available' => $available, 'total' => $total, 'reserve' => $reserve, 'order_count' => (int)$order_count, ); } //=============================// public function wc_notice(): void { $pro_active = defined('ABPTB_DIR_PRO'); $offer_key = 'abptb_pro_offer_dismissed_' . get_current_user_id(); $offer_dismissed = get_transient($offer_key); if (ABPTB_WC < 2) { $type = ABPTB_WC == 1 ? 'wc_active' : 'wc_install_active'; $btn = ABPTB_WC == 1 ? __('Active Now', 'abp-transport-booking') : __('Install & Active Now', 'abp-transport-booking'); ?>

ABP50 at checkout.', 'abp-transport-booking')); ?>
__('Invalid security token or Insufficient permissions.', 'abp-transport-booking'), 'type' => 'warn'], 403); } $offer_key = 'abptb_pro_offer_dismissed_' . get_current_user_id(); set_transient($offer_key, 1, 24 * HOUR_IN_SECONDS); wp_send_json_success(['msg' => __('Offer dismissed.', 'abp-transport-booking')]); } public function wc_config(): void { if (!check_ajax_referer('abptb_admin_ajax_nonce', 'nonce', false) || !current_user_can('manage_options')) { wp_send_json_error(['msg' => __('Invalid security token or Insufficient permissions.', 'abp-transport-booking'), 'type' => 'warn'], 403); } $post_val = fn($key, $default = '') => isset($_POST[$key]) ? sanitize_text_field(wp_unslash($_POST[$key])) : $default; $page_type = $post_val('type'); if ($page_type == 'wc_install_active') { include_once(ABSPATH . 'wp-admin/includes/plugin-install.php'); include_once(ABSPATH . 'wp-admin/includes/file.php'); include_once(ABSPATH . 'wp-admin/includes/misc.php'); include_once(ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'); $plugin = 'woocommerce'; $api = plugins_api('plugin_information', array( 'slug' => $plugin, 'fields' => array( 'short_description' => false, 'sections' => false, 'requires' => false, 'rating' => false, 'ratings' => false, 'downloaded' => false, 'last_updated' => false, 'added' => false, 'tags' => false, 'compatibility' => false, 'homepage' => false, 'donate_link' => false, ), )); if (is_wp_error($api)) { wp_send_json_error(['html' => '', 'msg' => $api->get_error_message()]); } $title = 'title'; $url = 'url'; $nonce = 'nonce'; $woocommerce_plugin = new Plugin_Upgrader(new Plugin_Installer_Skin(compact('title', 'url', 'nonce', 'plugin', 'api'))); $installed = $woocommerce_plugin->install($api->download_link); if (is_wp_error($installed)) { wp_send_json_error(['msg' => $installed->get_error_message(), 'type' => 'warn']); } $activated = activate_plugin('woocommerce/woocommerce.php'); if (is_wp_error($activated)) { wp_send_json_error(['msg' => $activated->get_error_message(), 'type' => 'warn']); } wp_send_json_success(['msg' => esc_html__('WooCommerce installed and activated successfully!', 'abp-transport-booking'), 'type' => 'success'], 200); } if ($page_type == 'wc_active') { if (defined('ABPTB_WC') && ABPTB_WC == 1) { $activated = activate_plugin('woocommerce/woocommerce.php'); if (is_wp_error($activated)) { wp_send_json_error(['msg' => $activated->get_error_message(), 'type' => 'warn']); } wp_send_json_success(['msg' => esc_html__('WooCommerce activated successfully!', 'abp-transport-booking'), 'type' => 'success'], 200); } } wp_send_json_error(['msg' => esc_html__('WooCommerce is either not installed or already active.', 'abp-transport-booking'), 'type' => 'warn'], 403); } } new ABPTB_Dashboard(); }