Files
oslstats/internal/view/seasonsview/fixture_detail.templ

1541 lines
55 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package seasonsview
import "git.haelnorr.com/h/oslstats/internal/db"
import "git.haelnorr.com/h/oslstats/internal/permissions"
import "git.haelnorr.com/h/oslstats/internal/contexts"
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
import "git.haelnorr.com/h/oslstats/internal/view/component/links"
import "fmt"
import "sort"
import "strings"
templ FixtureDetailPage(
fixture *db.Fixture,
currentSchedule *db.FixtureSchedule,
history []*db.FixtureSchedule,
canSchedule bool,
userTeamID int,
result *db.FixtureResult,
rosters map[string][]*db.PlayerWithPlayStatus,
activeTab string,
nominatedFreeAgents []*db.FixtureFreeAgent,
availableFreeAgents []*db.SeasonLeagueFreeAgent,
) {
{{
permCache := contexts.Permissions(ctx)
canManage := permCache.HasPermission(permissions.FixturesManage)
backURL := fmt.Sprintf("/seasons/%s/leagues/%s/fixtures", fixture.Season.ShortName, fixture.League.ShortName)
isFinalized := result != nil && result.Finalized
if activeTab == "" {
activeTab = "overview"
}
// Force overview if schedule tab is hidden (result finalized)
if isFinalized && activeTab == "schedule" {
activeTab = "overview"
}
}}
@baseview.Layout(fmt.Sprintf("%s vs %s", fixture.HomeTeam.Name, fixture.AwayTeam.Name)) {
<div class="max-w-screen-lg mx-auto px-4 py-8">
<!-- Header -->
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden mb-6">
<div class="bg-surface0 border-b border-surface1 px-6 py-8">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<div class="flex items-center gap-4 mb-2">
<h1 class="text-3xl font-bold text-text">
{ fixture.HomeTeam.Name }
<span class="text-subtext0 font-normal">vs</span>
{ fixture.AwayTeam.Name }
</h1>
</div>
<div class="flex items-center gap-2 flex-wrap">
<span class="px-2 py-0.5 bg-surface1 text-subtext0 rounded text-xs font-mono">
Round { fmt.Sprint(fixture.Round) }
</span>
if fixture.GameWeek != nil {
<span class="px-2 py-0.5 bg-surface1 text-subtext0 rounded text-xs font-mono">
Game Week { fmt.Sprint(*fixture.GameWeek) }
</span>
}
<span class="text-subtext1 text-sm">
{ fixture.Season.Name } — { fixture.League.Name }
</span>
</div>
</div>
<a
href={ templ.SafeURL(backURL) }
class="rounded-lg px-4 py-2 hover:cursor-pointer text-center
bg-surface1 hover:bg-surface2 text-text transition"
>
Back to Fixtures
</a>
</div>
</div>
<!-- Tab Navigation (hidden when only one tab) -->
if !isFinalized {
<nav class="bg-surface0 border-b border-surface1">
<ul class="flex flex-wrap">
@fixtureTabItem("overview", "Overview", activeTab, fixture)
@fixtureTabItem("schedule", "Schedule", activeTab, fixture)
</ul>
</nav>
}
</div>
<!-- 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>
}
}
templ fixtureTabItem(section string, label string, activeTab string, fixture *db.Fixture) {
{{
isActive := section == activeTab
baseClasses := "inline-block px-6 py-3 transition-colors cursor-pointer border-b-2"
activeClasses := "border-blue text-blue font-semibold"
inactiveClasses := "border-transparent text-subtext0 hover:text-text hover:border-surface2"
url := fmt.Sprintf("/fixtures/%d", fixture.ID)
if section != "overview" {
url = fmt.Sprintf("/fixtures/%d?tab=%s", fixture.ID, section)
}
}}
<li class="inline-block">
<a
href={ templ.SafeURL(url) }
class={ baseClasses, templ.KV(activeClasses, isActive), templ.KV(inactiveClasses, !isActive) }
>
{ label }
</a>
</li>
}
// ==================== Overview Tab ====================
templ fixtureOverviewTab(
fixture *db.Fixture,
currentSchedule *db.FixtureSchedule,
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 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2">
if result != nil {
@fixtureResultDisplay(fixture, result)
} else if canManage {
@fixtureUploadPrompt(fixture)
} else {
<div class="bg-mantle border border-surface1 rounded-lg p-6 text-center">
<p class="text-subtext1 text-sm">No result has been uploaded for this fixture yet.</p>
</div>
}
</div>
<div>
@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, fixture.Season, fixture.League)
@fixtureTeamSection(fixture.AwayTeam, rosters["away"], "away", result, fixture.Season, fixture.League)
</div>
</div>
}
templ fixtureScheduleSummary(fixture *db.Fixture, schedule *db.FixtureSchedule, result *db.FixtureResult) {
{{
isPlayed := result != nil && result.Finalized
}}
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden h-full">
<div class="bg-surface0 border-b border-surface1 px-4 py-3">
<h2 class="text-lg font-bold text-text">Schedule</h2>
</div>
<div class="p-4 flex flex-col justify-center h-[calc(100%-3rem)]">
if schedule == nil {
<div class="text-center">
<p class="text-subtext1 text-sm">No time scheduled</p>
</div>
} else if schedule.Status == db.ScheduleStatusAccepted && schedule.ScheduledTime != nil {
<div class="text-center space-y-2">
if isPlayed {
<span class="px-2 py-0.5 bg-blue/20 text-blue rounded text-xs font-medium">
Played
</span>
} else {
<span class="px-2 py-0.5 bg-green/20 text-green rounded text-xs font-medium">
Confirmed
</span>
}
<p class="text-text font-medium">
@localtime(schedule.ScheduledTime, "date")
</p>
<p class="text-text text-lg font-bold">
@localtime(schedule.ScheduledTime, "time")
</p>
</div>
} else if schedule.Status == db.ScheduleStatusPending && schedule.ScheduledTime != nil {
<div class="text-center space-y-2">
<span class="px-2 py-0.5 bg-yellow/20 text-yellow rounded text-xs font-medium">
Proposed
</span>
<p class="text-text font-medium">
@localtime(schedule.ScheduledTime, "date")
</p>
<p class="text-text text-lg font-bold">
@localtime(schedule.ScheduledTime, "time")
</p>
<p class="text-subtext1 text-xs">Awaiting confirmation</p>
</div>
} else if schedule.Status == db.ScheduleStatusCancelled {
<div class="text-center space-y-2">
<span class="px-2 py-0.5 bg-red/20 text-red rounded text-xs font-medium">
Forfeit
</span>
if schedule.RescheduleReason != nil {
<p class="text-subtext1 text-xs">{ *schedule.RescheduleReason }</p>
}
</div>
} else {
<div class="text-center">
<p class="text-subtext1 text-sm">No time confirmed</p>
</div>
}
</div>
</div>
}
templ fixtureResultDisplay(fixture *db.Fixture, result *db.FixtureResult) {
{{
isOT := strings.EqualFold(result.EndReason, "Overtime")
homeWon := result.Winner == "home"
awayWon := result.Winner == "away"
isForfeit := result.IsForfeit
isMutualForfeit := isForfeit && result.ForfeitType != nil && *result.ForfeitType == "mutual"
isOutrightForfeit := isForfeit && result.ForfeitType != nil && *result.ForfeitType == "outright"
_ = isMutualForfeit
forfeitTeamName := ""
if isOutrightForfeit && result.ForfeitTeam != nil {
if *result.ForfeitTeam == "home" {
forfeitTeamName = fixture.HomeTeam.Name
} else {
forfeitTeamName = fixture.AwayTeam.Name
}
}
}}
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
<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">Match Result</h2>
<div class="flex items-center gap-2">
if isForfeit {
<span class="px-2 py-0.5 bg-red/20 text-red rounded text-xs font-medium">
Forfeited
</span>
}
if result.Finalized {
<span class="px-2 py-0.5 bg-green/20 text-green rounded text-xs font-medium">
Finalized
</span>
} else {
<span class="px-2 py-0.5 bg-yellow/20 text-yellow rounded text-xs font-medium">
Pending Review
</span>
}
if !result.Finalized && result.TamperingDetected {
<span class="px-2 py-0.5 bg-red/20 text-red rounded text-xs font-medium">
Inconsistent Data
</span>
}
if result.Finalized && result.TamperingDetected && result.TamperingReason != nil {
<span class="relative group">
<span class="text-yellow text-lg cursor-help">&#9888;</span>
<span
class="absolute right-0 top-full mt-1 w-72 bg-crust border border-surface1 rounded-lg
p-3 text-xs text-subtext0 shadow-lg opacity-0 invisible
group-hover:opacity-100 group-hover:visible transition-all z-50"
>
<span class="text-yellow font-semibold block mb-1">Inconsistent Data</span>
{ *result.TamperingReason }
</span>
</span>
}
</div>
</div>
<div class="p-6">
if !result.Finalized && result.TamperingDetected && result.TamperingReason != nil {
<div class="bg-red/10 border border-red/30 rounded-lg p-4 mb-4">
<div class="flex items-center gap-2 mb-1">
<span class="text-red font-medium text-sm">Warning: Inconsistent Data</span>
</div>
<p class="text-red/80 text-xs">{ *result.TamperingReason }</p>
</div>
}
if isForfeit {
<!-- Forfeit Display -->
<div class="flex flex-col items-center py-4 space-y-4">
if isMutualForfeit {
<div class="flex items-center justify-center gap-6">
<div class="flex items-center gap-3">
if fixture.HomeTeam.Color != "" {
<span
class="px-2.5 py-1 rounded text-sm font-bold"
style={ fmt.Sprintf("background-color: %s22; color: %s; border: 1px solid %s44",
fixture.HomeTeam.Color, fixture.HomeTeam.Color, fixture.HomeTeam.Color) }
>
{ fixture.HomeTeam.ShortName }
</span>
} else {
<span class="px-2.5 py-1 bg-surface1 text-text rounded text-sm font-bold">
{ fixture.HomeTeam.ShortName }
</span>
}
</div>
<div class="flex flex-col items-center">
<span class="px-3 py-1.5 bg-peach/20 text-peach rounded-lg text-sm font-bold">
MUTUAL FORFEIT
</span>
</div>
<div class="flex items-center gap-3">
if fixture.AwayTeam.Color != "" {
<span
class="px-2.5 py-1 rounded text-sm font-bold"
style={ fmt.Sprintf("background-color: %s22; color: %s; border: 1px solid %s44",
fixture.AwayTeam.Color, fixture.AwayTeam.Color, fixture.AwayTeam.Color) }
>
{ fixture.AwayTeam.ShortName }
</span>
} else {
<span class="px-2.5 py-1 bg-surface1 text-text rounded text-sm font-bold">
{ fixture.AwayTeam.ShortName }
</span>
}
</div>
</div>
<p class="text-sm text-subtext0">Both teams receive an overtime loss</p>
} else if isOutrightForfeit {
<div class="flex items-center justify-center gap-6">
<div class="flex items-center gap-3">
if homeWon {
<span class="text-2xl">&#127942;</span>
}
if fixture.HomeTeam.Color != "" {
<span
class="px-2.5 py-1 rounded text-sm font-bold"
style={ fmt.Sprintf("background-color: %s22; color: %s; border: 1px solid %s44",
fixture.HomeTeam.Color, fixture.HomeTeam.Color, fixture.HomeTeam.Color) }
>
{ fixture.HomeTeam.ShortName }
</span>
} else {
<span class="px-2.5 py-1 bg-surface1 text-text rounded text-sm font-bold">
{ fixture.HomeTeam.ShortName }
</span>
}
</div>
<div class="flex flex-col items-center">
<span class="px-3 py-1.5 bg-red/20 text-red rounded-lg text-sm font-bold">
FORFEIT
</span>
</div>
<div class="flex items-center gap-3">
if fixture.AwayTeam.Color != "" {
<span
class="px-2.5 py-1 rounded text-sm font-bold"
style={ fmt.Sprintf("background-color: %s22; color: %s; border: 1px solid %s44",
fixture.AwayTeam.Color, fixture.AwayTeam.Color, fixture.AwayTeam.Color) }
>
{ fixture.AwayTeam.ShortName }
</span>
} else {
<span class="px-2.5 py-1 bg-surface1 text-text rounded text-sm font-bold">
{ fixture.AwayTeam.ShortName }
</span>
}
if awayWon {
<span class="text-2xl">&#127942;</span>
}
</div>
</div>
<p class="text-sm text-subtext0">
{ forfeitTeamName } forfeited the match
</p>
}
if result.ForfeitReason != nil && *result.ForfeitReason != "" {
<div class="bg-surface0 border border-surface1 rounded-lg p-3 max-w-md w-full">
<p class="text-xs text-subtext1 font-medium mb-1">Reason</p>
<p class="text-sm text-subtext0">{ *result.ForfeitReason }</p>
</div>
}
</div>
} else {
<!-- Normal Score Display -->
<div class="flex items-center justify-center gap-6 py-4">
<div class="flex items-center gap-3">
if homeWon {
<span class="text-2xl">&#127942;</span>
}
if fixture.HomeTeam.Color != "" {
<span
class="px-2.5 py-1 rounded text-sm font-bold"
style={ fmt.Sprintf("background-color: %s22; color: %s; border: 1px solid %s44",
fixture.HomeTeam.Color, fixture.HomeTeam.Color, fixture.HomeTeam.Color) }
>
{ fixture.HomeTeam.ShortName }
</span>
} else {
<span class="px-2.5 py-1 bg-surface1 text-text rounded text-sm font-bold">
{ fixture.HomeTeam.ShortName }
</span>
}
<span class="text-4xl font-bold text-text">{ fmt.Sprint(result.HomeScore) }</span>
</div>
<div class="flex flex-col items-center">
<span class="text-4xl text-subtext0 font-light leading-none"></span>
if isOT {
<span class="px-1.5 py-0.5 bg-peach/20 text-peach rounded text-xs font-semibold mt-1">
OT
</span>
}
</div>
<div class="flex items-center gap-3">
<span class="text-4xl font-bold text-text">{ fmt.Sprint(result.AwayScore) }</span>
if fixture.AwayTeam.Color != "" {
<span
class="px-2.5 py-1 rounded text-sm font-bold"
style={ fmt.Sprintf("background-color: %s22; color: %s; border: 1px solid %s44",
fixture.AwayTeam.Color, fixture.AwayTeam.Color, fixture.AwayTeam.Color) }
>
{ fixture.AwayTeam.ShortName }
</span>
} else {
<span class="px-2.5 py-1 bg-surface1 text-text rounded text-sm font-bold">
{ fixture.AwayTeam.ShortName }
</span>
}
if awayWon {
<span class="text-2xl">&#127942;</span>
}
</div>
</div>
}
</div>
</div>
}
templ fixtureUploadPrompt(fixture *db.Fixture) {
<div class="bg-mantle border border-surface1 rounded-lg p-6 text-center">
<div class="text-4xl mb-3">📋</div>
<p class="text-lg text-text font-medium mb-2">No Result Uploaded</p>
<p class="text-sm text-subtext1 mb-4">Upload match log files to record the result of this fixture.</p>
<div class="flex items-center justify-center gap-3">
<a
href={ templ.SafeURL(fmt.Sprintf("/fixtures/%d/results/upload", fixture.ID)) }
class="inline-block px-4 py-2 bg-blue hover:bg-blue/80 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Upload Match Logs
</a>
<button
type="button"
@click="$dispatch('open-forfeit-modal')"
class="inline-block px-4 py-2 bg-red hover:bg-red/80 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Forfeit Match
</button>
</div>
</div>
@forfeitModal(fixture)
}
templ forfeitModal(fixture *db.Fixture) {
<div
x-data="{
open: false,
forfeitType: 'outright',
forfeitTeam: '',
forfeitReason: '',
}"
@open-forfeit-modal.window="open = true"
x-show="open"
x-cloak
class="fixed inset-0 z-50 overflow-y-auto"
role="dialog"
aria-modal="true"
>
<!-- Background overlay -->
<div
x-show="open"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 bg-base/75 transition-opacity"
@click="open = false"
></div>
<!-- Modal panel -->
<div class="flex min-h-full items-center justify-center p-4">
<div
x-show="open"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="relative transform overflow-hidden rounded-lg bg-mantle border-2 border-surface1 shadow-xl transition-all sm:w-full sm:max-w-lg"
@click.stop
>
<form
hx-post={ fmt.Sprintf("/fixtures/%d/forfeit", fixture.ID) }
hx-swap="none"
>
<div class="bg-mantle px-4 pb-4 pt-5 sm:p-6">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red/10 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"></path>
</svg>
</div>
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left w-full">
<h3 class="text-lg font-semibold leading-6 text-text">Forfeit Match</h3>
<div class="mt-2">
<p class="text-sm text-subtext0 mb-4">
This will record a forfeit result. This action is immediate and cannot be undone.
</p>
<!-- Forfeit Type Selection -->
<div class="space-y-3">
<label class="text-sm font-medium text-text">Forfeit Type</label>
<div class="space-y-2">
<label class="flex items-center gap-3 p-3 bg-surface0 border border-surface1 rounded-lg hover:bg-surface1 transition hover:cursor-pointer"
:class="forfeitType === 'outright' && 'border-red/50 bg-red/5'"
>
<input
type="radio"
name="forfeit_type"
value="outright"
x-model="forfeitType"
class="text-red focus:ring-red hover:cursor-pointer"
/>
<div>
<span class="text-sm font-medium text-text">Outright Forfeit</span>
<p class="text-xs text-subtext0">One team forfeits. They receive a loss, the opponent receives a win.</p>
</div>
</label>
<label class="flex items-center gap-3 p-3 bg-surface0 border border-surface1 rounded-lg hover:bg-surface1 transition hover:cursor-pointer"
:class="forfeitType === 'mutual' && 'border-peach/50 bg-peach/5'"
>
<input
type="radio"
name="forfeit_type"
value="mutual"
x-model="forfeitType"
class="text-peach focus:ring-peach hover:cursor-pointer"
/>
<div>
<span class="text-sm font-medium text-text">Mutual Forfeit</span>
<p class="text-xs text-subtext0">Both teams forfeit. Each receives an overtime loss.</p>
</div>
</label>
</div>
</div>
<!-- Team Selection (outright only) -->
<div x-show="forfeitType === 'outright'" x-cloak class="mt-4 space-y-2">
<label class="text-sm font-medium text-text">Which team is forfeiting?</label>
<select
name="forfeit_team"
x-model="forfeitTeam"
class="w-full px-3 py-2 bg-surface0 border border-surface1 rounded-lg text-text focus:border-red focus:outline-none hover:cursor-pointer"
>
<option value="">Select a team...</option>
<option value="home">{ fixture.HomeTeam.Name } (Home)</option>
<option value="away">{ fixture.AwayTeam.Name } (Away)</option>
</select>
</div>
<!-- Reason -->
<div class="mt-4 space-y-2">
<label class="text-sm font-medium text-text">Reason (optional)</label>
<textarea
name="forfeit_reason"
x-model="forfeitReason"
placeholder="Provide a reason for the forfeit..."
class="w-full px-3 py-2 bg-surface0 border border-surface1 rounded-lg text-text
focus:border-blue focus:outline-none resize-none"
rows="3"
></textarea>
</div>
</div>
</div>
</div>
</div>
<div class="bg-surface0 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 gap-2">
<button
type="submit"
class="inline-flex w-full justify-center rounded-lg bg-red px-4 py-2 text-sm font-semibold text-mantle shadow-sm hover:bg-red/75 hover:cursor-pointer transition sm:w-auto"
:disabled="forfeitType === 'outright' && forfeitTeam === ''"
:class="forfeitType === 'outright' && forfeitTeam === '' && 'opacity-50 cursor-not-allowed'"
>
Confirm Forfeit
</button>
<button
type="button"
@click="open = false"
class="mt-3 inline-flex w-full justify-center rounded-lg bg-surface1 px-4 py-2 text-sm font-semibold text-text shadow-sm hover:bg-surface2 hover:cursor-pointer transition sm:mt-0 sm:w-auto"
>
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
}
templ fixtureTeamSection(team *db.Team, players []*db.PlayerWithPlayStatus, side string, result *db.FixtureResult, season *db.Season, league *db.League) {
{{
// Separate playing and bench players
var playing []*db.PlayerWithPlayStatus
var bench []*db.PlayerWithPlayStatus
for _, p := range players {
if result == nil || p.Played {
playing = append(playing, p)
} else {
bench = append(bench, p)
}
}
showStats := result != nil && result.Finalized
if showStats {
// Sort playing players by score descending
sort.Slice(playing, func(i, j int) bool {
si, sj := 0, 0
if playing[i].Stats != nil && playing[i].Stats.Score != nil {
si = *playing[i].Stats.Score
}
if playing[j].Stats != nil && playing[j].Stats.Score != nil {
sj = *playing[j].Stats.Score
}
return si > sj
})
} else {
// Sort with managers first
sort.SliceStable(playing, func(i, j int) bool {
return playing[i].IsManager && !playing[j].IsManager
})
sort.SliceStable(bench, func(i, j int) bool {
return bench[i].IsManager && !bench[j].IsManager
})
}
}}
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
<div class="bg-surface0 border-b border-surface1 px-4 py-3 flex items-center justify-between">
<h3 class="text-md font-bold">
@links.TeamNameLinkInSeason(team, season, league)
</h3>
if team.Color != "" {
<span
class="w-4 h-4 rounded-full border border-surface1"
style={ fmt.Sprintf("background-color: %s", team.Color) }
></span>
}
</div>
if len(players) == 0 {
<div class="p-4">
<p class="text-subtext1 text-sm text-center py-2">No players on roster.</p>
</div>
} else if showStats {
<!-- Stats table view for finalized results -->
if len(playing) > 0 {
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-surface0 border-b border-surface1">
<tr>
<th class="px-3 py-2 text-left text-xs font-semibold text-text">Player</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Periods Played">PP</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Score">SC</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Goals">G</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Assists">A</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Saves">SV</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Shots">SH</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Blocks">BL</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Passes">PA</th>
</tr>
</thead>
<tbody class="divide-y divide-surface1">
for _, p := range playing {
<tr class="hover:bg-surface0 transition-colors">
<td class="px-3 py-2 text-sm">
<span class="flex items-center gap-1.5">
@links.PlayerLink(p.Player)
if p.IsManager {
<span class="px-1.5 py-0.5 bg-yellow/20 text-yellow rounded text-xs font-medium">
&#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 {
<td class="px-2 py-2 text-center text-sm text-subtext0">{ intPtrStr(p.Stats.PeriodsPlayed) }</td>
<td class="px-2 py-2 text-center text-sm font-medium text-text">{ intPtrStr(p.Stats.Score) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(p.Stats.Goals) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(p.Stats.Assists) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(p.Stats.Saves) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(p.Stats.Shots) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(p.Stats.Blocks) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(p.Stats.Passes) }</td>
} else {
<td colspan="8" class="px-2 py-2 text-center text-xs text-subtext1">—</td>
}
</tr>
}
</tbody>
</table>
</div>
}
if len(bench) > 0 {
<div class="border-t border-surface1 px-4 py-3">
<p class="text-xs text-subtext0 font-semibold uppercase mb-2">Bench</p>
<div class="space-y-1">
for _, p := range bench {
<div class="flex items-center gap-2 px-2 py-1.5 rounded">
<span class="text-sm text-subtext1">
@links.PlayerLink(p.Player)
</span>
if p.IsManager {
<span class="px-1.5 py-0.5 bg-yellow/20 text-yellow rounded text-xs font-medium">
&#9733; Manager
</span>
}
</div>
}
</div>
</div>
}
} else {
<!-- Simple list view (no result or pending review) -->
<div class="p-4">
if len(playing) > 0 {
if result != nil {
<p class="text-xs text-subtext0 font-semibold uppercase mb-2">Playing</p>
}
<div class="space-y-1">
for _, p := range playing {
<div class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-surface0 transition">
<span class="text-sm">
@links.PlayerLink(p.Player)
</span>
if p.IsManager {
<span class="px-1.5 py-0.5 bg-yellow/20 text-yellow rounded text-xs font-medium">
&#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>
}
if result != nil && len(bench) > 0 {
<p class="text-xs text-subtext0 font-semibold uppercase mt-4 mb-2">Bench</p>
<div class="space-y-1">
for _, p := range bench {
<div class="flex items-center gap-2 px-2 py-1.5 rounded">
<span class="text-sm text-subtext1">
@links.PlayerLink(p.Player)
</span>
if p.IsManager {
<span class="px-1.5 py-0.5 bg-yellow/20 text-yellow rounded text-xs font-medium">
&#9733; Manager
</span>
}
</div>
}
</div>
}
</div>
}
</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">
@links.PlayerLink(n.Player)
</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">
@links.PlayerLink(n.Player)
</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,
currentSchedule *db.FixtureSchedule,
history []*db.FixtureSchedule,
canSchedule bool,
canManage bool,
userTeamID int,
) {
<div class="space-y-6">
@fixtureScheduleStatus(fixture, currentSchedule, canSchedule, canManage, userTeamID)
@fixtureScheduleActions(fixture, currentSchedule, canSchedule, canManage, userTeamID)
@fixtureScheduleHistory(fixture, history)
</div>
}
templ fixtureScheduleStatus(
fixture *db.Fixture,
current *db.FixtureSchedule,
canSchedule bool,
canManage bool,
userTeamID int,
) {
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
<div class="bg-surface0 border-b border-surface1 px-4 py-3">
<h2 class="text-lg font-bold text-text">Schedule Status</h2>
</div>
<div class="p-6">
if current == nil {
<!-- No schedule yet -->
<div class="text-center py-4">
<div class="text-4xl mb-3">📅</div>
<p class="text-lg text-text font-medium">No time scheduled</p>
<p class="text-sm text-subtext1 mt-1">
if canSchedule {
Use the form to propose a time for this fixture.
} else {
A team manager needs to propose a time for this fixture.
}
</p>
</div>
} else if current.Status == db.ScheduleStatusPending && current.ScheduledTime != nil {
<!-- Pending proposal with time -->
<div class="text-center py-4">
<div class="text-4xl mb-3">⏳</div>
<p class="text-lg text-text font-medium">
Proposed:
@localtime(current.ScheduledTime, "datetime")
</p>
<p class="text-sm text-subtext1 mt-1">
Proposed by
<span class="text-text font-medium">{ current.ProposedBy.Name }</span>
— awaiting response from the other team
</p>
if canSchedule && userTeamID != current.ProposedByTeamID {
<div class="flex justify-center gap-3 mt-4">
<form
hx-post={ fmt.Sprintf("/fixtures/%d/schedule/%d/accept", fixture.ID, current.ID) }
hx-swap="none"
>
<button
type="submit"
class="px-4 py-2 bg-green hover:bg-green/75 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Accept
</button>
</form>
<form
hx-post={ fmt.Sprintf("/fixtures/%d/schedule/%d/reject", fixture.ID, current.ID) }
hx-swap="none"
>
<button
type="submit"
class="px-4 py-2 bg-red hover:bg-red/80 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Reject
</button>
</form>
</div>
}
if canSchedule && userTeamID == current.ProposedByTeamID {
<div class="flex justify-center mt-4">
<button
type="button"
@click={ "window.dispatchEvent(new CustomEvent('confirm-action', { detail: { title: 'Withdraw Proposal', message: 'Are you sure you want to withdraw your proposed time?', action: () => htmx.ajax('POST', '/fixtures/" + fmt.Sprint(fixture.ID) + "/schedule/" + fmt.Sprint(current.ID) + "/withdraw', { swap: 'none' }) } }))" }
class="px-4 py-2 bg-surface1 hover:bg-surface2 text-text rounded-lg
font-medium transition hover:cursor-pointer"
>
Withdraw Proposal
</button>
</div>
}
</div>
} else if current.Status == db.ScheduleStatusAccepted {
<!-- Accepted / Confirmed -->
<div class="text-center py-4">
<div class="text-4xl mb-3">✅</div>
<p class="text-lg text-green font-medium">
Confirmed:
@localtime(current.ScheduledTime, "datetime")
</p>
<p class="text-sm text-subtext1 mt-1">
Both teams have agreed on this time.
</p>
</div>
} else if current.Status == db.ScheduleStatusRejected {
<!-- Rejected -->
<div class="text-center py-4">
<div class="text-4xl mb-3">❌</div>
<p class="text-lg text-red font-medium">Proposal Rejected</p>
<p class="text-sm text-subtext1 mt-1">
The proposed time was rejected. A new time needs to be proposed.
</p>
</div>
} else if current.Status == db.ScheduleStatusCancelled {
<!-- Cancelled / Forfeit -->
<div class="text-center py-4">
<div class="text-4xl mb-3">🚫</div>
<p class="text-lg text-red font-medium">Fixture Forfeited</p>
if current.RescheduleReason != nil {
<p class="text-sm text-subtext1 mt-1">
{ *current.RescheduleReason }
</p>
}
</div>
} else if current.Status == db.ScheduleStatusRescheduled {
<!-- Rescheduled (terminal - new proposal should follow) -->
<div class="text-center py-4">
<div class="text-4xl mb-3">🔄</div>
<p class="text-lg text-yellow font-medium">Rescheduled</p>
if current.RescheduleReason != nil {
<p class="text-sm text-subtext1 mt-1">
Reason: { *current.RescheduleReason }
</p>
}
<p class="text-sm text-subtext1 mt-1">
A new time needs to be proposed.
</p>
</div>
} else if current.Status == db.ScheduleStatusPostponed {
<!-- Postponed (terminal - new proposal should follow) -->
<div class="text-center py-4">
<div class="text-4xl mb-3">⏸️</div>
<p class="text-lg text-peach font-medium">Postponed</p>
if current.RescheduleReason != nil {
<p class="text-sm text-subtext1 mt-1">
Reason: { *current.RescheduleReason }
</p>
}
<p class="text-sm text-subtext1 mt-1">
A new time needs to be proposed.
</p>
</div>
} else if current.Status == db.ScheduleStatusWithdrawn {
<!-- Withdrawn -->
<div class="text-center py-4">
<div class="text-4xl mb-3">↩️</div>
<p class="text-lg text-subtext0 font-medium">Proposal Withdrawn</p>
<p class="text-sm text-subtext1 mt-1">
The proposed time was withdrawn. A new time needs to be proposed.
</p>
</div>
}
</div>
</div>
}
templ fixtureScheduleActions(
fixture *db.Fixture,
current *db.FixtureSchedule,
canSchedule bool,
canManage bool,
userTeamID int,
) {
{{
// Determine what actions are available
showPropose := false
showReschedule := false
showPostpone := false
showCancel := false
if canSchedule {
if current == nil {
showPropose = true
} else if current.Status == db.ScheduleStatusRejected {
showPropose = true
} else if current.Status == db.ScheduleStatusRescheduled {
showPropose = true
} else if current.Status == db.ScheduleStatusPostponed {
showPropose = true
} else if current.Status == db.ScheduleStatusWithdrawn {
showPropose = true
} else if current.Status == db.ScheduleStatusAccepted {
showReschedule = true
showPostpone = true
}
}
if canManage && current != nil && !current.Status.IsTerminal() {
showCancel = true
}
}}
if showPropose || showReschedule || showPostpone || showCancel {
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Propose Time -->
if showPropose {
<div class="bg-mantle border border-surface1 rounded-lg">
<div class="bg-surface0 border-b border-surface1 px-4 py-3 rounded-t-lg">
<h3 class="text-md font-bold text-text">Propose Time</h3>
</div>
<div class="p-4">
<form
hx-post={ fmt.Sprintf("/fixtures/%d/schedule", fixture.ID) }
hx-swap="none"
class="space-y-4"
>
<div>
<label class="block text-sm font-medium text-subtext0 mb-1">
Date & Time
<span class="relative group inline-block ml-1">
<span class="px-1.5 py-0.5 bg-blue/20 text-blue rounded text-xs font-medium cursor-help">AEST/AEDT</span>
<span
class="absolute left-0 top-full mt-1 w-56 bg-crust border border-surface1 rounded-lg
p-2 text-xs text-subtext0 shadow-lg opacity-0 invisible
group-hover:opacity-100 group-hover:visible transition-all z-50"
>
Enter the time in Australian Eastern time (AEST/AEDT). It will be displayed in each user's local timezone.
</span>
</span>
</label>
<input
type="datetime-local"
name="scheduled_time"
required
class="w-full px-3 py-2 bg-base border border-surface1 rounded-lg text-text
focus:border-blue focus:outline-none"
/>
</div>
<button
type="submit"
class="w-full px-4 py-2 bg-blue hover:bg-blue/80 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Propose Time
</button>
</form>
</div>
</div>
}
<!-- Reschedule -->
if showReschedule {
<div class="bg-mantle border border-surface1 rounded-lg">
<div class="bg-surface0 border-b border-surface1 px-4 py-3 rounded-t-lg">
<h3 class="text-md font-bold text-text">Reschedule</h3>
</div>
<div class="p-4">
<form
hx-post={ fmt.Sprintf("/fixtures/%d/schedule/reschedule", fixture.ID) }
hx-swap="none"
class="space-y-4"
>
<div>
<label class="block text-sm font-medium text-subtext0 mb-1">
New Date & Time
<span class="relative group inline-block ml-1">
<span class="px-1.5 py-0.5 bg-blue/20 text-blue rounded text-xs font-medium cursor-help">AEST/AEDT</span>
<span
class="absolute left-0 top-full mt-1 w-56 bg-crust border border-surface1 rounded-lg
p-2 text-xs text-subtext0 shadow-lg opacity-0 invisible
group-hover:opacity-100 group-hover:visible transition-all z-50"
>
Enter the time in Australian Eastern time (AEST/AEDT). It will be displayed in each user's local timezone.
</span>
</span>
</label>
<input
type="datetime-local"
name="scheduled_time"
required
class="w-full px-3 py-2 bg-base border border-surface1 rounded-lg text-text
focus:border-blue focus:outline-none"
/>
</div>
<div>
<label class="block text-sm font-medium text-subtext0 mb-1">Reason</label>
@rescheduleReasonSelect(fixture)
</div>
<button
type="submit"
class="w-full px-4 py-2 bg-yellow hover:bg-yellow/80 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Reschedule
</button>
</form>
</div>
</div>
}
<!-- Postpone -->
if showPostpone {
<div class="bg-mantle border border-surface1 rounded-lg">
<div class="bg-surface0 border-b border-surface1 px-4 py-3 rounded-t-lg">
<h3 class="text-md font-bold text-text">Postpone</h3>
</div>
<div class="p-4">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-subtext0 mb-1">Reason</label>
@rescheduleReasonSelect(fixture)
</div>
<button
type="button"
@click={ fmt.Sprintf("if (!$el.parentElement.querySelector('select').value) { return; } window.dispatchEvent(new CustomEvent('confirm-action', { detail: { title: 'Postpone Fixture', message: 'Are you sure you want to postpone this fixture? The current schedule will be cleared.', action: () => htmx.ajax('POST', '/fixtures/%d/schedule/postpone', { swap: 'none', values: { reschedule_reason: $el.parentElement.querySelector('select').value } }) } }))", fixture.ID) }
class="w-full px-4 py-2 bg-peach hover:bg-peach/80 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Postpone Fixture
</button>
</div>
</div>
</div>
}
<!-- Declare Forfeit (moderator only) -->
if showCancel {
<div class="bg-mantle border border-red/30 rounded-lg">
<div class="bg-red/10 border-b border-red/30 px-4 py-3 rounded-t-lg">
<h3 class="text-md font-bold text-red">Declare Forfeit</h3>
</div>
<div class="p-4">
<p class="text-xs text-red/80 mb-3 font-medium">
This action is irreversible. Declaring a forfeit will permanently cancel the fixture schedule.
</p>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-subtext0 mb-1">Forfeiting Team</label>
@forfeitReasonSelect(fixture)
</div>
<button
type="button"
@click={ fmt.Sprintf("if (!$el.parentElement.querySelector('select').value) { return; } window.dispatchEvent(new CustomEvent('confirm-action', { detail: { title: 'Declare Forfeit', message: 'This action is IRREVERSIBLE. The fixture schedule will be permanently cancelled. Are you sure?', action: () => htmx.ajax('POST', '/fixtures/%d/schedule/cancel', { swap: 'none', values: { reschedule_reason: $el.parentElement.querySelector('select').value } }) } }))", fixture.ID) }
class="w-full px-4 py-2 bg-red hover:bg-red/80 text-mantle rounded-lg
font-medium transition hover:cursor-pointer"
>
Declare Forfeit
</button>
</div>
</div>
</div>
}
</div>
} else {
if !canSchedule && !canManage {
<div class="bg-mantle border border-surface1 rounded-lg p-6 text-center">
<p class="text-subtext1 text-sm">
Only team managers can manage fixture scheduling.
</p>
</div>
}
}
}
templ forfeitReasonSelect(fixture *db.Fixture) {
<select
name="reschedule_reason"
required
class="w-full px-3 py-2 bg-base border border-surface1 rounded-lg text-text
focus:border-blue focus:outline-none hover:cursor-pointer"
>
<option value="" disabled selected>Select forfeiting team</option>
<option value={ fmt.Sprintf("%s Forfeit", fixture.HomeTeam.Name) }>
{ fixture.HomeTeam.Name } Forfeit
</option>
<option value={ fmt.Sprintf("%s Forfeit", fixture.AwayTeam.Name) }>
{ fixture.AwayTeam.Name } Forfeit
</option>
</select>
}
templ rescheduleReasonSelect(fixture *db.Fixture) {
<select
name="reschedule_reason"
required
class="w-full px-3 py-2 bg-base border border-surface1 rounded-lg text-text
focus:border-blue focus:outline-none hover:cursor-pointer"
>
<option value="" disabled selected>Select a reason</option>
<option value="Mutually Agreed">Mutually Agreed</option>
<option value={ fmt.Sprintf("%s Unavailable", fixture.HomeTeam.Name) }>
{ fixture.HomeTeam.Name } Unavailable
</option>
<option value={ fmt.Sprintf("%s Unavailable", fixture.AwayTeam.Name) }>
{ fixture.AwayTeam.Name } Unavailable
</option>
<option value={ fmt.Sprintf("%s No-show", fixture.HomeTeam.Name) }>
{ fixture.HomeTeam.Name } No-show
</option>
<option value={ fmt.Sprintf("%s No-show", fixture.AwayTeam.Name) }>
{ fixture.AwayTeam.Name } No-show
</option>
</select>
}
templ fixtureScheduleHistory(fixture *db.Fixture, history []*db.FixtureSchedule) {
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
<div class="bg-surface0 border-b border-surface1 px-4 py-3">
<h2 class="text-lg font-bold text-text">Schedule History</h2>
</div>
<div class="p-4">
if len(history) == 0 {
<p class="text-subtext1 text-sm text-center py-4">No scheduling activity yet.</p>
} else {
<div class="space-y-3">
for i := len(history) - 1; i >= 0; i-- {
@scheduleHistoryItem(history[i], i == len(history)-1)
}
</div>
}
</div>
</div>
}
templ scheduleHistoryItem(schedule *db.FixtureSchedule, isCurrent bool) {
{{
statusColor := "text-subtext0"
statusBg := "bg-surface1"
statusLabel := string(schedule.Status)
switch schedule.Status {
case db.ScheduleStatusPending:
statusColor = "text-blue"
statusBg = "bg-blue/20"
statusLabel = "Pending"
case db.ScheduleStatusAccepted:
statusColor = "text-green"
statusBg = "bg-green/20"
statusLabel = "Accepted"
case db.ScheduleStatusRejected:
statusColor = "text-red"
statusBg = "bg-red/20"
statusLabel = "Rejected"
case db.ScheduleStatusRescheduled:
statusColor = "text-yellow"
statusBg = "bg-yellow/20"
statusLabel = "Rescheduled"
case db.ScheduleStatusPostponed:
statusColor = "text-peach"
statusBg = "bg-peach/20"
statusLabel = "Postponed"
case db.ScheduleStatusCancelled:
statusColor = "text-red"
statusBg = "bg-red/20"
statusLabel = "Cancelled"
case db.ScheduleStatusWithdrawn:
statusColor = "text-subtext0"
statusBg = "bg-surface1"
statusLabel = "Withdrawn"
}
}}
<div class={ "border rounded-lg p-3", templ.KV("border-surface1", !isCurrent), templ.KV("border-blue/30 bg-blue/5", isCurrent) }>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
if isCurrent {
<span class="text-xs px-1.5 py-0.5 bg-blue/20 text-blue rounded font-medium">
CURRENT
</span>
}
<span class={ "text-xs px-2 py-0.5 rounded font-medium", statusBg, statusColor }>
{ statusLabel }
</span>
</div>
<span class="text-xs text-subtext1">
@localtimeUnix(schedule.CreatedAt, "histdate")
</span>
</div>
<div class="space-y-1">
<div class="flex items-center gap-2 text-sm">
<span class="text-subtext0">Proposed by:</span>
<span class="text-text font-medium">{ schedule.ProposedBy.Name }</span>
</div>
if schedule.ScheduledTime != nil {
<div class="flex items-center gap-2 text-sm">
<span class="text-subtext0">Time:</span>
<span class="text-text">
@localtime(schedule.ScheduledTime, "datetime")
</span>
</div>
} else {
<div class="flex items-center gap-2 text-sm">
<span class="text-subtext0">Time:</span>
<span class="text-subtext1 italic">No time set</span>
</div>
}
if schedule.AcceptedBy != nil {
<div class="flex items-center gap-2 text-sm">
<span class="text-subtext0">Accepted by:</span>
<span class="text-text font-medium">{ schedule.AcceptedBy.Name }</span>
</div>
}
if schedule.RescheduleReason != nil {
<div class="flex items-center gap-2 text-sm">
<span class="text-subtext0">Reason:</span>
<span class="text-text">{ *schedule.RescheduleReason }</span>
</div>
}
</div>
</div>
}