eventbrite-for-the-events-c.../includes/class-eb4tec-attendees.php
Laurence Horrocks-Barlow f3bc795d9a 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.
2026-05-17 08:48:04 +01:00

278 lines
9 KiB
PHP

<?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;
}
}