For the purposes of this article in the main I will be showing you how I added a dashboard to manage bookings sold using the Woocommerce Bookings plugin via the Woocommerce Product Vendors plugin, because that is what I did the code for recently and so it is all packaged up nice and neat and all tested like. However, it could easily be used to manage products from other extensions, and also other options such as shipping etc from the same place.

I have added the code for this to a Gist - so please check there.

Now - I build all my Woocommerce extensions using a framework of my own creation, and the first step in this framework is to only run the functionality if the plugins it needs are active:

In this case we need WooCommerce, WooCommerce Product Vendors and WooCommere Bookings so let’s find out if we have them:

 class WVBM {

    private $textdomain = "woocommerce-vendors-bookings-management";
    private $required_plugins = array('woocommerce', 'woocommerce-bookings', 'woocommerce-product-vendors');

    function have_required_plugins() {
        $active_plugins = (array) get_option('active_plugins', array());
        if (is_multisite()) {
            $active_plugins = array_merge($active_plugins, get_site_option('active_sitewide_plugins', array()));
        foreach ($this->required_plugins as $key => $required) {
            $required = (!is_numeric($key)) ? "{$key}/{$required}.php" : "{$required}/{$required}.php";
            if (!in_array($required, $active_plugins) && !array_key_exists($required, $active_plugins))
                return false;
        return true;

    function __construct() {
        if (!$this->have_required_plugins())

So as you can see we store the three plugin names to an array, the have_required_plugins function then collects up all the places where activated plugins are stored, and checks for the required ones. Note active plugins are stored by their foldername/filename - in most cases both are the same, but if you come across one where the filename is different then just store it as a key=>value element like:

private $required_plugins = array('woocommerce', 'woocommerce-bookings' => 'woo-bookings', 'woocommerce-product-vendors');

The code in have_required_plugins is ready to handle such cases.

Now - we are going to have our vendors dashboard on its own url - /vendors-dashboard. There is a simple way to do this: simply add a page and then introduce the dashboard code via a shortcode. However, we want to do it the new shiny cool way, which is adding a custom endpoint.

In order to do this you have to register the new query var into the set (existing set is things like ‘page’, ‘post’ etc), you have to register that new query var as an endpoint, and then handle the processing of code when someone visits the endpoint (url). Adding new rewrite rules to WordPress also means you need to flush out the system.

The best way I have found to add the endpoint and rewrite rules is to do it on activation of your plugin.

register_activation_hook(__FILE__, array($this, 'rewrite_flush'));

function rewrite_flush() {

So to register the endpoint’s query var:

add_filter('query_vars', array($this, 'add_query_vars'));

function add_query_vars($vars) {
        $vars[] = 'vendors-dashboard';
        return $vars;

Now on init we add our endpoint:


function endpoint_keeper() {
         add_rewrite_endpoint('vendors-dashboard', EP_ALL);

Now to handle the requests for our endpoint (a request is what is made to the server to receive the content for a given url).

add_action('parse_request', array($this, 'parse_request'), 0);

function parse_request($request) {
        global $wp;
        $key = 'vendors-dashboard';
    if ( isset( $_GET[ $key ] ) ) {
            $wp->query_vars[ $key ] = $_GET[ $key ];
    else if ( isset( $wp->query_vars[ $key ] ) ) {
            $wp->query_vars[ $key ] = $wp->query_vars[ $key ];


We simply check if we are serving our endpoint, and if we are build the query var into the $wp array. We then use that query var to check if we are on our page and serve up our custom content like so:


 function get_template($template) {
        global $wp;
        if (isset($wp->query_vars['vendors-dashboard']))
            $template = trailingslashit(plugin_dir_path(__FILE__)) . "vendors-dashboard.php";
        return $template;

The only other thing we do in our main file is hook a function for processing the action of confirming the bookings - I could have used ajax for this but chose to use php:


function process_confirm() {
        if ($_GET['wvbm_action'] == "confirm") {
            if (get_post_type($_GET['booking_id']) != "wc_booking" || !wp_verify_nonce($_REQUEST['security'],'vendor-booking-confirm-noncerator')) {
                wc_add_notice("Invalid request - booking not confirmed","error");
                $url = site_url() . "/vendors-dashboard/".get_query_var('vendors-dashboard')."/";
                header("Location: ".$url);
            $booking = new WC_Booking($_GET['booking_id']);
            wc_add_notice("Booking ".$_GET['booking_id']." confirmed","success");

Now onto the vendors-dashboard.php template:

global $WVBM;
?><div id="content" class="woocommerce container">
        <div class="row product content-area">

            <main id="main" class="site-main col-xl-12" role="main">
<?php $WVBM->show_dashboard(); ?>
<?php get_footer(); ?>

Note two key parts here:

global $WVBM
  • this is the global we have stored the main plugin class into, and bringing it in allows us to use the biggest function from that class:

function show_dashboard() {
        /* extract(shortcode_atts(array(
          'active_tabs' => 'bookings'//comma seperated list of active tabs
          ),$atts)); */
        $vendor = get_term_by('slug',get_query_var('vendors-dashboard'),'wcpv_product_vendors');
        if (!$vendor) {
            echo "

Vendor not found!

"; return; } $user = get_current_user_id(); if (!current_user_can('administrator') && !is_vendor_admin($vendor->term_id,$user)) { echo "

You do not have permission to view this page

"; return; } $cols = array('Booking ID', 'Parent Order', 'Date', 'Start Time', 'End Time', '# of Guests', 'Price', 'User', 'Date Applied', 'Status','Actions'); $tabs = array('bookings'); $posts_per_page = 10; $page = (get_query_var('paged')) ? 1 : get_query_var('paged'); $bookings = new WP_Query(array( 'post_type' => 'wc_booking', 'posts_per_page' => $posts_per_page, 'meta_key' => '_booking_vendor', 'meta_value' => $vendor->term_id, 'post_status' => 'any', 'paged' => get_query_var('paged') )); $data = array(); foreach($bookings->posts as $key => $booking) { $_booking = new WC_Booking($booking->ID); $user = get_post_meta($booking->ID,'_booking_customer_id',true); $actions = array(); if ($booking->post_status != "cancelled") array_push($actions,"cancel"); if ($booking->post_status == "pending-confirmation") array_push($actions,"confirm"); $action_strings = array(); foreach($actions as $action) { $action_url = add_query_arg(array('wvbm_action'=>$action,'booking_id'=>$booking->ID,'security'=>wp_create_nonce('vendor-booking-confirm-noncerator')),site_url()."/vendors-dashboard/".get_query_var('vendors-dashboard')."/"); $action_strings[] = ($action == "cancel") ? "Cancel" : "".ucfirst($action).""; } $data[$booking->ID] = array( 'Booking ID' => $booking->ID, 'Parent Order' => $booking->post_parent, 'Date' => date("Y-m-d",strtotime(get_post_meta($booking->ID,'_booking_start',true))), 'Start Time' => date("h:ia",strtotime(get_post_meta($booking->ID,'_booking_start',true))), 'End Time' => date("h:ia",strtotime(get_post_meta($booking->ID,'_booking_end',true))), '# of Guests' => count(get_post_meta($booking->ID,'_booking_persons',true)), 'Price' => get_post_meta($booking->ID,'_booking_cost',true), 'User' => (get_user_meta($user,'billing_first_name',true) !== "") ? get_user_meta($user,'billing_first_name',true) . " " . get_user_meta($user,'billing_last_name',true) : $user->user_login, 'Date Applied' => $booking->post_date, 'Status' => $booking->post_status, 'Actions' => implode(" | ",$action_strings) ); } wc_print_notices(); ?><div class="woocommerce-tabs"> <ul class="tabs"> <?php foreach ($tabs as $key => $tab) { $class = ($key == 0) ? "active" : ""; ?><li class="dashboard_tab $tab) { ?><div class="panel entry-content" id="tab-<?php echo $tab; ?>" style="display: block;"> <table class="<?php echo $tab; ?>-table"> <thead> <tr> <?php foreach ($cols as $key => $col) { ?> <th><?php echo $col; ?></th><?php } ?> </tr> </thead> <tbody> <?php foreach ($data as $booking_id => $booking) { ?> <tr><?php foreach($cols as $key => $col) { ?> <td><?php echo $booking[$col]; ?></td> <?php } ?> </tr><?php } ?> </tbody> </table> </div><?php } $big = 999999999; // need an unlikely integer echo paginate_links( array( 'base' => str_replace( $big, '%#%', esc_url( get_pagenum_link( $big ) ) ), 'format' => '?paged=%#%', 'current' => max( 1, get_query_var('paged') ), 'total' => $bookings->max_num_pages ) );?> </div><?php }

That is pretty straightforward stuff, it pulls up the bookings for that vendor’s products and displays them in a html table complete with links for cancelling and confirming the booking. Simples…