Plugin Directory

Changeset 3444584


Ignore:
Timestamp:
01/22/2026 07:05:52 AM (2 months ago)
Author:
savvasha
Message:

Version 1.5

Location:
auto-fixture-generator-for-sportspress
Files:
16 edited
1 copied

Legend:

Unmodified
Added
Removed
  • auto-fixture-generator-for-sportspress/tags/1.5/assets/admin.css

    r3388275 r3444584  
    101101}
    102102
     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
    103128/* Responsive design */
    104129@media (max-width: 768px) {
     
    115140        height: 25px;
    116141    }
     142   
     143    .afgsp-events-per-slot {
     144        width: 50px;
     145        margin-left: 5px;
     146    }
    117147}
  • auto-fixture-generator-for-sportspress/tags/1.5/assets/admin.js

    r3435431 r3444584  
    5555
    5656        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        });
    57134    }
    58135
     
    433510    $(document).on('change', '#afgsp_gameweek_builder input[type="checkbox"]', function() {
    434511        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);
    435518    });
    436519
     
    446529        updateEventsDescription();
    447530        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();
    448559    });
    449560
  • auto-fixture-generator-for-sportspress/tags/1.5/auto-fixture-generator-for-sportspress.php

    r3437103 r3444584  
    44 * Plugin Name: Auto Fixture Generator for SportsPress
    55 * Description: Automatically generate fixtures (events) for a selected SportsPress league and season using pluggable scheduling algorithms.
    6  * Version: 1.4
     6 * Version: 1.5
    77 * Author: Savvas
    88 * Author URI: https://savvasha.com
     
    7070     */
    7171    if ( !defined( 'AFGSP_VERSION' ) ) {
    72         define( 'AFGSP_VERSION', '1.4' );
     72        define( 'AFGSP_VERSION', '1.5' );
    7373    }
    7474    if ( !defined( 'AFGSP_PLUGIN_FILE' ) ) {
  • auto-fixture-generator-for-sportspress/tags/1.5/includes/class-afgsp-admin.php

    r3437103 r3444584  
    202202        }, (array) $raw_schedule['blocked_dates'] ) ) ) : array() );
    203203        $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();
    204209        $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,
    210217        );
    211218        if ( empty( $schedule['start_date'] ) ) {
     
    225232        }
    226233        try {
    227             $fixtures = call_user_func( $callable, $team_ids, array(
     234            $raw_fixtures = call_user_func( $callable, $team_ids, array(
    228235                'schedule' => $schedule,
    229236            ) + $options );
    230             if ( !is_array( $fixtures ) ) {
     237            if ( !is_array( $raw_fixtures ) ) {
    231238                wp_send_json_error( array(
    232239                    'message' => __( 'Algorithm returned an invalid response.', 'auto-fixture-generator-for-sportspress' ),
     
    238245            ), 500 );
    239246        }
    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 );
    241256        if ( $total <= 0 ) {
    242257            wp_send_json_error( array(
     
    252267            'season_id'           => $season_id,
    253268            'algorithm'           => $algorithm,
    254             'fixtures'            => array_values( $fixtures ),
     269            'fixtures'            => array_values( $prepared_fixtures ),
    255270            'next_index'          => 0,
    256271            'total'               => $total,
     
    259274            'gameweeks'           => array(),
    260275            '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,
    267276            'round_size'          => max( 1, $round_size ),
    268277            'create_calendar'     => !empty( $options['create_calendar'] ),
     
    272281            'shuffle_teams'       => !empty( $options['shuffle_teams'] ),
    273282            'no_consecutive_away' => !empty( $options['no_consecutive_away'] ),
     283            'algorithm_options'   => $options,
    274284            'messages'            => array(),
    275285            'created_entities'    => false,
     
    305315            ), 400 );
    306316        }
     317        // Extract state - fixtures are now pre-scheduled with datetime and gameweek.
    307318        $fixtures = ( isset( $state['fixtures'] ) ? (array) $state['fixtures'] : array() );
    308319        $next_index = (int) ($state['next_index'] ?? 0);
     
    313324        $league_id = (int) ($state['league_id'] ?? 0);
    314325        $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() );
    321326        $messages = ( isset( $state['messages'] ) && is_array( $state['messages'] ) ? (array) $state['messages'] : array() );
    322327        $created_entities = !empty( $state['created_entities'] );
     
    330335        $dry_run_fixtures = ( isset( $state['dry_run_fixtures'] ) && is_array( $state['dry_run_fixtures'] ) ? $state['dry_run_fixtures'] : array() );
    331336        $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'] : '' );
    336338        // If already complete, ensure we still return any completion messages.
    337339        if ( $next_index >= $total ) {
     
    341343                    'league_id'           => $league_id,
    342344                    'season_id'           => $season_id,
    343                     'algorithm'           => $algorithm ?? '',
     345                    'algorithm'           => $algorithm,
     346                    'algorithm_options'   => ( isset( $state['algorithm_options'] ) ? (array) $state['algorithm_options'] : array() ),
    344347                    'schedule'            => $schedule,
    345348                    'team_ids'            => $team_ids,
     
    376379            ) );
    377380        }
    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);
    381384        $processed_this_batch = 0;
    382         $batch_date = '';
    383385        // Track algorithm to determine if duplicate checking should be skipped.
    384         $algorithm = ( isset( $state['algorithm'] ) ? (string) $state['algorithm'] : '' );
    385386        $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 ) {
    388388            $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() );
    392392            if ( $home_id <= 0 || $away_id <= 0 || $home_id === $away_id ) {
    393393                $next_index++;
    394394                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                 }
    440395            }
    441396            if ( $dry_run ) {
     
    480435                }
    481436            }
    482             $last_processed_gameweek = $current_gameweek;
    483437            $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++;
    488439        }
    489440        $state['next_index'] = $next_index;
     
    491442        $state['duplicates'] = $duplicates;
    492443        $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;
    499444        $state['messages'] = $messages;
    500445        $state['created_entities'] = $created_entities;
     
    509454                    'season_id'           => $season_id,
    510455                    'algorithm'           => $algorithm,
     456                    'algorithm_options'   => ( isset( $state['algorithm_options'] ) ? (array) $state['algorithm_options'] : array() ),
    511457                    'schedule'            => $schedule,
    512458                    'team_ids'            => $team_ids,
     
    721667        }
    722668        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's
    729      * 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;
    803669    }
    804670
     
    13401206                        <tr>
    13411207                            <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
    13421240        esc_html_e( 'Time slots per match day', 'auto-fixture-generator-for-sportspress' );
    13431241        ?></th>
    13441242                            <td>
    13451243                                <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>
    13491250                                    <?php
    13501251        ?>
  • auto-fixture-generator-for-sportspress/tags/1.5/includes/class-afgsp-debug.php

    r3437103 r3444584  
    122122
    123123        // 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        }
    125134
    126135        // Blocked Dates.
     
    130139        self::log_line( '' );
    131140
    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).
    134142        self::log_line( '--- Selected Teams (' . count( $team_ids ) . ') ---' );
    135143        foreach ( $team_ids as $team_id ) {
     
    146154        $algorithm_label = isset( $algorithms[ $algorithm_slug ]['label'] ) ? $algorithms[ $algorithm_slug ]['label'] : ucfirst( $algorithm_slug );
    147155        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        }
    148162        self::log_line( '' );
    149163
  • auto-fixture-generator-for-sportspress/tags/1.5/includes/class-afgsp-generator.php

    r3388275 r3444584  
    2121class AFGSP_Generator {
    2222    /**
     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    /**
    23212     * Run generation process.
    24213     *
    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.
    29218     * @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).
    30221     */
    31222    public static function run(
     
    34225        string $algorithm_slug,
    35226        array $options = array(),
    36         array $selected_team_ids = array()
     227        array $selected_team_ids = array(),
     228        bool $dry_run = false
    37229    ) : array {
    38230        $result = array(
     
    40232            'errors'   => array(),
    41233            'messages' => array(),
     234            'fixtures' => array(),
    42235        );
    43236        // Load teams for the selected league & season if not explicitly provided.
     
    65258        }
    66259        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 ) ) {
    69262                $result['errors'][] = __( 'Algorithm returned an invalid response.', 'auto-fixture-generator-for-sportspress' );
    70263                return $result;
     
    78271            return $result;
    79272        }
    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.
    87274        $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;
    141306                }
    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            }
    191315        }
    192316        return $result;
  • auto-fixture-generator-for-sportspress/tags/1.5/includes/helpers.php

    r3437103 r3444584  
    398398
    399399/**
     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 */
     410function 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/**
    400421 * Sort gameweek days in proper chronological order.
    401422 *
  • auto-fixture-generator-for-sportspress/tags/1.5/readme.txt

    r3437103 r3444584  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.4
     7Stable tag: 1.5
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl.html
     
    69693. Generator screen with algorithm selection and options (part3)
    70704. Succesfull creation of events
     715. Debug / Dry Run mode available if needed.
    7172
    7273== 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
    7381
    7482= 1.4 =
  • auto-fixture-generator-for-sportspress/trunk/assets/admin.css

    r3388275 r3444584  
    101101}
    102102
     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
    103128/* Responsive design */
    104129@media (max-width: 768px) {
     
    115140        height: 25px;
    116141    }
     142   
     143    .afgsp-events-per-slot {
     144        width: 50px;
     145        margin-left: 5px;
     146    }
    117147}
  • auto-fixture-generator-for-sportspress/trunk/assets/admin.js

    r3435431 r3444584  
    5555
    5656        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        });
    57134    }
    58135
     
    433510    $(document).on('change', '#afgsp_gameweek_builder input[type="checkbox"]', function() {
    434511        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);
    435518    });
    436519
     
    446529        updateEventsDescription();
    447530        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();
    448559    });
    449560
  • auto-fixture-generator-for-sportspress/trunk/auto-fixture-generator-for-sportspress.php

    r3437103 r3444584  
    44 * Plugin Name: Auto Fixture Generator for SportsPress
    55 * Description: Automatically generate fixtures (events) for a selected SportsPress league and season using pluggable scheduling algorithms.
    6  * Version: 1.4
     6 * Version: 1.5
    77 * Author: Savvas
    88 * Author URI: https://savvasha.com
     
    7070     */
    7171    if ( !defined( 'AFGSP_VERSION' ) ) {
    72         define( 'AFGSP_VERSION', '1.4' );
     72        define( 'AFGSP_VERSION', '1.5' );
    7373    }
    7474    if ( !defined( 'AFGSP_PLUGIN_FILE' ) ) {
  • auto-fixture-generator-for-sportspress/trunk/includes/class-afgsp-admin.php

    r3437103 r3444584  
    202202        }, (array) $raw_schedule['blocked_dates'] ) ) ) : array() );
    203203        $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();
    204209        $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,
    210217        );
    211218        if ( empty( $schedule['start_date'] ) ) {
     
    225232        }
    226233        try {
    227             $fixtures = call_user_func( $callable, $team_ids, array(
     234            $raw_fixtures = call_user_func( $callable, $team_ids, array(
    228235                'schedule' => $schedule,
    229236            ) + $options );
    230             if ( !is_array( $fixtures ) ) {
     237            if ( !is_array( $raw_fixtures ) ) {
    231238                wp_send_json_error( array(
    232239                    'message' => __( 'Algorithm returned an invalid response.', 'auto-fixture-generator-for-sportspress' ),
     
    238245            ), 500 );
    239246        }
    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 );
    241256        if ( $total <= 0 ) {
    242257            wp_send_json_error( array(
     
    252267            'season_id'           => $season_id,
    253268            'algorithm'           => $algorithm,
    254             'fixtures'            => array_values( $fixtures ),
     269            'fixtures'            => array_values( $prepared_fixtures ),
    255270            'next_index'          => 0,
    256271            'total'               => $total,
     
    259274            'gameweeks'           => array(),
    260275            '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,
    267276            'round_size'          => max( 1, $round_size ),
    268277            'create_calendar'     => !empty( $options['create_calendar'] ),
     
    272281            'shuffle_teams'       => !empty( $options['shuffle_teams'] ),
    273282            'no_consecutive_away' => !empty( $options['no_consecutive_away'] ),
     283            'algorithm_options'   => $options,
    274284            'messages'            => array(),
    275285            'created_entities'    => false,
     
    305315            ), 400 );
    306316        }
     317        // Extract state - fixtures are now pre-scheduled with datetime and gameweek.
    307318        $fixtures = ( isset( $state['fixtures'] ) ? (array) $state['fixtures'] : array() );
    308319        $next_index = (int) ($state['next_index'] ?? 0);
     
    313324        $league_id = (int) ($state['league_id'] ?? 0);
    314325        $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() );
    321326        $messages = ( isset( $state['messages'] ) && is_array( $state['messages'] ) ? (array) $state['messages'] : array() );
    322327        $created_entities = !empty( $state['created_entities'] );
     
    330335        $dry_run_fixtures = ( isset( $state['dry_run_fixtures'] ) && is_array( $state['dry_run_fixtures'] ) ? $state['dry_run_fixtures'] : array() );
    331336        $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'] : '' );
    336338        // If already complete, ensure we still return any completion messages.
    337339        if ( $next_index >= $total ) {
     
    341343                    'league_id'           => $league_id,
    342344                    'season_id'           => $season_id,
    343                     'algorithm'           => $algorithm ?? '',
     345                    'algorithm'           => $algorithm,
     346                    'algorithm_options'   => ( isset( $state['algorithm_options'] ) ? (array) $state['algorithm_options'] : array() ),
    344347                    'schedule'            => $schedule,
    345348                    'team_ids'            => $team_ids,
     
    376379            ) );
    377380        }
    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);
    381384        $processed_this_batch = 0;
    382         $batch_date = '';
    383385        // Track algorithm to determine if duplicate checking should be skipped.
    384         $algorithm = ( isset( $state['algorithm'] ) ? (string) $state['algorithm'] : '' );
    385386        $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 ) {
    388388            $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() );
    392392            if ( $home_id <= 0 || $away_id <= 0 || $home_id === $away_id ) {
    393393                $next_index++;
    394394                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                 }
    440395            }
    441396            if ( $dry_run ) {
     
    480435                }
    481436            }
    482             $last_processed_gameweek = $current_gameweek;
    483437            $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++;
    488439        }
    489440        $state['next_index'] = $next_index;
     
    491442        $state['duplicates'] = $duplicates;
    492443        $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;
    499444        $state['messages'] = $messages;
    500445        $state['created_entities'] = $created_entities;
     
    509454                    'season_id'           => $season_id,
    510455                    'algorithm'           => $algorithm,
     456                    'algorithm_options'   => ( isset( $state['algorithm_options'] ) ? (array) $state['algorithm_options'] : array() ),
    511457                    'schedule'            => $schedule,
    512458                    'team_ids'            => $team_ids,
     
    721667        }
    722668        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's
    729      * 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;
    803669    }
    804670
     
    13401206                        <tr>
    13411207                            <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
    13421240        esc_html_e( 'Time slots per match day', 'auto-fixture-generator-for-sportspress' );
    13431241        ?></th>
    13441242                            <td>
    13451243                                <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>
    13491250                                    <?php
    13501251        ?>
  • auto-fixture-generator-for-sportspress/trunk/includes/class-afgsp-debug.php

    r3437103 r3444584  
    122122
    123123        // 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        }
    125134
    126135        // Blocked Dates.
     
    130139        self::log_line( '' );
    131140
    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).
    134142        self::log_line( '--- Selected Teams (' . count( $team_ids ) . ') ---' );
    135143        foreach ( $team_ids as $team_id ) {
     
    146154        $algorithm_label = isset( $algorithms[ $algorithm_slug ]['label'] ) ? $algorithms[ $algorithm_slug ]['label'] : ucfirst( $algorithm_slug );
    147155        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        }
    148162        self::log_line( '' );
    149163
  • auto-fixture-generator-for-sportspress/trunk/includes/class-afgsp-generator.php

    r3388275 r3444584  
    2121class AFGSP_Generator {
    2222    /**
     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    /**
    23212     * Run generation process.
    24213     *
    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.
    29218     * @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).
    30221     */
    31222    public static function run(
     
    34225        string $algorithm_slug,
    35226        array $options = array(),
    36         array $selected_team_ids = array()
     227        array $selected_team_ids = array(),
     228        bool $dry_run = false
    37229    ) : array {
    38230        $result = array(
     
    40232            'errors'   => array(),
    41233            'messages' => array(),
     234            'fixtures' => array(),
    42235        );
    43236        // Load teams for the selected league & season if not explicitly provided.
     
    65258        }
    66259        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 ) ) {
    69262                $result['errors'][] = __( 'Algorithm returned an invalid response.', 'auto-fixture-generator-for-sportspress' );
    70263                return $result;
     
    78271            return $result;
    79272        }
    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.
    87274        $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;
    141306                }
    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            }
    191315        }
    192316        return $result;
  • auto-fixture-generator-for-sportspress/trunk/includes/helpers.php

    r3437103 r3444584  
    398398
    399399/**
     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 */
     410function 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/**
    400421 * Sort gameweek days in proper chronological order.
    401422 *
  • auto-fixture-generator-for-sportspress/trunk/readme.txt

    r3437103 r3444584  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.4
     7Stable tag: 1.5
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl.html
     
    69693. Generator screen with algorithm selection and options (part3)
    70704. Succesfull creation of events
     715. Debug / Dry Run mode available if needed.
    7172
    7273== 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
    7381
    7482= 1.4 =
Note: See TracChangeset for help on using the changeset viewer.