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.
278 lines
9 KiB
PHP
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;
|
|
}
|
|
}
|