diff --git a/assets/print-calendar.css b/assets/print-calendar.css index a0146ad..619bb26 100644 --- a/assets/print-calendar.css +++ b/assets/print-calendar.css @@ -27,10 +27,7 @@ body { .print-shell { margin: 0; - width: calc(100% / var(--sheet-scale)); background: #fff; - transform-origin: top left; - transform: scale(var(--sheet-scale)); } .print-page { @@ -48,7 +45,10 @@ body { } body.print-preview .print-shell { + flex: 0 0 auto; box-shadow: 0 10px 30px rgba(17, 24, 39, 0.2); + transform-origin: top left; + transform: scale(var(--sheet-scale)); } body.print-preview .print-shell.letter { @@ -60,6 +60,103 @@ body { width: 11in; min-height: 17in; } + + body.print-preview.month-pages { + display: block; + overflow-x: auto; + } + + body.print-preview.month-pages .print-shell, + body.print-preview.month-pages .print-shell.letter, + body.print-preview.month-pages .print-shell.ledger { + width: auto; + min-height: auto; + background: transparent; + box-shadow: none; + transform: none; + } + + body.print-preview.month-pages .print-page { + padding: 0; + } + + body.print-preview.month-pages .month-page { + box-sizing: border-box; + width: 8.5in; + min-height: 11in; + margin-right: auto; + margin-left: auto; + padding: var(--pc-page-padding); + background: #fff; + box-shadow: 0 10px 30px rgba(17, 24, 39, 0.2); + } + + body.print-preview.month-pages.ledger .month-page { + width: 11in; + min-height: 17in; + } + + body.print-preview.month-pages .month-page + .month-page { + margin-top: 24px; + } + + body.print-preview.month-pages .month-page .header { + margin-bottom: 24px; + } + + body.print-preview.month-pages .month-page .footer-meta { + margin-top: 24px; + } + + body.print-preview.month-pages .sheet-grid { + display: block; + } + +body.print-preview.month-pages .month { + width: 100%; +} + +body.month-pages .month-title { + font-size: calc(26px * var(--month-font-scale)); + padding: 4px; +} + +body.month-pages .dow span { + font-size: calc(12px * var(--month-font-scale)); + padding: 4px 2px; +} + +body.month-pages .day-num { + font-size: calc(14px * var(--month-font-scale)); +} + +body.month-pages .event-name { + font-size: calc(13px * var(--month-font-scale)); +} + +body.month-pages .matchup-name { + font-size: calc(13px * var(--month-font-scale)); + line-height: 1.05; +} + +body.month-pages .event-time { + font-size: calc(14px * var(--month-font-scale)); +} + +body.month-pages .event.matchup .event-time { + font-size: calc(11px * var(--month-font-scale)); +} + +body.month-pages .event.matchup .event-venue { + font-size: calc(10px * var(--month-font-scale)); +} +} + +@media screen and (max-width: 900px) { + body.print-preview { + justify-content: flex-start; + overflow-x: auto; + } } .header { @@ -252,12 +349,35 @@ body { color: var(--day-num-color, #fff); } +.day.has-matchups { + background: #fff; + border: 1px solid var(--pc-border); +} + +body.month-pages .day .day-num { + top: 1px; + left: 1px; + width: var(--corner-badge-size); + height: var(--corner-badge-size); + border-radius: 0; + background: #fff; + color: var(--team-ink, #111); + text-shadow: none; +} + .events-stack { height: 100%; display: grid; grid-template-rows: repeat(var(--event-count), minmax(0, 1fr)); } +.day.has-matchups .events-stack { + box-sizing: border-box; + grid-template-rows: repeat(var(--event-count), minmax(0, 1fr)); + gap: 2px; + padding: calc(var(--corner-badge-size) + 5px) 2px 2px; +} + .event { --event-top-band: calc(var(--corner-badge-size, 11px) + var(--corner-badge-offset, 2px)); --event-bottom-band: 26px; @@ -275,6 +395,17 @@ body { --event-bg: var(--team-link-color, var(--team-secondary, #8b3f1f)); } +.event.matchup { + display: grid; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr) auto; + gap: 1px; + min-height: 0; + padding: 2px 3px; + border-radius: 2px; + overflow: hidden; +} + .event-center { position: absolute; top: var(--event-top-band); @@ -293,6 +424,17 @@ body { text-align: center; } +.event.matchup .event-center { + position: relative; + top: auto; + left: auto; + right: auto; + bottom: auto; + min-height: 0; + align-items: center; + justify-content: center; +} + .event-center img { width: auto; height: var(--event-logo-height); @@ -322,10 +464,36 @@ body { font-family: var(--pc-font-display); font-weight: 700; font-variation-settings: "wdth" 30, "wght" 700; - letter-spacing: -0.01em; text-transform: uppercase; } +.matchup-name { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0; + padding: 0; + font-size: calc(8.5px * var(--month-font-scale)); + line-height: 0.98; + opacity: 1; +} + +.matchup-vs { + font-size: 0.66em; + font-weight: 900; + opacity: 0.78; +} + +.matchup-team { + display: block; + width: 100%; + overflow: hidden; + text-align: center; + text-overflow: clip; + white-space: nowrap; +} + .ha-flag { position: absolute; top: var(--corner-badge-offset, 2px); @@ -341,7 +509,7 @@ body { font-weight: 900; font-variation-settings: "wdth" 84, "wght" 900; line-height: 1; - letter-spacing: -0.01em; + letter-spacing: 0; background: #111; color: #fff; } @@ -366,6 +534,19 @@ body { overflow: hidden; } +.event.matchup .event-meta { + position: relative; + left: auto; + right: auto; + bottom: auto; + display: grid; + grid-template-columns: minmax(0, auto) minmax(0, 1fr); + align-items: center; + justify-content: center; + column-gap: 3px; + padding: 0; +} + .event-time { order: 1; max-width: 100%; @@ -381,6 +562,12 @@ body { opacity: 0.95; } +.event.matchup .event-time { + min-width: 0; + font-size: calc(7.5px * var(--month-font-scale)); + font-weight: 900; +} + .event-venue { order: 2; max-width: 100%; @@ -395,6 +582,12 @@ body { opacity: 0.88; } +.event.matchup .event-venue { + min-width: 0; + font-size: calc(7px * var(--month-font-scale)); + text-align: left; +} + .empty { padding: 16px; border: 2px dashed #c8d2de; @@ -487,6 +680,7 @@ body { width: auto; min-height: auto; box-shadow: none; + transform: none; } .print-page { @@ -500,4 +694,27 @@ body { .title { font-size: calc(26px * var(--month-font-scale)); } + + body.month-pages .sheet-grid { + display: block; + } + + body.month-pages .month-page { + break-after: page; + page-break-after: always; + } + + body.month-pages .month-page:last-child { + break-after: auto; + page-break-after: auto; + } + + body.month-pages .month { + width: 100%; + } + + body.month-pages .grid { + break-inside: avoid; + page-break-inside: avoid; + } } diff --git a/includes/sp-printable-calendars.php b/includes/sp-printable-calendars.php index a0dc180..2532bf5 100644 --- a/includes/sp-printable-calendars.php +++ b/includes/sp-printable-calendars.php @@ -146,8 +146,12 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { $vars[] = 'sp_team'; $vars[] = 'sp_season'; $vars[] = 'sp_league'; + $vars[] = 'sp_field'; + $vars[] = 'team_label'; + $vars[] = 'field_label'; $vars[] = 'paper'; $vars[] = 'autoprint'; + $vars[] = 'month_pages'; return $vars; } @@ -221,16 +225,24 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { $this->default_settings() ); - $current = wp_parse_args( - is_array( $input ) ? $input : array(), - $existing - ); + $current = is_array( $input ) ? $input : array(); + $active_season_id = isset( $current['active_season_id'] ) ? absint( (string) $current['active_season_id'] ) : 0; return array( 'calendar_feed_url' => isset( $existing['calendar_feed_url'] ) && is_string( $existing['calendar_feed_url'] ) ? $existing['calendar_feed_url'] : '', 'sync_interval_minutes' => isset( $existing['sync_interval_minutes'] ) ? absint( (string) $existing['sync_interval_minutes'] ) : 60, - 'venue_color_overrides' => $this->sanitize_venue_color_overrides( isset( $current['venue_color_overrides'] ) ? $current['venue_color_overrides'] : array() ), - 'venue_use_team_primary' => $this->sanitize_venue_primary_flags( isset( $current['venue_use_team_primary'] ) ? $current['venue_use_team_primary'] : array() ), + 'venue_color_overrides' => $this->merge_sanitized_season_settings( + isset( $existing['venue_color_overrides'] ) && is_array( $existing['venue_color_overrides'] ) ? $existing['venue_color_overrides'] : array(), + $this->sanitize_venue_color_overrides( isset( $current['venue_color_overrides'] ) ? $current['venue_color_overrides'] : array() ), + $active_season_id, + true + ), + 'venue_use_team_primary' => $this->merge_sanitized_season_settings( + isset( $existing['venue_use_team_primary'] ) && is_array( $existing['venue_use_team_primary'] ) ? $existing['venue_use_team_primary'] : array(), + $this->sanitize_venue_primary_flags( isset( $current['venue_use_team_primary'] ) ? $current['venue_use_team_primary'] : array() ), + $active_season_id, + false + ), 'printable_venue_label_mode' => $this->sanitize_venue_label_mode( isset( $current['printable_venue_label_mode'] ) ? $current['printable_venue_label_mode'] : '' ), ); } @@ -322,6 +334,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo '
'; settings_fields( self::OPTION_GROUP ); + echo ''; echo '

' . esc_html__( 'Field Colors By Season', 'tonys-sportspress-enhancements' ) . '

'; echo '

' . esc_html__( 'Pick venue colors per season. Colors are darkened automatically when needed so white text still reads clearly.', 'tonys-sportspress-enhancements' ) . '

'; @@ -359,11 +372,11 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo ''; echo ''; echo ''; - echo '

' . esc_html__( 'Choose whether the printable schedule shows the venue full name, abbreviation, or short name below each game time.', 'tonys-sportspress-enhancements' ) . '

'; + echo '

' . esc_html__( 'Default field label mode for printable URLs that do not include a field_label parameter.', 'tonys-sportspress-enhancements' ) . '

'; echo ''; echo ''; echo ''; @@ -426,6 +439,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { private function render_printable_url_builder( $season_id ) { $leagues = function_exists( 'tse_sp_schedule_exporter_get_leagues' ) ? tse_sp_schedule_exporter_get_leagues() : array(); $teams = function_exists( 'tse_sp_schedule_exporter_get_teams' ) ? tse_sp_schedule_exporter_get_teams() : array(); + $fields = function_exists( 'tse_sp_schedule_exporter_get_fields' ) ? tse_sp_schedule_exporter_get_fields() : array(); $paper = '11x17'; echo '
'; @@ -470,6 +484,18 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo ''; echo ''; + echo ''; + echo ''; + echo ''; + echo ''; echo ''; echo ''; + echo ''; + echo ''; + echo ''; + + echo ''; + echo ''; + echo ''; + echo '' . esc_html__( 'Options', 'tonys-sportspress-enhancements' ) . ''; echo ''; + echo '
'; + echo ''; echo ''; echo ''; @@ -522,8 +569,12 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { var team = root.querySelector('#tse-printable-builder-team'); var season = root.querySelector('#tse-printable-builder-season'); var league = root.querySelector('#tse-printable-builder-league'); + var field = root.querySelector('#tse-printable-builder-field'); var paper = root.querySelector('#tse-printable-builder-paper'); + var teamLabel = root.querySelector('#tse-printable-builder-team-label'); + var fieldLabel = root.querySelector('#tse-printable-builder-field-label'); var autoprint = root.querySelector('#tse-printable-builder-autoprint'); + var monthPages = root.querySelector('#tse-printable-builder-month-pages'); var output = root.querySelector('#tse-printable-builder-output'); var copyButton = root.querySelector('#tse-printable-builder-copy'); var openLink = root.querySelector('#tse-printable-builder-open'); @@ -550,22 +601,42 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { url.searchParams.delete('sp_league'); } + if (field.value && field.value !== '0') { + url.searchParams.set('sp_field', field.value); + } else { + url.searchParams.delete('sp_field'); + } + if (paper.value) { url.searchParams.set('paper', paper.value); } + if (teamLabel.value) { + url.searchParams.set('team_label', teamLabel.value); + } + + if (fieldLabel.value) { + url.searchParams.set('field_label', fieldLabel.value); + } + if (autoprint.checked) { url.searchParams.set('autoprint', '1'); } else { url.searchParams.delete('autoprint'); } + if (monthPages.checked) { + url.searchParams.set('month_pages', '1'); + } else { + url.searchParams.delete('month_pages'); + } + output.value = url.toString(); openLink.href = url.toString(); openLink.toggleAttribute('disabled', !(team.value && team.value !== '0')); } - [team, season, league, paper, autoprint].forEach(function(input){ + [team, season, league, field, paper, teamLabel, fieldLabel, autoprint, monthPages].forEach(function(input){ input.addEventListener('change', buildUrl); }); @@ -618,8 +689,8 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { return; } - $team_id = absint( (string) get_query_var( 'sp_team' ) ); - if ( $team_id <= 0 || 'sp_team' !== get_post_type( $team_id ) ) { + $team_ids = $this->parse_team_ids( get_query_var( 'sp_team' ) ); + if ( empty( $team_ids ) ) { status_header( 400 ); nocache_headers(); echo esc_html__( 'Missing or invalid team id.', 'tonys-sportspress-enhancements' ); @@ -631,23 +702,31 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { $season_id = absint( (string) get_option( 'sportspress_season', '0' ) ); } $league_id = absint( (string) get_query_var( 'sp_league' ) ); + $field_ids = $this->parse_term_ids( get_query_var( 'sp_field' ), 'sp_venue' ); + $team_label_mode = $this->sanitize_label_mode( get_query_var( 'team_label' ), 'abbreviation' ); + $field_label_mode = $this->sanitize_label_mode( get_query_var( 'field_label' ), $this->get_venue_label_mode() ); $paper = $this->normalize_paper_size( (string) get_query_var( 'paper' ) ); $autoprint = '1' === (string) get_query_var( 'autoprint' ); + $month_pages = '1' === (string) get_query_var( 'month_pages' ); - $team_name = get_the_title( $team_id ); - $team_logo = get_the_post_thumbnail( $team_id, array( 72, 72 ), array( 'class' => 'team-logo-img' ) ); + $is_multi_team = count( $team_ids ) > 1; + $primary_team_id = isset( $team_ids[0] ) ? (int) $team_ids[0] : 0; + $team_name = $this->get_printable_title( $team_ids, $team_label_mode ); + $team_logo = $is_multi_team ? '' : get_the_post_thumbnail( $primary_team_id, array( 72, 72 ), array( 'class' => 'team-logo-img' ) ); $brand_logo = $this->get_header_brand_logo(); $site_url = home_url( '/' ); $qr_url = 'https://api.qrserver.com/v1/create-qr-code/?size=144x144&data=' . rawurlencode( $site_url ); $season_name = ''; - $entries = $this->get_schedule_entries( $team_id, $season_id, $league_id ); - $team_palette = $this->get_team_color_palette( $team_id ); - $team_primary_for_fields = $this->get_strict_team_primary_color( $team_id ); + $field_name = count( $field_ids ) === 1 ? $this->get_term_label( (int) $field_ids[0], 'sp_venue', $field_label_mode ) : ''; + $entries = $this->get_schedule_entries( $team_ids, $season_id, $league_id, $field_ids, $team_label_mode, $field_label_mode ); + $team_palette = $is_multi_team ? $this->get_default_printable_palette() : $this->get_team_color_palette( $primary_team_id ); + $team_primary_for_fields = $is_multi_team ? '' : $this->get_strict_team_primary_color( $primary_team_id ); $entries_by_day = array(); $venue_colors = array(); $month_keys = array(); $layout = array(); + $suppress_event_venue = 1 === count( $field_ids ); if ( $season_id > 0 ) { $season = get_term( $season_id, 'sp_season' ); @@ -661,14 +740,14 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { if ( '' !== $entry['venue_name'] && is_string( $entry['venue_key'] ) && ! isset( $venue_colors[ $entry['venue_key'] ] ) ) { $venue_colors[ $entry['venue_key'] ] = array( - 'name' => (string) $entry['venue_name'], - 'color' => $this->get_venue_color( (string) $entry['venue_name'], $season_id, (int) $entry['venue_id'], $team_primary_for_fields ), + 'name' => ! empty( $entry['venue_label'] ) ? (string) $entry['venue_label'] : (string) $entry['venue_name'], + 'color' => $this->get_venue_color( (string) $entry['venue_name'], $season_id, (int) $entry['venue_id'], $team_primary_for_fields, $is_multi_team ), ); } } $month_keys = $this->get_month_keys( $entries ); - $layout = $this->get_sheet_layout( count( $month_keys ), $paper ); + $layout = $this->get_sheet_layout( $month_pages ? 1 : count( $month_keys ), $paper ); status_header( 200 ); nocache_headers(); @@ -688,7 +767,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo ''; } echo ''; - echo ''; + echo ''; $root_vars = array( '--sheet-scale:' . $layout['sheet_scale'], @@ -710,7 +789,55 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { ); echo '
'; - echo '
'; + echo ''; + exit; + } + + /** + * Render the printable page header. + * + * @param string $team_name Header title. + * @param array $meta_parts Header metadata strings. + * @param string $team_logo Team logo markup. + * @param string $brand_logo Brand logo markup. + * @return void + */ + private function render_printable_header( $team_name, $meta_parts, $team_logo, $brand_logo ) { echo '
'; echo '
'; if ( '' !== $team_logo ) { @@ -718,7 +845,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { } echo '
'; echo '

' . esc_html( $team_name ) . '

'; - echo '

' . esc_html( $season_name ? $season_name : __( 'Current', 'tonys-sportspress-enhancements' ) ) . '

'; + echo '

' . esc_html( implode( ' | ', array_filter( array_map( 'strval', $meta_parts ) ) ) ) . '

'; echo '
'; echo '
'; @@ -726,19 +853,17 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo ''; } echo '
'; + } - if ( empty( $entries ) ) { - echo '
'; - echo '

' . esc_html__( 'No SportsPress events were found for this team and season.', 'tonys-sportspress-enhancements' ) . '

'; - echo '
'; - } else { - echo '
'; - foreach ( $month_keys as $month_key ) { - $this->render_month_grid( $month_key, $entries_by_day, $venue_colors, $team_palette ); - } - echo '
'; - } - + /** + * Render the printable page footer. + * + * @param array $venue_colors Venue legend data. + * @param string $site_url Site URL. + * @param string $qr_url QR code image URL. + * @return void + */ + private function render_printable_footer( $venue_colors, $site_url, $qr_url ) { echo '
'; if ( ! empty( $venue_colors ) ) { echo '
'; @@ -762,10 +887,6 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo '' . esc_attr__( 'QR code for website', 'tonys-sportspress-enhancements' ) . ''; echo '
'; echo '
'; - echo '
'; - echo ''; - echo ''; - exit; } /** @@ -776,8 +897,9 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { * @param string $paper Paper size. * @return string */ - private function build_url( $team_id, $season_id, $paper, $league_id = 0 ) { + private function build_url( $team_id, $season_id, $paper, $league_id = 0, $field_id = 0, $month_pages = false, $team_label_mode = 'name', $field_label_mode = '' ) { $paper = $this->normalize_paper_size( $paper ); + $field_label_mode = '' === $field_label_mode ? $this->get_venue_label_mode() : $field_label_mode; return add_query_arg( array( @@ -785,12 +907,130 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { 'sp_team' => (string) absint( $team_id ), 'sp_season' => $season_id > 0 ? (string) absint( $season_id ) : '', 'sp_league' => $league_id > 0 ? (string) absint( $league_id ) : '', + 'sp_field' => $field_id > 0 ? (string) absint( $field_id ) : '', + 'team_label' => $this->sanitize_label_mode( $team_label_mode, 'name' ), + 'field_label' => $this->sanitize_label_mode( $field_label_mode, $this->get_venue_label_mode() ), 'paper' => (string) $paper, + 'month_pages' => $month_pages ? '1' : '', ), home_url( '/' ) ); } + /** + * Parse and validate one or more team IDs. + * + * @param mixed $value Raw query value. + * @return int[] + */ + private function parse_team_ids( $value ) { + $ids = $this->parse_id_list( $value ); + $valid = array(); + + foreach ( $ids as $id ) { + if ( 'sp_team' === get_post_type( $id ) ) { + $valid[] = $id; + } + } + + return array_values( array_unique( $valid ) ); + } + + /** + * Parse and validate one or more term IDs for a taxonomy. + * + * @param mixed $value Raw query value. + * @param string $taxonomy Taxonomy name. + * @return int[] + */ + private function parse_term_ids( $value, $taxonomy ) { + $ids = $this->parse_id_list( $value ); + $valid = array(); + + foreach ( $ids as $id ) { + $term = get_term( $id, $taxonomy ); + if ( $term && ! is_wp_error( $term ) ) { + $valid[] = $id; + } + } + + return array_values( array_unique( $valid ) ); + } + + /** + * Parse scalar, array, or comma-delimited ID values. + * + * @param mixed $value Raw value. + * @return int[] + */ + private function parse_id_list( $value ) { + $values = is_array( $value ) ? $value : explode( ',', (string) $value ); + $ids = array(); + + foreach ( $values as $raw_value ) { + $id = absint( trim( (string) $raw_value ) ); + if ( $id > 0 ) { + $ids[] = $id; + } + } + + return array_values( array_unique( $ids ) ); + } + + /** + * Build the printable title for selected teams. + * + * @param int[] $team_ids Team IDs. + * @return string + */ + private function get_printable_title( $team_ids, $mode = 'name' ) { + $names = array(); + + foreach ( $team_ids as $team_id ) { + $title = $this->get_team_label( $team_id, $mode ); + if ( is_string( $title ) && '' !== trim( $title ) ) { + $names[] = trim( $title ); + } + } + + if ( empty( $names ) ) { + return __( 'Printable Schedule', 'tonys-sportspress-enhancements' ); + } + + return implode( ', ', $names ); + } + + /** + * Get a term display name. + * + * @param int $term_id Term ID. + * @param string $taxonomy Taxonomy name. + * @return string + */ + private function get_term_name( $term_id, $taxonomy ) { + $term = get_term( $term_id, $taxonomy ); + + return $term && ! is_wp_error( $term ) && isset( $term->name ) ? (string) $term->name : ''; + } + + /** + * Get a term display label. + * + * @param int $term_id Term ID. + * @param string $taxonomy Taxonomy name. + * @param string $mode Label mode. + * @return string + */ + private function get_term_label( $term_id, $taxonomy, $mode = 'name' ) { + $term = get_term( $term_id, $taxonomy ); + + if ( ! $term || is_wp_error( $term ) || ! isset( $term->name ) ) { + return ''; + } + + return $this->get_configured_venue_label( $term, $mode ); + } + /** * Default option payload. * @@ -802,7 +1042,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { 'sync_interval_minutes' => 60, 'venue_color_overrides' => array(), 'venue_use_team_primary'=> array(), - 'printable_venue_label_mode' => 'full_name', + 'printable_venue_label_mode' => 'name', ); } @@ -836,18 +1076,44 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { } /** - * Get supported venue label modes. + * Get supported label modes. * * @return array */ - private function get_venue_label_mode_options() { + private function get_label_mode_options() { return array( - 'full_name' => __( 'Full Name', 'tonys-sportspress-enhancements' ), + 'name' => __( 'Name', 'tonys-sportspress-enhancements' ), + 'shortname' => __( 'Short Name', 'tonys-sportspress-enhancements' ), 'abbreviation' => __( 'Abbreviation', 'tonys-sportspress-enhancements' ), - 'short_name' => __( 'Short Name', 'tonys-sportspress-enhancements' ), ); } + /** + * Sanitize label mode. + * + * @param mixed $value Raw value. + * @param string $fallback Fallback mode. + * @return string + */ + private function sanitize_label_mode( $value, $fallback = 'name' ) { + $mode = is_string( $value ) ? sanitize_key( $value ) : ''; + $aliases = array( + 'full_name' => 'name', + 'fullname' => 'name', + 'short_name' => 'shortname', + 'short' => 'shortname', + 'abbr' => 'abbreviation', + ); + if ( isset( $aliases[ $mode ] ) ) { + $mode = $aliases[ $mode ]; + } + + $allowed = array_keys( $this->get_label_mode_options() ); + $fallback = isset( $aliases[ $fallback ] ) ? $aliases[ $fallback ] : $fallback; + + return in_array( $mode, $allowed, true ) ? $mode : ( in_array( $fallback, $allowed, true ) ? $fallback : 'name' ); + } + /** * Sanitize venue label mode. * @@ -855,10 +1121,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { * @return string */ private function sanitize_venue_label_mode( $value ) { - $mode = is_string( $value ) ? sanitize_key( $value ) : ''; - $allowed = array_keys( $this->get_venue_label_mode_options() ); - - return in_array( $mode, $allowed, true ) ? $mode : 'full_name'; + return $this->sanitize_label_mode( $value, 'name' ); } /** @@ -943,6 +1206,36 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { return $sanitized; } + /** + * Merge submitted season-scoped settings without preserving unchecked boxes. + * + * @param array $existing Existing settings. + * @param array $submitted Sanitized submitted settings. + * @param int $active_season_id Active season ID. + * @param bool $preserve_missing Whether to preserve season if missing. + * @return array + */ + private function merge_sanitized_season_settings( $existing, $submitted, $active_season_id, $preserve_missing ) { + $merged = is_array( $existing ) ? $existing : array(); + + if ( $active_season_id <= 0 ) { + return ! empty( $submitted ) ? $submitted : $merged; + } + + $season_key = (string) $active_season_id; + if ( isset( $submitted[ $season_key ] ) && is_array( $submitted[ $season_key ] ) ) { + $merged[ $season_key ] = $submitted[ $season_key ]; + } elseif ( $preserve_missing ) { + if ( isset( $existing[ $season_key ] ) && is_array( $existing[ $season_key ] ) ) { + $merged[ $season_key ] = $existing[ $season_key ]; + } + } else { + unset( $merged[ $season_key ] ); + } + + return $merged; + } + /** * Get seasons list. * @@ -1085,12 +1378,24 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { /** * Collect event entries for the calendar. * - * @param int $team_id Team ID. - * @param int $season_id Season ID. - * @param int $league_id League ID. + * @param int|int[] $team_ids Team IDs. + * @param int $season_id Season ID. + * @param int $league_id League ID. + * @param int[] $field_ids Field IDs. * @return array */ - private function get_schedule_entries( $team_id, $season_id, $league_id = 0 ) { + private function get_schedule_entries( $team_ids, $season_id, $league_id = 0, $field_ids = array(), $team_label_mode = 'abbreviation', $field_label_mode = '' ) { + $team_ids = is_array( $team_ids ) ? array_values( array_filter( array_map( 'absint', $team_ids ) ) ) : array( absint( $team_ids ) ); + $primary_team = isset( $team_ids[0] ) ? (int) $team_ids[0] : 0; + $is_multi_team = count( $team_ids ) > 1; + $field_ids = is_array( $field_ids ) ? array_values( array_filter( array_map( 'absint', $field_ids ) ) ) : array(); + $team_label_mode = $this->sanitize_label_mode( $team_label_mode, 'abbreviation' ); + $field_label_mode = '' === $field_label_mode ? $this->get_venue_label_mode() : $this->sanitize_label_mode( $field_label_mode, $this->get_venue_label_mode() ); + + if ( empty( $team_ids ) ) { + return array(); + } + $args = array( 'post_type' => 'sp_event', 'post_status' => array( 'publish', 'future' ), @@ -1101,7 +1406,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { 'meta_query' => array( array( 'key' => 'sp_team', - 'value' => array( (string) $team_id ), + 'value' => array_map( 'strval', $team_ids ), 'compare' => 'IN', ), ), @@ -1125,6 +1430,14 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { ); } + if ( ! empty( $field_ids ) ) { + $tax_query[] = array( + 'taxonomy' => 'sp_venue', + 'field' => 'term_id', + 'terms' => $field_ids, + ); + } + if ( ! empty( $tax_query ) ) { if ( count( $tax_query ) > 1 ) { $tax_query['relation'] = 'AND'; @@ -1143,7 +1456,8 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { } $teams = array_values( array_unique( array_map( 'intval', get_post_meta( $event_id, 'sp_team', false ) ) ) ); - if ( ! in_array( $team_id, $teams, true ) ) { + $matching_team_ids = array_values( array_intersect( $team_ids, $teams ) ); + if ( empty( $matching_team_ids ) ) { continue; } @@ -1152,11 +1466,16 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { continue; } + $home_id = isset( $teams[0] ) ? (int) $teams[0] : 0; + $away_id = isset( $teams[1] ) ? (int) $teams[1] : 0; + $context_id = $is_multi_team ? 0 : $primary_team; $opponent_id = 0; - foreach ( $teams as $team_option_id ) { - if ( $team_option_id !== $team_id ) { - $opponent_id = $team_option_id; - break; + if ( ! $is_multi_team ) { + foreach ( $teams as $team_option_id ) { + if ( $team_option_id !== $context_id ) { + $opponent_id = $team_option_id; + break; + } } } @@ -1167,7 +1486,7 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { if ( $venue ) { $venue_name = (string) $venue->name; $venue_id = (int) $venue->term_id; - $venue_label = $this->get_configured_venue_label( $venue ); + $venue_label = $this->get_configured_venue_label( $venue, $field_label_mode ); } $entries[] = array( @@ -1175,9 +1494,14 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { 'day_key' => wp_date( 'Y-m-d', $timestamp ), 'month_key' => wp_date( 'Y-m', $timestamp ), 'timestamp' => $timestamp, - 'is_home' => isset( $teams[0] ) && $teams[0] === $team_id, + 'is_home' => ! $is_multi_team && $home_id === $context_id, + 'is_matchup' => $is_multi_team, 'opponent_id' => $opponent_id, - 'opponent_name' => $opponent_id > 0 ? $this->get_team_label( $opponent_id ) : __( 'TBD', 'tonys-sportspress-enhancements' ), + 'opponent_name' => $opponent_id > 0 ? $this->get_team_label( $opponent_id, $team_label_mode ) : __( 'TBD', 'tonys-sportspress-enhancements' ), + 'home_team_id' => $home_id, + 'away_team_id' => $away_id, + 'home_team_name' => $home_id > 0 ? $this->get_team_label( $home_id, $team_label_mode ) : __( 'TBD', 'tonys-sportspress-enhancements' ), + 'away_team_name' => $away_id > 0 ? $this->get_team_label( $away_id, $team_label_mode ) : __( 'TBD', 'tonys-sportspress-enhancements' ), 'event_time' => function_exists( 'sp_get_time' ) ? sp_get_time( $event_id ) : get_post_time( get_option( 'time_format' ), false, $event_id, true ), 'venue_name' => $venue_name, 'venue_label' => $venue_label, @@ -1230,8 +1554,10 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { * @param array $entries_by_day Entries keyed by day. * @param array $venue_colors Venue colors. * @param array $team_palette Team palette. + * @param bool $is_multi_team Whether this is a combined team schedule. + * @param bool $suppress_venue Whether to hide per-event venue labels. */ - private function render_month_grid( $month_key, $entries_by_day, $venue_colors, $team_palette ) { + private function render_month_grid( $month_key, $entries_by_day, $venue_colors, $team_palette, $is_multi_team = false, $suppress_venue = false ) { $month = DateTimeImmutable::createFromFormat( 'Y-m-d', $month_key . '-01' ); if ( ! $month instanceof DateTimeImmutable ) { return; @@ -1257,15 +1583,26 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { for ( $day = 1; $day <= $days_in_month; $day++ ) { $day_key = sprintf( '%s-%02d', $month_key, $day ); $day_entries = isset( $entries_by_day[ $day_key ] ) ? $entries_by_day[ $day_key ] : array(); - $day_class = ! empty( $day_entries ) ? 'day has-events' : 'day no-events'; + $has_matchup = false; + foreach ( $day_entries as $day_entry ) { + if ( ! empty( $day_entry['is_matchup'] ) || $is_multi_team ) { + $has_matchup = true; + break; + } + } + $day_class = ! empty( $day_entries ) ? 'day has-events' : 'day no-events'; + if ( $has_matchup ) { + $day_class .= ' has-matchups'; + } $day_style = ''; if ( ! empty( $day_entries ) ) { $first_entry = $day_entries[0]; $first_is_home = ! empty( $first_entry['is_home'] ); + $first_is_matchup = ! empty( $first_entry['is_matchup'] ) || $is_multi_team; $first_venue_key = isset( $first_entry['venue_key'] ) && is_string( $first_entry['venue_key'] ) ? $first_entry['venue_key'] : ''; $first_venue_color = ( '' !== $first_venue_key && isset( $venue_colors[ $first_venue_key ]['color'] ) ) ? (string) $venue_colors[ $first_venue_key ]['color'] : ''; - $first_background = '' !== $first_venue_color ? $first_venue_color : ( $first_is_home ? $team_palette['primary'] : $team_palette['secondary'] ); + $first_background = '' !== $first_venue_color ? $first_venue_color : ( $first_is_home || $first_is_matchup ? $team_palette['primary'] : $team_palette['secondary'] ); $first_foreground = $this->get_readable_text_color( $first_background ); $first_background = $this->ensure_minimum_contrast( $first_background, $first_foreground, self::MIN_WHITE_CONTRAST ); $first_foreground = $this->get_readable_text_color( $first_background ); @@ -1280,13 +1617,14 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { echo '
'; foreach ( $day_entries as $entry ) { $is_home = ! empty( $entry['is_home'] ); - $event_class = $is_home ? 'h' : 'a'; + $is_matchup = ! empty( $entry['is_matchup'] ) || $is_multi_team; + $event_class = $is_matchup ? 'matchup' : ( $is_home ? 'h' : 'a' ); $venue_key = isset( $entry['venue_key'] ) && is_string( $entry['venue_key'] ) ? $entry['venue_key'] : ''; $venue_color = ( '' !== $venue_key && isset( $venue_colors[ $venue_key ]['color'] ) ) ? (string) $venue_colors[ $venue_key ]['color'] : ''; $opponent_id = isset( $entry['opponent_id'] ) ? (int) $entry['opponent_id'] : 0; - $logo = $opponent_id > 0 ? get_the_post_thumbnail( $opponent_id, 'medium', array( 'class' => 'event-logo-img', 'loading' => 'eager', 'decoding' => 'async' ) ) : ''; + $logo = ( ! $is_matchup && $opponent_id > 0 ) ? get_the_post_thumbnail( $opponent_id, 'medium', array( 'class' => 'event-logo-img', 'loading' => 'eager', 'decoding' => 'async' ) ) : ''; $has_logo = '' !== $logo; - $event_background = '' !== $venue_color ? $venue_color : ( $is_home ? $team_palette['primary'] : $team_palette['secondary'] ); + $event_background = '' !== $venue_color ? $venue_color : ( $is_home || $is_matchup ? $team_palette['primary'] : $team_palette['secondary'] ); $event_foreground = $this->get_readable_text_color( $event_background ); $event_background = $this->ensure_minimum_contrast( $event_background, $event_foreground, self::MIN_WHITE_CONTRAST ); $event_foreground = $this->get_readable_text_color( $event_background ); @@ -1297,13 +1635,21 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { if ( $has_logo ) { echo wp_kses_post( $logo ); } else { - echo '' . esc_html( isset( $entry['opponent_name'] ) ? (string) $entry['opponent_name'] : '' ) . ''; + if ( $is_matchup ) { + $away_name = isset( $entry['away_team_name'] ) ? (string) $entry['away_team_name'] : __( 'TBD', 'tonys-sportspress-enhancements' ); + $home_name = isset( $entry['home_team_name'] ) ? (string) $entry['home_team_name'] : __( 'TBD', 'tonys-sportspress-enhancements' ); + echo '' . esc_html( $away_name ) . '' . esc_html__( 'vs', 'tonys-sportspress-enhancements' ) . '' . esc_html( $home_name ) . ''; + } else { + echo '' . esc_html( isset( $entry['opponent_name'] ) ? (string) $entry['opponent_name'] : '' ) . ''; + } } echo '
'; - echo '' . esc_html( $is_home ? 'H' : 'A' ) . ''; + if ( ! $is_matchup ) { + echo '' . esc_html( $is_home ? 'H' : 'A' ) . ''; + } echo '
'; echo '
' . esc_html( isset( $entry['event_time'] ) ? (string) $entry['event_time'] : '' ) . '
'; - if ( ! empty( $entry['venue_label'] ) ) { + if ( ! $suppress_venue && ! empty( $entry['venue_label'] ) ) { echo '
' . esc_html( (string) $entry['venue_label'] ) . '
'; } echo '
'; @@ -1333,9 +1679,32 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { * @param int $team_id Team ID. * @return string */ - private function get_team_label( $team_id ) { - if ( function_exists( 'sp_team_abbreviation' ) ) { - $label = (string) sp_team_abbreviation( $team_id ); + private function get_team_label( $team_id, $mode = 'abbreviation' ) { + $mode = $this->sanitize_label_mode( $mode, 'abbreviation' ); + + if ( 'shortname' === $mode ) { + if ( function_exists( 'sp_team_short_name' ) ) { + $label = trim( (string) sp_team_short_name( $team_id ) ); + if ( '' !== $label ) { + return $label; + } + } + + $label = trim( (string) get_post_meta( $team_id, 'sp_short_name', true ) ); + if ( '' !== $label ) { + return $label; + } + } + + if ( 'abbreviation' === $mode ) { + if ( function_exists( 'sp_team_abbreviation' ) ) { + $label = trim( (string) sp_team_abbreviation( $team_id ) ); + if ( '' !== $label ) { + return $label; + } + } + + $label = trim( (string) get_post_meta( $team_id, 'sp_abbreviation', true ) ); if ( '' !== $label ) { return $label; } @@ -1369,21 +1738,21 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { * @param WP_Term $venue Venue term. * @return string */ - private function get_configured_venue_label( $venue ) { + private function get_configured_venue_label( $venue, $mode = '' ) { $full_name = isset( $venue->name ) ? trim( (string) $venue->name ) : ''; if ( '' === $full_name || ! isset( $venue->term_id ) ) { return ''; } $term_id = (int) $venue->term_id; - $mode = $this->get_venue_label_mode(); + $mode = '' === $mode ? $this->get_venue_label_mode() : $this->sanitize_label_mode( $mode, $this->get_venue_label_mode() ); if ( 'abbreviation' === $mode ) { $abbreviation = trim( (string) get_term_meta( $term_id, 'tse_abbreviation', true ) ); return '' !== $abbreviation ? $abbreviation : $full_name; } - if ( 'short_name' === $mode ) { + if ( 'shortname' === $mode ) { $short_name = trim( (string) get_term_meta( $term_id, 'tse_short_name', true ) ); return '' !== $short_name ? $short_name : $full_name; } @@ -1397,15 +1766,27 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { * @param string $venue_name Venue name. * @param int $season_id Season ID. * @param int $venue_id Venue ID. - * @param string $team_primary Team primary color. + * @param string $team_primary Team primary color. + * @param bool $ignore_team_primary Whether to ignore venue primary flags. * @return string */ - private function get_venue_color( $venue_name, $season_id, $venue_id, $team_primary ) { - if ( $this->should_use_team_primary_for_venue( $season_id, $venue_id ) && '' !== $this->sanitize_color( $team_primary ) ) { + private function get_venue_color( $venue_name, $season_id, $venue_id, $team_primary, $ignore_team_primary = false ) { + $custom = $this->get_custom_venue_color( $season_id, $venue_id ); + if ( $ignore_team_primary && '' !== $custom ) { + return $custom; + } + + if ( $ignore_team_primary ) { + $suggested = $this->get_suggested_venue_color( $season_id, $venue_id ); + if ( '' !== $suggested ) { + return $suggested; + } + } + + if ( ! $ignore_team_primary && $this->should_use_team_primary_for_venue( $season_id, $venue_id ) && '' !== $this->sanitize_color( $team_primary ) ) { return $this->sanitize_color( $team_primary ); } - $custom = $this->get_custom_venue_color( $season_id, $venue_id ); if ( '' !== $custom ) { return $custom; } @@ -1416,6 +1797,36 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { return $palette[ $index ]; } + /** + * Get the settings-screen suggested venue color for a season. + * + * @param int $season_id Season ID. + * @param int $venue_id Venue ID. + * @return string + */ + private function get_suggested_venue_color( $season_id, $venue_id ) { + if ( $season_id <= 0 || $venue_id <= 0 ) { + return ''; + } + + $venues = $this->get_venues_for_season( $season_id ); + if ( empty( $venues ) ) { + return ''; + } + + $palette_count = count( $this->suggested_palette ); + foreach ( $venues as $index => $venue ) { + if ( ! is_array( $venue ) || ! isset( $venue['id'] ) || (int) $venue['id'] !== (int) $venue_id ) { + continue; + } + + $suggested = isset( $this->suggested_palette[ $index % max( 1, $palette_count ) ] ) ? $this->suggested_palette[ $index % max( 1, $palette_count ) ] : ''; + return '' !== $suggested ? $this->adjust_for_white_text( $suggested, self::MIN_WHITE_CONTRAST ) : ''; + } + + return ''; + } + /** * Resolve primary team color without site fallbacks. * @@ -1615,6 +2026,21 @@ if ( ! class_exists( 'Tony_Sportspress_Printable_Calendars' ) ) { ); } + /** + * Get neutral printable colors for combined-team schedules. + * + * @return array + */ + private function get_default_printable_palette() { + return array( + 'primary' => '#334155', + 'secondary' => '#64748B', + 'accent' => '#475569', + 'ink' => '#111827', + 'muted_ink' => '#334155', + ); + } + /** * Sanitize a six-digit hex color. * diff --git a/includes/sp-schedule-exporter.php b/includes/sp-schedule-exporter.php index 5f6b160..b673cbc 100644 --- a/includes/sp-schedule-exporter.php +++ b/includes/sp-schedule-exporter.php @@ -86,11 +86,14 @@ function tse_sp_schedule_exporter_render_shortcode() { $seasons = tse_sp_schedule_exporter_get_seasons(); $season_id = tse_sp_schedule_exporter_resolve_season_id( $seasons ); $teams = tse_sp_schedule_exporter_get_teams( $league_id, $season_id ); - $team_id = tse_sp_schedule_exporter_resolve_team_id( $teams ); + $team_ids = tse_sp_schedule_exporter_resolve_team_ids( $teams ); $fields = tse_sp_schedule_exporter_get_fields(); $field_id = tse_sp_schedule_exporter_resolve_field_id( $fields ); $export_type = tse_sp_schedule_exporter_resolve_export_type(); $subformat = tse_sp_schedule_exporter_resolve_subformat(); + if ( 'team' === $subformat && count( $team_ids ) > 1 ) { + $subformat = 'matchup'; + } if ( empty( $teams ) ) { return '

' . esc_html__( 'No SportsPress teams match the selected league and season.', 'tonys-sportspress-enhancements' ) . '

'; @@ -152,14 +155,14 @@ function tse_sp_schedule_exporter_render_shortcode() {

- - +

@@ -174,15 +177,22 @@ function tse_sp_schedule_exporter_render_shortcode() {
+
+ +
+
$team_id, 'season_id' => $season_id, 'league_id' => $league_id, 'field_id' => $field_id, 'format' => $subformat ), 'csv' ); - $ics_url = tse_sp_event_export_get_feed_url( array( 'team_id' => $team_id, 'season_id' => $season_id, 'league_id' => $league_id, 'field_id' => $field_id ), 'ics' ); - $print_url = tse_sp_schedule_exporter_get_printable_url( $team_id, $season_id, 'letter', $league_id ); + $csv_url = tse_sp_event_export_get_feed_url( array( 'team_id' => $team_ids, 'season_id' => $season_id, 'league_id' => $league_id, 'field_id' => $field_id, 'format' => $subformat ), 'csv' ); + $ics_url = tse_sp_event_export_get_feed_url( array( 'team_id' => $team_ids, 'season_id' => $season_id, 'league_id' => $league_id, 'field_id' => $field_id ), 'ics' ); + $print_url = tse_sp_schedule_exporter_get_printable_url( $team_ids, $season_id, 'letter', $league_id, false, $field_id ); $current_url = tse_sp_schedule_exporter_get_output_url( $export_type, $csv_url, $ics_url, $print_url ); ?>
@@ -331,26 +341,34 @@ function tse_sp_schedule_exporter_get_teams( $league_id = 0, $season_id = 0 ) { } /** - * Resolve selected team ID. + * Resolve selected team IDs. * * @param WP_Post[] $teams Team posts. - * @return int + * @return int[] */ -function tse_sp_schedule_exporter_resolve_team_id( $teams ) { - $requested = isset( $_GET['team_id'] ) ? absint( wp_unslash( $_GET['team_id'] ) ) : 0; - if ( $requested > 0 && 'sp_team' === get_post_type( $requested ) ) { - foreach ( $teams as $team ) { - if ( $team instanceof WP_Post && (int) $team->ID === $requested ) { - return $requested; - } +function tse_sp_schedule_exporter_resolve_team_ids( $teams ) { + $available = array(); + foreach ( $teams as $team ) { + if ( $team instanceof WP_Post ) { + $available[] = (int) $team->ID; } } - if ( isset( $teams[0] ) && $teams[0] instanceof WP_Post ) { - return (int) $teams[0]->ID; + $requested = array(); + if ( isset( $_GET['team_id'] ) ) { + $requested = tse_sp_event_export_parse_id_list( wp_unslash( $_GET['team_id'] ) ); } - return 0; + $selected = array_values( array_intersect( $requested, $available ) ); + if ( ! empty( $selected ) ) { + return $selected; + } + + if ( isset( $available[0] ) ) { + return array( (int) $available[0] ); + } + + return array(); } /** @@ -700,17 +718,25 @@ function tse_sp_schedule_exporter_get_primary_venue( $event_id ) { * @param int $season_id Season ID. * @param string $paper Paper size. * @param int $league_id League ID. + * @param string $team_label_mode Team label mode. + * @param string $field_label_mode Field label mode. * @return string */ -function tse_sp_schedule_exporter_get_printable_url( $team_id, $season_id, $paper, $league_id = 0, $autoprint = false ) { +function tse_sp_schedule_exporter_get_printable_url( $team_id, $season_id, $paper, $league_id = 0, $autoprint = false, $field_id = 0, $month_pages = false, $team_label_mode = 'name', $field_label_mode = 'name' ) { + $team_ids = is_array( $team_id ) ? array_values( array_filter( array_map( 'absint', $team_id ) ) ) : array( absint( $team_id ) ); + return add_query_arg( array( Tony_Sportspress_Printable_Calendars::QUERY_FLAG => '1', - 'sp_team' => (string) absint( $team_id ), + 'sp_team' => implode( ',', $team_ids ), 'sp_season' => $season_id > 0 ? (string) absint( $season_id ) : '', 'sp_league' => $league_id > 0 ? (string) absint( $league_id ) : '', + 'sp_field' => $field_id > 0 ? (string) absint( $field_id ) : '', + 'team_label' => sanitize_key( (string) $team_label_mode ), + 'field_label' => sanitize_key( (string) $field_label_mode ), 'paper' => $paper, 'autoprint' => $autoprint ? '1' : '', + 'month_pages' => $month_pages ? '1' : '', ), home_url( '/' ) ); @@ -758,19 +784,34 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) { var league = form.querySelector('[name="league_id"]'); var season = form.querySelector('[name="season_id"]'); - var team = form.querySelector('[name="team_id"]'); + var team = form.querySelector('[name="team_id[]"], [name="team_id"]'); var exportType = form.querySelector('[name="export_type"]'); var subformat = form.querySelector('[name="subformat"]'); var field = form.querySelector('[name="field_id"]'); + var monthPages = form.querySelector('[name="month_pages"]'); var outputUrl = scope.querySelector('.tse-output-url'); var openButton = scope.querySelector('.tse-open-link'); var iosButton = scope.querySelector('.tse-ics-ios-link'); var androidButton = scope.querySelector('.tse-ics-android-link'); var outputNote = scope.querySelector('.tse-output-note'); var copyButton = scope.querySelector('.tse-copy-link'); - var teamValue = team ? (team.value || '0') : '0'; + var teamValues = team ? Array.prototype.slice.call(team.selectedOptions || []).map(function(option){ + return option.value; + }).filter(function(value){ + return value && value !== '0'; + }) : []; + if (!teamValues.length && team && team.value && team.value !== '0') { + teamValues = [team.value]; + } + var teamValue = teamValues.length ? teamValues.join(',') : '0'; var activeSubformat = subformat ? (subformat.value || 'matchup') : 'matchup'; var selectedExportType = exportType ? (exportType.value || 'csv') : 'csv'; + if (teamValues.length > 1 && activeSubformat === 'team') { + activeSubformat = 'matchup'; + if (subformat) { + subformat.value = 'matchup'; + } + } scope.querySelectorAll('[data-column-group]').forEach(function(group){ var visible = selectedExportType === 'csv' && group.getAttribute('data-column-group') === activeSubformat; @@ -816,6 +857,12 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) { if (league) printUrl.searchParams.set('sp_league', league.value || '0'); if (season) printUrl.searchParams.set('sp_season', season.value || '0'); if (team) printUrl.searchParams.set('sp_team', teamValue); + if (field) printUrl.searchParams.set('sp_field', field.value || '0'); + if (monthPages && monthPages.checked) { + printUrl.searchParams.set('month_pages', '1'); + } else { + printUrl.searchParams.delete('month_pages'); + } printUrl.searchParams.set('paper', 'letter'); } @@ -833,11 +880,11 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) { resolvedUrl = printUrl.toString(); if (teamValue === '0') { disabled = true; - note = 'Printable requires a specific team. All teams is not supported.'; + note = 'Printable requires at least one selected team.'; } } else if (selectedExportType === 'csv' && activeSubformat === 'team' && teamValue === '0') { disabled = true; - note = 'CSV team layout requires a specific team. All teams is not supported.'; + note = 'CSV team layout requires one selected team.'; } if (outputUrl) { @@ -885,10 +932,10 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) { syncLinks(scope); - scope.querySelectorAll('.tse-schedule-exporter-form select').forEach(function(select){ - select.addEventListener('change', function(){ - if (select.dataset.autoSubmit === '1') { - select.form.submit(); + scope.querySelectorAll('.tse-schedule-exporter-form select, .tse-schedule-exporter-form input[type="checkbox"]').forEach(function(input){ + input.addEventListener('change', function(){ + if (input.dataset.autoSubmit === '1') { + input.form.submit(); return; } diff --git a/tests/test-sp-schedule-exporter.php b/tests/test-sp-schedule-exporter.php new file mode 100644 index 0000000..b15ef82 --- /dev/null +++ b/tests/test-sp-schedule-exporter.php @@ -0,0 +1,242 @@ +original_get = $_GET; + + foreach ( array( 'sp_venue', 'sp_league', 'sp_season' ) as $taxonomy ) { + if ( ! taxonomy_exists( $taxonomy ) ) { + register_taxonomy( $taxonomy, 'sp_event' ); + } + } + } + + /** + * Restore request globals. + */ + public function tear_down() { + $_GET = $this->original_get; + + parent::tear_down(); + } + + /** + * Create a SportsPress team post. + * + * @param string $name Team name. + * @return int + */ + private function create_team( $name ) { + return self::factory()->post->create( + array( + 'post_type' => 'sp_team', + 'post_status' => 'publish', + 'post_title' => $name, + ) + ); + } + + /** + * Create a SportsPress event with ordered teams. + * + * @param int $home_id Home team ID. + * @param int $away_id Away team ID. + * @param int[] $venue_ids Venue IDs. + * @return int + */ + private function create_event( $home_id, $away_id, $venue_ids = array() ) { + $event_id = self::factory()->post->create( + array( + 'post_type' => 'sp_event', + 'post_status' => 'publish', + 'post_title' => 'Game', + 'post_date' => '2026-05-20 18:00:00', + 'post_date_gmt' => '2026-05-20 23:00:00', + ) + ); + + add_post_meta( $event_id, 'sp_team', (string) $home_id ); + add_post_meta( $event_id, 'sp_team', (string) $away_id ); + + if ( ! empty( $venue_ids ) ) { + wp_set_object_terms( $event_id, $venue_ids, 'sp_venue' ); + } + + return $event_id; + } + + /** + * Multiple selected team IDs should be retained in request order. + */ + public function test_resolve_team_ids_accepts_multiple_request_values() { + $team_one = $this->create_team( 'Blue' ); + $team_two = $this->create_team( 'Red' ); + $teams = array( get_post( $team_one ), get_post( $team_two ) ); + + $_GET['team_id'] = array( (string) $team_one, (string) $team_two ); + + $this->assertSame( array( $team_one, $team_two ), tse_sp_schedule_exporter_resolve_team_ids( $teams ) ); + } + + /** + * Printable URLs should carry multiple team IDs and the selected field. + */ + public function test_printable_url_accepts_multiple_teams_and_field() { + $url = tse_sp_schedule_exporter_get_printable_url( array( 12, 34 ), 56, 'letter', 78, false, 90, true ); + $query = array(); + + wp_parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query ); + + $this->assertSame( '12,34', $query['sp_team'] ); + $this->assertSame( '90', $query['sp_field'] ); + $this->assertSame( 'name', $query['team_label'] ); + $this->assertSame( 'name', $query['field_label'] ); + $this->assertSame( '1', $query['month_pages'] ); + } + + /** + * Printable URLs should carry selected team and field label modes. + */ + public function test_printable_url_accepts_label_modes() { + $url = tse_sp_schedule_exporter_get_printable_url( 12, 56, 'letter', 78, false, 90, false, 'shortname', 'abbreviation' ); + $query = array(); + + wp_parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query ); + + $this->assertSame( 'shortname', $query['team_label'] ); + $this->assertSame( 'abbreviation', $query['field_label'] ); + } + + /** + * Single-team printable entries should keep the existing opponent perspective. + */ + public function test_printable_single_team_entries_keep_opponent_perspective() { + $home_id = $this->create_team( 'Home Team' ); + $away_id = $this->create_team( 'Away Team' ); + $venue_id = self::factory()->term->create( array( 'taxonomy' => 'sp_venue', 'name' => 'North Field' ) ); + $this->create_event( $home_id, $away_id, array( $venue_id ) ); + + $printable = Tony_Sportspress_Printable_Calendars::instance(); + $method = new ReflectionMethod( $printable, 'get_schedule_entries' ); + $method->setAccessible( true ); + + $entries = $method->invoke( $printable, $home_id, 0, 0, array() ); + + $this->assertCount( 1, $entries ); + $this->assertFalse( $entries[0]['is_matchup'] ); + $this->assertTrue( $entries[0]['is_home'] ); + $this->assertSame( 'Away Team', $entries[0]['opponent_name'] ); + } + + /** + * Printable entries should honor requested team and field labels. + */ + public function test_printable_entries_honor_label_modes() { + $home_id = $this->create_team( 'Home Team' ); + $away_id = $this->create_team( 'Away Team' ); + $venue_id = self::factory()->term->create( array( 'taxonomy' => 'sp_venue', 'name' => 'North Field' ) ); + update_post_meta( $away_id, 'sp_abbreviation', 'AWY' ); + update_term_meta( $venue_id, 'tse_abbreviation', 'NF' ); + $this->create_event( $home_id, $away_id, array( $venue_id ) ); + + $printable = Tony_Sportspress_Printable_Calendars::instance(); + $method = new ReflectionMethod( $printable, 'get_schedule_entries' ); + $method->setAccessible( true ); + + $entries = $method->invoke( $printable, $home_id, 0, 0, array(), 'abbreviation', 'abbreviation' ); + + $this->assertCount( 1, $entries ); + $this->assertSame( 'AWY', $entries[0]['opponent_name'] ); + $this->assertSame( 'NF', $entries[0]['venue_label'] ); + } + + /** + * Multi-team printable entries should return one matchup row per event. + */ + public function test_printable_multi_team_entries_are_matchup_rows() { + $home_id = $this->create_team( 'Home Team' ); + $away_id = $this->create_team( 'Away Team' ); + $venue_id = self::factory()->term->create( array( 'taxonomy' => 'sp_venue', 'name' => 'North Field' ) ); + $this->create_event( $home_id, $away_id, array( $venue_id ) ); + + $printable = Tony_Sportspress_Printable_Calendars::instance(); + $method = new ReflectionMethod( $printable, 'get_schedule_entries' ); + $method->setAccessible( true ); + + $entries = $method->invoke( $printable, array( $home_id, $away_id ), 0, 0, array() ); + + $this->assertCount( 1, $entries ); + $this->assertTrue( $entries[0]['is_matchup'] ); + $this->assertSame( 'Away Team', $entries[0]['away_team_name'] ); + $this->assertSame( 'Home Team', $entries[0]['home_team_name'] ); + } + + /** + * Exactly one selected field should suppress per-event venue labels. + */ + public function test_render_month_grid_can_suppress_event_venue_label() { + $printable = Tony_Sportspress_Printable_Calendars::instance(); + $method = new ReflectionMethod( $printable, 'render_month_grid' ); + $method->setAccessible( true ); + + $entries = array( + '2026-05-20' => array( + array( + 'day_key' => '2026-05-20', + 'month_key' => '2026-05', + 'timestamp' => strtotime( '2026-05-20 18:00:00' ), + 'is_matchup' => true, + 'away_team_name' => 'Away', + 'home_team_name' => 'Home', + 'event_time' => '6:00 PM', + 'venue_label' => 'North', + 'venue_name' => 'North Field', + 'venue_key' => 'v:1', + ), + ), + ); + + ob_start(); + $method->invoke( + $printable, + '2026-05', + $entries, + array( 'v:1' => array( 'name' => 'North Field', 'color' => '#1D4ED8' ) ), + array( + 'primary' => '#1D4ED8', + 'secondary' => '#DC2626', + 'accent' => '#1D4ED8', + 'ink' => '#111827', + 'muted_ink' => '#334155', + ), + true, + true + ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'Away', $output ); + $this->assertStringContainsString( 'Home', $output ); + $this->assertStringNotContainsString( 'class="event-venue"', $output ); + } +}