Files
oslstats/internal/view/seasonsview/series_review_result.templ
2026-03-15 17:43:39 +11:00

375 lines
14 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/view/baseview"
import "git.haelnorr.com/h/oslstats/internal/view/component/links"
import "fmt"
// SeriesGameResult holds the parsed result for a single game in the series review
type SeriesGameResult struct {
GameNumber int
Result *db.FixtureResult
UnmappedPlayers []string
FreeAgentWarnings []FreeAgentWarning
}
templ SeriesReviewResultPage(
series *db.PlayoffSeries,
gameResults []*SeriesGameResult,
) {
{{
backURL := fmt.Sprintf("/series/%d", series.ID)
team1Name := seriesTeamName(series.Team1)
team2Name := seriesTeamName(series.Team2)
// Calculate series score from the results
team1Wins := 0
team2Wins := 0
for _, gr := range gameResults {
if gr.Result != nil {
if gr.Result.Winner == "home" {
team1Wins++
} else {
team2Wins++
}
}
}
}}
@baseview.Layout(fmt.Sprintf("Review Series Result — %s vs %s", team1Name, team2Name)) {
<div class="max-w-screen-xl 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-6">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 class="text-2xl font-bold text-text mb-1">Review Series Result</h1>
<p class="text-sm text-subtext1">
{ team1Name } vs { team2Name }
<span class="text-subtext0 ml-1">{ series.Label }</span>
</p>
</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 Series
</a>
</div>
</div>
</div>
<!-- Series Score Summary -->
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden mb-6">
<div class="bg-surface0 border-b border-surface1 px-4 py-3">
<h2 class="text-lg font-bold text-text">Series Result</h2>
</div>
<div class="p-6">
<div class="flex items-center justify-center gap-8 py-2">
<div class="flex flex-col items-center text-center">
if series.Team1 != nil && series.Team1.Color != "" {
<div
class="w-10 h-10 rounded-full border-2 border-surface1 mb-2 shrink-0"
style={ "background-color: " + templ.SafeCSS(series.Team1.Color) }
></div>
}
<p class="text-sm font-medium text-subtext0 mb-1">{ team1Name }</p>
<p class={ "text-5xl font-bold", templ.KV("text-green", team1Wins > team2Wins), templ.KV("text-text", team1Wins <= team2Wins) }>
{ fmt.Sprint(team1Wins) }
</p>
if team1Wins > team2Wins {
<span class="mt-1 px-2 py-0.5 bg-green/20 text-green rounded-full text-xs font-bold uppercase tracking-wider">Winner</span>
}
</div>
<span class="text-3xl text-subtext0 font-light"></span>
<div class="flex flex-col items-center text-center">
if series.Team2 != nil && series.Team2.Color != "" {
<div
class="w-10 h-10 rounded-full border-2 border-surface1 mb-2 shrink-0"
style={ "background-color: " + templ.SafeCSS(series.Team2.Color) }
></div>
}
<p class="text-sm font-medium text-subtext0 mb-1">{ team2Name }</p>
<p class={ "text-5xl font-bold", templ.KV("text-green", team2Wins > team1Wins), templ.KV("text-text", team2Wins <= team1Wins) }>
{ fmt.Sprint(team2Wins) }
</p>
if team2Wins > team1Wins {
<span class="mt-1 px-2 py-0.5 bg-green/20 text-green rounded-full text-xs font-bold uppercase tracking-wider">Winner</span>
}
</div>
</div>
<p class="text-center text-sm text-subtext1 mt-3">
{ fmt.Sprint(len(gameResults)) } game(s) played
</p>
</div>
</div>
<!-- Per-Game Results -->
<div class="space-y-4 mb-6">
for _, gr := range gameResults {
@seriesReviewGameCard(series, gr)
}
</div>
<!-- Actions -->
<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">Actions</h2>
</div>
<div class="p-6">
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<button
type="button"
hx-post={ fmt.Sprintf("/series/%d/results/finalize", series.ID) }
hx-swap="none"
class="px-6 py-3 bg-green hover:bg-green/75 text-mantle rounded-lg
font-semibold transition hover:cursor-pointer text-lg"
>
Finalize Series
</button>
<button
type="button"
onclick={ templ.JSUnsafeFuncCall(fmt.Sprintf("window.dispatchEvent(new CustomEvent('confirm-action', { detail: { title: 'Discard All Results', message: 'Are you sure you want to discard all uploaded results? You will need to re-upload the match logs for every game.', action: () => htmx.ajax('POST', '/series/%d/results/discard', { swap: 'none' }) } }))", series.ID)) }
class="px-6 py-3 bg-red hover:bg-red/80 text-mantle rounded-lg
font-medium transition hover:cursor-pointer text-lg"
>
Discard & Re-upload
</button>
</div>
</div>
</div>
</div>
}
}
templ seriesReviewGameCard(series *db.PlayoffSeries, gr *SeriesGameResult) {
{{
team1Name := seriesTeamName(series.Team1)
team2Name := seriesTeamName(series.Team2)
result := gr.Result
homeWon := result.Winner == "home"
winnerName := team2Name
if homeWon {
winnerName = team1Name
}
hasWarnings := result.TamperingDetected || len(gr.UnmappedPlayers) > 0 || len(gr.FreeAgentWarnings) > 0
}}
<div
class="bg-mantle border border-surface1 rounded-lg overflow-hidden"
x-data="{ expanded: true }"
>
<!-- Game Header (clickable to expand/collapse) -->
<div
class="bg-surface0 border-b border-surface1 px-4 py-3 flex items-center justify-between
hover:bg-surface1 transition hover:cursor-pointer"
@click="expanded = !expanded"
>
<div class="flex items-center gap-3">
<h3 class="text-md font-bold text-text">Game { fmt.Sprint(gr.GameNumber) }</h3>
if hasWarnings {
<span class="text-yellow text-sm" title="Has warnings">⚠</span>
}
</div>
<div class="flex items-center gap-3">
<span class="text-sm text-subtext0">
{ team1Name }
<span class="font-bold text-text mx-1">{ fmt.Sprint(result.HomeScore) }</span>
-
<span class="font-bold text-text mx-1">{ fmt.Sprint(result.AwayScore) }</span>
{ team2Name }
</span>
<span class="px-2 py-0.5 bg-green/20 text-green rounded text-xs font-medium">
{ winnerName }
</span>
<!-- Expand/collapse indicator -->
<svg
class="w-4 h-4 text-subtext0 transition-transform"
:class="expanded && 'rotate-180'"
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
<!-- Collapsible Content -->
<div x-show="expanded" x-collapse>
<!-- Warnings -->
if hasWarnings {
<div class="p-4 space-y-3 border-b border-surface1">
if result.TamperingDetected && result.TamperingReason != nil {
<div class="bg-red/10 border border-red/30 rounded-lg p-3">
<div class="flex items-center gap-2 mb-1">
<span class="text-red font-bold text-sm">⚠ Inconsistent Data Detected</span>
</div>
<p class="text-red/80 text-sm">{ *result.TamperingReason }</p>
<p class="text-red/60 text-xs mt-1">
This does not block finalization but should be reviewed carefully.
</p>
</div>
}
if len(gr.FreeAgentWarnings) > 0 {
<div class="bg-yellow/10 border border-yellow/30 rounded-lg p-3">
<div class="flex items-center gap-2 mb-1">
<span class="text-yellow font-bold text-sm">⚠ Free Agent Issues</span>
</div>
<ul class="list-disc list-inside text-yellow/70 text-xs space-y-0.5">
for _, fa := range gr.FreeAgentWarnings {
<li>
<span class="text-yellow font-medium">{ fa.Name }</span>
<span class="text-yellow/60"> — { fa.Reason }</span>
</li>
}
</ul>
</div>
}
if len(gr.UnmappedPlayers) > 0 {
<div class="bg-yellow/10 border border-yellow/30 rounded-lg p-3">
<div class="flex items-center gap-2 mb-1">
<span class="text-yellow font-bold text-sm">⚠ Unmapped Players</span>
</div>
<p class="text-yellow/80 text-sm mb-1">
Could not be matched to registered players.
</p>
<ul class="list-disc list-inside text-yellow/70 text-xs space-y-0.5">
for _, p := range gr.UnmappedPlayers {
<li>{ p }</li>
}
</ul>
</div>
}
</div>
}
<!-- Score Display -->
<div class="p-6 border-b border-surface1">
<div class="flex items-center justify-center gap-8 py-2">
<div class="text-center">
<p class="text-sm text-subtext0 mb-1">{ team1Name }</p>
<p class={ "text-4xl font-bold", templ.KV("text-green", homeWon), templ.KV("text-text", !homeWon) }>
{ fmt.Sprint(result.HomeScore) }
</p>
</div>
<div class="text-2xl text-subtext0 font-light">—</div>
<div class="text-center">
<p class="text-sm text-subtext0 mb-1">{ team2Name }</p>
<p class={ "text-4xl font-bold", templ.KV("text-green", !homeWon), templ.KV("text-text", homeWon) }>
{ fmt.Sprint(result.AwayScore) }
</p>
</div>
</div>
<div class="flex flex-wrap items-center justify-center gap-4 mt-2 text-xs text-subtext1">
if result.Arena != "" {
<span>{ result.Arena }</span>
}
if result.EndReason != "" {
<span>{ result.EndReason }</span>
}
<span>
Winner: { winnerName }
</span>
</div>
</div>
<!-- Player Stats Tables -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-0 lg:divide-x divide-surface1">
if series.Team1 != nil {
@seriesReviewTeamStats(series.Team1, result, "home", series.Bracket.Season, series.Bracket.League)
}
if series.Team2 != nil {
@seriesReviewTeamStats(series.Team2, result, "away", series.Bracket.Season, series.Bracket.League)
}
</div>
</div>
</div>
}
templ seriesReviewTeamStats(team *db.Team, result *db.FixtureResult, side string, season *db.Season, league *db.League) {
{{
type playerStat struct {
Username string
PlayerID *int
Stats *db.FixtureResultPlayerStats
}
finalStats := []*playerStat{}
seen := map[string]bool{}
for _, ps := range result.PlayerStats {
if ps.Team == side && ps.PeriodNum == 3 {
if !seen[ps.PlayerGameUserID] {
seen[ps.PlayerGameUserID] = true
finalStats = append(finalStats, &playerStat{
Username: ps.PlayerUsername,
PlayerID: ps.PlayerID,
Stats: ps,
})
}
}
}
}}
<div>
<div class="bg-surface0 border-b border-surface1 px-4 py-2 flex items-center gap-2">
if team.Color != "" {
<span
class="w-3 h-3 rounded-full shrink-0"
style={ "background-color: " + templ.SafeCSS(team.Color) }
></span>
}
<h4 class="text-sm font-bold text-text">
if side == "home" {
Team 1 —
} else {
Team 2 —
}
@links.TeamNameLinkInSeason(team, season, league)
</h4>
</div>
<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="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>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Score">SC</th>
</tr>
</thead>
<tbody class="divide-y divide-surface1">
for _, ps := range finalStats {
<tr class="hover:bg-surface0 transition-colors">
<td class="px-3 py-2 text-sm">
<span class="flex items-center gap-1.5">
if ps.PlayerID != nil {
@links.PlayerLinkFromStats(*ps.PlayerID, ps.Username)
} else {
<span class="text-text">{ ps.Username }</span>
<span class="text-yellow text-xs" title="Unmapped player">?</span>
}
if ps.Stats.IsFreeAgent {
<span class="px-1.5 py-0.5 bg-peach/20 text-peach rounded text-xs font-medium">
FA
</span>
}
</span>
</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ intPtrStr(ps.Stats.PeriodsPlayed) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Goals) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Assists) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Saves) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Shots) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Blocks) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Passes) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Score) }</td>
</tr>
}
if len(finalStats) == 0 {
<tr>
<td colspan="9" class="px-3 py-4 text-center text-sm text-subtext1">
No player stats recorded
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}