__( 'Any answer', 'rooster-lms' ), 'yes' => RsvpAnswer::YES->label(), 'no' => RsvpAnswer::NO->label(), 'maybe' => RsvpAnswer::MAYBE->label(), 'none' => RsvpAnswer::NONE->label(), ) ), new Filter( 'state', __( 'Sent', 'rooster-lms' ), array( '' => __( 'Any', 'rooster-lms' ), 'queued' => InviteeState::QUEUED->label(), 'sent' => InviteeState::SENT->label(), 'failed' => InviteeState::FAILED->label(), 'suppressed' => InviteeState::SUPPRESSED->label(), ) ), new Filter( 'outcome', __( 'Outcome', 'rooster-lms' ), array( '' => __( 'Any outcome', 'rooster-lms' ), 'attended' => InviteeOutcome::ATTENDED->label(), 'enrolled' => InviteeOutcome::ENROLLED->label(), 'joined' => InviteeOutcome::JOINED->label(), 'none' => InviteeOutcome::NONE->label(), ) ), ); } public function local_filters(): array { return array( 'answer' => array(), 'state' => array(), 'outcome' => array(), ); } public function filter_values( mixed $row ): array { if ( ! is_array( $row ) ) { return array(); } $answer = RsvpAnswer::tryFrom( (int) $row['answer'] ); $state = match ( InviteeState::tryFrom( (int) $row['state'] ) ) { InviteeState::QUEUED => 'queued', InviteeState::SENT => 'sent', InviteeState::FAILED => 'failed', InviteeState::SUPPRESSED => 'suppressed', default => '', }; $outcome = InviteeOutcome::tryFrom( (int) ( $row['outcome'] ?? 0 ) ); return array( 'answer' => null === $answer ? array() : array( RsvpAnswer::NONE === $answer ? 'none' : $answer->slug() ), 'state' => '' === $state ? array() : array( $state ), 'outcome' => null === $outcome ? array() : array( $outcome->slug() ), ); } public function local_sorts(): array { return array( 'person' => array( 'type' => 'text', 'tie' => 'dir', ), 'answered' => array( 'type' => 'date', 'tie' => 'dir', ), ); } public function sort_values( mixed $row ): array { if ( ! is_array( $row ) ) { return array(); } $own = 0 === (int) $row['user_id'] || in_array( (int) $row['source'], array( InviteeSource::CSV->value, InviteeSource::EXTERNAL->value ), true ); return array( '_id' => (int) $row['id'], 'person' => (string) ( $own ? $row['last_name'] : ( $row['display_name'] ?? '' ) ), 'answered' => $row['answered_at'] ?? null, ); } public function default_order(): array { return array( 'id', 'asc' ); } public function arg_prefix(): string { return 'rli_'; } public function mode(): string { return 'rest'; } public function caption(): string { return __( 'Invited people', 'rooster-lms' ); } public function empty_text( bool $filtered ): string { return $filtered ? __( 'Nobody matches these filters.', 'rooster-lms' ) : __( 'Nobody is invited yet.', 'rooster-lms' ); } public function source(): TableSource { return $this; } public function can_view( int $user_id, array $context ): bool { $inv = InvitationRepository::get( (int) ( $context['invitation'] ?? 0 ) ); return null !== $inv && InvitationService::may_manage_invitation( $inv, $user_id ); } public function page( TableQuery $q ): TablePage { $id = (int) ( $this->context()['invitation'] ?? 0 ); if ( $id <= 0 ) { return new TablePage( array(), 0 ); } global $wpdb; $from = SqlPart::of( ' FROM %i i LEFT JOIN %i u ON u.ID = i.user_id WHERE i.invitation_id = %d', Db::table( 'invitees' ), $wpdb->users, $id ); $answer = (string) $q->filter( 'answer' ); $value = 'none' === $answer ? RsvpAnswer::NONE : RsvpAnswer::from_slug( $answer ); if ( null !== $value ) { $from = $from->add( SqlPart::of( ' AND i.answer = %d', $value->value ) ); } $state = match ( (string) $q->filter( 'state' ) ) { 'queued' => InviteeState::QUEUED, 'sent' => InviteeState::SENT, 'failed' => InviteeState::FAILED, 'suppressed' => InviteeState::SUPPRESSED, default => null, }; if ( null !== $state ) { $from = $from->add( SqlPart::of( ' AND i.state = %d', $state->value ) ); } $outcome = match ( (string) $q->filter( 'outcome' ) ) { 'attended' => InviteeOutcome::ATTENDED, 'enrolled' => InviteeOutcome::ENROLLED, 'joined' => InviteeOutcome::JOINED, 'none' => InviteeOutcome::NONE, default => null, }; if ( null !== $outcome ) { $from = $from->add( SqlPart::of( ' AND i.outcome = %d', $outcome->value ) ); } $dir = 'desc' === $q->order ? 'DESC' : 'ASC'; $person = 'COALESCE( CASE WHEN i.user_id = 0 OR i.source IN (' . InviteeSource::CSV->value . ', ' . InviteeSource::EXTERNAL->value . ") THEN i.last_name ELSE u.display_name END, '' )"; $order = match ( $q->orderby ) { 'person' => $person . " = '', " . $person . ' ' . $dir . ', i.id ' . $dir, 'answered' => 'i.answered_at IS NULL, i.answered_at ' . $dir . ', i.id ' . $dir, default => 'i.id ' . $dir, }; $rows = Db::rows( SqlPart::of( 'SELECT /* outside-tx */ ' . self::COLUMNS )->add( $from )->add( ' ORDER BY ' . $order )->add( SqlPart::of( ' LIMIT %d OFFSET %d', $q->per_page, $q->offset() ) ) ); $total = $q->known_total ?? ( 1 === $q->page && count( $rows ) < $q->per_page ? count( $rows ) : (int) Db::value( SqlPart::of( 'SELECT /* outside-tx */ COUNT(*)' )->add( $from ) ) ); return new TablePage( $rows, $total ); } public static function export_rows( int $invitation_id, int $after, int $limit ): array { global $wpdb; return Db::rows( SqlPart::of( 'SELECT /* outside-tx */ ' . self::COLUMNS )->add( SqlPart::of( ' FROM %i i LEFT JOIN %i u ON u.ID = i.user_id WHERE i.invitation_id = %d AND i.id > %d ORDER BY i.id LIMIT %d', Db::table( 'invitees' ), $wpdb->users, $invitation_id, $after, max( 1, $limit ) ) ) ); } public function row( mixed $row ): string { $row = (array) $row; $cells = self::cells( $row ); $answer = RsvpAnswer::tryFrom( (int) $row['answer'] ) ?? RsvpAnswer::NONE; $tone = match ( $answer ) { RsvpAnswer::YES => 'success', RsvpAnswer::NO => 'danger', RsvpAnswer::MAYBE => 'warning', default => 'secondary', }; return '' . '' . esc_html( $cells['person'] ) . '' . '' . esc_html( $cells['email'] ) . '' . '' . esc_html( $cells['source'] ) . '' . '' . esc_html( $cells['state'] ) . '' . '' . Ui::badge( $answer->label(), $tone . ' sm' ) . '' . '' . esc_html( $cells['answered'] ) . '' . '' . esc_html( $cells['via'] ) . '' . '' . esc_html( $cells['outcome'] ) . ''; } public static function cells( array $row ): array { $source = InviteeSource::tryFrom( (int) $row['source'] ); $own = (int) $row['user_id'] <= 0 || InviteeSource::CSV === $source || InviteeSource::EXTERNAL === $source; $name = $own ? trim( (string) $row['first_name'] . ' ' . (string) $row['last_name'] ) : (string) ( $row['display_name'] ?? '' ); $email = $own ? (string) $row['email'] : (string) ( $row['user_email'] ?? '' ); if ( $own && '' === $email && (int) $row['user_id'] > 0 ) { $account = (string) ( $row['user_email'] ?? '' ); $email = '' !== $account && hash_equals( (string) ( $row['email_hash'] ?? '' ), Tokens::email_hash( $account ) ) ? strtolower( trim( $account ) ) : ''; } $erased = (int) $row['user_id'] <= 0 && '' === $email; $state = InviteeState::tryFrom( (int) $row['state'] ) ?? InviteeState::QUEUED; $via = AnswerSource::tryFrom( (int) $row['answer_src'] ); $answer = RsvpAnswer::tryFrom( (int) $row['answer'] ) ?? RsvpAnswer::NONE; $outcome = InviteeOutcome::tryFrom( (int) ( $row['outcome'] ?? 0 ) ) ?? InviteeOutcome::NONE; return array( 'person' => '' !== $name ? $name : ( $erased ? __( '(erased)', 'rooster-lms' ) : '' ), 'email' => $email, 'source' => null === $source ? '' : $source->label(), 'state' => $state->label(), 'answer' => $answer->label(), 'answered' => Format::date( null === $row['answered_at'] ? null : (string) $row['answered_at'], (string) get_option( 'date_format', 'j F Y' ) . ' ' . (string) get_option( 'time_format', 'H:i' ) ), 'via' => null === $via ? '' : $via->label(), 'outcome' => InviteeOutcome::NONE === $outcome ? '' : $outcome->label(), ); } }