A web-based admin wizard that replaces the agl:new Artisan command for creating a new season. It's a 6-step Livewire single file component at /admin/seasons/create.
resources/views/livewire/admin/seasons/create.blade.php— Livewire 4 single file component (6-step wizard)tests/Feature/Admin/SeasonWizardTest.php— 18 Pest tests (all passing)
routes/web.php— AddedRoute::livewire('/admin/seasons/create', 'admin.seasons.create')inside theIsAdminmiddleware groupresources/views/components/layouts/admin.blade.php— Added "New Season" nav item withplus-circleicon in the sidebarflux:navlistCLAUDE.md— Added "Livewire Components" section documenting single file component patterns.env.testing— AddedAPP_KEY(required for Livewire component rendering with Flux UI)
app/Console/Commands/NewYear.php— The CLI command this wizard replaces
- Year — Name (defaults to current year) and active toggle
- Teams — Count (default 6, named 1-N automatically)
- Weeks — Count (default 20), start date, optional skip date
- Previous Year — Dropdown of existing years for handicap lookup
- Players — One team per page (4 positions per team). Native
<select>for each player, handicap input, yellow tees toggle for position 4. Auto-fills handicap from previous year'shc_next_yearwhen a user is selected. - Review — Read-only summary, "Generate Blank Scorecards" toggle, "Create Season" button
Step 5 pages through teams one at a time ($currentTeam property). "Continue" advances to next team, "Back" goes to previous team. Badge indicators show team progress.
All in a DB transaction inside createSeason():
createYear()— Creates Year record (Year model'ssavingevent handles deactivating others)createTeams()— Creates N teams named "1" through "N"createWeeks()— Creates weeks with matchup rotation and side game alternationcreatePlayers()— Creates Player records with handicap and tee selectioncreateScores()— Creates blank weekly scores + quarterly/season averages per player
- Matchups rotate based on last digit of
week_order(seecreateWeek()method) - Tee time rotation shifts the matchup array: +2 positions for weeks 6-10, +4 for weeks 11-15
- Side games alternate Net/Pin starting from Net (initial state is 'Putts', first flip is to 'Net')
a_first_id/a_second_idetc. store team name numbers (1-6), NOT actual team DB IDs
The player dropdowns only show active users: User::where('active', true). The active column doesn't exist in the SQLite test schema, so with() uses Schema::hasColumn('users', 'active') to conditionally apply the filter.
Symptom: After assigning 4 players on Team 1, advancing to Team 2, and selecting the first couple players, the selected value changes to a different user than what was picked.
Root Cause: Native <select> elements with wire:model.live combined with a dynamically filtered options list. When a player is selected, wire:model.live triggers a server roundtrip. The availableUsersForSlot() method recalculates the available options (excluding newly assigned users). When the <option> list shifts, the browser's selected index can become mismatched with the intended value. Livewire's DOM diffing then reassigns the wrong user.
Possible Fixes (in order of preference):
- Use
wire:modelinstead ofwire:model.liveon the player selects, and add a separate "Confirm Team" or usewire:changeto trigger the handicap lookup. This avoids the mid-selection re-render. - Don't filter out assigned users from the dropdown — show all active users in every dropdown and instead validate duplicates only on "Next Team". This eliminates the shifting options problem entirely.
- Use
flux:select variant="listbox"(withoutsearchable) — Flux listbox manages its own state better than native<select>with Livewire. This was previously too slow withsearchablebut without it, and with only 4 selects per page and ~24 active users, it may be fine. - Use
wire:model.liveonly for the handicap auto-fill, and use Alpine.jsx-on:changeto handle the selection locally before syncing to Livewire.
The handicap auto-fill depends on wire:model.live — the updatedPlayerAssignments() hook fires when a user is selected and looks up their previous year handicap. Whatever fix is chosen needs to preserve this behavior.
Tests use Livewire::test() which bypasses route middleware, so they don't need the admin column (which is missing from the SQLite test migration). The test file has helper functions:
advanceToStep5($component)— CallsnextStep()4 times (steps 1→5)assignAllPlayersAndAdvance($component, $users, $teamCount)— Assigns users to all teams and advances through each team page to step 6
- Step navigation forward and backward
- Validation on each step (year name required/unique, team count min 2, start date required)
- Team paging within step 5 (forward, backward, back to step 4)
- Handicap auto-population from previous year
- User exclusion from dropdowns when already assigned
- Full season creation (year, 6 teams, 20 weeks, 24 players, 600 scores)
- Week matchup rotation correctness
- Side game alternation (Net/Pin)
- Skip date handling
- Yellow tees for position 4
- Year deactivation when creating an active season
- The component uses
$step(1-6) and$currentTeam(1-N) to control which page is shown - Player assignments stored in
public array $playerAssignments— nested[team][position] => [user_id, handicap, yellow_tees] getAssignedUserIdsProperty()is a Livewire computed property (cached per request)availableUsersForSlot()takes the full users collection as a parameter to avoid re-querying- Validation on step 5 validates only the current team (
validateTeamAssignments()) - The review step (6) validates nothing —
createSeason()callsvalidateCurrentStep()which hits thedefault => nullcase