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" // FixtureDetailLayout renders the fixture detail page layout with header and // tab navigation. Tab content is rendered as children. templ FixtureDetailLayout(activeTab string, fixture *db.Fixture, result *db.FixtureResult) { {{ backURL := fmt.Sprintf("/seasons/%s/leagues/%s/fixtures", fixture.Season.ShortName, fixture.League.ShortName) isFinalized := result != nil && result.Finalized }} @baseview.Layout(fmt.Sprintf("%s vs %s", fixture.HomeTeam.Name, fixture.AwayTeam.Name)) {

{ fixture.HomeTeam.Name } vs { fixture.AwayTeam.Name }

Round { fmt.Sprint(fixture.Round) } if fixture.GameWeek != nil { Game Week { fmt.Sprint(*fixture.GameWeek) } } { fixture.Season.Name } β€” { fixture.League.Name }
Back to Fixtures
{ children... }
} } 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/%s", fixture.ID, section) }}
  • { label }
  • } // ==================== Full page wrappers (for GET requests / direct navigation) ==================== templ FixtureDetailOverviewPage( fixture *db.Fixture, currentSchedule *db.FixtureSchedule, canSchedule bool, userTeamID int, result *db.FixtureResult, rosters map[string][]*db.PlayerWithPlayStatus, nominatedFreeAgents []*db.FixtureFreeAgent, availableFreeAgents []*db.SeasonLeagueFreeAgent, ) { @FixtureDetailLayout("overview", fixture, result) { @FixtureDetailOverviewContent(fixture, currentSchedule, canSchedule, userTeamID, result, rosters, nominatedFreeAgents, availableFreeAgents) } } templ FixtureDetailPreviewPage( fixture *db.Fixture, result *db.FixtureResult, rosters map[string][]*db.PlayerWithPlayStatus, previewData *db.MatchPreviewData, ) { @FixtureDetailLayout("preview", fixture, result) { @FixtureDetailPreviewContent(fixture, rosters, previewData) } } templ FixtureDetailAnalysisPage( fixture *db.Fixture, result *db.FixtureResult, rosters map[string][]*db.PlayerWithPlayStatus, previewData *db.MatchPreviewData, ) { @FixtureDetailLayout("analysis", fixture, result) { @FixtureDetailAnalysisContent(fixture, result, rosters, previewData) } } templ FixtureDetailSchedulePage( fixture *db.Fixture, currentSchedule *db.FixtureSchedule, history []*db.FixtureSchedule, canSchedule bool, userTeamID int, ) { @FixtureDetailLayout("scheduling", fixture, nil) { @FixtureDetailScheduleContent(fixture, currentSchedule, history, canSchedule, userTeamID) } } // ==================== Tab content components (for POST requests / HTMX swaps) ==================== templ FixtureDetailOverviewContent( fixture *db.Fixture, currentSchedule *db.FixtureSchedule, canSchedule bool, userTeamID int, result *db.FixtureResult, rosters map[string][]*db.PlayerWithPlayStatus, nominatedFreeAgents []*db.FixtureFreeAgent, availableFreeAgents []*db.SeasonLeagueFreeAgent, ) { {{ permCache := contexts.Permissions(ctx) canManage := permCache.HasPermission(permissions.FixturesManage) }} @fixtureOverviewTab(fixture, currentSchedule, result, rosters, canManage, canSchedule, userTeamID, nominatedFreeAgents, availableFreeAgents) } templ FixtureDetailPreviewContent( fixture *db.Fixture, rosters map[string][]*db.PlayerWithPlayStatus, previewData *db.MatchPreviewData, ) { @fixtureMatchPreviewTab(fixture, rosters, previewData) } templ FixtureDetailAnalysisContent( fixture *db.Fixture, result *db.FixtureResult, rosters map[string][]*db.PlayerWithPlayStatus, previewData *db.MatchPreviewData, ) { @fixtureMatchAnalysisTab(fixture, result, rosters, previewData) } templ FixtureDetailScheduleContent( fixture *db.Fixture, currentSchedule *db.FixtureSchedule, history []*db.FixtureSchedule, canSchedule bool, userTeamID int, ) { {{ permCache := contexts.Permissions(ctx) canManage := permCache.HasPermission(permissions.FixturesManage) }} @fixtureScheduleTab(fixture, currentSchedule, history, canSchedule, canManage, userTeamID) } // ==================== 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, ) {
    if result != nil { @fixtureResultDisplay(fixture, result) } else if canManage { @fixtureUploadPrompt(fixture) } else {

    No result has been uploaded for this fixture yet.

    }
    @fixtureScheduleSummary(fixture, currentSchedule, result)
    if (result == nil || !result.Finalized) && (canSchedule || canManage || len(nominatedFreeAgents) > 0) { @fixtureFreeAgentSection(fixture, canSchedule, canManage, userTeamID, nominatedFreeAgents, availableFreeAgents) }
    @fixtureTeamSection(fixture.HomeTeam, rosters["home"], "home", result, fixture.Season, fixture.League) @fixtureTeamSection(fixture.AwayTeam, rosters["away"], "away", result, fixture.Season, fixture.League)
    } templ fixtureScheduleSummary(fixture *db.Fixture, schedule *db.FixtureSchedule, result *db.FixtureResult) { {{ isPlayed := result != nil && result.Finalized }}

    Schedule

    if schedule == nil {

    No time scheduled

    } else if schedule.Status == db.ScheduleStatusAccepted && schedule.ScheduledTime != nil {
    if isPlayed { Played } else { Confirmed }

    @localtime(schedule.ScheduledTime, "date")

    @localtime(schedule.ScheduledTime, "time")

    } else if schedule.Status == db.ScheduleStatusPending && schedule.ScheduledTime != nil {
    Proposed

    @localtime(schedule.ScheduledTime, "date")

    @localtime(schedule.ScheduledTime, "time")

    Awaiting confirmation

    } else if schedule.Status == db.ScheduleStatusCancelled {
    Forfeit if schedule.RescheduleReason != nil {

    { *schedule.RescheduleReason }

    }
    } else {

    No time confirmed

    }
    } 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 } } }}

    Match Result

    if isForfeit { Forfeited } if result.Finalized { Finalized } else { Pending Review } if !result.Finalized && result.TamperingDetected { Inconsistent Data } if result.Finalized && result.TamperingDetected && result.TamperingReason != nil { }
    if !result.Finalized && result.TamperingDetected && result.TamperingReason != nil {
    Warning: Inconsistent Data

    { *result.TamperingReason }

    } if isForfeit {
    if isMutualForfeit {
    if fixture.HomeTeam.Color != "" { { fixture.HomeTeam.ShortName } } else { { fixture.HomeTeam.ShortName } }
    MUTUAL FORFEIT
    if fixture.AwayTeam.Color != "" { { fixture.AwayTeam.ShortName } } else { { fixture.AwayTeam.ShortName } }

    Both teams receive an overtime loss

    } else if isOutrightForfeit {
    if homeWon { 🏆 } if fixture.HomeTeam.Color != "" { { fixture.HomeTeam.ShortName } } else { { fixture.HomeTeam.ShortName } }
    FORFEIT
    if fixture.AwayTeam.Color != "" { { fixture.AwayTeam.ShortName } } else { { fixture.AwayTeam.ShortName } } if awayWon { 🏆 }

    { forfeitTeamName } forfeited the match

    } if result.ForfeitReason != nil && *result.ForfeitReason != "" {

    Reason

    { *result.ForfeitReason }

    }
    } else {
    if homeWon { 🏆 } if fixture.HomeTeam.Color != "" { { fixture.HomeTeam.ShortName } } else { { fixture.HomeTeam.ShortName } } { fmt.Sprint(result.HomeScore) }
    – if isOT { OT }
    { fmt.Sprint(result.AwayScore) } if fixture.AwayTeam.Color != "" { { fixture.AwayTeam.ShortName } } else { { fixture.AwayTeam.ShortName } } if awayWon { 🏆 }
    }
    } templ fixtureUploadPrompt(fixture *db.Fixture) {
    πŸ“‹

    No Result Uploaded

    Upload match log files to record the result of this fixture.

    Upload Match Logs
    @forfeitModal(fixture)
    } templ forfeitModal(fixture *db.Fixture) { } 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 }) } }}

    @links.TeamNameLinkInSeason(team, season, league)

    if team.Color != "" { }
    if len(players) == 0 {

    No players on roster.

    } else if showStats { if len(playing) > 0 {
    for _, p := range playing { if p.Stats != nil { } else { } }
    Player PP SC G A SV SH BL PA
    @links.PlayerLink(p.Player) if p.IsManager { } if p.IsFreeAgent { FREE AGENT } { intPtrStr(p.Stats.PeriodsPlayed) } { intPtrStr(p.Stats.Score) } { intPtrStr(p.Stats.Goals) } { intPtrStr(p.Stats.Assists) } { intPtrStr(p.Stats.Saves) } { intPtrStr(p.Stats.Shots) } { intPtrStr(p.Stats.Blocks) } { intPtrStr(p.Stats.Passes) }β€”
    } if len(bench) > 0 {

    Bench

    for _, p := range bench {
    @links.PlayerLink(p.Player) if p.IsManager { ★ Manager }
    }
    } } else {
    if len(playing) > 0 { if result != nil {

    Playing

    }
    for _, p := range playing {
    @links.PlayerLink(p.Player) if p.IsManager { ★ Manager } if p.IsFreeAgent { FREE AGENT }
    }
    } if result != nil && len(bench) > 0 {

    Bench

    for _, p := range bench {
    @links.PlayerLink(p.Player) if p.IsManager { ★ Manager }
    }
    }
    }
    } // ==================== 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 }}

    Free Agent Nominations

    if canNominate { }
    if len(nominated) == 0 {

    No free agents nominated for this fixture.

    } else {

    { fixture.HomeTeam.Name }

    if len(homeNominated) == 0 {

    None

    } else {
    for _, n := range homeNominated {
    @links.PlayerLink(n.Player) FA if canManage || (canSchedule && userTeamID == fixture.HomeTeamID) {
    }
    }
    }

    { fixture.AwayTeam.Name }

    if len(awayNominated) == 0 {

    None

    } else {
    for _, n := range awayNominated {
    @links.PlayerLink(n.Player) FA if canManage || (canSchedule && userTeamID == fixture.AwayTeamID) {
    }
    }
    }
    }
    if canNominate { }
    } // ==================== Schedule Tab ==================== templ fixtureScheduleTab( fixture *db.Fixture, currentSchedule *db.FixtureSchedule, history []*db.FixtureSchedule, canSchedule bool, canManage bool, userTeamID int, ) {
    @fixtureScheduleStatus(fixture, currentSchedule, canSchedule, canManage, userTeamID) @fixtureScheduleActions(fixture, currentSchedule, canSchedule, canManage, userTeamID) @fixtureScheduleHistory(fixture, history)
    } templ fixtureScheduleStatus( fixture *db.Fixture, current *db.FixtureSchedule, canSchedule bool, canManage bool, userTeamID int, ) {

    Schedule Status

    if current == nil {
    πŸ“…

    No time scheduled

    if canSchedule { Use the form to propose a time for this fixture. } else { A team manager needs to propose a time for this fixture. }

    } else if current.Status == db.ScheduleStatusPending && current.ScheduledTime != nil {
    ⏳

    Proposed: @localtime(current.ScheduledTime, "datetime")

    Proposed by { current.ProposedBy.Name } β€” awaiting response from the other team

    if canSchedule && userTeamID != current.ProposedByTeamID {
    } if canSchedule && userTeamID == current.ProposedByTeamID {
    }
    } else if current.Status == db.ScheduleStatusAccepted {
    βœ…

    Confirmed: @localtime(current.ScheduledTime, "datetime")

    Both teams have agreed on this time.

    } else if current.Status == db.ScheduleStatusRejected {
    ❌

    Proposal Rejected

    The proposed time was rejected. A new time needs to be proposed.

    } else if current.Status == db.ScheduleStatusCancelled {
    🚫

    Fixture Forfeited

    if current.RescheduleReason != nil {

    { *current.RescheduleReason }

    }
    } else if current.Status == db.ScheduleStatusRescheduled {
    πŸ”„

    Rescheduled

    if current.RescheduleReason != nil {

    Reason: { *current.RescheduleReason }

    }

    A new time needs to be proposed.

    } else if current.Status == db.ScheduleStatusPostponed {
    ⏸️

    Postponed

    if current.RescheduleReason != nil {

    Reason: { *current.RescheduleReason }

    }

    A new time needs to be proposed.

    } else if current.Status == db.ScheduleStatusWithdrawn {
    ↩️

    Proposal Withdrawn

    The proposed time was withdrawn. A new time needs to be proposed.

    }
    } 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 {
    if showPropose {

    Propose Time

    } if showReschedule {

    Reschedule

    @rescheduleReasonSelect(fixture)
    } if showPostpone {

    Postpone

    @rescheduleReasonSelect(fixture)
    } if showCancel {

    Declare Forfeit

    This action is irreversible. Declaring a forfeit will permanently cancel the fixture schedule.

    @forfeitReasonSelect(fixture)
    }
    } else { if !canSchedule && !canManage {

    Only team managers can manage fixture scheduling.

    } } } templ forfeitReasonSelect(fixture *db.Fixture) { } templ rescheduleReasonSelect(fixture *db.Fixture) { } templ fixtureScheduleHistory(fixture *db.Fixture, history []*db.FixtureSchedule) {

    Schedule History

    if len(history) == 0 {

    No scheduling activity yet.

    } else {
    for i := len(history) - 1; i >= 0; i-- { @scheduleHistoryItem(history[i], i == len(history)-1) }
    }
    } 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" } }}
    if isCurrent { CURRENT } { statusLabel }
    @localtimeUnix(schedule.CreatedAt, "histdate")
    Proposed by: { schedule.ProposedBy.Name }
    if schedule.ScheduledTime != nil {
    Time: @localtime(schedule.ScheduledTime, "datetime")
    } else {
    Time: No time set
    } if schedule.AcceptedBy != nil {
    Accepted by: { schedule.AcceptedBy.Name }
    } if schedule.RescheduleReason != nil {
    Reason: { *schedule.RescheduleReason }
    }
    }