query = $query ?? new Eventin_Stats_Query(); } /** * An Eventin admin page, as the assistant should link it. * * @param string $title Page name to say. * @param string $route Eventin route, e.g. "/bookings". * @return array{title:string,url:string} */ protected function page( string $title, string $route ): array { return [ 'title' => $title, 'url' => admin_url( 'admin.php?page=eventin#' . $route ), ]; } /** * The edit page of one event. * * @param int $event_id Event ID. * @param string $title Event title. * @return array{title:string,url:string} */ protected function event_page( int $event_id, string $title ): array { return $this->page( $title, '/events/edit/' . $event_id . '/basic' ); } /** * Work out which event the caller means. * * @param int|null $event_id ID, when the model has one. * @param string|null $event_name Title or part of it. * @param array $page Page to send them to when it is unclear. * @return array{0:int,1:string,2:array|null} The event ID (0 when no event * was asked for), its title, and a finished answer to return * instead when the event could not be pinned down. */ protected function resolve_event( ?int $event_id, ?string $event_name, array $page ): array { if ( null !== $event_id && $event_id > 0 ) { $titles = $this->query->event_titles( [ $event_id ] ); if ( ! isset( $titles[ $event_id ] ) ) { return [ 0, '', $this->not_found( "No event has ID {$event_id}.", $page ) ]; } return [ $event_id, $titles[ $event_id ], null ]; } $event_name = trim( (string) $event_name ); if ( '' === $event_name ) { return [ 0, '', null ]; } $matches = $this->query->find_events( $event_name ); if ( empty( $matches ) ) { return [ 0, '', $this->not_found( "No event title contains \"{$event_name}\".", $this->page( __( 'Events', 'aisentic' ), '/events' ) ) ]; } if ( count( $matches ) > 1 ) { // If all matches have the same title and start date, send the // person to the Events page instead of asking them to pick. // Why: the model asked the caller to choose by ID, which nobody // on a call knows. $labels = array_unique( array_map( static fn( $m ) => $m['title'] . '|' . $m['start_date'], $matches ) ); $same_name = 1 === count( $labels ); return [ 0, '', [ 'status' => 'needs_clarification', 'reason' => $same_name ? count( $matches ) . " events have the same title \"{$matches[0]['title']}\" and the same start date." : "More than one event matches \"{$event_name}\".", 'matches' => $matches, 'say' => $same_name ? 'Do not pick one and do not read out IDs. Say there are several events with exactly that name and date, and open the Events page so they can choose.' : 'Do not pick one. Read out the titles with their start dates, never the IDs, and ask which one they mean. Then call this tool again with that event_id.', 'page' => $this->page( __( 'Events', 'aisentic' ), '/events' ), ], ]; } return [ $matches[0]['id'], $matches[0]['title'], null ]; } /** * Nothing matched. * * @param string $reason What was looked for. * @param array $page Where they can look themselves. * @return array */ protected function not_found( string $reason, array $page ): array { return [ 'status' => 'not_found', 'reason' => $reason, 'say' => 'Say plainly that you could not find it. Do not guess a similar one. Offer to open the page.', 'page' => $page, ]; } /** * A value the tool could not read, so it will not guess. * * @param string $reason What was unclear. * @param array $page Where they can look themselves. * @param array $options Values that would work, if there is a fixed set. * @return array */ protected function unclear( string $reason, array $page, array $options = [] ): array { $result = [ 'status' => 'needs_clarification', 'reason' => $reason, 'say' => 'Say you are not sure what they meant and ask one short question. Or offer to open the page.', 'page' => $page, ]; if ( ! empty( $options ) ) { $result['options'] = $options; } return $result; } /** * Read a Y-m-d date from the model. * * @param string|null $date Date from the model. * @return string|null|false Null when empty, false when it cannot be read. */ protected function read_date( ?string $date ) { $date = trim( (string) $date ); if ( '' === $date ) { return null; } $parsed = \DateTime::createFromFormat( '!Y-m-d', $date ); if ( false === $parsed || $parsed->format( 'Y-m-d' ) !== $date ) { return false; } return $date; } /** * Keep the biggest groups and fold the rest into one number. * * Why: a per-event split on a busy site can have 2,000 rows. That is * too long for a call and for the model's context. * * @param array $groups Counts, biggest first. * @param int $keep How many to keep. * @return array{0:array,1:int,2:int} Kept groups, how many * groups were folded, and their combined count. */ protected function top_groups( array $groups, int $keep = 10 ): array { $kept = array_slice( $groups, 0, $keep, true ); $rest = array_slice( $groups, $keep, null, true ); $others = array_sum( $rest ); return [ $kept, count( $rest ), $others ]; } /** * Name the rows of a per-event split. * * @param array $groups Event ID => count. * @return array */ protected function named_event_groups( array $groups ): array { $titles = $this->query->event_titles( array_map( 'intval', array_keys( $groups ) ) ); $rows = []; foreach ( $groups as $id => $count ) { $rows[] = [ 'event_id' => (int) $id, 'title' => $titles[ (int) $id ] ?? __( '(deleted event)', 'aisentic' ), 'count' => $count, ]; } return $rows; } }