added free agents

This commit is contained in:
2026-02-22 22:44:17 +11:00
parent 4185ab58e2
commit 3c866551a4
15 changed files with 1549 additions and 98 deletions

View File

@@ -17,6 +17,8 @@ templ FixtureDetailPage(
result *db.FixtureResult,
rosters map[string][]*db.PlayerWithPlayStatus,
activeTab string,
nominatedFreeAgents []*db.FixtureFreeAgent,
availableFreeAgents []*db.SeasonLeagueFreeAgent,
) {
{{
permCache := contexts.Permissions(ctx)
@@ -78,10 +80,10 @@ templ FixtureDetailPage(
</nav>
}
</div>
<!-- Tab Content -->
if activeTab == "overview" {
@fixtureOverviewTab(fixture, currentSchedule, result, rosters, canManage)
} else if activeTab == "schedule" {
<!-- Tab Content -->
if activeTab == "overview" {
@fixtureOverviewTab(fixture, currentSchedule, result, rosters, canManage, canSchedule, userTeamID, nominatedFreeAgents, availableFreeAgents)
} else if activeTab == "schedule" {
@fixtureScheduleTab(fixture, currentSchedule, history, canSchedule, canManage, userTeamID)
}
</div>
@@ -116,6 +118,10 @@ templ fixtureOverviewTab(
result *db.FixtureResult,
rosters map[string][]*db.PlayerWithPlayStatus,
canManage bool,
canSchedule bool,
userTeamID int,
nominatedFreeAgents []*db.FixtureFreeAgent,
availableFreeAgents []*db.SeasonLeagueFreeAgent,
) {
<div class="space-y-6">
<!-- Result + Schedule Row -->
@@ -135,6 +141,10 @@ templ fixtureOverviewTab(
@fixtureScheduleSummary(fixture, currentSchedule, result)
</div>
</div>
<!-- Free Agent Nominations (hidden when result is finalized) -->
if (result == nil || !result.Finalized) && (canSchedule || canManage || len(nominatedFreeAgents) > 0) {
@fixtureFreeAgentSection(fixture, canSchedule, canManage, userTeamID, nominatedFreeAgents, availableFreeAgents)
}
<!-- Team Rosters -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@fixtureTeamSection(fixture.HomeTeam, rosters["home"], "home", result)
@@ -400,6 +410,11 @@ templ fixtureTeamSection(team *db.Team, players []*db.PlayerWithPlayStatus, side
&#9733;
</span>
}
if p.IsFreeAgent {
<span class="px-1.5 py-0.5 bg-peach/20 text-peach rounded text-xs font-medium">
FREE AGENT
</span>
}
</span>
</td>
if p.Stats != nil {
@@ -456,6 +471,11 @@ templ fixtureTeamSection(team *db.Team, players []*db.PlayerWithPlayStatus, side
&#9733; Manager
</span>
}
if p.IsFreeAgent {
<span class="px-1.5 py-0.5 bg-peach/20 text-peach rounded text-xs font-medium">
FREE AGENT
</span>
}
</div>
}
</div>
@@ -482,6 +502,256 @@ templ fixtureTeamSection(team *db.Team, players []*db.PlayerWithPlayStatus, side
</div>
}
// ==================== Free Agent Section ====================
templ fixtureFreeAgentSection(
fixture *db.Fixture,
canSchedule bool,
canManage bool,
userTeamID int,
nominated []*db.FixtureFreeAgent,
available []*db.SeasonLeagueFreeAgent,
) {
{{
// Split nominated by team
homeNominated := []*db.FixtureFreeAgent{}
awayNominated := []*db.FixtureFreeAgent{}
for _, n := range nominated {
if n.TeamID == fixture.HomeTeamID {
homeNominated = append(homeNominated, n)
} else {
awayNominated = append(awayNominated, n)
}
}
// Filter available: exclude already nominated players
nominatedIDs := map[int]bool{}
for _, n := range nominated {
nominatedIDs[n.PlayerID] = true
}
filteredAvailable := []*db.SeasonLeagueFreeAgent{}
for _, fa := range available {
if !nominatedIDs[fa.PlayerID] {
filteredAvailable = append(filteredAvailable, fa)
}
}
// Can the user nominate?
canNominate := (canSchedule || canManage) && len(filteredAvailable) > 0
}}
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden" x-data="{ showNominateModal: false, selectedPlayerId: '', selectedTeamId: '' }">
<div class="bg-surface0 border-b border-surface1 px-4 py-3 flex items-center justify-between">
<h2 class="text-lg font-bold text-text">Free Agent Nominations</h2>
if canNominate {
<button
@click="showNominateModal = true"
class="rounded-lg px-3 py-1.5 hover:cursor-pointer text-center text-xs
bg-peach hover:bg-peach/75 text-mantle transition"
>
Nominate Free Agent
</button>
}
</div>
<div class="p-4">
if len(nominated) == 0 {
<p class="text-subtext1 text-sm text-center py-2">No free agents nominated for this fixture.</p>
} else {
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Home team nominations -->
<div>
<p class="text-xs text-subtext0 font-semibold uppercase mb-2">{ fixture.HomeTeam.Name }</p>
if len(homeNominated) == 0 {
<p class="text-subtext1 text-xs italic">None</p>
} else {
<div class="space-y-1">
for _, n := range homeNominated {
<div class="flex items-center justify-between px-2 py-1.5 rounded hover:bg-surface0 transition">
<span class="flex items-center gap-2">
<span class="text-sm text-text">{ n.Player.DisplayName() }</span>
<span class="px-1.5 py-0.5 bg-peach/20 text-peach rounded text-xs font-medium">
FA
</span>
</span>
if canManage || (canSchedule && userTeamID == fixture.HomeTeamID) {
<form
hx-post={ fmt.Sprintf("/fixtures/%d/free-agents/%d/remove", fixture.ID, n.PlayerID) }
hx-swap="none"
class="inline"
>
<button
type="submit"
class="px-2 py-0.5 text-xs bg-red/20 hover:bg-red/40 text-red rounded
transition hover:cursor-pointer"
>
Remove
</button>
</form>
}
</div>
}
</div>
}
</div>
<!-- Away team nominations -->
<div>
<p class="text-xs text-subtext0 font-semibold uppercase mb-2">{ fixture.AwayTeam.Name }</p>
if len(awayNominated) == 0 {
<p class="text-subtext1 text-xs italic">None</p>
} else {
<div class="space-y-1">
for _, n := range awayNominated {
<div class="flex items-center justify-between px-2 py-1.5 rounded hover:bg-surface0 transition">
<span class="flex items-center gap-2">
<span class="text-sm text-text">{ n.Player.DisplayName() }</span>
<span class="px-1.5 py-0.5 bg-peach/20 text-peach rounded text-xs font-medium">
FA
</span>
</span>
if canManage || (canSchedule && userTeamID == fixture.AwayTeamID) {
<form
hx-post={ fmt.Sprintf("/fixtures/%d/free-agents/%d/remove", fixture.ID, n.PlayerID) }
hx-swap="none"
class="inline"
>
<button
type="submit"
class="px-2 py-0.5 text-xs bg-red/20 hover:bg-red/40 text-red rounded
transition hover:cursor-pointer"
>
Remove
</button>
</form>
}
</div>
}
</div>
}
</div>
</div>
}
</div>
<!-- Nominate Modal -->
if canNominate {
<div
x-show="showNominateModal"
@keydown.escape.window="showNominateModal = false"
class="fixed inset-0 z-50 overflow-y-auto"
style="display: none;"
>
<div
class="fixed inset-0 bg-crust/80 transition-opacity"
@click="showNominateModal = false"
></div>
<div class="flex min-h-full items-center justify-center p-4">
<div
class="relative bg-mantle border-2 border-surface1 rounded-xl shadow-xl max-w-md w-full p-6"
@click.stop
>
<h3 class="text-2xl font-bold text-text mb-4">Nominate Free Agent</h3>
<form
hx-post={ fmt.Sprintf("/fixtures/%d/free-agents/nominate", fixture.ID) }
hx-swap="none"
>
if canManage && !canSchedule {
<!-- Manager (not on either team): show team selector -->
<div class="mb-4">
<label for="fa_team_id" class="block text-sm font-medium mb-2">Nominating Team</label>
<select
id="fa_team_id"
name="team_id"
x-model="selectedTeamId"
required
class="w-full py-3 px-4 rounded-lg text-sm bg-base border-2 border-overlay0
focus:border-blue outline-none"
>
<option value="">Choose a team...</option>
<option value={ fmt.Sprint(fixture.HomeTeamID) }>
{ fixture.HomeTeam.Name }
</option>
<option value={ fmt.Sprint(fixture.AwayTeamID) }>
{ fixture.AwayTeam.Name }
</option>
</select>
</div>
} else if canManage && canSchedule {
<!-- Manager who is also on a team: show team selector pre-filled -->
<div class="mb-4">
<label for="fa_team_id" class="block text-sm font-medium mb-2">Nominating Team</label>
<select
id="fa_team_id"
name="team_id"
x-model="selectedTeamId"
x-init={ fmt.Sprintf("selectedTeamId = '%d'", userTeamID) }
required
class="w-full py-3 px-4 rounded-lg text-sm bg-base border-2 border-overlay0
focus:border-blue outline-none"
>
<option value="">Choose a team...</option>
<option value={ fmt.Sprint(fixture.HomeTeamID) }>
{ fixture.HomeTeam.Name }
</option>
<option value={ fmt.Sprint(fixture.AwayTeamID) }>
{ fixture.AwayTeam.Name }
</option>
</select>
</div>
} else {
<!-- Regular team manager: fixed to their team -->
<input type="hidden" name="team_id" value={ fmt.Sprint(userTeamID) }/>
}
<div class="mb-4">
<label for="fa_player_id" class="block text-sm font-medium mb-2">Select Free Agent</label>
<select
id="fa_player_id"
name="player_id"
x-model="selectedPlayerId"
required
class="w-full py-3 px-4 rounded-lg text-sm bg-base border-2 border-overlay0
focus:border-blue outline-none"
>
<option value="">Choose a free agent...</option>
for _, fa := range filteredAvailable {
<option value={ fmt.Sprint(fa.PlayerID) }>
{ fa.Player.DisplayName() }
</option>
}
</select>
</div>
<div class="flex gap-3 justify-end">
<button
type="button"
@click="showNominateModal = false"
class="px-4 py-2 rounded-lg bg-surface0 hover:bg-surface1 text-text transition hover:cursor-pointer"
>
Cancel
</button>
if canManage {
<button
type="submit"
:disabled="!selectedPlayerId || !selectedTeamId"
class="px-4 py-2 rounded-lg bg-peach hover:bg-peach/75 text-mantle transition
disabled:bg-peach/40 disabled:cursor-not-allowed hover:cursor-pointer"
>
Nominate
</button>
} else {
<button
type="submit"
:disabled="!selectedPlayerId"
class="px-4 py-2 rounded-lg bg-peach hover:bg-peach/75 text-mantle transition
disabled:bg-peach/40 disabled:cursor-not-allowed hover:cursor-pointer"
>
Nominate
</button>
}
</div>
</form>
</div>
</div>
</div>
}
</div>
}
// ==================== Schedule Tab ====================
templ fixtureScheduleTab(
fixture *db.Fixture,