Initial release of Eventbrite for The Events Calendar (v1.0.0)
Bidirectional sync between Eventbrite and The Events Calendar, with WooCommerce ticket purchasing that bypasses Eventbrite's processing fees by registering buyers as free attendees via API. Includes venue/ organizer sync, QR code ticket generation, attendee management with CSV export, scheduled sync via WP-Cron, and real-time Eventbrite webhooks.
This commit is contained in:
commit
f3bc795d9a
15 changed files with 3132 additions and 0 deletions
278
includes/class-eb4tec-attendees.php
Normal file
278
includes/class-eb4tec-attendees.php
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_Attendees {
|
||||
|
||||
public function __construct( private readonly EB4TEC_API_Client $api ) {
|
||||
add_action( 'admin_menu', [ $this, 'add_admin_menu' ] );
|
||||
add_action( 'admin_post_eb4tec_export_attendees', [ $this, 'handle_csv_export' ] );
|
||||
}
|
||||
|
||||
public function add_admin_menu(): void {
|
||||
add_submenu_page(
|
||||
'edit.php?post_type=tribe_events',
|
||||
__( 'Attendees (Eventbrite)', 'eb4tec' ),
|
||||
__( 'Attendees (Eventbrite)', 'eb4tec' ),
|
||||
'manage_options',
|
||||
'eb4tec-attendees',
|
||||
[ $this, 'render_attendees_page' ]
|
||||
);
|
||||
}
|
||||
|
||||
public function render_attendees_page(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$selected_post_id = (int) ( $_GET['event_post_id'] ?? 0 );
|
||||
$page_num = max( 1, (int) ( $_GET['paged'] ?? 1 ) );
|
||||
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html__( 'Eventbrite Attendees', 'eb4tec' ) . '</h1>';
|
||||
|
||||
$this->render_event_selector( $selected_post_id );
|
||||
|
||||
if ( ! $selected_post_id ) {
|
||||
echo '<p>' . esc_html__( 'Select an event above to view its attendees.', 'eb4tec' ) . '</p>';
|
||||
echo '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$eb_event_id = (string) get_post_meta( $selected_post_id, '_eb4tec_event_id', true );
|
||||
if ( ! $eb_event_id ) {
|
||||
echo '<p class="notice notice-warning">' . esc_html__( 'This event is not linked to an Eventbrite event.', 'eb4tec' ) . '</p>';
|
||||
echo '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $this->api->get_attendees( $eb_event_id, $page_num );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html( $response->get_error_message() ) . '</p></div>';
|
||||
echo '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$attendees = $response['attendees'] ?? [];
|
||||
$pagination = $response['pagination'] ?? [];
|
||||
$total = (int) ( $pagination['object_count'] ?? 0 );
|
||||
$page_count = (int) ( $pagination['page_count'] ?? 1 );
|
||||
|
||||
// Export button.
|
||||
echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="margin-bottom:12px;">';
|
||||
wp_nonce_field( "eb4tec_export_attendees_{$eb_event_id}", '_eb4tec_export_nonce' );
|
||||
echo '<input type="hidden" name="action" value="eb4tec_export_attendees">';
|
||||
echo '<input type="hidden" name="eb_event_id" value="' . esc_attr( $eb_event_id ) . '">';
|
||||
echo '<input type="hidden" name="event_post_id" value="' . esc_attr( $selected_post_id ) . '">';
|
||||
submit_button( sprintf( __( 'Export All %d Attendees to CSV', 'eb4tec' ), $total ), 'secondary', 'submit', false );
|
||||
echo '</form>';
|
||||
|
||||
$this->render_attendee_table( $attendees, $selected_post_id );
|
||||
$this->render_pagination( $page_num, $page_count, $selected_post_id );
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_event_selector( int $selected_post_id ): void {
|
||||
$events = get_posts( [
|
||||
'post_type' => 'tribe_events',
|
||||
'post_status' => [ 'publish', 'draft' ],
|
||||
'posts_per_page' => 200,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'meta_query' => [ [
|
||||
'key' => '_eb4tec_event_id',
|
||||
'compare' => 'EXISTS',
|
||||
] ],
|
||||
] );
|
||||
|
||||
$base_url = admin_url( 'edit.php?post_type=tribe_events&page=eb4tec-attendees' );
|
||||
|
||||
echo '<p>';
|
||||
echo '<label for="eb4tec_event_selector"><strong>' . esc_html__( 'Event:', 'eb4tec' ) . '</strong></label> ';
|
||||
echo '<select id="eb4tec_event_selector" onchange="window.location=this.options[this.selectedIndex].value">';
|
||||
echo '<option value="' . esc_url( $base_url ) . '">' . esc_html__( '— Select an event —', 'eb4tec' ) . '</option>';
|
||||
|
||||
foreach ( $events as $event ) {
|
||||
$url = esc_url( add_query_arg( 'event_post_id', $event->ID, $base_url ) );
|
||||
$label = $event->post_title ?: __( '(untitled)', 'eb4tec' );
|
||||
printf(
|
||||
'<option value="%s" %s>%s</option>',
|
||||
$url,
|
||||
selected( $selected_post_id, $event->ID, false ),
|
||||
esc_html( $label )
|
||||
);
|
||||
}
|
||||
|
||||
echo '</select>';
|
||||
echo '</p>';
|
||||
}
|
||||
|
||||
private function render_attendee_table( array $attendees, int $post_id ): void {
|
||||
if ( empty( $attendees ) ) {
|
||||
echo '<p>' . esc_html__( 'No attendees found for this event.', 'eb4tec' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a map of EB attendee ID → WC order number from stored order meta.
|
||||
$order_map = $this->build_order_map( $post_id );
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped">';
|
||||
echo '<thead><tr>';
|
||||
$headers = [
|
||||
__( 'Name', 'eb4tec' ),
|
||||
__( 'Email', 'eb4tec' ),
|
||||
__( 'WC Order #', 'eb4tec' ),
|
||||
__( 'Ticket Class', 'eb4tec' ),
|
||||
__( 'Status', 'eb4tec' ),
|
||||
__( 'Checked In', 'eb4tec' ),
|
||||
__( 'Registered', 'eb4tec' ),
|
||||
];
|
||||
foreach ( $headers as $h ) {
|
||||
echo '<th>' . esc_html( $h ) . '</th>';
|
||||
}
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
foreach ( $attendees as $attendee ) {
|
||||
$profile = $attendee['profile'] ?? [];
|
||||
$name = esc_html( $profile['name'] ?? '' );
|
||||
$email = esc_html( $profile['email'] ?? '' );
|
||||
$status = esc_html( $attendee['status'] ?? '' );
|
||||
$checked = ! empty( $attendee['checked_in'] ) ? esc_html__( 'Yes', 'eb4tec' ) : esc_html__( 'No', 'eb4tec' );
|
||||
$tc_name = esc_html( $attendee['ticket_class'] ['name'] ?? '' );
|
||||
$created = esc_html( $attendee['created'] ?? '' );
|
||||
$eb_att_id = $attendee['id'] ?? '';
|
||||
$order_num = isset( $order_map[ $eb_att_id ] ) ? '#' . $order_map[ $eb_att_id ] : '—';
|
||||
|
||||
echo "<tr><td>{$name}</td><td>{$email}</td><td>{$order_num}</td><td>{$tc_name}</td><td>{$status}</td><td>{$checked}</td><td>{$created}</td></tr>";
|
||||
}
|
||||
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
|
||||
private function render_pagination( int $current, int $total_pages, int $post_id ): void {
|
||||
if ( $total_pages <= 1 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$base_url = add_query_arg( 'event_post_id', $post_id, admin_url( 'edit.php?post_type=tribe_events&page=eb4tec-attendees' ) );
|
||||
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
for ( $p = 1; $p <= $total_pages; $p++ ) {
|
||||
$url = esc_url( add_query_arg( 'paged', $p, $base_url ) );
|
||||
$class = ( $p === $current ) ? 'button button-primary' : 'button';
|
||||
echo "<a href=\"{$url}\" class=\"{$class}\">{$p}</a> ";
|
||||
}
|
||||
echo '</div></div>';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CSV export
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function handle_csv_export(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( esc_html__( 'Insufficient permissions.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
$eb_event_id = sanitize_text_field( $_POST['eb_event_id'] ?? '' );
|
||||
$post_id = (int) ( $_POST['event_post_id'] ?? 0 );
|
||||
|
||||
if ( ! $eb_event_id ) {
|
||||
wp_die( esc_html__( 'No event ID provided.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
check_admin_referer( "eb4tec_export_attendees_{$eb_event_id}", '_eb4tec_export_nonce' );
|
||||
|
||||
$order_map = $this->build_order_map( $post_id );
|
||||
$attendees = $this->fetch_all_attendees( $eb_event_id );
|
||||
|
||||
if ( is_wp_error( $attendees ) ) {
|
||||
wp_die( esc_html( $attendees->get_error_message() ) );
|
||||
}
|
||||
|
||||
$event_title = $post_id ? sanitize_file_name( get_the_title( $post_id ) ) : $eb_event_id;
|
||||
$filename = "attendees-{$event_title}-" . date( 'Y-m-d' ) . '.csv';
|
||||
|
||||
header( 'Content-Type: text/csv; charset=utf-8' );
|
||||
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
|
||||
header( 'Pragma: no-cache' );
|
||||
header( 'Expires: 0' );
|
||||
|
||||
$output = fopen( 'php://output', 'w' );
|
||||
|
||||
fputcsv( $output, [
|
||||
__( 'Name', 'eb4tec' ),
|
||||
__( 'Email', 'eb4tec' ),
|
||||
__( 'Order Number', 'eb4tec' ),
|
||||
__( 'Ticket Class', 'eb4tec' ),
|
||||
__( 'Status', 'eb4tec' ),
|
||||
__( 'Checked In', 'eb4tec' ),
|
||||
__( 'Registration Date', 'eb4tec' ),
|
||||
] );
|
||||
|
||||
foreach ( $attendees as $attendee ) {
|
||||
$profile = $attendee['profile'] ?? [];
|
||||
$eb_att_id = $attendee['id'] ?? '';
|
||||
$order_num = $order_map[ $eb_att_id ] ?? '';
|
||||
|
||||
fputcsv( $output, [
|
||||
$profile['name'] ?? '',
|
||||
$profile['email'] ?? '',
|
||||
$order_num,
|
||||
$attendee['ticket_class']['name'] ?? '',
|
||||
$attendee['status'] ?? '',
|
||||
! empty( $attendee['checked_in'] ) ? 'Yes' : 'No',
|
||||
$attendee['created'] ?? '',
|
||||
] );
|
||||
}
|
||||
|
||||
fclose( $output );
|
||||
exit;
|
||||
}
|
||||
|
||||
private function fetch_all_attendees( string $eb_event_id ): array|WP_Error {
|
||||
$page = 1;
|
||||
$attendees = [];
|
||||
|
||||
do {
|
||||
$response = $this->api->get_attendees( $eb_event_id, $page );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
$attendees = array_merge( $attendees, $response['attendees'] ?? [] );
|
||||
$has_more = ! empty( $response['pagination']['has_more_items'] );
|
||||
$page++;
|
||||
} while ( $has_more );
|
||||
|
||||
return $attendees;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a map of Eventbrite attendee ID → WooCommerce order number.
|
||||
* Searches orders that have this event's post ID stored in meta.
|
||||
*/
|
||||
private function build_order_map( int $post_id ): array {
|
||||
if ( ! $post_id ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$orders = wc_get_orders( [
|
||||
'meta_key' => '_eb4tec_event_post_id',
|
||||
'meta_value' => $post_id,
|
||||
'limit' => -1,
|
||||
'return' => 'ids',
|
||||
] );
|
||||
|
||||
$map = [];
|
||||
foreach ( $orders as $order_id ) {
|
||||
$attendee_ids = json_decode( get_post_meta( $order_id, '_eb4tec_attendee_ids', true ) ?: '[]', true );
|
||||
foreach ( $attendee_ids as $att_id ) {
|
||||
$map[ $att_id ] = $order_id;
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue