/**
* Plugin Name: Event Ticket Totals (custom)
* Description: Returns event-level totals (capacity, sold, available) for events in a category by inspecting Tribe tickets and WooCommerce orders.
* Version: 1.0
* Author: ChatGPT (example)
*/
add_action( 'rest_api_init', function() {
register_rest_route( 'greathall/v1', '/event-totals', array(
'methods' => 'GET',
'callback' => 'greathall_event_totals_with_events_handler_cached',
'permission_callback' => function () { return true; }, // adjust as needed
) );
}, 9 ); // run before some plugins that hook into rest
function greathall_set_cors_headers_for_request() {
$allowed_origins = array(
'http://localhost:3000',
'https://greathalltheatrical.com',
);
$origin = isset( $_SERVER['HTTP_ORIGIN'] ) ? trim( wp_unslash( $_SERVER['HTTP_ORIGIN'] ) ) : '';
if ( in_array( $origin, $allowed_origins, true ) ) {
header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
header( 'Access-Control-Allow-Credentials: true' );
}
header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS' );
header( 'Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Nonce, X-Requested-With' );
header( 'Access-Control-Expose-Headers: X-WP-Total, X-WP-TotalPages, Link' );
// quick respond to OPTIONS preflight on this route
if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'OPTIONS' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) {
http_response_code(200);
echo '';
exit;
}
}
function greathall_parse_woo_product_id( $global_id ) {
if ( empty( $global_id ) ) return null;
if ( preg_match( '/[?&]id=(\d+)/', $global_id, $m ) ) return intval( $m[1] );
if ( preg_match( '/(\d+)$/', $global_id, $m2 ) ) return intval( $m2[1] );
return null;
}
function greathall_prices_from_ticket_details( $ticket_details ) {
$regular_low = null; $regular_high = null;
$current_low = null; $current_high = null;
$to_float = function( $v ) {
if ( $v === null ) return null;
if ( $v === '' ) return null;
if ( is_string( $v ) ) {
$v = trim( $v );
if ( $v === '' ) return null;
}
return is_numeric( $v ) ? floatval( $v ) : null;
};
$update = function( &$min, &$max, $val ) {
if ( $val === null ) return;
if ( $min === null || $val < $min ) $min = $val;
if ( $max === null || $val > $max ) $max = $val;
};
if ( ! is_array( $ticket_details ) ) {
$ticket_details = array();
}
foreach ( $ticket_details as $td ) {
$pi = isset( $td['price_info'] ) ? $td['price_info'] : null;
if ( ! is_array( $pi ) ) continue;
$price = array_key_exists( 'price', $pi ) ? $to_float( $pi['price'] ) : null;
$regular = array_key_exists( 'regular_price', $pi ) ? $to_float( $pi['regular_price'] ) : null;
$sale = array_key_exists( 'sale_price', $pi ) ? $to_float( $pi['sale_price'] ) : null;
if ( $regular === null ) $regular = $price;
$is_on_sale = ! empty( $pi['is_on_sale'] );
$current = null;
if ( $is_on_sale && $sale !== null ) {
$current = $sale;
}
if ( $current === null ) $current = $price;
if ( $current === null ) $current = $regular;
$update( $regular_low, $regular_high, $regular );
$update( $current_low, $current_high, $current );
}
return array(
'regular' => array( 'low' => $regular_low, 'high' => $regular_high ),
'current' => array( 'low' => $current_low, 'high' => $current_high ),
);
}
/**
* Compute totals (capacity, sold, available) for a list of event IDs,
* and include per-ticket product price info (regular, sale, current).
*
* Returns an associative array:
* [ event_id => [
* 'total_capacity' => int|null,
* 'total_sold' => int,
* 'total_available' => int|null,
* 'tickets_count' => int,
* 'ticket_details' => [ { ticket_id, provider, capacity, product_id, sold, price_info }, ... ],
* 'prices' => [
* 'regular' => [ 'low' => float|null, 'high' => float|null ],
* 'current' => [ 'low' => float|null, 'high' => float|null ],
* ],
* ]
* ]
*/
function greathall_compute_totals_for_event_ids( $event_ids = array() ) {
global $wpdb;
if ( empty( $event_ids ) || ! is_array( $event_ids ) ) {
return array();
}
// Normalize and unique event IDs
$event_ids = array_map( 'intval', $event_ids );
$event_ids = array_values( array_filter( array_unique( $event_ids ), function( $v ) { return $v > 0; } ) );
if ( empty( $event_ids ) ) return array();
// Helper: parse product id from global_id string
$parse_product_id = function( $global_id ) {
if ( empty( $global_id ) ) return null;
if ( preg_match( '/[?&]id=(\d+)/', $global_id, $m ) ) return intval( $m[1] );
if ( preg_match( '/(\d+)$/', $global_id, $m2 ) ) return intval( $m2[1] );
return null;
};
// Helper: normalize numeric values to float or null
$to_float_or_null = function( $v ) {
if ( $v === null ) return null;
if ( $v === '' ) return null;
if ( is_string( $v ) ) {
$v = trim( $v );
if ( $v === '' ) return null;
}
if ( ! is_numeric( $v ) ) return null;
return floatval( $v );
};
// Helper: update min/max
$update_minmax = function( &$min, &$max, $val ) {
if ( $val === null ) return;
if ( $min === null || $val < $min ) $min = $val;
if ( $max === null || $val > $max ) $max = $val;
};
// Map to hold per-event ticket info and collect product ids
$event_ticket_map = array(); // event_id => [ ticket arrays... ]
$all_product_ids = array(); // product_id => true (set)
// For each event, call the internal tribe tickets route to get tickets
foreach ( $event_ids as $eid ) {
$route = '/tribe/tickets/v1/tickets';
$req = new WP_REST_Request( 'GET', $route );
$req->set_query_params( array( 'include_post' => $eid, 'per_page' => 100 ) );
$resp = rest_do_request( $req );
$tickets = array();
if ( ! $resp->is_error() ) {
$data = $resp->get_data();
if ( isset( $data['tickets'] ) && is_array( $data['tickets'] ) ) {
$tickets = $data['tickets'];
} elseif ( is_array( $data ) ) {
$tickets = $data;
}
}
$event_ticket_map[ $eid ] = array();
foreach ( $tickets as $t ) {
$tid = isset( $t['id'] ) ? intval( $t['id'] ) : null;
$provider = isset( $t['provider'] ) ? $t['provider'] : '';
$capacity = isset( $t['capacity'] ) ? intval( $t['capacity'] ) : null;
$global_id = isset( $t['global_id'] ) ? $t['global_id'] : '';
$product_id = null;
if ( $provider === 'woo' ) {
// In TEC Woo tickets, ticket "id" should be the Woo product ID.
$candidate = $tid ? intval( $tid ) : null;
// Sanity-check: confirm it's a real WC product (or variation). If not, fall back to global_id parsing.
$is_product = false;
if ( $candidate ) {
$pt = get_post_type( $candidate );
if ( $pt === 'product' || $pt === 'product_variation' ) {
$is_product = true;
}
}
if ( $is_product ) {
$product_id = $candidate;
} else {
// Fallback only if TEC is returning a non-product ticket ID for some reason.
$product_id = $parse_product_id( $global_id );
}
if ( $product_id ) {
$all_product_ids[ $product_id ] = true;
}
}
$event_ticket_map[ $eid ][] = array(
'ticket_id' => $tid,
'provider' => $provider,
'capacity' => $capacity,
'product_id' => $product_id,
);
}
}
// Build list of product IDs (integers)
$product_ids = array_map( 'intval', array_keys( $all_product_ids ) );
$product_sold_counts = array(); // product_id => total sold qty
if ( ! empty( $product_ids ) ) {
// Prepare placeholders for the IN clause
$placeholders = implode( ',', array_fill( 0, count( $product_ids ), '%d' ) );
// Table names with prefix
$prefix = $wpdb->prefix;
$order_items_table = $prefix . 'woocommerce_order_items';
$order_itemmeta_table = $prefix . 'woocommerce_order_itemmeta';
$posts_table = $prefix . 'posts';
// SQL: sum _qty grouped by _product_id for shop_order with desired statuses
$sql = "
SELECT pm2.meta_value AS product_id, SUM( CAST(pm1.meta_value AS UNSIGNED) ) AS total_qty
FROM {$order_items_table} AS oi
INNER JOIN {$order_itemmeta_table} AS pm1 ON oi.order_item_id = pm1.order_item_id AND pm1.meta_key = '_qty'
INNER JOIN {$order_itemmeta_table} AS pm2 ON oi.order_item_id = pm2.order_item_id AND pm2.meta_key = '_product_id'
INNER JOIN {$posts_table} AS p ON oi.order_id = p.ID
WHERE p.post_type = 'shop_order'
AND p.post_status IN ( 'wc-completed', 'wc-processing' )
AND pm2.meta_value IN ( {$placeholders} )
GROUP BY pm2.meta_value
";
// Build prepare args (first param is SQL string)
$prepare_args = array_merge( array( $sql ), $product_ids );
$prepared = call_user_func_array( array( $wpdb, 'prepare' ), $prepare_args );
$rows = $wpdb->get_results( $prepared );
if ( $rows ) {
foreach ( $rows as $r ) {
$pid = intval( $r->product_id );
$qty = intval( $r->total_qty );
$product_sold_counts[ $pid ] = $qty;
}
}
}
// Preload product price info using wc_get_product (if available)
$product_price_info = array(); // product_id => [ price, regular_price, sale_price, is_on_sale ]
if ( function_exists( 'wc_get_product' ) && ! empty( $product_ids ) ) {
foreach ( $product_ids as $pid ) {
$prod = wc_get_product( $pid );
if ( $prod ) {
$product_price_info[ $pid ] = array(
'price' => $prod->get_price(), // current effective price (string)
'regular_price' => $prod->get_regular_price(), // string or ''
'sale_price' => $prod->get_sale_price(), // string or ''
'is_on_sale' => $prod->is_on_sale() ? true : false,
);
} else {
$product_price_info[ $pid ] = array(
'price' => null,
'regular_price' => null,
'sale_price' => null,
'is_on_sale' => false,
);
}
}
}
// Now compute totals for each event using its tickets and the product_sold_counts
$results = array();
foreach ( $event_ticket_map as $eid => $tickets ) {
// Deduplicate tickets by ticket_id if provided
$seen = array();
$unique = array();
foreach ( $tickets as $t ) {
if ( $t['ticket_id'] && isset( $seen[ $t['ticket_id'] ] ) ) continue;
if ( $t['ticket_id'] ) $seen[ $t['ticket_id'] ] = true;
$unique[] = $t;
}
$total_capacity = 0;
$found_positive_capacity = false;
$total_sold = 0;
// Price ranges (per event)
$regular_low = null;
$regular_high = null;
$current_low = null;
$current_high = null;
// Build ticket_details to include price info
$ticket_details = array();
foreach ( $unique as $t ) {
$cap = $t['capacity'];
if ( $cap && $cap > 0 ) {
$found_positive_capacity = true;
$total_capacity += intval( $cap );
}
$ticket_product_sold = 0;
$price_info = null;
if ( $t['provider'] === 'woo' && $t['product_id'] ) {
$pid = intval( $t['product_id'] );
$ticket_product_sold = isset( $product_sold_counts[ $pid ] ) ? intval( $product_sold_counts[ $pid ] ) : 0;
$price_info = isset( $product_price_info[ $pid ] ) ? $product_price_info[ $pid ] : null;
// Update price ranges from this ticket
if ( is_array( $price_info ) ) {
// Regular/original price: prefer regular_price, else fallback to price
$regular = null;
if ( array_key_exists( 'regular_price', $price_info ) ) {
$regular = $to_float_or_null( $price_info['regular_price'] );
}
if ( $regular === null && array_key_exists( 'price', $price_info ) ) {
$regular = $to_float_or_null( $price_info['price'] );
}
// Current/effective price: prefer sale_price if on sale, else price, else regular
$current = null;
$is_on_sale = ! empty( $price_info['is_on_sale'] );
if ( $is_on_sale && array_key_exists( 'sale_price', $price_info ) ) {
$current = $to_float_or_null( $price_info['sale_price'] );
}
if ( $current === null && array_key_exists( 'price', $price_info ) ) {
$current = $to_float_or_null( $price_info['price'] );
}
if ( $current === null ) {
$current = $regular;
}
$update_minmax( $regular_low, $regular_high, $regular );
$update_minmax( $current_low, $current_high, $current );
}
}
$total_sold += $ticket_product_sold;
$ticket_details[] = array(
'ticket_id' => $t['ticket_id'],
'provider' => $t['provider'],
'capacity' => $t['capacity'],
'product_id' => $t['product_id'],
'sold' => $ticket_product_sold,
'price_info' => $price_info,
);
}
if ( ! $found_positive_capacity ) {
$total_capacity = null;
}
$total_available = ( $total_capacity !== null )
? max( 0, (int) $total_capacity - (int) $total_sold )
: null;
$results[ $eid ] = array(
'total_capacity' => $total_capacity,
'total_sold' => $total_sold,
'total_available' => $total_available,
'tickets_count' => count( $unique ),
'ticket_details' => $ticket_details,
'prices' => array(
'regular' => array(
'low' => $regular_low,
'high' => $regular_high,
),
'current' => array(
'low' => $current_low,
'high' => $current_high,
),
),
);
}
return $results;
}
/* ------------------------------------------------------------
Greathall: merged events + ticket totals endpoint with caching
- returns full tribe event objects with ticketTotals (and price_info)
- caches per-category result in a transient
- invalidates cache on product/order/ticket changes
------------------------------------------------------------ */
/**
* Handler: returns merged events with ticket totals & price details.
* Uses a per-category transient cache.
*/
function greathall_event_totals_with_events_handler_cached( $request ) {
// allow CORS for dev/prod origins (reuse existing function)
if ( function_exists( 'greathall_set_cors_headers_for_request' ) ) {
greathall_set_cors_headers_for_request();
}
$params = $request->get_params();
$cat_id = isset( $params['category'] ) ? intval( $params['category'] ) : 0;
if ( ! $cat_id ) {
return new WP_Error( 'missing_category', 'Please provide category query param: ?category=ID', array( 'status' => 400 ) );
}
// cache key and TTL (seconds)
$transient_key = 'ght_event_totals_cat_' . $cat_id;
$cache_ttl = 300; // 5 minutes — adjust as you like
// return cached if present
$cached = get_transient( $transient_key );
if ( false !== $cached ) {
return rest_ensure_response( $cached );
}
// 1) fetch tribe events internally
$route = '/tribe/events/v1/events';
$evReq = new WP_REST_Request( 'GET', $route );
$evReq->set_query_params( array( 'categories' => $cat_id, 'per_page' => 100 ) );
$evResp = rest_do_request( $evReq );
if ( $evResp->is_error() ) {
return rest_ensure_response( array( 'error' => 'Failed to get tribe events' ) );
}
$eventsData = $evResp->get_data();
// normalize shapes
if ( isset( $eventsData['events'] ) && is_array( $eventsData['events'] ) ) {
$events = $eventsData['events'];
} elseif ( is_array( $eventsData ) ) {
$events = $eventsData;
} else {
$events = array();
}
// collect event ids
$event_ids = array();
foreach ( $events as $e ) {
$eid = isset( $e['id'] ) ? intval( $e['id'] ) : null;
if ( $eid ) $event_ids[] = $eid;
}
$event_ids = array_values( array_unique( $event_ids ) );
// 2) compute totals map using your fast function
// greathall_compute_totals_for_event_ids() should return per-event arrays
if ( function_exists( 'greathall_compute_totals_for_event_ids' ) ) {
$totals_map = greathall_compute_totals_for_event_ids( $event_ids );
} else {
$totals_map = array();
}
// 3) merge and compute a small price summary per event
$merged = array();
foreach ( $events as $ev ) {
$id = isset( $ev['id'] ) ? intval( $ev['id'] ) : null;
$ticketTotals = isset( $totals_map[ $id ] ) ? $totals_map[ $id ] : null;
if ( is_array( $ticketTotals ) ) {
$tds = isset( $ticketTotals['ticket_details'] ) ? $ticketTotals['ticket_details'] : array();
$ticketTotals['prices'] = greathall_prices_from_ticket_details( $tds );
}
$ev['ticketTotals'] = $ticketTotals;
// Optional: also inject ticketPrices so the client always has it
if ( is_array( $ticketTotals ) && isset( $ticketTotals['prices'] ) ) {
$ev['ticketPrices'] = array( 'prices' => $ticketTotals['prices'] );
}
// Build price summary from ticket_details if present
$price_summary = null;
if ( is_array( $ticketTotals ) && ! empty( $ticketTotals['ticket_details'] ) ) {
$min_sale = null;
$max_regular = null;
foreach ( $ticketTotals['ticket_details'] as $td ) {
$pi = isset( $td['price_info'] ) ? $td['price_info'] : null;
if ( is_array( $pi ) ) {
// use numeric floats if possible
$sale = isset( $pi['sale_price'] ) && $pi['sale_price'] !== '' ? floatval( $pi['sale_price'] ) : null;
$regular = isset( $pi['regular_price'] ) && $pi['regular_price'] !== '' ? floatval( $pi['regular_price'] ) : null;
if ( $sale !== null ) {
if ( $min_sale === null || $sale < $min_sale ) $min_sale = $sale;
}
if ( $regular !== null ) {
if ( $max_regular === null || $regular > $max_regular ) $max_regular = $regular;
}
}
}
$price_summary = array(
'min_sale_price' => $min_sale,
'max_regular_price'=> $max_regular,
);
}
// attach to event object (nest to avoid collisions)
$ev['ticketTotals'] = $ticketTotals;
$ev['priceSummary'] = $price_summary;
$merged[] = $ev;
}
// 4) cache and return
greathall_build_and_save_mapping_from_merged( $merged );
set_transient( $transient_key, $merged, $cache_ttl );
return rest_ensure_response( $merged );
}
/* ------------------------------------------------------------------
Surgical cache invalidation for greathall event totals (per-category)
- Maintains a mapping option to know which categories are affected by each product/event
- Invalidates only the affected category transients on changes
------------------------------------------------------------------ */
/**
* Save mapping data used for targeted invalidation.
* Structure saved in option 'ght_cache_map':
* [
* 'product_to_categories' => [ product_id => [cat_id, ...], ... ],
* 'event_to_categories' => [ event_id => [cat_id, ...], ... ],
* 'updated' => timestamp
* ]
*/
function greathall_save_cache_map( $map ) {
if ( ! is_array( $map ) ) $map = array();
update_option( 'ght_cache_map', $map, true );
}
/**
* Get the mapping option, or empty array.
*/
function greathall_get_cache_map() {
$map = get_option( 'ght_cache_map', array() );
if ( ! is_array( $map ) ) $map = array();
return $map;
}
/**
* Delete category transients for the provided category IDs.
*/
function greathall_clear_transients_for_categories( $category_ids = array() ) {
if ( empty( $category_ids ) || ! is_array( $category_ids ) ) return;
foreach ( $category_ids as $cat_id ) {
$cat_id = intval( $cat_id );
if ( $cat_id <= 0 ) continue;
$key = 'ght_event_totals_cat_' . $cat_id;
delete_transient( $key );
}
}
/**
* Fallback: delete all ght_event_totals_cat_* transients (used if mapping missing)
*/
function greathall_clear_all_event_totals_cache() {
global $wpdb;
$opts = $wpdb->options;
$like_values = $wpdb->esc_like( '_transient_ght_event_totals_cat_' ) . '%';
$like_timeouts = $wpdb->esc_like( '_transient_timeout_ght_event_totals_cat_' ) . '%';
$wpdb->query( $wpdb->prepare(
"DELETE FROM {$opts} WHERE option_name LIKE %s OR option_name LIKE %s",
$like_values,
$like_timeouts
) );
}
/**
* Utility: add categories to map arrays (ensures uniqueness)
*/
function greathall_map_add_categories( &$map_array, $key_id, $categories ) {
if ( empty( $categories ) ) return;
if ( ! isset( $map_array[ $key_id ] ) || ! is_array( $map_array[ $key_id ] ) ) {
$map_array[ $key_id ] = array();
}
foreach ( $categories as $c ) {
$cid = intval( $c );
if ( $cid && ! in_array( $cid, $map_array[ $key_id ], true ) ) {
$map_array[ $key_id ][] = $cid;
}
}
}
/**
* Rebuild mapping for a category result set when we generate (or refresh) it.
* Call this inside your cached handler right *before* set_transient($transient_key, $merged, $cache_ttl)
* so the mapping reflects the data just cached.
*
* Expects $merged: array of event objects returned to client (each with id and ticketTotals.ticket_details)
*/
function greathall_build_and_save_mapping_from_merged( $merged ) {
// structure
$map = array(
'product_to_categories' => array(),
'event_to_categories' => array(),
'updated' => time(),
);
foreach ( $merged as $ev ) {
$eid = isset( $ev['id'] ) ? intval( $ev['id'] ) : null;
// tribe events payload usually has categories as array of { id, name, ... } — be defensive
$cats = array();
if ( isset( $ev['categories'] ) && is_array( $ev['categories'] ) ) {
foreach ( $ev['categories'] as $c ) {
if ( is_array( $c ) && isset( $c['id'] ) ) $cats[] = intval( $c['id'] );
elseif ( is_numeric( $c ) ) $cats[] = intval( $c );
}
}
if ( $eid ) {
greathall_map_add_categories( $map['event_to_categories'], $eid, $cats );
}
// tickets may be nested under ticketTotals.ticket_details
$tds = isset( $ev['ticketTotals']['ticket_details'] ) ? $ev['ticketTotals']['ticket_details'] : array();
if ( is_array( $tds ) && ! empty( $tds ) ) {
foreach ( $tds as $td ) {
$pid = isset( $td['product_id'] ) ? intval( $td['product_id'] ) : null;
if ( $pid ) {
greathall_map_add_categories( $map['product_to_categories'], $pid, $cats );
}
}
}
}
greathall_save_cache_map( $map );
}
/* ------------------------------------------------------------------
Targeted invalidation hooks
- product save/update
- product (woocommerce_update_product)
- order status changes (invalidate categories touched by order's products)
- ticket/event saves
------------------------------------------------------------------ */
/**
* When a product is updated (save_post), clear only categories associated with that product.
* Use the mapping to find categories.
*/
add_action( 'save_post', function( $post_id, $post, $update ) {
if ( $post->post_type !== 'product' ) return;
$pid = intval( $post_id );
$map = greathall_get_cache_map();
if ( empty( $map['product_to_categories'] ) ) {
greathall_clear_all_event_totals_cache();
return;
}
if ( isset( $map['product_to_categories'][ $pid ] ) ) {
greathall_clear_transients_for_categories( $map['product_to_categories'][ $pid ] );
} else {
// If product not in map, safe fallback: do nothing or clear all (choose fallback)
// We'll clear nothing to avoid heavy churn. If you prefer safety, uncomment next line:
// greathall_clear_all_event_totals_cache();
}
}, 20, 3 );
/**
* Woo hook (extra safety) when product updated
*/
add_action( 'woocommerce_update_product', function( $product_id ) {
$map = greathall_get_cache_map();
if ( ! empty( $map['product_to_categories'][ $product_id ] ) ) {
greathall_clear_transients_for_categories( $map['product_to_categories'][ $product_id ] );
}
} );
/**
* When an order changes status, clear categories for products in that order.
*/
add_action( 'woocommerce_order_status_changed', function( $order_id, $old_status, $new_status ) {
try {
$order = wc_get_order( $order_id );
if ( ! $order ) return;
$product_ids = array();
foreach ( $order->get_items() as $item ) {
$pid = $item->get_product_id();
if ( $pid ) $product_ids[] = intval( $pid );
}
$product_ids = array_values( array_filter( array_unique( $product_ids ) ) );
if ( empty( $product_ids ) ) return;
$map = greathall_get_cache_map();
if ( empty( $map['product_to_categories'] ) ) {
greathall_clear_all_event_totals_cache();
return;
}
$cats_to_clear = array();
foreach ( $product_ids as $pid ) {
if ( isset( $map['product_to_categories'][ $pid ] ) ) {
$cats_to_clear = array_merge( $cats_to_clear, $map['product_to_categories'][ $pid ] );
}
}
$cats_to_clear = array_values( array_unique( array_map( 'intval', $cats_to_clear ) ) );
if ( ! empty( $cats_to_clear ) ) {
greathall_clear_transients_for_categories( $cats_to_clear );
}
} catch ( Exception $e ) {
// fallback - if anything goes wrong, clear all
greathall_clear_all_event_totals_cache();
}
}, 10, 3 );
/**
* When a tribe event (or ticket) is saved, clear the categories for that event only.
* We'll look up the event's categories from event_to_categories map or from the event itself.
*/
add_action( 'save_post', function( $post_id, $post, $update ) {
// handle tribe_events post saves
if ( $post->post_type === 'tribe_events' ) {
$eid = intval( $post_id );
$map = greathall_get_cache_map();
// If mapping knows categories for this event, clear them
if ( ! empty( $map['event_to_categories'][ $eid ] ) ) {
greathall_clear_transients_for_categories( $map['event_to_categories'][ $eid ] );
return;
}
// fallback: get categories from event's terms if possible
$terms = wp_get_object_terms( $eid, 'tribe_events_cat', array( 'fields' => 'ids' ) );
if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
greathall_clear_transients_for_categories( $terms );
return;
}
// last resort: clear all
greathall_clear_all_event_totals_cache();
}
// handle ticket post saves if ticket is stored as post type (some installs)
if ( $post->post_type === 'tribe_tickets' ) {
// ticket -> post_id points to event id; try to retrieve
$ticket_id = intval( $post_id );
// internal REST request to get ticket detail and its post_id
$route = '/tribe/tickets/v1/tickets/' . $ticket_id;
$req = new WP_REST_Request( 'GET', $route );
$resp = rest_do_request( $req );
if ( ! $resp->is_error() ) {
$data = $resp->get_data();
$event_post_id = isset( $data['post_id'] ) ? intval( $data['post_id'] ) : null;
if ( $event_post_id ) {
$map = greathall_get_cache_map();
if ( ! empty( $map['event_to_categories'][ $event_post_id ] ) ) {
greathall_clear_transients_for_categories( $map['event_to_categories'][ $event_post_id ] );
return;
}
$terms = wp_get_object_terms( $event_post_id, 'tribe_events_cat', array( 'fields' => 'ids' ) );
if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
greathall_clear_transients_for_categories( $terms );
return;
}
}
}
// fallback:
greathall_clear_all_event_totals_cache();
}
}, 11, 3 );
Past Events from June 20, 2025 – May 26, 2025 – Page 5 – Great Hall Theatrics
-
-
document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });
$18.00 – $26.00
-
-
document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });
$16.00 – $24.00
-
document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });
$16.00 – $24.00
-
document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });
$16.00 – $24.00
-
document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });
$18.00 – $26.00
-
document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });
$16.00 – $24.00
-
document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });
$16.00 – $24.00
-
document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });
$16.00 – $24.00
-
document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });
$16.00 – $24.00
-
document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });
$16.00 – $24.00