Changeset 3444584
- Timestamp:
- 01/22/2026 07:05:52 AM (2 months ago)
- Location:
- auto-fixture-generator-for-sportspress
- Files:
-
- 16 edited
- 1 copied
-
tags/1.5 (copied) (copied from auto-fixture-generator-for-sportspress/trunk)
-
tags/1.5/assets/admin.css (modified) (2 diffs)
-
tags/1.5/assets/admin.js (modified) (3 diffs)
-
tags/1.5/auto-fixture-generator-for-sportspress.php (modified) (2 diffs)
-
tags/1.5/includes/class-afgsp-admin.php (modified) (16 diffs)
-
tags/1.5/includes/class-afgsp-debug.php (modified) (3 diffs)
-
tags/1.5/includes/class-afgsp-generator.php (modified) (5 diffs)
-
tags/1.5/includes/helpers.php (modified) (1 diff)
-
tags/1.5/readme.txt (modified) (2 diffs)
-
trunk/assets/admin.css (modified) (2 diffs)
-
trunk/assets/admin.js (modified) (3 diffs)
-
trunk/auto-fixture-generator-for-sportspress.php (modified) (2 diffs)
-
trunk/includes/class-afgsp-admin.php (modified) (16 diffs)
-
trunk/includes/class-afgsp-debug.php (modified) (3 diffs)
-
trunk/includes/class-afgsp-generator.php (modified) (5 diffs)
-
trunk/includes/helpers.php (modified) (1 diff)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
auto-fixture-generator-for-sportspress/tags/1.5/assets/admin.css
r3388275 r3444584 101 101 } 102 102 103 /* Events per timeslot number inputs */ 104 .afgsp-events-per-slot { 105 width: 60px; 106 margin-left: 8px; 107 text-align: center; 108 } 109 110 .afgsp-events-per-slot:disabled { 111 background-color: #f5f5f5; 112 color: #666; 113 } 114 115 /* Time slots container layout */ 116 #afgsp_time_slots > div { 117 display: flex; 118 align-items: center; 119 margin-bottom: 5px; 120 flex-wrap: wrap; 121 gap: 5px; 122 } 123 124 #afgsp_time_slots input[type="time"] { 125 width: 120px; 126 } 127 103 128 /* Responsive design */ 104 129 @media (max-width: 768px) { … … 115 140 height: 25px; 116 141 } 142 143 .afgsp-events-per-slot { 144 width: 50px; 145 margin-left: 5px; 146 } 117 147 } -
auto-fixture-generator-for-sportspress/tags/1.5/assets/admin.js
r3435431 r3444584 55 55 56 56 return matches; 57 } 58 59 /** 60 * Get the count of selected teams. 61 * For premium users, counts checked checkboxes. 62 * For free users, returns the cached count from AJAX response. 63 * 64 * @return {number} Number of teams. 65 */ 66 function getTeamsCount() { 67 // For premium users with team checkboxes visible 68 if ($('#afgsp_teams').is(':visible') && $('#afgsp_teams input:checked').length > 0) { 69 return $('#afgsp_teams input:checked').length; 70 } 71 // For free users, use cached count if available 72 if (window.AFGSP_ADMIN && typeof window.AFGSP_ADMIN.teamsCount === 'number') { 73 return window.AFGSP_ADMIN.teamsCount; 74 } 75 return 0; 76 } 77 78 /** 79 * Get the count of selected days in the gameweek builder. 80 * 81 * @return {number} Number of selected days. 82 */ 83 function getSelectedDaysCount() { 84 var count = $('#afgsp_gameweek_builder input[type="checkbox"]:checked').length; 85 return count > 0 ? count : 2; // Default to 2 (Sat, Sun) 86 } 87 88 /** 89 * Calculate events per timeslot using the AUTO mode formula. 90 * 91 * @param {number} teamsCount Number of teams. 92 * @param {number} daysCount Number of selected match days. 93 * @param {number} slotsCount Number of time slots. 94 * @return {number} Events per timeslot. 95 */ 96 function calculateEventsPerSlot(teamsCount, daysCount, slotsCount) { 97 teamsCount = Math.max(2, parseInt(teamsCount) || 2); 98 daysCount = Math.max(1, parseInt(daysCount) || 1); 99 slotsCount = Math.max(1, parseInt(slotsCount) || 1); 100 101 var matchesPerRound = Math.floor(teamsCount / 2); 102 var matchesPerDay = Math.ceil(matchesPerRound / daysCount); 103 var eventsPerSlot = Math.ceil(matchesPerDay / slotsCount); 104 105 return Math.max(1, eventsPerSlot); 106 } 107 108 /** 109 * Update all events per slot input fields with the calculated auto value. 110 * In AUTO mode, inputs are disabled and show the calculated value. 111 * In MANUAL mode (premium only), inputs are enabled for custom values. 112 */ 113 function updateEventsPerSlotInputs() { 114 var teamsCount = getTeamsCount(); 115 var daysCount = getSelectedDaysCount(); 116 var slotsCount = $('#afgsp_time_slots input[type="time"]').length || 1; 117 var autoValue = calculateEventsPerSlot(teamsCount, daysCount, slotsCount); 118 119 var mode = $('#afgsp_events_mode').val() || 'auto'; 120 var isPremium = window.AFGSP_ADMIN && window.AFGSP_ADMIN.isPremium; 121 122 $('.afgsp-events-per-slot').each(function() { 123 // In AUTO mode or for free users, always show auto value and disable 124 if (mode === 'auto' || !isPremium) { 125 $(this).val(autoValue).prop('disabled', true); 126 } else if (mode === 'manual' && isPremium) { 127 // In MANUAL mode for premium, enable but keep current value if set 128 if (!$(this).val() || $(this).val() === '0') { 129 $(this).val(autoValue); 130 } 131 $(this).prop('disabled', false); 132 } 133 }); 57 134 } 58 135 … … 433 510 $(document).on('change', '#afgsp_gameweek_builder input[type="checkbox"]', function() { 434 511 updateGameweekPreview(); 512 updateEventsPerSlotInputs(); 513 }); 514 515 // Update events per slot when time slots are added/removed 516 $(document).on('DOMNodeInserted DOMNodeRemoved', '#afgsp_time_slots', function() { 517 setTimeout(updateEventsPerSlotInputs, 50); 435 518 }); 436 519 … … 446 529 updateEventsDescription(); 447 530 updateTeamsDescriptionForFree(); 531 updateEventsPerSlotInputs(); 532 }); 533 534 // Update events per slot when league/season changes (for free users to get team count) 535 $(document).on('change', '#afgsp_league, #afgsp_season', function () { 536 var leagueId = $('#afgsp_league').val(); 537 var seasonId = $('#afgsp_season').val(); 538 539 if (leagueId && seasonId) { 540 $.getJSON((window.AFGSP_ADMIN && window.AFGSP_ADMIN.ajaxUrl) || '', { 541 action: 'afgsp_get_teams', 542 league: leagueId, 543 season: seasonId, 544 nonce: (window.AFGSP_ADMIN && window.AFGSP_ADMIN.nonce) || '' 545 }).done(function (resp) { 546 var teams = (resp && resp.data && Array.isArray(resp.data.teams) && resp.data.teams) || []; 547 // Cache the teams count for free users 548 if (window.AFGSP_ADMIN) { 549 window.AFGSP_ADMIN.teamsCount = teams.length; 550 } 551 updateEventsPerSlotInputs(); 552 }); 553 } 554 }); 555 556 // Update events per slot when teams selection changes (for premium users) 557 $(document).on('change', '#afgsp_teams input[type="checkbox"]', function () { 558 updateEventsPerSlotInputs(); 448 559 }); 449 560 -
auto-fixture-generator-for-sportspress/tags/1.5/auto-fixture-generator-for-sportspress.php
r3437103 r3444584 4 4 * Plugin Name: Auto Fixture Generator for SportsPress 5 5 * Description: Automatically generate fixtures (events) for a selected SportsPress league and season using pluggable scheduling algorithms. 6 * Version: 1. 46 * Version: 1.5 7 7 * Author: Savvas 8 8 * Author URI: https://savvasha.com … … 70 70 */ 71 71 if ( !defined( 'AFGSP_VERSION' ) ) { 72 define( 'AFGSP_VERSION', '1. 4' );72 define( 'AFGSP_VERSION', '1.5' ); 73 73 } 74 74 if ( !defined( 'AFGSP_PLUGIN_FILE' ) ) { -
auto-fixture-generator-for-sportspress/tags/1.5/includes/class-afgsp-admin.php
r3437103 r3444584 202 202 }, (array) $raw_schedule['blocked_dates'] ) ) ) : array() ); 203 203 $blocked_dates = array(); 204 // Process events mode and events per slot (premium feature). 205 $events_mode = ( isset( $raw_schedule['events_mode'] ) ? sanitize_text_field( (string) $raw_schedule['events_mode'] ) : 'auto' ); 206 $events_per_slot = ( isset( $raw_schedule['events_per_slot'] ) && is_array( $raw_schedule['events_per_slot'] ) ? array_map( 'absint', $raw_schedule['events_per_slot'] ) : array() ); 207 $events_mode = 'auto'; 208 $events_per_slot = array(); 204 209 $schedule = array( 205 'start_date' => ( isset( $raw_schedule['start_date'] ) ? sanitize_text_field( (string) $raw_schedule['start_date'] ) : '' ), 206 'days' => ( isset( $raw_schedule['days'] ) && is_array( $raw_schedule['days'] ) ? array_values( array_unique( array_map( 'intval', (array) $raw_schedule['days'] ) ) ) : array(6, 0) ), 207 'time_slots' => $time_slots, 208 'blocked_dates' => $blocked_dates, 209 'gameweek_name' => ( isset( $raw_schedule['gameweek_name'] ) ? sanitize_text_field( (string) $raw_schedule['gameweek_name'] ) : 'Gameweek %No%' ), 210 'start_date' => ( isset( $raw_schedule['start_date'] ) ? sanitize_text_field( (string) $raw_schedule['start_date'] ) : '' ), 211 'days' => ( isset( $raw_schedule['days'] ) && is_array( $raw_schedule['days'] ) ? array_values( array_unique( array_map( 'intval', (array) $raw_schedule['days'] ) ) ) : array(6, 0) ), 212 'time_slots' => $time_slots, 213 'blocked_dates' => $blocked_dates, 214 'gameweek_name' => ( isset( $raw_schedule['gameweek_name'] ) ? sanitize_text_field( (string) $raw_schedule['gameweek_name'] ) : 'Gameweek %No%' ), 215 'events_mode' => $events_mode, 216 'events_per_slot' => $events_per_slot, 210 217 ); 211 218 if ( empty( $schedule['start_date'] ) ) { … … 225 232 } 226 233 try { 227 $ fixtures = call_user_func( $callable, $team_ids, array(234 $raw_fixtures = call_user_func( $callable, $team_ids, array( 228 235 'schedule' => $schedule, 229 236 ) + $options ); 230 if ( !is_array( $ fixtures ) ) {237 if ( !is_array( $raw_fixtures ) ) { 231 238 wp_send_json_error( array( 232 239 'message' => __( 'Algorithm returned an invalid response.', 'auto-fixture-generator-for-sportspress' ), … … 238 245 ), 500 ); 239 246 } 240 $total = count( $fixtures ); 247 // Apply scheduling using centralized prepare_fixtures method. 248 // This ensures both normal mode and dry-run mode use identical scheduling logic. 249 $prepared_fixtures = AFGSP_Generator::prepare_fixtures( 250 $raw_fixtures, 251 $schedule, 252 count( $team_ids ), 253 $algorithm 254 ); 255 $total = count( $prepared_fixtures ); 241 256 if ( $total <= 0 ) { 242 257 wp_send_json_error( array( … … 252 267 'season_id' => $season_id, 253 268 'algorithm' => $algorithm, 254 'fixtures' => array_values( $ fixtures ),269 'fixtures' => array_values( $prepared_fixtures ), 255 270 'next_index' => 0, 256 271 'total' => $total, … … 259 274 'gameweeks' => array(), 260 275 'schedule' => $schedule, 261 'cursor' => strtotime( $schedule['start_date'] . ' 00:00:00' ),262 'blocked' => array_fill_keys( $schedule['blocked_dates'], true ),263 'days' => $schedule['days'],264 'time_slots' => $schedule['time_slots'],265 'gameweek_name' => $schedule['gameweek_name'],266 'slot_index' => 0,267 276 'round_size' => max( 1, $round_size ), 268 277 'create_calendar' => !empty( $options['create_calendar'] ), … … 272 281 'shuffle_teams' => !empty( $options['shuffle_teams'] ), 273 282 'no_consecutive_away' => !empty( $options['no_consecutive_away'] ), 283 'algorithm_options' => $options, 274 284 'messages' => array(), 275 285 'created_entities' => false, … … 305 315 ), 400 ); 306 316 } 317 // Extract state - fixtures are now pre-scheduled with datetime and gameweek. 307 318 $fixtures = ( isset( $state['fixtures'] ) ? (array) $state['fixtures'] : array() ); 308 319 $next_index = (int) ($state['next_index'] ?? 0); … … 313 324 $league_id = (int) ($state['league_id'] ?? 0); 314 325 $season_id = (int) ($state['season_id'] ?? 0); 315 $cursor = (int) ($state['cursor'] ?? 0);316 $days = ( isset( $state['days'] ) ? array_map( 'intval', (array) $state['days'] ) : array() );317 $time_slots = ( isset( $state['time_slots'] ) ? array_values( (array) $state['time_slots'] ) : array() );318 $gameweek_name = ( isset( $state['gameweek_name'] ) ? (string) $state['gameweek_name'] : 'Gameweek %No%' );319 $slot_index = (int) ($state['slot_index'] ?? 0);320 $blocked = ( isset( $state['blocked'] ) && is_array( $state['blocked'] ) ? (array) $state['blocked'] : array() );321 326 $messages = ( isset( $state['messages'] ) && is_array( $state['messages'] ) ? (array) $state['messages'] : array() ); 322 327 $created_entities = !empty( $state['created_entities'] ); … … 330 335 $dry_run_fixtures = ( isset( $state['dry_run_fixtures'] ) && is_array( $state['dry_run_fixtures'] ) ? $state['dry_run_fixtures'] : array() ); 331 336 $schedule = ( isset( $state['schedule'] ) && is_array( $state['schedule'] ) ? $state['schedule'] : array() ); 332 // Gameweek tracking. 333 $current_gameweek = (int) ($state['current_gameweek'] ?? 1); 334 $current_gameweek_start = ( isset( $state['current_gameweek_start'] ) ? (int) $state['current_gameweek_start'] : null ); 335 $current_gameweek_end = ( isset( $state['current_gameweek_end'] ) ? (int) $state['current_gameweek_end'] : null ); 337 $algorithm = ( isset( $state['algorithm'] ) ? (string) $state['algorithm'] : '' ); 336 338 // If already complete, ensure we still return any completion messages. 337 339 if ( $next_index >= $total ) { … … 341 343 'league_id' => $league_id, 342 344 'season_id' => $season_id, 343 'algorithm' => $algorithm ?? '', 345 'algorithm' => $algorithm, 346 'algorithm_options' => ( isset( $state['algorithm_options'] ) ? (array) $state['algorithm_options'] : array() ), 344 347 'schedule' => $schedule, 345 348 'team_ids' => $team_ids, … … 376 379 ) ); 377 380 } 378 // Determine capacity for a single matchday: either number of time slots or algorithm round size, whichever is larger.379 $max_per_day = ( !empty( $time_slots ) ? count( $time_slots ) : 1 );380 $ max_per_day = max( $max_per_day, (int) ($state['round_size'] ?? 1));381 // Process a batch of pre-scheduled fixtures. 382 // Fixtures now have home_id, away_id, extra_meta (with datetime and sp_day), and gameweek already assigned. 383 $batch_size = (int) ($state['round_size'] ?? 4); 381 384 $processed_this_batch = 0; 382 $batch_date = '';383 385 // Track algorithm to determine if duplicate checking should be skipped. 384 $algorithm = ( isset( $state['algorithm'] ) ? (string) $state['algorithm'] : '' );385 386 $skip_duplicate_check = 'fixed-week-season' === $algorithm; 386 $last_processed_gameweek = (int) ($state['last_processed_gameweek'] ?? 0); 387 while ( $next_index < $total ) { 387 while ( $next_index < $total && $processed_this_batch < $batch_size ) { 388 388 $fixture = $fixtures[$next_index]; 389 $home_id = (int) ($fixture['home '] ?? 0);390 $away_id = (int) ($fixture['away '] ?? 0);391 $extra_meta = ( isset( $fixture[' meta'] ) && is_array( $fixture['meta'] ) ? $fixture['meta'] : array() );389 $home_id = (int) ($fixture['home_id'] ?? 0); 390 $away_id = (int) ($fixture['away_id'] ?? 0); 391 $extra_meta = ( isset( $fixture['extra_meta'] ) && is_array( $fixture['extra_meta'] ) ? $fixture['extra_meta'] : array() ); 392 392 if ( $home_id <= 0 || $away_id <= 0 || $home_id === $away_id ) { 393 393 $next_index++; 394 394 continue; 395 }396 // Calculate and assign gameweek (sp_day) based on fixture index and round size.397 $round_size = (int) ($state['round_size'] ?? 1);398 $current_gameweek = (int) floor( $next_index / $round_size ) + 1;399 $gameweek_display_name = str_replace( '%No%', (string) $current_gameweek, $gameweek_name );400 $extra_meta['sp_day'] = $gameweek_display_name;401 // If gameweek changed, advance cursor to the next week's first selected day.402 if ( $last_processed_gameweek > 0 && $current_gameweek > $last_processed_gameweek ) {403 // Reset slot index for new gameweek.404 $slot_index = 0;405 // Advance cursor to skip remaining days of current week and find next week's first selected day.406 $cursor = $this->advance_cursor_to_next_week( $cursor, $days, $blocked );407 }408 // Assign datetime if not present, mirroring generator logic.409 if ( !isset( $extra_meta['datetime'] ) && $cursor ) {410 $assigned = false;411 while ( !$assigned ) {412 $weekday = (int) gmdate( 'w', $cursor );413 $ymd = gmdate( 'Y-m-d', $cursor );414 if ( isset( $blocked[$ymd] ) ) {415 $cursor = strtotime( '+1 day', $cursor );416 continue;417 }418 if ( in_array( $weekday, $days, true ) ) {419 $slot = ( !empty( $time_slots ) ? $time_slots[$slot_index % count( $time_slots )] : '15:00' );420 $extra_meta['datetime'] = $ymd . ' ' . $slot;421 $slot_index++;422 $assigned = true;423 // Track batch date and capacity.424 if ( '' === $batch_date ) {425 $batch_date = $ymd;426 }427 $processed_this_batch++;428 // Advance cursor: next day after consuming all slots.429 if ( !empty( $time_slots ) ) {430 if ( 0 === $slot_index % count( $time_slots ) ) {431 $cursor = strtotime( '+1 day', $cursor );432 }433 } else {434 $cursor = strtotime( '+1 day', $cursor );435 }436 } else {437 $cursor = strtotime( '+1 day', $cursor );438 }439 }440 395 } 441 396 if ( $dry_run ) { … … 480 435 } 481 436 } 482 $last_processed_gameweek = $current_gameweek;483 437 $next_index++; 484 // Stop when we reach a full matchday capacity. 485 if ( $processed_this_batch >= $max_per_day ) { 486 break; 487 } 438 $processed_this_batch++; 488 439 } 489 440 $state['next_index'] = $next_index; … … 491 442 $state['duplicates'] = $duplicates; 492 443 $state['gameweeks'] = $gameweeks; 493 $state['cursor'] = $cursor;494 $state['slot_index'] = $slot_index;495 $state['current_gameweek'] = $current_gameweek;496 $state['current_gameweek_start'] = $current_gameweek_start;497 $state['current_gameweek_end'] = $current_gameweek_end;498 $state['last_processed_gameweek'] = $last_processed_gameweek;499 444 $state['messages'] = $messages; 500 445 $state['created_entities'] = $created_entities; … … 509 454 'season_id' => $season_id, 510 455 'algorithm' => $algorithm, 456 'algorithm_options' => ( isset( $state['algorithm_options'] ) ? (array) $state['algorithm_options'] : array() ), 511 457 'schedule' => $schedule, 512 458 'team_ids' => $team_ids, … … 721 667 } 722 668 return strtotime( '+' . $days_to_add . ' days', $start_date ); 723 }724 725 /**726 * Advance the cursor to the next week's first selected day.727 *728 * This ensures that when a new gameweek starts, it begins on the next calendar week's729 * first selected day, not continuing from the previous gameweek's last day.730 *731 * @param int $cursor Current cursor timestamp.732 * @param array $days Array of selected day numbers (0=Sunday, 1=Monday, etc.).733 * @param array $blocked Map of blocked dates (Y-m-d => true).734 * @return int New cursor timestamp pointing to next week's first selected day.735 */736 private function advance_cursor_to_next_week( int $cursor, array $days, array $blocked ) : int {737 if ( empty( $days ) ) {738 // Default to Saturday if no days selected.739 $days = array(6);740 }741 // Use helper to get days in proper gameweek order (handles week boundary crossing).742 $sorted_days = afgsp_sort_gameweek_days( $days );743 $first_selected_day = (int) reset( $sorted_days );744 $last_selected_day = (int) end( $sorted_days );745 // Detect if week boundary crossing (needed for cursor advancement logic).746 $has_sunday = in_array( 0, $days, true );747 $has_monday = in_array( 1, $days, true );748 $late_week = array_filter( $days, function ( $d ) {749 return $d >= 5;750 } );751 $crosses_week_boundary = ($has_sunday || $has_monday) && !empty( $late_week );752 // Get current weekday.753 $current_weekday = (int) gmdate( 'w', $cursor );754 // Calculate days to advance past the current gameweek entirely.755 // We need to get past the last selected day and then find the next first selected day.756 if ( $crosses_week_boundary ) {757 // For week-boundary-crossing gameweeks (e.g., Fri-Sat-Sun, Sat-Sun-Mon):758 // After the last day, advance to the NEXT occurrence of first_selected_day.759 if ( $current_weekday === $last_selected_day || 0 === $current_weekday ) {760 // Currently on last day or Sunday, advance to next first_selected_day.761 $days_to_first = ($first_selected_day - $current_weekday + 7) % 7;762 if ( 0 === $days_to_first ) {763 $days_to_first = 7;764 }765 } else {766 // Currently on another day, advance to next first_selected_day.767 $days_to_first = ($first_selected_day - $current_weekday + 7) % 7;768 if ( 0 === $days_to_first ) {769 // Already on first_selected_day, advance to next week.770 $days_to_first = 7;771 }772 }773 } else {774 // Normal case: advance to next occurrence of first selected day.775 $days_to_first = ($first_selected_day - $current_weekday + 7) % 7;776 if ( 0 === $days_to_first ) {777 // Same day, advance to next week.778 $days_to_first = 7;779 }780 }781 $cursor = strtotime( '+' . $days_to_first . ' days', $cursor );782 // Now find the next valid (non-blocked) selected day.783 $max_iterations = 14;784 // Safety limit.785 $iterations = 0;786 while ( $iterations < $max_iterations ) {787 $weekday = (int) gmdate( 'w', $cursor );788 $ymd = gmdate( 'Y-m-d', $cursor );789 // Skip blocked dates.790 if ( isset( $blocked[$ymd] ) ) {791 $cursor = strtotime( '+1 day', $cursor );792 $iterations++;793 continue;794 }795 // Found a selected day.796 if ( in_array( $weekday, $days, true ) ) {797 return $cursor;798 }799 $cursor = strtotime( '+1 day', $cursor );800 $iterations++;801 }802 return $cursor;803 669 } 804 670 … … 1340 1206 <tr> 1341 1207 <th scope="row"><?php 1208 esc_html_e( 'Events per timeslot mode', 'auto-fixture-generator-for-sportspress' ); 1209 ?></th> 1210 <td> 1211 <select name="schedule[events_mode]" id="afgsp_events_mode" <?php 1212 echo ( !afgsp_fs()->can_use_premium_code__premium_only() ? 'disabled' : '' ); 1213 ?>> 1214 <option value="auto" selected><?php 1215 esc_html_e( 'AUTO', 'auto-fixture-generator-for-sportspress' ); 1216 ?></option> 1217 <?php 1218 ?> 1219 </select> 1220 <p class="description"> 1221 <?php 1222 ?> 1223 <?php 1224 esc_html_e( 'Events per slot are calculated automatically. Upgrade to premium for manual control.', 'auto-fixture-generator-for-sportspress' ); 1225 ?> 1226 <a href="<?php 1227 echo esc_url( 'https://savvasha.com/auto-fixture-generator-for-sportspress-premium/' ); 1228 ?>" target="_blank" style="font-weight: 600;"> 1229 <?php 1230 esc_html_e( 'Upgrade to premium version now!', 'auto-fixture-generator-for-sportspress' ); 1231 ?> → 1232 </a> 1233 <?php 1234 ?> 1235 </p> 1236 </td> 1237 </tr> 1238 <tr> 1239 <th scope="row"><?php 1342 1240 esc_html_e( 'Time slots per match day', 'auto-fixture-generator-for-sportspress' ); 1343 1241 ?></th> 1344 1242 <td> 1345 1243 <div id="afgsp_time_slots"> 1346 <div><input type="time" name="schedule[time_slots][]" value="20:00" /> 1347 <?php 1348 ?></div> 1244 <div> 1245 <input type="time" name="schedule[time_slots][]" value="20:00" /> 1246 <input type="number" name="schedule[events_per_slot][]" class="afgsp-events-per-slot small-text" min="1" value="" disabled /> 1247 <?php 1248 ?> 1249 </div> 1349 1250 <?php 1350 1251 ?> -
auto-fixture-generator-for-sportspress/tags/1.5/includes/class-afgsp-debug.php
r3437103 r3444584 122 122 123 123 // Events per Timeslot. 124 self::log_line( 'Events per Timeslot: Not implemented yet' ); 124 $team_ids = isset( $context['team_ids'] ) && is_array( $context['team_ids'] ) ? $context['team_ids'] : array(); 125 $events_mode = isset( $schedule['events_mode'] ) ? (string) $schedule['events_mode'] : 'auto'; 126 127 if ( 'manual' === $events_mode && ! empty( $schedule['events_per_slot'] ) && is_array( $schedule['events_per_slot'] ) ) { 128 $manual_values = implode( ', ', array_map( 'intval', $schedule['events_per_slot'] ) ); 129 self::log_line( 'Events per Timeslot: ' . $manual_values . ' (MANUAL mode)' ); 130 } else { 131 $events_per_slot = afgsp_calculate_events_per_timeslot( count( $team_ids ), count( $days ), count( $time_slots ) ); 132 self::log_line( 'Events per Timeslot: ' . $events_per_slot . ' (AUTO mode)' ); 133 } 125 134 126 135 // Blocked Dates. … … 130 139 self::log_line( '' ); 131 140 132 // Selected Teams. 133 $team_ids = isset( $context['team_ids'] ) && is_array( $context['team_ids'] ) ? $context['team_ids'] : array(); 141 // Selected Teams (already extracted above for events per slot calculation). 134 142 self::log_line( '--- Selected Teams (' . count( $team_ids ) . ') ---' ); 135 143 foreach ( $team_ids as $team_id ) { … … 146 154 $algorithm_label = isset( $algorithms[ $algorithm_slug ]['label'] ) ? $algorithms[ $algorithm_slug ]['label'] : ucfirst( $algorithm_slug ); 147 155 self::log_line( 'Selected Algorithm: ' . $algorithm_label . ' (' . $algorithm_slug . ')' ); 156 157 // Algorithm-specific options. 158 $algorithm_options = isset( $context['algorithm_options'] ) && is_array( $context['algorithm_options'] ) ? $context['algorithm_options'] : array(); 159 if ( 'fixed-week-season' === $algorithm_slug && isset( $algorithm_options['season_weeks'] ) ) { 160 self::log_line( 'Season Weeks: ' . (int) $algorithm_options['season_weeks'] ); 161 } 148 162 self::log_line( '' ); 149 163 -
auto-fixture-generator-for-sportspress/tags/1.5/includes/class-afgsp-generator.php
r3388275 r3444584 21 21 class AFGSP_Generator { 22 22 /** 23 * Prepare fixtures with scheduling applied. 24 * 25 * This method calls the algorithm, applies scheduling (dates, times, gameweeks), 26 * and returns the prepared fixtures array. Used by both normal and dry-run modes 27 * to ensure consistent scheduling logic. 28 * 29 * @param array $raw_fixtures Raw fixtures from algorithm (array of home/away/meta). 30 * @param array $schedule Schedule settings (start_date, days, time_slots, blocked_dates, gameweek_name). 31 * @param int $teams_count Number of teams (for calculating matches per round). 32 * @param string $algorithm Algorithm slug (used to determine duplicate handling). 33 * @return array Prepared fixtures with datetime and gameweek assigned. 34 */ 35 public static function prepare_fixtures( 36 array $raw_fixtures, 37 array $schedule, 38 int $teams_count, 39 string $algorithm = '' 40 ) : array { 41 // For round-robin: matches per round = floor(n/2) (when n is odd, one team has bye). 42 $matches_per_round = max( 1, (int) floor( $teams_count / 2 ) ); 43 // Parse schedule settings. 44 $start_date = ( isset( $schedule['start_date'] ) ? (string) $schedule['start_date'] : '' ); 45 $days = ( isset( $schedule['days'] ) && is_array( $schedule['days'] ) ? array_map( 'intval', (array) $schedule['days'] ) : array() ); 46 $time_slots = ( isset( $schedule['time_slots'] ) && is_array( $schedule['time_slots'] ) ? array_values( array_filter( (array) $schedule['time_slots'] ) ) : array() ); 47 $blocked = ( isset( $schedule['blocked_dates'] ) && is_array( $schedule['blocked_dates'] ) ? array_values( array_filter( (array) $schedule['blocked_dates'] ) ) : array() ); 48 $blocked_map = array_fill_keys( $blocked, true ); 49 $gameweek_name = ( isset( $schedule['gameweek_name'] ) ? (string) $schedule['gameweek_name'] : 'Gameweek %No%' ); 50 // Parse events mode settings (premium feature). 51 $events_mode = ( isset( $schedule['events_mode'] ) ? (string) $schedule['events_mode'] : 'auto' ); 52 $events_per_slot_limits = ( isset( $schedule['events_per_slot'] ) && is_array( $schedule['events_per_slot'] ) ? array_map( 'intval', $schedule['events_per_slot'] ) : array() ); 53 // Initialize scheduling state. 54 $slot_index = 0; 55 $matches_on_current_day = 0; 56 $last_gameweek = 0; 57 // Track events assigned per slot for MANUAL mode (reset daily). 58 $slots_count = ( !empty( $time_slots ) ? count( $time_slots ) : 1 ); 59 $events_assigned_to_slots = array_fill( 0, $slots_count, 0 ); 60 // Calculate how many matches should be scheduled per day (AUTO mode). 61 // This ensures fixtures are distributed across available days in a gameweek. 62 $matches_per_day = ( count( $days ) > 0 ? (int) ceil( $matches_per_round / count( $days ) ) : $matches_per_round ); 63 $cursor = ( $start_date ? strtotime( $start_date . ' 00:00:00' ) : false ); 64 if ( $cursor && empty( $days ) ) { 65 // Default to weekend if start date provided but no specific days selected. 66 $days = array(6, 0); 67 // Saturday, Sunday. 68 } 69 $prepared_fixtures = array(); 70 $seen_pairs = array(); 71 $fixture_index = 0; 72 foreach ( $raw_fixtures as $fixture ) { 73 $home_id = ( isset( $fixture['home'] ) ? (int) $fixture['home'] : 0 ); 74 $away_id = ( isset( $fixture['away'] ) ? (int) $fixture['away'] : 0 ); 75 $extra_meta = ( isset( $fixture['meta'] ) && is_array( $fixture['meta'] ) ? $fixture['meta'] : array() ); 76 // Skip invalid fixtures. 77 if ( $home_id <= 0 || $away_id <= 0 || $home_id === $away_id ) { 78 continue; 79 } 80 // Suppress duplicates within a single generation run. 81 // Skip duplicate check for fixed-week-season as it allows teams to play multiple times. 82 $skip_duplicate_check = 'fixed-week-season' === $algorithm; 83 if ( !$skip_duplicate_check ) { 84 $pair_key = $home_id . '_' . $away_id; 85 if ( isset( $seen_pairs[$pair_key] ) ) { 86 continue; 87 } 88 $seen_pairs[$pair_key] = true; 89 } 90 // Calculate gameweek based on fixture index and round size. 91 $current_gameweek = (int) floor( $fixture_index / $matches_per_round ) + 1; 92 $gameweek_display_name = str_replace( '%No%', (string) $current_gameweek, $gameweek_name ); 93 $extra_meta['sp_day'] = $gameweek_display_name; 94 // If gameweek changed, advance cursor to next week and reset counters. 95 if ( $last_gameweek > 0 && $current_gameweek > $last_gameweek ) { 96 $slot_index = 0; 97 $matches_on_current_day = 0; 98 $events_assigned_to_slots = array_fill( 0, $slots_count, 0 ); 99 $cursor = self::advance_cursor_to_next_week( $cursor, $days, $blocked_map ); 100 } 101 // Assign datetime if not already set by algorithm. 102 if ( !isset( $extra_meta['datetime'] ) && $cursor ) { 103 $assigned = false; 104 while ( !$assigned ) { 105 $weekday = (int) gmdate( 'w', $cursor ); 106 $ymd = gmdate( 'Y-m-d', $cursor ); 107 // Skip blocked dates. 108 if ( isset( $blocked_map[$ymd] ) ) { 109 $cursor = strtotime( '+1 day', $cursor ); 110 continue; 111 } 112 // Check if current day is a selected match day. 113 if ( in_array( $weekday, $days, true ) ) { 114 if ( 'manual' === $events_mode && !empty( $events_per_slot_limits ) && !empty( $time_slots ) ) { 115 // MANUAL mode: find a slot that hasn't reached its limit. 116 $slot_found = false; 117 for ($i = 0; $i < $slots_count; $i++) { 118 $try_slot = ($slot_index + $i) % $slots_count; 119 $limit = ( isset( $events_per_slot_limits[$try_slot] ) ? (int) $events_per_slot_limits[$try_slot] : PHP_INT_MAX ); 120 if ( $events_assigned_to_slots[$try_slot] < $limit ) { 121 $slot = $time_slots[$try_slot]; 122 $extra_meta['datetime'] = $ymd . ' ' . $slot; 123 $events_assigned_to_slots[$try_slot]++; 124 $slot_index = $try_slot + 1; 125 $slot_found = true; 126 $matches_on_current_day++; 127 $assigned = true; 128 break; 129 } 130 } 131 if ( !$slot_found ) { 132 // All slots full for today, advance to next day. 133 $cursor = strtotime( '+1 day', $cursor ); 134 $matches_on_current_day = 0; 135 $events_assigned_to_slots = array_fill( 0, $slots_count, 0 ); 136 continue; 137 } 138 } else { 139 // AUTO mode: cycle through available slots. 140 $slot = ( !empty( $time_slots ) ? $time_slots[$slot_index % $slots_count] : '15:00' ); 141 $extra_meta['datetime'] = $ymd . ' ' . $slot; 142 $slot_index++; 143 $matches_on_current_day++; 144 $assigned = true; 145 // Advance cursor when daily quota is met. 146 if ( $matches_on_current_day >= $matches_per_day ) { 147 $cursor = strtotime( '+1 day', $cursor ); 148 $matches_on_current_day = 0; 149 $events_assigned_to_slots = array_fill( 0, $slots_count, 0 ); 150 } 151 } 152 } else { 153 $cursor = strtotime( '+1 day', $cursor ); 154 } 155 } 156 } 157 $prepared_fixtures[] = array( 158 'home_id' => $home_id, 159 'away_id' => $away_id, 160 'extra_meta' => $extra_meta, 161 'gameweek' => $current_gameweek, 162 ); 163 $last_gameweek = $current_gameweek; 164 $fixture_index++; 165 } 166 return $prepared_fixtures; 167 } 168 169 /** 170 * Advance cursor to the next week's first selected day. 171 * 172 * @param int $cursor Current timestamp. 173 * @param array $days Selected days of the week (0=Sun, 6=Sat). 174 * @param array $blocked_map Map of blocked dates. 175 * @return int New cursor timestamp. 176 */ 177 private static function advance_cursor_to_next_week( int $cursor, array $days, array $blocked_map ) : int { 178 if ( empty( $days ) ) { 179 return $cursor; 180 } 181 // Move to next day first. 182 $cursor = strtotime( '+1 day', $cursor ); 183 // Find the first day in the gameweek structure (not numeric min). 184 // Use the sorted gameweek days to get the correct starting day. 185 // For example, with days [6, 0] (Sat, Sun), Saturday is the first day, not Sunday. 186 $sorted_days = \AFGSP\afgsp_sort_gameweek_days( $days ); 187 $first_day = $sorted_days[0]; 188 // Advance until we reach the next occurrence of the first gameweek day. 189 $max_iterations = 14; 190 // Safety limit. 191 $iterations = 0; 192 while ( $iterations < $max_iterations ) { 193 $weekday = (int) gmdate( 'w', $cursor ); 194 $ymd = gmdate( 'Y-m-d', $cursor ); 195 // Skip blocked dates. 196 if ( isset( $blocked_map[$ymd] ) ) { 197 $cursor = strtotime( '+1 day', $cursor ); 198 $iterations++; 199 continue; 200 } 201 // Check if we've reached the first day of the next gameweek. 202 if ( $weekday === $first_day ) { 203 return $cursor; 204 } 205 $cursor = strtotime( '+1 day', $cursor ); 206 $iterations++; 207 } 208 return $cursor; 209 } 210 211 /** 23 212 * Run generation process. 24 213 * 25 * @param int $league_term_id League term ID.26 * @param int $season_term_id Season term ID.27 * @param string $algorithm_slug Algorithm slug.28 * @param array $options Algorithm options.214 * @param int $league_term_id League term ID. 215 * @param int $season_term_id Season term ID. 216 * @param string $algorithm_slug Algorithm slug. 217 * @param array $options Algorithm options. 29 218 * @param array $selected_team_ids Optional selected team IDs. 219 * @param bool $dry_run If true, skip event creation and return fixture data only. 220 * @return array Result with created count, errors, messages, and fixtures (for dry run). 30 221 */ 31 222 public static function run( … … 34 225 string $algorithm_slug, 35 226 array $options = array(), 36 array $selected_team_ids = array() 227 array $selected_team_ids = array(), 228 bool $dry_run = false 37 229 ) : array { 38 230 $result = array( … … 40 232 'errors' => array(), 41 233 'messages' => array(), 234 'fixtures' => array(), 42 235 ); 43 236 // Load teams for the selected league & season if not explicitly provided. … … 65 258 } 66 259 try { 67 $ fixtures = call_user_func( $callable, $team_ids, $options );68 if ( !is_array( $ fixtures ) ) {260 $raw_fixtures = call_user_func( $callable, $team_ids, $options ); 261 if ( !is_array( $raw_fixtures ) ) { 69 262 $result['errors'][] = __( 'Algorithm returned an invalid response.', 'auto-fixture-generator-for-sportspress' ); 70 263 return $result; … … 78 271 return $result; 79 272 } 80 // Get algorithm info to determine rounds/gameweeks. 81 $algorithm_info = AFGSP_Registry::get_algorithm_info( $algorithm_slug ); 82 $total_rounds = ( isset( $algorithm_info['rounds'] ) ? (int) $algorithm_info['rounds'] : 1 ); 83 // For single round-robin: rounds = n-1, matches per round = floor(n/2) (when n is odd, one team has bye). 84 $n = count( $team_ids ); 85 $matches_per_round = (int) floor( $n / 2 ); 86 // Apply scheduling constraints: assign dates/times if not already set by the algorithm. 273 // Extract schedule from options. 87 274 $schedule = ( isset( $options['schedule'] ) && is_array( $options['schedule'] ) ? (array) $options['schedule'] : array() ); 88 $start_date = ( isset( $schedule['start_date'] ) ? (string) $schedule['start_date'] : '' ); 89 $days = ( isset( $schedule['days'] ) && is_array( $schedule['days'] ) ? (array) $schedule['days'] : array() ); 90 $time_slots = ( isset( $schedule['time_slots'] ) && is_array( $schedule['time_slots'] ) ? array_values( array_filter( (array) $schedule['time_slots'] ) ) : array() ); 91 $blocked = ( isset( $schedule['blocked_dates'] ) && is_array( $schedule['blocked_dates'] ) ? array_values( array_filter( (array) $schedule['blocked_dates'] ) ) : array() ); 92 $blocked_map = array_fill_keys( $blocked, true ); 93 $gameweek_name = ( isset( $schedule['gameweek_name'] ) ? (string) $schedule['gameweek_name'] : 'Gameweek %No%' ); 94 $round_robin_time_index = 0; 95 // Calculate how many matches should be scheduled per day within a gameweek. 96 $matches_per_day = ( count( $days ) > 0 ? (int) ceil( $matches_per_round / count( $days ) ) : $matches_per_round ); 97 $matches_on_current_day = 0; 98 $cursor = ( $start_date ? strtotime( $start_date . ' 00:00:00' ) : false ); 99 if ( $cursor && empty( $days ) ) { 100 // Default to weekend if start date provided but no specific days selected. 101 $days = array(6, 0); 102 // Saturday, Sunday. 103 } 104 // First pass: assign dates to all fixtures. 105 $fixtures_with_dates = array(); 106 $seen_pairs_in_run = array(); 107 foreach ( $fixtures as $fixture ) { 108 $home_id = ( isset( $fixture['home'] ) ? (int) $fixture['home'] : 0 ); 109 $away_id = ( isset( $fixture['away'] ) ? (int) $fixture['away'] : 0 ); 110 $extra_meta = ( isset( $fixture['meta'] ) && is_array( $fixture['meta'] ) ? $fixture['meta'] : array() ); 111 // Suppress duplicates within a single generation run to avoid unnecessary duplicate checks/notices. 112 $pair_key_in_run = $home_id . '_' . $away_id; 113 if ( isset( $seen_pairs_in_run[$pair_key_in_run] ) ) { 114 continue; 115 } 116 $seen_pairs_in_run[$pair_key_in_run] = true; 117 // If algorithm didn't provide datetime, assign based on scheduling settings. 118 if ( !isset( $extra_meta['datetime'] ) && $cursor ) { 119 $assigned = false; 120 while ( !$assigned ) { 121 $weekday = (int) gmdate( 'w', $cursor ); 122 $ymd = gmdate( 'Y-m-d', $cursor ); 123 if ( isset( $blocked_map[$ymd] ) ) { 124 $cursor = strtotime( '+1 day', $cursor ); 125 continue; 126 } 127 if ( in_array( $weekday, $days, true ) ) { 128 $slot = ( !empty( $time_slots ) ? $time_slots[$round_robin_time_index % count( $time_slots )] : '15:00' ); 129 $extra_meta['datetime'] = $ymd . ' ' . $slot; 130 $round_robin_time_index++; 131 $matches_on_current_day++; 132 $assigned = true; 133 // Advance cursor only when daily quota is met. 134 if ( $matches_on_current_day >= $matches_per_day ) { 135 $cursor = strtotime( '+1 day', $cursor ); 136 $matches_on_current_day = 0; 137 } 138 } else { 139 $cursor = strtotime( '+1 day', $cursor ); 140 } 275 // Prepare fixtures with scheduling applied (centralized logic). 276 $prepared_fixtures = self::prepare_fixtures( 277 $raw_fixtures, 278 $schedule, 279 count( $team_ids ), 280 $algorithm_slug 281 ); 282 // Process fixtures: create events or collect for dry run. 283 foreach ( $prepared_fixtures as $fixture_data ) { 284 if ( $dry_run ) { 285 // Dry run: collect fixture data without creating events. 286 $result['fixtures'][] = $fixture_data; 287 $result['created']++; 288 $result['messages'][] = sprintf( 289 '%1$s vs %2$s (%3$s)', 290 get_the_title( $fixture_data['home_id'] ), 291 get_the_title( $fixture_data['away_id'] ), 292 $fixture_data['extra_meta']['sp_day'] ?? '' 293 ); 294 } else { 295 // Normal mode: create events in database. 296 $created = \AFGSP\afgsp_create_event( 297 $fixture_data['home_id'], 298 $fixture_data['away_id'], 299 $league_term_id, 300 $season_term_id, 301 $fixture_data['extra_meta'] 302 ); 303 if ( is_wp_error( $created ) ) { 304 $result['errors'][] = $created->get_error_message(); 305 continue; 141 306 } 142 } 143 if ( $home_id <= 0 || $away_id <= 0 || $home_id === $away_id ) { 144 $result['errors'][] = __( 'Invalid fixture data encountered and skipped.', 'auto-fixture-generator-for-sportspress' ); 145 continue; 146 } 147 $fixtures_with_dates[] = array( 148 'home_id' => $home_id, 149 'away_id' => $away_id, 150 'extra_meta' => $extra_meta, 151 ); 152 } 153 // Group fixtures by algorithm rounds (gameweeks). 154 $fixtures_by_gameweek = array(); 155 $fixtures_per_gameweek = (int) $matches_per_round; 156 $fixture_count = 0; 157 foreach ( $fixtures_with_dates as $fixture_data ) { 158 // Determine gameweek based on algorithm structure, not dates. 159 $gameweek_number = (int) (floor( $fixture_count / $fixtures_per_gameweek ) + 1); 160 // Add gameweek name to meta. 161 $gameweek_display_name = str_replace( '%No%', (string) $gameweek_number, $gameweek_name ); 162 $fixture_data['extra_meta']['sp_day'] = $gameweek_display_name; 163 $fixtures_by_gameweek[] = array( 164 'home_id' => $fixture_data['home_id'], 165 'away_id' => $fixture_data['away_id'], 166 'extra_meta' => $fixture_data['extra_meta'], 167 'gameweek' => $gameweek_number, 168 ); 169 $fixture_count++; 170 } 171 // Second pass: create events. 172 foreach ( $fixtures_by_gameweek as $fixture_data ) { 173 $created = \AFGSP\afgsp_create_event( 174 $fixture_data['home_id'], 175 $fixture_data['away_id'], 176 $league_term_id, 177 $season_term_id, 178 $fixture_data['extra_meta'] 179 ); 180 if ( is_wp_error( $created ) ) { 181 $result['errors'][] = $created->get_error_message(); 182 continue; 183 } 184 $result['created']++; 185 $result['messages'][] = sprintf( 186 '%1$s vs %2$s (%3$s)', 187 get_the_title( $fixture_data['home_id'] ), 188 get_the_title( $fixture_data['away_id'] ), 189 $fixture_data['extra_meta']['sp_day'] ?? '' 190 ); 307 $result['created']++; 308 $result['messages'][] = sprintf( 309 '%1$s vs %2$s (%3$s)', 310 get_the_title( $fixture_data['home_id'] ), 311 get_the_title( $fixture_data['away_id'] ), 312 $fixture_data['extra_meta']['sp_day'] ?? '' 313 ); 314 } 191 315 } 192 316 return $result; -
auto-fixture-generator-for-sportspress/tags/1.5/includes/helpers.php
r3437103 r3444584 398 398 399 399 /** 400 * Calculate the number of events (fixtures) per time slot in AUTO mode. 401 * 402 * This centralizes the scheduling calculation used by both the generator 403 * and debug logging to ensure consistent results. 404 * 405 * @param int $teams_count Number of teams. 406 * @param int $days_count Number of selected match days per gameweek. 407 * @param int $slots_count Number of time slots per day. 408 * @return int Number of fixtures per time slot. 409 */ 410 function afgsp_calculate_events_per_timeslot( int $teams_count, int $days_count, int $slots_count ): int { 411 $matches_per_round = max( 1, (int) floor( $teams_count / 2 ) ); 412 $days_count = max( 1, $days_count ); 413 $slots_count = max( 1, $slots_count ); 414 $matches_per_day = (int) ceil( $matches_per_round / $days_count ); 415 $events_per_slot = (int) ceil( $matches_per_day / $slots_count ); 416 417 return $events_per_slot; 418 } 419 420 /** 400 421 * Sort gameweek days in proper chronological order. 401 422 * -
auto-fixture-generator-for-sportspress/tags/1.5/readme.txt
r3437103 r3444584 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1. 47 Stable tag: 1.5 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl.html … … 69 69 3. Generator screen with algorithm selection and options (part3) 70 70 4. Succesfull creation of events 71 5. Debug / Dry Run mode available if needed. 71 72 72 73 == Changelog == 74 75 = 1.5 = 76 * NEW: Events per timeslot mode - AUTO calculates optimal distribution, MANUAL lets you set custom limits per slot (Premium version only) 77 * NEW: Algorithm-specific options now displayed in dry run debug log (e.g. Season Weeks for Fixed Week Season) 78 * FIX: Dry run mode now uses the same scheduling logic as normal mode, ensuring consistent fixture results. 79 * FIX: Fixed Week Season algorithm now generates correct number of gameweeks (rematches no longer incorrectly removed) 80 * FIX: Gameweeks no longer share dates - each gameweek properly advances to the next week period 73 81 74 82 = 1.4 = -
auto-fixture-generator-for-sportspress/trunk/assets/admin.css
r3388275 r3444584 101 101 } 102 102 103 /* Events per timeslot number inputs */ 104 .afgsp-events-per-slot { 105 width: 60px; 106 margin-left: 8px; 107 text-align: center; 108 } 109 110 .afgsp-events-per-slot:disabled { 111 background-color: #f5f5f5; 112 color: #666; 113 } 114 115 /* Time slots container layout */ 116 #afgsp_time_slots > div { 117 display: flex; 118 align-items: center; 119 margin-bottom: 5px; 120 flex-wrap: wrap; 121 gap: 5px; 122 } 123 124 #afgsp_time_slots input[type="time"] { 125 width: 120px; 126 } 127 103 128 /* Responsive design */ 104 129 @media (max-width: 768px) { … … 115 140 height: 25px; 116 141 } 142 143 .afgsp-events-per-slot { 144 width: 50px; 145 margin-left: 5px; 146 } 117 147 } -
auto-fixture-generator-for-sportspress/trunk/assets/admin.js
r3435431 r3444584 55 55 56 56 return matches; 57 } 58 59 /** 60 * Get the count of selected teams. 61 * For premium users, counts checked checkboxes. 62 * For free users, returns the cached count from AJAX response. 63 * 64 * @return {number} Number of teams. 65 */ 66 function getTeamsCount() { 67 // For premium users with team checkboxes visible 68 if ($('#afgsp_teams').is(':visible') && $('#afgsp_teams input:checked').length > 0) { 69 return $('#afgsp_teams input:checked').length; 70 } 71 // For free users, use cached count if available 72 if (window.AFGSP_ADMIN && typeof window.AFGSP_ADMIN.teamsCount === 'number') { 73 return window.AFGSP_ADMIN.teamsCount; 74 } 75 return 0; 76 } 77 78 /** 79 * Get the count of selected days in the gameweek builder. 80 * 81 * @return {number} Number of selected days. 82 */ 83 function getSelectedDaysCount() { 84 var count = $('#afgsp_gameweek_builder input[type="checkbox"]:checked').length; 85 return count > 0 ? count : 2; // Default to 2 (Sat, Sun) 86 } 87 88 /** 89 * Calculate events per timeslot using the AUTO mode formula. 90 * 91 * @param {number} teamsCount Number of teams. 92 * @param {number} daysCount Number of selected match days. 93 * @param {number} slotsCount Number of time slots. 94 * @return {number} Events per timeslot. 95 */ 96 function calculateEventsPerSlot(teamsCount, daysCount, slotsCount) { 97 teamsCount = Math.max(2, parseInt(teamsCount) || 2); 98 daysCount = Math.max(1, parseInt(daysCount) || 1); 99 slotsCount = Math.max(1, parseInt(slotsCount) || 1); 100 101 var matchesPerRound = Math.floor(teamsCount / 2); 102 var matchesPerDay = Math.ceil(matchesPerRound / daysCount); 103 var eventsPerSlot = Math.ceil(matchesPerDay / slotsCount); 104 105 return Math.max(1, eventsPerSlot); 106 } 107 108 /** 109 * Update all events per slot input fields with the calculated auto value. 110 * In AUTO mode, inputs are disabled and show the calculated value. 111 * In MANUAL mode (premium only), inputs are enabled for custom values. 112 */ 113 function updateEventsPerSlotInputs() { 114 var teamsCount = getTeamsCount(); 115 var daysCount = getSelectedDaysCount(); 116 var slotsCount = $('#afgsp_time_slots input[type="time"]').length || 1; 117 var autoValue = calculateEventsPerSlot(teamsCount, daysCount, slotsCount); 118 119 var mode = $('#afgsp_events_mode').val() || 'auto'; 120 var isPremium = window.AFGSP_ADMIN && window.AFGSP_ADMIN.isPremium; 121 122 $('.afgsp-events-per-slot').each(function() { 123 // In AUTO mode or for free users, always show auto value and disable 124 if (mode === 'auto' || !isPremium) { 125 $(this).val(autoValue).prop('disabled', true); 126 } else if (mode === 'manual' && isPremium) { 127 // In MANUAL mode for premium, enable but keep current value if set 128 if (!$(this).val() || $(this).val() === '0') { 129 $(this).val(autoValue); 130 } 131 $(this).prop('disabled', false); 132 } 133 }); 57 134 } 58 135 … … 433 510 $(document).on('change', '#afgsp_gameweek_builder input[type="checkbox"]', function() { 434 511 updateGameweekPreview(); 512 updateEventsPerSlotInputs(); 513 }); 514 515 // Update events per slot when time slots are added/removed 516 $(document).on('DOMNodeInserted DOMNodeRemoved', '#afgsp_time_slots', function() { 517 setTimeout(updateEventsPerSlotInputs, 50); 435 518 }); 436 519 … … 446 529 updateEventsDescription(); 447 530 updateTeamsDescriptionForFree(); 531 updateEventsPerSlotInputs(); 532 }); 533 534 // Update events per slot when league/season changes (for free users to get team count) 535 $(document).on('change', '#afgsp_league, #afgsp_season', function () { 536 var leagueId = $('#afgsp_league').val(); 537 var seasonId = $('#afgsp_season').val(); 538 539 if (leagueId && seasonId) { 540 $.getJSON((window.AFGSP_ADMIN && window.AFGSP_ADMIN.ajaxUrl) || '', { 541 action: 'afgsp_get_teams', 542 league: leagueId, 543 season: seasonId, 544 nonce: (window.AFGSP_ADMIN && window.AFGSP_ADMIN.nonce) || '' 545 }).done(function (resp) { 546 var teams = (resp && resp.data && Array.isArray(resp.data.teams) && resp.data.teams) || []; 547 // Cache the teams count for free users 548 if (window.AFGSP_ADMIN) { 549 window.AFGSP_ADMIN.teamsCount = teams.length; 550 } 551 updateEventsPerSlotInputs(); 552 }); 553 } 554 }); 555 556 // Update events per slot when teams selection changes (for premium users) 557 $(document).on('change', '#afgsp_teams input[type="checkbox"]', function () { 558 updateEventsPerSlotInputs(); 448 559 }); 449 560 -
auto-fixture-generator-for-sportspress/trunk/auto-fixture-generator-for-sportspress.php
r3437103 r3444584 4 4 * Plugin Name: Auto Fixture Generator for SportsPress 5 5 * Description: Automatically generate fixtures (events) for a selected SportsPress league and season using pluggable scheduling algorithms. 6 * Version: 1. 46 * Version: 1.5 7 7 * Author: Savvas 8 8 * Author URI: https://savvasha.com … … 70 70 */ 71 71 if ( !defined( 'AFGSP_VERSION' ) ) { 72 define( 'AFGSP_VERSION', '1. 4' );72 define( 'AFGSP_VERSION', '1.5' ); 73 73 } 74 74 if ( !defined( 'AFGSP_PLUGIN_FILE' ) ) { -
auto-fixture-generator-for-sportspress/trunk/includes/class-afgsp-admin.php
r3437103 r3444584 202 202 }, (array) $raw_schedule['blocked_dates'] ) ) ) : array() ); 203 203 $blocked_dates = array(); 204 // Process events mode and events per slot (premium feature). 205 $events_mode = ( isset( $raw_schedule['events_mode'] ) ? sanitize_text_field( (string) $raw_schedule['events_mode'] ) : 'auto' ); 206 $events_per_slot = ( isset( $raw_schedule['events_per_slot'] ) && is_array( $raw_schedule['events_per_slot'] ) ? array_map( 'absint', $raw_schedule['events_per_slot'] ) : array() ); 207 $events_mode = 'auto'; 208 $events_per_slot = array(); 204 209 $schedule = array( 205 'start_date' => ( isset( $raw_schedule['start_date'] ) ? sanitize_text_field( (string) $raw_schedule['start_date'] ) : '' ), 206 'days' => ( isset( $raw_schedule['days'] ) && is_array( $raw_schedule['days'] ) ? array_values( array_unique( array_map( 'intval', (array) $raw_schedule['days'] ) ) ) : array(6, 0) ), 207 'time_slots' => $time_slots, 208 'blocked_dates' => $blocked_dates, 209 'gameweek_name' => ( isset( $raw_schedule['gameweek_name'] ) ? sanitize_text_field( (string) $raw_schedule['gameweek_name'] ) : 'Gameweek %No%' ), 210 'start_date' => ( isset( $raw_schedule['start_date'] ) ? sanitize_text_field( (string) $raw_schedule['start_date'] ) : '' ), 211 'days' => ( isset( $raw_schedule['days'] ) && is_array( $raw_schedule['days'] ) ? array_values( array_unique( array_map( 'intval', (array) $raw_schedule['days'] ) ) ) : array(6, 0) ), 212 'time_slots' => $time_slots, 213 'blocked_dates' => $blocked_dates, 214 'gameweek_name' => ( isset( $raw_schedule['gameweek_name'] ) ? sanitize_text_field( (string) $raw_schedule['gameweek_name'] ) : 'Gameweek %No%' ), 215 'events_mode' => $events_mode, 216 'events_per_slot' => $events_per_slot, 210 217 ); 211 218 if ( empty( $schedule['start_date'] ) ) { … … 225 232 } 226 233 try { 227 $ fixtures = call_user_func( $callable, $team_ids, array(234 $raw_fixtures = call_user_func( $callable, $team_ids, array( 228 235 'schedule' => $schedule, 229 236 ) + $options ); 230 if ( !is_array( $ fixtures ) ) {237 if ( !is_array( $raw_fixtures ) ) { 231 238 wp_send_json_error( array( 232 239 'message' => __( 'Algorithm returned an invalid response.', 'auto-fixture-generator-for-sportspress' ), … … 238 245 ), 500 ); 239 246 } 240 $total = count( $fixtures ); 247 // Apply scheduling using centralized prepare_fixtures method. 248 // This ensures both normal mode and dry-run mode use identical scheduling logic. 249 $prepared_fixtures = AFGSP_Generator::prepare_fixtures( 250 $raw_fixtures, 251 $schedule, 252 count( $team_ids ), 253 $algorithm 254 ); 255 $total = count( $prepared_fixtures ); 241 256 if ( $total <= 0 ) { 242 257 wp_send_json_error( array( … … 252 267 'season_id' => $season_id, 253 268 'algorithm' => $algorithm, 254 'fixtures' => array_values( $ fixtures ),269 'fixtures' => array_values( $prepared_fixtures ), 255 270 'next_index' => 0, 256 271 'total' => $total, … … 259 274 'gameweeks' => array(), 260 275 'schedule' => $schedule, 261 'cursor' => strtotime( $schedule['start_date'] . ' 00:00:00' ),262 'blocked' => array_fill_keys( $schedule['blocked_dates'], true ),263 'days' => $schedule['days'],264 'time_slots' => $schedule['time_slots'],265 'gameweek_name' => $schedule['gameweek_name'],266 'slot_index' => 0,267 276 'round_size' => max( 1, $round_size ), 268 277 'create_calendar' => !empty( $options['create_calendar'] ), … … 272 281 'shuffle_teams' => !empty( $options['shuffle_teams'] ), 273 282 'no_consecutive_away' => !empty( $options['no_consecutive_away'] ), 283 'algorithm_options' => $options, 274 284 'messages' => array(), 275 285 'created_entities' => false, … … 305 315 ), 400 ); 306 316 } 317 // Extract state - fixtures are now pre-scheduled with datetime and gameweek. 307 318 $fixtures = ( isset( $state['fixtures'] ) ? (array) $state['fixtures'] : array() ); 308 319 $next_index = (int) ($state['next_index'] ?? 0); … … 313 324 $league_id = (int) ($state['league_id'] ?? 0); 314 325 $season_id = (int) ($state['season_id'] ?? 0); 315 $cursor = (int) ($state['cursor'] ?? 0);316 $days = ( isset( $state['days'] ) ? array_map( 'intval', (array) $state['days'] ) : array() );317 $time_slots = ( isset( $state['time_slots'] ) ? array_values( (array) $state['time_slots'] ) : array() );318 $gameweek_name = ( isset( $state['gameweek_name'] ) ? (string) $state['gameweek_name'] : 'Gameweek %No%' );319 $slot_index = (int) ($state['slot_index'] ?? 0);320 $blocked = ( isset( $state['blocked'] ) && is_array( $state['blocked'] ) ? (array) $state['blocked'] : array() );321 326 $messages = ( isset( $state['messages'] ) && is_array( $state['messages'] ) ? (array) $state['messages'] : array() ); 322 327 $created_entities = !empty( $state['created_entities'] ); … … 330 335 $dry_run_fixtures = ( isset( $state['dry_run_fixtures'] ) && is_array( $state['dry_run_fixtures'] ) ? $state['dry_run_fixtures'] : array() ); 331 336 $schedule = ( isset( $state['schedule'] ) && is_array( $state['schedule'] ) ? $state['schedule'] : array() ); 332 // Gameweek tracking. 333 $current_gameweek = (int) ($state['current_gameweek'] ?? 1); 334 $current_gameweek_start = ( isset( $state['current_gameweek_start'] ) ? (int) $state['current_gameweek_start'] : null ); 335 $current_gameweek_end = ( isset( $state['current_gameweek_end'] ) ? (int) $state['current_gameweek_end'] : null ); 337 $algorithm = ( isset( $state['algorithm'] ) ? (string) $state['algorithm'] : '' ); 336 338 // If already complete, ensure we still return any completion messages. 337 339 if ( $next_index >= $total ) { … … 341 343 'league_id' => $league_id, 342 344 'season_id' => $season_id, 343 'algorithm' => $algorithm ?? '', 345 'algorithm' => $algorithm, 346 'algorithm_options' => ( isset( $state['algorithm_options'] ) ? (array) $state['algorithm_options'] : array() ), 344 347 'schedule' => $schedule, 345 348 'team_ids' => $team_ids, … … 376 379 ) ); 377 380 } 378 // Determine capacity for a single matchday: either number of time slots or algorithm round size, whichever is larger.379 $max_per_day = ( !empty( $time_slots ) ? count( $time_slots ) : 1 );380 $ max_per_day = max( $max_per_day, (int) ($state['round_size'] ?? 1));381 // Process a batch of pre-scheduled fixtures. 382 // Fixtures now have home_id, away_id, extra_meta (with datetime and sp_day), and gameweek already assigned. 383 $batch_size = (int) ($state['round_size'] ?? 4); 381 384 $processed_this_batch = 0; 382 $batch_date = '';383 385 // Track algorithm to determine if duplicate checking should be skipped. 384 $algorithm = ( isset( $state['algorithm'] ) ? (string) $state['algorithm'] : '' );385 386 $skip_duplicate_check = 'fixed-week-season' === $algorithm; 386 $last_processed_gameweek = (int) ($state['last_processed_gameweek'] ?? 0); 387 while ( $next_index < $total ) { 387 while ( $next_index < $total && $processed_this_batch < $batch_size ) { 388 388 $fixture = $fixtures[$next_index]; 389 $home_id = (int) ($fixture['home '] ?? 0);390 $away_id = (int) ($fixture['away '] ?? 0);391 $extra_meta = ( isset( $fixture[' meta'] ) && is_array( $fixture['meta'] ) ? $fixture['meta'] : array() );389 $home_id = (int) ($fixture['home_id'] ?? 0); 390 $away_id = (int) ($fixture['away_id'] ?? 0); 391 $extra_meta = ( isset( $fixture['extra_meta'] ) && is_array( $fixture['extra_meta'] ) ? $fixture['extra_meta'] : array() ); 392 392 if ( $home_id <= 0 || $away_id <= 0 || $home_id === $away_id ) { 393 393 $next_index++; 394 394 continue; 395 }396 // Calculate and assign gameweek (sp_day) based on fixture index and round size.397 $round_size = (int) ($state['round_size'] ?? 1);398 $current_gameweek = (int) floor( $next_index / $round_size ) + 1;399 $gameweek_display_name = str_replace( '%No%', (string) $current_gameweek, $gameweek_name );400 $extra_meta['sp_day'] = $gameweek_display_name;401 // If gameweek changed, advance cursor to the next week's first selected day.402 if ( $last_processed_gameweek > 0 && $current_gameweek > $last_processed_gameweek ) {403 // Reset slot index for new gameweek.404 $slot_index = 0;405 // Advance cursor to skip remaining days of current week and find next week's first selected day.406 $cursor = $this->advance_cursor_to_next_week( $cursor, $days, $blocked );407 }408 // Assign datetime if not present, mirroring generator logic.409 if ( !isset( $extra_meta['datetime'] ) && $cursor ) {410 $assigned = false;411 while ( !$assigned ) {412 $weekday = (int) gmdate( 'w', $cursor );413 $ymd = gmdate( 'Y-m-d', $cursor );414 if ( isset( $blocked[$ymd] ) ) {415 $cursor = strtotime( '+1 day', $cursor );416 continue;417 }418 if ( in_array( $weekday, $days, true ) ) {419 $slot = ( !empty( $time_slots ) ? $time_slots[$slot_index % count( $time_slots )] : '15:00' );420 $extra_meta['datetime'] = $ymd . ' ' . $slot;421 $slot_index++;422 $assigned = true;423 // Track batch date and capacity.424 if ( '' === $batch_date ) {425 $batch_date = $ymd;426 }427 $processed_this_batch++;428 // Advance cursor: next day after consuming all slots.429 if ( !empty( $time_slots ) ) {430 if ( 0 === $slot_index % count( $time_slots ) ) {431 $cursor = strtotime( '+1 day', $cursor );432 }433 } else {434 $cursor = strtotime( '+1 day', $cursor );435 }436 } else {437 $cursor = strtotime( '+1 day', $cursor );438 }439 }440 395 } 441 396 if ( $dry_run ) { … … 480 435 } 481 436 } 482 $last_processed_gameweek = $current_gameweek;483 437 $next_index++; 484 // Stop when we reach a full matchday capacity. 485 if ( $processed_this_batch >= $max_per_day ) { 486 break; 487 } 438 $processed_this_batch++; 488 439 } 489 440 $state['next_index'] = $next_index; … … 491 442 $state['duplicates'] = $duplicates; 492 443 $state['gameweeks'] = $gameweeks; 493 $state['cursor'] = $cursor;494 $state['slot_index'] = $slot_index;495 $state['current_gameweek'] = $current_gameweek;496 $state['current_gameweek_start'] = $current_gameweek_start;497 $state['current_gameweek_end'] = $current_gameweek_end;498 $state['last_processed_gameweek'] = $last_processed_gameweek;499 444 $state['messages'] = $messages; 500 445 $state['created_entities'] = $created_entities; … … 509 454 'season_id' => $season_id, 510 455 'algorithm' => $algorithm, 456 'algorithm_options' => ( isset( $state['algorithm_options'] ) ? (array) $state['algorithm_options'] : array() ), 511 457 'schedule' => $schedule, 512 458 'team_ids' => $team_ids, … … 721 667 } 722 668 return strtotime( '+' . $days_to_add . ' days', $start_date ); 723 }724 725 /**726 * Advance the cursor to the next week's first selected day.727 *728 * This ensures that when a new gameweek starts, it begins on the next calendar week's729 * first selected day, not continuing from the previous gameweek's last day.730 *731 * @param int $cursor Current cursor timestamp.732 * @param array $days Array of selected day numbers (0=Sunday, 1=Monday, etc.).733 * @param array $blocked Map of blocked dates (Y-m-d => true).734 * @return int New cursor timestamp pointing to next week's first selected day.735 */736 private function advance_cursor_to_next_week( int $cursor, array $days, array $blocked ) : int {737 if ( empty( $days ) ) {738 // Default to Saturday if no days selected.739 $days = array(6);740 }741 // Use helper to get days in proper gameweek order (handles week boundary crossing).742 $sorted_days = afgsp_sort_gameweek_days( $days );743 $first_selected_day = (int) reset( $sorted_days );744 $last_selected_day = (int) end( $sorted_days );745 // Detect if week boundary crossing (needed for cursor advancement logic).746 $has_sunday = in_array( 0, $days, true );747 $has_monday = in_array( 1, $days, true );748 $late_week = array_filter( $days, function ( $d ) {749 return $d >= 5;750 } );751 $crosses_week_boundary = ($has_sunday || $has_monday) && !empty( $late_week );752 // Get current weekday.753 $current_weekday = (int) gmdate( 'w', $cursor );754 // Calculate days to advance past the current gameweek entirely.755 // We need to get past the last selected day and then find the next first selected day.756 if ( $crosses_week_boundary ) {757 // For week-boundary-crossing gameweeks (e.g., Fri-Sat-Sun, Sat-Sun-Mon):758 // After the last day, advance to the NEXT occurrence of first_selected_day.759 if ( $current_weekday === $last_selected_day || 0 === $current_weekday ) {760 // Currently on last day or Sunday, advance to next first_selected_day.761 $days_to_first = ($first_selected_day - $current_weekday + 7) % 7;762 if ( 0 === $days_to_first ) {763 $days_to_first = 7;764 }765 } else {766 // Currently on another day, advance to next first_selected_day.767 $days_to_first = ($first_selected_day - $current_weekday + 7) % 7;768 if ( 0 === $days_to_first ) {769 // Already on first_selected_day, advance to next week.770 $days_to_first = 7;771 }772 }773 } else {774 // Normal case: advance to next occurrence of first selected day.775 $days_to_first = ($first_selected_day - $current_weekday + 7) % 7;776 if ( 0 === $days_to_first ) {777 // Same day, advance to next week.778 $days_to_first = 7;779 }780 }781 $cursor = strtotime( '+' . $days_to_first . ' days', $cursor );782 // Now find the next valid (non-blocked) selected day.783 $max_iterations = 14;784 // Safety limit.785 $iterations = 0;786 while ( $iterations < $max_iterations ) {787 $weekday = (int) gmdate( 'w', $cursor );788 $ymd = gmdate( 'Y-m-d', $cursor );789 // Skip blocked dates.790 if ( isset( $blocked[$ymd] ) ) {791 $cursor = strtotime( '+1 day', $cursor );792 $iterations++;793 continue;794 }795 // Found a selected day.796 if ( in_array( $weekday, $days, true ) ) {797 return $cursor;798 }799 $cursor = strtotime( '+1 day', $cursor );800 $iterations++;801 }802 return $cursor;803 669 } 804 670 … … 1340 1206 <tr> 1341 1207 <th scope="row"><?php 1208 esc_html_e( 'Events per timeslot mode', 'auto-fixture-generator-for-sportspress' ); 1209 ?></th> 1210 <td> 1211 <select name="schedule[events_mode]" id="afgsp_events_mode" <?php 1212 echo ( !afgsp_fs()->can_use_premium_code__premium_only() ? 'disabled' : '' ); 1213 ?>> 1214 <option value="auto" selected><?php 1215 esc_html_e( 'AUTO', 'auto-fixture-generator-for-sportspress' ); 1216 ?></option> 1217 <?php 1218 ?> 1219 </select> 1220 <p class="description"> 1221 <?php 1222 ?> 1223 <?php 1224 esc_html_e( 'Events per slot are calculated automatically. Upgrade to premium for manual control.', 'auto-fixture-generator-for-sportspress' ); 1225 ?> 1226 <a href="<?php 1227 echo esc_url( 'https://savvasha.com/auto-fixture-generator-for-sportspress-premium/' ); 1228 ?>" target="_blank" style="font-weight: 600;"> 1229 <?php 1230 esc_html_e( 'Upgrade to premium version now!', 'auto-fixture-generator-for-sportspress' ); 1231 ?> → 1232 </a> 1233 <?php 1234 ?> 1235 </p> 1236 </td> 1237 </tr> 1238 <tr> 1239 <th scope="row"><?php 1342 1240 esc_html_e( 'Time slots per match day', 'auto-fixture-generator-for-sportspress' ); 1343 1241 ?></th> 1344 1242 <td> 1345 1243 <div id="afgsp_time_slots"> 1346 <div><input type="time" name="schedule[time_slots][]" value="20:00" /> 1347 <?php 1348 ?></div> 1244 <div> 1245 <input type="time" name="schedule[time_slots][]" value="20:00" /> 1246 <input type="number" name="schedule[events_per_slot][]" class="afgsp-events-per-slot small-text" min="1" value="" disabled /> 1247 <?php 1248 ?> 1249 </div> 1349 1250 <?php 1350 1251 ?> -
auto-fixture-generator-for-sportspress/trunk/includes/class-afgsp-debug.php
r3437103 r3444584 122 122 123 123 // Events per Timeslot. 124 self::log_line( 'Events per Timeslot: Not implemented yet' ); 124 $team_ids = isset( $context['team_ids'] ) && is_array( $context['team_ids'] ) ? $context['team_ids'] : array(); 125 $events_mode = isset( $schedule['events_mode'] ) ? (string) $schedule['events_mode'] : 'auto'; 126 127 if ( 'manual' === $events_mode && ! empty( $schedule['events_per_slot'] ) && is_array( $schedule['events_per_slot'] ) ) { 128 $manual_values = implode( ', ', array_map( 'intval', $schedule['events_per_slot'] ) ); 129 self::log_line( 'Events per Timeslot: ' . $manual_values . ' (MANUAL mode)' ); 130 } else { 131 $events_per_slot = afgsp_calculate_events_per_timeslot( count( $team_ids ), count( $days ), count( $time_slots ) ); 132 self::log_line( 'Events per Timeslot: ' . $events_per_slot . ' (AUTO mode)' ); 133 } 125 134 126 135 // Blocked Dates. … … 130 139 self::log_line( '' ); 131 140 132 // Selected Teams. 133 $team_ids = isset( $context['team_ids'] ) && is_array( $context['team_ids'] ) ? $context['team_ids'] : array(); 141 // Selected Teams (already extracted above for events per slot calculation). 134 142 self::log_line( '--- Selected Teams (' . count( $team_ids ) . ') ---' ); 135 143 foreach ( $team_ids as $team_id ) { … … 146 154 $algorithm_label = isset( $algorithms[ $algorithm_slug ]['label'] ) ? $algorithms[ $algorithm_slug ]['label'] : ucfirst( $algorithm_slug ); 147 155 self::log_line( 'Selected Algorithm: ' . $algorithm_label . ' (' . $algorithm_slug . ')' ); 156 157 // Algorithm-specific options. 158 $algorithm_options = isset( $context['algorithm_options'] ) && is_array( $context['algorithm_options'] ) ? $context['algorithm_options'] : array(); 159 if ( 'fixed-week-season' === $algorithm_slug && isset( $algorithm_options['season_weeks'] ) ) { 160 self::log_line( 'Season Weeks: ' . (int) $algorithm_options['season_weeks'] ); 161 } 148 162 self::log_line( '' ); 149 163 -
auto-fixture-generator-for-sportspress/trunk/includes/class-afgsp-generator.php
r3388275 r3444584 21 21 class AFGSP_Generator { 22 22 /** 23 * Prepare fixtures with scheduling applied. 24 * 25 * This method calls the algorithm, applies scheduling (dates, times, gameweeks), 26 * and returns the prepared fixtures array. Used by both normal and dry-run modes 27 * to ensure consistent scheduling logic. 28 * 29 * @param array $raw_fixtures Raw fixtures from algorithm (array of home/away/meta). 30 * @param array $schedule Schedule settings (start_date, days, time_slots, blocked_dates, gameweek_name). 31 * @param int $teams_count Number of teams (for calculating matches per round). 32 * @param string $algorithm Algorithm slug (used to determine duplicate handling). 33 * @return array Prepared fixtures with datetime and gameweek assigned. 34 */ 35 public static function prepare_fixtures( 36 array $raw_fixtures, 37 array $schedule, 38 int $teams_count, 39 string $algorithm = '' 40 ) : array { 41 // For round-robin: matches per round = floor(n/2) (when n is odd, one team has bye). 42 $matches_per_round = max( 1, (int) floor( $teams_count / 2 ) ); 43 // Parse schedule settings. 44 $start_date = ( isset( $schedule['start_date'] ) ? (string) $schedule['start_date'] : '' ); 45 $days = ( isset( $schedule['days'] ) && is_array( $schedule['days'] ) ? array_map( 'intval', (array) $schedule['days'] ) : array() ); 46 $time_slots = ( isset( $schedule['time_slots'] ) && is_array( $schedule['time_slots'] ) ? array_values( array_filter( (array) $schedule['time_slots'] ) ) : array() ); 47 $blocked = ( isset( $schedule['blocked_dates'] ) && is_array( $schedule['blocked_dates'] ) ? array_values( array_filter( (array) $schedule['blocked_dates'] ) ) : array() ); 48 $blocked_map = array_fill_keys( $blocked, true ); 49 $gameweek_name = ( isset( $schedule['gameweek_name'] ) ? (string) $schedule['gameweek_name'] : 'Gameweek %No%' ); 50 // Parse events mode settings (premium feature). 51 $events_mode = ( isset( $schedule['events_mode'] ) ? (string) $schedule['events_mode'] : 'auto' ); 52 $events_per_slot_limits = ( isset( $schedule['events_per_slot'] ) && is_array( $schedule['events_per_slot'] ) ? array_map( 'intval', $schedule['events_per_slot'] ) : array() ); 53 // Initialize scheduling state. 54 $slot_index = 0; 55 $matches_on_current_day = 0; 56 $last_gameweek = 0; 57 // Track events assigned per slot for MANUAL mode (reset daily). 58 $slots_count = ( !empty( $time_slots ) ? count( $time_slots ) : 1 ); 59 $events_assigned_to_slots = array_fill( 0, $slots_count, 0 ); 60 // Calculate how many matches should be scheduled per day (AUTO mode). 61 // This ensures fixtures are distributed across available days in a gameweek. 62 $matches_per_day = ( count( $days ) > 0 ? (int) ceil( $matches_per_round / count( $days ) ) : $matches_per_round ); 63 $cursor = ( $start_date ? strtotime( $start_date . ' 00:00:00' ) : false ); 64 if ( $cursor && empty( $days ) ) { 65 // Default to weekend if start date provided but no specific days selected. 66 $days = array(6, 0); 67 // Saturday, Sunday. 68 } 69 $prepared_fixtures = array(); 70 $seen_pairs = array(); 71 $fixture_index = 0; 72 foreach ( $raw_fixtures as $fixture ) { 73 $home_id = ( isset( $fixture['home'] ) ? (int) $fixture['home'] : 0 ); 74 $away_id = ( isset( $fixture['away'] ) ? (int) $fixture['away'] : 0 ); 75 $extra_meta = ( isset( $fixture['meta'] ) && is_array( $fixture['meta'] ) ? $fixture['meta'] : array() ); 76 // Skip invalid fixtures. 77 if ( $home_id <= 0 || $away_id <= 0 || $home_id === $away_id ) { 78 continue; 79 } 80 // Suppress duplicates within a single generation run. 81 // Skip duplicate check for fixed-week-season as it allows teams to play multiple times. 82 $skip_duplicate_check = 'fixed-week-season' === $algorithm; 83 if ( !$skip_duplicate_check ) { 84 $pair_key = $home_id . '_' . $away_id; 85 if ( isset( $seen_pairs[$pair_key] ) ) { 86 continue; 87 } 88 $seen_pairs[$pair_key] = true; 89 } 90 // Calculate gameweek based on fixture index and round size. 91 $current_gameweek = (int) floor( $fixture_index / $matches_per_round ) + 1; 92 $gameweek_display_name = str_replace( '%No%', (string) $current_gameweek, $gameweek_name ); 93 $extra_meta['sp_day'] = $gameweek_display_name; 94 // If gameweek changed, advance cursor to next week and reset counters. 95 if ( $last_gameweek > 0 && $current_gameweek > $last_gameweek ) { 96 $slot_index = 0; 97 $matches_on_current_day = 0; 98 $events_assigned_to_slots = array_fill( 0, $slots_count, 0 ); 99 $cursor = self::advance_cursor_to_next_week( $cursor, $days, $blocked_map ); 100 } 101 // Assign datetime if not already set by algorithm. 102 if ( !isset( $extra_meta['datetime'] ) && $cursor ) { 103 $assigned = false; 104 while ( !$assigned ) { 105 $weekday = (int) gmdate( 'w', $cursor ); 106 $ymd = gmdate( 'Y-m-d', $cursor ); 107 // Skip blocked dates. 108 if ( isset( $blocked_map[$ymd] ) ) { 109 $cursor = strtotime( '+1 day', $cursor ); 110 continue; 111 } 112 // Check if current day is a selected match day. 113 if ( in_array( $weekday, $days, true ) ) { 114 if ( 'manual' === $events_mode && !empty( $events_per_slot_limits ) && !empty( $time_slots ) ) { 115 // MANUAL mode: find a slot that hasn't reached its limit. 116 $slot_found = false; 117 for ($i = 0; $i < $slots_count; $i++) { 118 $try_slot = ($slot_index + $i) % $slots_count; 119 $limit = ( isset( $events_per_slot_limits[$try_slot] ) ? (int) $events_per_slot_limits[$try_slot] : PHP_INT_MAX ); 120 if ( $events_assigned_to_slots[$try_slot] < $limit ) { 121 $slot = $time_slots[$try_slot]; 122 $extra_meta['datetime'] = $ymd . ' ' . $slot; 123 $events_assigned_to_slots[$try_slot]++; 124 $slot_index = $try_slot + 1; 125 $slot_found = true; 126 $matches_on_current_day++; 127 $assigned = true; 128 break; 129 } 130 } 131 if ( !$slot_found ) { 132 // All slots full for today, advance to next day. 133 $cursor = strtotime( '+1 day', $cursor ); 134 $matches_on_current_day = 0; 135 $events_assigned_to_slots = array_fill( 0, $slots_count, 0 ); 136 continue; 137 } 138 } else { 139 // AUTO mode: cycle through available slots. 140 $slot = ( !empty( $time_slots ) ? $time_slots[$slot_index % $slots_count] : '15:00' ); 141 $extra_meta['datetime'] = $ymd . ' ' . $slot; 142 $slot_index++; 143 $matches_on_current_day++; 144 $assigned = true; 145 // Advance cursor when daily quota is met. 146 if ( $matches_on_current_day >= $matches_per_day ) { 147 $cursor = strtotime( '+1 day', $cursor ); 148 $matches_on_current_day = 0; 149 $events_assigned_to_slots = array_fill( 0, $slots_count, 0 ); 150 } 151 } 152 } else { 153 $cursor = strtotime( '+1 day', $cursor ); 154 } 155 } 156 } 157 $prepared_fixtures[] = array( 158 'home_id' => $home_id, 159 'away_id' => $away_id, 160 'extra_meta' => $extra_meta, 161 'gameweek' => $current_gameweek, 162 ); 163 $last_gameweek = $current_gameweek; 164 $fixture_index++; 165 } 166 return $prepared_fixtures; 167 } 168 169 /** 170 * Advance cursor to the next week's first selected day. 171 * 172 * @param int $cursor Current timestamp. 173 * @param array $days Selected days of the week (0=Sun, 6=Sat). 174 * @param array $blocked_map Map of blocked dates. 175 * @return int New cursor timestamp. 176 */ 177 private static function advance_cursor_to_next_week( int $cursor, array $days, array $blocked_map ) : int { 178 if ( empty( $days ) ) { 179 return $cursor; 180 } 181 // Move to next day first. 182 $cursor = strtotime( '+1 day', $cursor ); 183 // Find the first day in the gameweek structure (not numeric min). 184 // Use the sorted gameweek days to get the correct starting day. 185 // For example, with days [6, 0] (Sat, Sun), Saturday is the first day, not Sunday. 186 $sorted_days = \AFGSP\afgsp_sort_gameweek_days( $days ); 187 $first_day = $sorted_days[0]; 188 // Advance until we reach the next occurrence of the first gameweek day. 189 $max_iterations = 14; 190 // Safety limit. 191 $iterations = 0; 192 while ( $iterations < $max_iterations ) { 193 $weekday = (int) gmdate( 'w', $cursor ); 194 $ymd = gmdate( 'Y-m-d', $cursor ); 195 // Skip blocked dates. 196 if ( isset( $blocked_map[$ymd] ) ) { 197 $cursor = strtotime( '+1 day', $cursor ); 198 $iterations++; 199 continue; 200 } 201 // Check if we've reached the first day of the next gameweek. 202 if ( $weekday === $first_day ) { 203 return $cursor; 204 } 205 $cursor = strtotime( '+1 day', $cursor ); 206 $iterations++; 207 } 208 return $cursor; 209 } 210 211 /** 23 212 * Run generation process. 24 213 * 25 * @param int $league_term_id League term ID.26 * @param int $season_term_id Season term ID.27 * @param string $algorithm_slug Algorithm slug.28 * @param array $options Algorithm options.214 * @param int $league_term_id League term ID. 215 * @param int $season_term_id Season term ID. 216 * @param string $algorithm_slug Algorithm slug. 217 * @param array $options Algorithm options. 29 218 * @param array $selected_team_ids Optional selected team IDs. 219 * @param bool $dry_run If true, skip event creation and return fixture data only. 220 * @return array Result with created count, errors, messages, and fixtures (for dry run). 30 221 */ 31 222 public static function run( … … 34 225 string $algorithm_slug, 35 226 array $options = array(), 36 array $selected_team_ids = array() 227 array $selected_team_ids = array(), 228 bool $dry_run = false 37 229 ) : array { 38 230 $result = array( … … 40 232 'errors' => array(), 41 233 'messages' => array(), 234 'fixtures' => array(), 42 235 ); 43 236 // Load teams for the selected league & season if not explicitly provided. … … 65 258 } 66 259 try { 67 $ fixtures = call_user_func( $callable, $team_ids, $options );68 if ( !is_array( $ fixtures ) ) {260 $raw_fixtures = call_user_func( $callable, $team_ids, $options ); 261 if ( !is_array( $raw_fixtures ) ) { 69 262 $result['errors'][] = __( 'Algorithm returned an invalid response.', 'auto-fixture-generator-for-sportspress' ); 70 263 return $result; … … 78 271 return $result; 79 272 } 80 // Get algorithm info to determine rounds/gameweeks. 81 $algorithm_info = AFGSP_Registry::get_algorithm_info( $algorithm_slug ); 82 $total_rounds = ( isset( $algorithm_info['rounds'] ) ? (int) $algorithm_info['rounds'] : 1 ); 83 // For single round-robin: rounds = n-1, matches per round = floor(n/2) (when n is odd, one team has bye). 84 $n = count( $team_ids ); 85 $matches_per_round = (int) floor( $n / 2 ); 86 // Apply scheduling constraints: assign dates/times if not already set by the algorithm. 273 // Extract schedule from options. 87 274 $schedule = ( isset( $options['schedule'] ) && is_array( $options['schedule'] ) ? (array) $options['schedule'] : array() ); 88 $start_date = ( isset( $schedule['start_date'] ) ? (string) $schedule['start_date'] : '' ); 89 $days = ( isset( $schedule['days'] ) && is_array( $schedule['days'] ) ? (array) $schedule['days'] : array() ); 90 $time_slots = ( isset( $schedule['time_slots'] ) && is_array( $schedule['time_slots'] ) ? array_values( array_filter( (array) $schedule['time_slots'] ) ) : array() ); 91 $blocked = ( isset( $schedule['blocked_dates'] ) && is_array( $schedule['blocked_dates'] ) ? array_values( array_filter( (array) $schedule['blocked_dates'] ) ) : array() ); 92 $blocked_map = array_fill_keys( $blocked, true ); 93 $gameweek_name = ( isset( $schedule['gameweek_name'] ) ? (string) $schedule['gameweek_name'] : 'Gameweek %No%' ); 94 $round_robin_time_index = 0; 95 // Calculate how many matches should be scheduled per day within a gameweek. 96 $matches_per_day = ( count( $days ) > 0 ? (int) ceil( $matches_per_round / count( $days ) ) : $matches_per_round ); 97 $matches_on_current_day = 0; 98 $cursor = ( $start_date ? strtotime( $start_date . ' 00:00:00' ) : false ); 99 if ( $cursor && empty( $days ) ) { 100 // Default to weekend if start date provided but no specific days selected. 101 $days = array(6, 0); 102 // Saturday, Sunday. 103 } 104 // First pass: assign dates to all fixtures. 105 $fixtures_with_dates = array(); 106 $seen_pairs_in_run = array(); 107 foreach ( $fixtures as $fixture ) { 108 $home_id = ( isset( $fixture['home'] ) ? (int) $fixture['home'] : 0 ); 109 $away_id = ( isset( $fixture['away'] ) ? (int) $fixture['away'] : 0 ); 110 $extra_meta = ( isset( $fixture['meta'] ) && is_array( $fixture['meta'] ) ? $fixture['meta'] : array() ); 111 // Suppress duplicates within a single generation run to avoid unnecessary duplicate checks/notices. 112 $pair_key_in_run = $home_id . '_' . $away_id; 113 if ( isset( $seen_pairs_in_run[$pair_key_in_run] ) ) { 114 continue; 115 } 116 $seen_pairs_in_run[$pair_key_in_run] = true; 117 // If algorithm didn't provide datetime, assign based on scheduling settings. 118 if ( !isset( $extra_meta['datetime'] ) && $cursor ) { 119 $assigned = false; 120 while ( !$assigned ) { 121 $weekday = (int) gmdate( 'w', $cursor ); 122 $ymd = gmdate( 'Y-m-d', $cursor ); 123 if ( isset( $blocked_map[$ymd] ) ) { 124 $cursor = strtotime( '+1 day', $cursor ); 125 continue; 126 } 127 if ( in_array( $weekday, $days, true ) ) { 128 $slot = ( !empty( $time_slots ) ? $time_slots[$round_robin_time_index % count( $time_slots )] : '15:00' ); 129 $extra_meta['datetime'] = $ymd . ' ' . $slot; 130 $round_robin_time_index++; 131 $matches_on_current_day++; 132 $assigned = true; 133 // Advance cursor only when daily quota is met. 134 if ( $matches_on_current_day >= $matches_per_day ) { 135 $cursor = strtotime( '+1 day', $cursor ); 136 $matches_on_current_day = 0; 137 } 138 } else { 139 $cursor = strtotime( '+1 day', $cursor ); 140 } 275 // Prepare fixtures with scheduling applied (centralized logic). 276 $prepared_fixtures = self::prepare_fixtures( 277 $raw_fixtures, 278 $schedule, 279 count( $team_ids ), 280 $algorithm_slug 281 ); 282 // Process fixtures: create events or collect for dry run. 283 foreach ( $prepared_fixtures as $fixture_data ) { 284 if ( $dry_run ) { 285 // Dry run: collect fixture data without creating events. 286 $result['fixtures'][] = $fixture_data; 287 $result['created']++; 288 $result['messages'][] = sprintf( 289 '%1$s vs %2$s (%3$s)', 290 get_the_title( $fixture_data['home_id'] ), 291 get_the_title( $fixture_data['away_id'] ), 292 $fixture_data['extra_meta']['sp_day'] ?? '' 293 ); 294 } else { 295 // Normal mode: create events in database. 296 $created = \AFGSP\afgsp_create_event( 297 $fixture_data['home_id'], 298 $fixture_data['away_id'], 299 $league_term_id, 300 $season_term_id, 301 $fixture_data['extra_meta'] 302 ); 303 if ( is_wp_error( $created ) ) { 304 $result['errors'][] = $created->get_error_message(); 305 continue; 141 306 } 142 } 143 if ( $home_id <= 0 || $away_id <= 0 || $home_id === $away_id ) { 144 $result['errors'][] = __( 'Invalid fixture data encountered and skipped.', 'auto-fixture-generator-for-sportspress' ); 145 continue; 146 } 147 $fixtures_with_dates[] = array( 148 'home_id' => $home_id, 149 'away_id' => $away_id, 150 'extra_meta' => $extra_meta, 151 ); 152 } 153 // Group fixtures by algorithm rounds (gameweeks). 154 $fixtures_by_gameweek = array(); 155 $fixtures_per_gameweek = (int) $matches_per_round; 156 $fixture_count = 0; 157 foreach ( $fixtures_with_dates as $fixture_data ) { 158 // Determine gameweek based on algorithm structure, not dates. 159 $gameweek_number = (int) (floor( $fixture_count / $fixtures_per_gameweek ) + 1); 160 // Add gameweek name to meta. 161 $gameweek_display_name = str_replace( '%No%', (string) $gameweek_number, $gameweek_name ); 162 $fixture_data['extra_meta']['sp_day'] = $gameweek_display_name; 163 $fixtures_by_gameweek[] = array( 164 'home_id' => $fixture_data['home_id'], 165 'away_id' => $fixture_data['away_id'], 166 'extra_meta' => $fixture_data['extra_meta'], 167 'gameweek' => $gameweek_number, 168 ); 169 $fixture_count++; 170 } 171 // Second pass: create events. 172 foreach ( $fixtures_by_gameweek as $fixture_data ) { 173 $created = \AFGSP\afgsp_create_event( 174 $fixture_data['home_id'], 175 $fixture_data['away_id'], 176 $league_term_id, 177 $season_term_id, 178 $fixture_data['extra_meta'] 179 ); 180 if ( is_wp_error( $created ) ) { 181 $result['errors'][] = $created->get_error_message(); 182 continue; 183 } 184 $result['created']++; 185 $result['messages'][] = sprintf( 186 '%1$s vs %2$s (%3$s)', 187 get_the_title( $fixture_data['home_id'] ), 188 get_the_title( $fixture_data['away_id'] ), 189 $fixture_data['extra_meta']['sp_day'] ?? '' 190 ); 307 $result['created']++; 308 $result['messages'][] = sprintf( 309 '%1$s vs %2$s (%3$s)', 310 get_the_title( $fixture_data['home_id'] ), 311 get_the_title( $fixture_data['away_id'] ), 312 $fixture_data['extra_meta']['sp_day'] ?? '' 313 ); 314 } 191 315 } 192 316 return $result; -
auto-fixture-generator-for-sportspress/trunk/includes/helpers.php
r3437103 r3444584 398 398 399 399 /** 400 * Calculate the number of events (fixtures) per time slot in AUTO mode. 401 * 402 * This centralizes the scheduling calculation used by both the generator 403 * and debug logging to ensure consistent results. 404 * 405 * @param int $teams_count Number of teams. 406 * @param int $days_count Number of selected match days per gameweek. 407 * @param int $slots_count Number of time slots per day. 408 * @return int Number of fixtures per time slot. 409 */ 410 function afgsp_calculate_events_per_timeslot( int $teams_count, int $days_count, int $slots_count ): int { 411 $matches_per_round = max( 1, (int) floor( $teams_count / 2 ) ); 412 $days_count = max( 1, $days_count ); 413 $slots_count = max( 1, $slots_count ); 414 $matches_per_day = (int) ceil( $matches_per_round / $days_count ); 415 $events_per_slot = (int) ceil( $matches_per_day / $slots_count ); 416 417 return $events_per_slot; 418 } 419 420 /** 400 421 * Sort gameweek days in proper chronological order. 401 422 * -
auto-fixture-generator-for-sportspress/trunk/readme.txt
r3437103 r3444584 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1. 47 Stable tag: 1.5 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl.html … … 69 69 3. Generator screen with algorithm selection and options (part3) 70 70 4. Succesfull creation of events 71 5. Debug / Dry Run mode available if needed. 71 72 72 73 == Changelog == 74 75 = 1.5 = 76 * NEW: Events per timeslot mode - AUTO calculates optimal distribution, MANUAL lets you set custom limits per slot (Premium version only) 77 * NEW: Algorithm-specific options now displayed in dry run debug log (e.g. Season Weeks for Fixed Week Season) 78 * FIX: Dry run mode now uses the same scheduling logic as normal mode, ensuring consistent fixture results. 79 * FIX: Fixed Week Season algorithm now generates correct number of gameweeks (rematches no longer incorrectly removed) 80 * FIX: Gameweeks no longer share dates - each gameweek properly advances to the next week period 73 81 74 82 = 1.4 =
Note: See TracChangeset
for help on using the changeset viewer.