293 lines
11 KiB
Plaintext
293 lines
11 KiB
Plaintext
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"
|
|
|
|
templ FixtureReviewResultPage(
|
|
fixture *db.Fixture,
|
|
result *db.FixtureResult,
|
|
unmappedPlayers []string,
|
|
unnominatedFreeAgents []FreeAgentWarning,
|
|
) {
|
|
{{
|
|
backURL := fmt.Sprintf("/fixtures/%d", fixture.ID)
|
|
}}
|
|
@baseview.Layout(fmt.Sprintf("Review Result — %s vs %s", fixture.HomeTeam.Name, fixture.AwayTeam.Name)) {
|
|
<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 Match Result</h1>
|
|
<p class="text-sm text-subtext1">
|
|
<span>
|
|
@links.TeamNameLinkInSeason(fixture.HomeTeam, fixture.Season, fixture.League)
|
|
</span>
|
|
vs
|
|
<span>
|
|
@links.TeamNameLinkInSeason(fixture.AwayTeam, fixture.Season, fixture.League)
|
|
</span>
|
|
<span class="text-subtext0 ml-1">
|
|
Round { fmt.Sprint(fixture.Round) }
|
|
</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 Fixture
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Warnings Section -->
|
|
if result.TamperingDetected || len(unmappedPlayers) > 0 || len(unnominatedFreeAgents) > 0 {
|
|
<div class="space-y-4 mb-6">
|
|
if result.TamperingDetected && result.TamperingReason != nil {
|
|
<div class="bg-red/10 border border-red/30 rounded-lg p-4">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<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-2">
|
|
This does not block finalization but should be reviewed carefully.
|
|
</p>
|
|
</div>
|
|
}
|
|
if len(unnominatedFreeAgents) > 0 {
|
|
<div class="bg-yellow/10 border border-yellow/30 rounded-lg p-4">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<span class="text-yellow font-bold text-sm">⚠ Free Agent Nomination Issues</span>
|
|
</div>
|
|
<p class="text-yellow/80 text-sm mb-2">
|
|
The following free agents have nomination issues that should be reviewed before finalizing.
|
|
</p>
|
|
<ul class="list-disc list-inside text-yellow/70 text-xs space-y-0.5">
|
|
for _, fa := range unnominatedFreeAgents {
|
|
<li>
|
|
<span class="text-yellow font-medium">{ fa.Name }</span>
|
|
<span class="text-yellow/60"> — { fa.Reason }</span>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
}
|
|
if len(unmappedPlayers) > 0 {
|
|
<div class="bg-yellow/10 border border-yellow/30 rounded-lg p-4">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<span class="text-yellow font-bold text-sm">⚠ Unmapped Players</span>
|
|
</div>
|
|
<p class="text-yellow/80 text-sm mb-2">
|
|
The following players could not be matched to registered players.
|
|
They may be free agents or have unregistered Slapshot IDs.
|
|
</p>
|
|
<ul class="list-disc list-inside text-yellow/70 text-xs space-y-0.5">
|
|
for _, p := range unmappedPlayers {
|
|
<li>{ p }</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
<!-- Score Overview -->
|
|
<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">Score</h2>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="flex items-center justify-center gap-8 py-4">
|
|
<div class="text-center">
|
|
<p class="text-sm text-subtext0 mb-1">
|
|
@links.TeamNameLinkInSeason(fixture.HomeTeam, fixture.Season, fixture.League)
|
|
</p>
|
|
<p class="text-4xl font-bold text-text">{ 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">
|
|
@links.TeamNameLinkInSeason(fixture.AwayTeam, fixture.Season, fixture.League)
|
|
</p>
|
|
<p class="text-4xl font-bold text-text">{ 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:
|
|
if result.Winner == "home" {
|
|
{ fixture.HomeTeam.Name }
|
|
} else if result.Winner == "away" {
|
|
{ fixture.AwayTeam.Name }
|
|
} else {
|
|
Draw
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Player Stats Tables -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
@reviewTeamStats(fixture.HomeTeam, result, "home", fixture.Season, fixture.League)
|
|
@reviewTeamStats(fixture.AwayTeam, result, "away", fixture.Season, fixture.League)
|
|
</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">
|
|
<form
|
|
hx-post={ fmt.Sprintf("/fixtures/%d/results/finalize", fixture.ID) }
|
|
hx-swap="none"
|
|
>
|
|
<button
|
|
type="submit"
|
|
class="px-6 py-3 bg-green hover:bg-green/75 text-mantle rounded-lg
|
|
font-medium transition hover:cursor-pointer text-lg"
|
|
>
|
|
Finalize Result
|
|
</button>
|
|
</form>
|
|
<button
|
|
type="button"
|
|
onclick={ templ.JSUnsafeFuncCall(fmt.Sprintf("window.dispatchEvent(new CustomEvent('confirm-action', { detail: { title: 'Discard Result', message: 'Are you sure you want to discard this result? You will need to re-upload the match logs.', action: () => htmx.ajax('POST', '/fixtures/%d/results/discard', { swap: 'none' }) } }))", fixture.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 reviewTeamStats(team *db.Team, result *db.FixtureResult, side string, season *db.Season, league *db.League) {
|
|
{{
|
|
// Collect unique players for this team across all periods
|
|
// We'll show the period 3 (final/cumulative) stats
|
|
type playerStat struct {
|
|
Username string
|
|
PlayerID *int
|
|
Stats *db.FixtureResultPlayerStats
|
|
}
|
|
finalStats := []*playerStat{}
|
|
seen := map[string]bool{}
|
|
// Find period 3 stats for this team (cumulative)
|
|
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 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 text-text">
|
|
if side == "home" {
|
|
Home —
|
|
} else {
|
|
Away —
|
|
}
|
|
@links.TeamNameLinkInSeason(team, season, league)
|
|
</h3>
|
|
</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">
|
|
FREE AGENT
|
|
</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>
|
|
}
|
|
|
|
func intPtrStr(v *int) string {
|
|
if v == nil {
|
|
return "-"
|
|
}
|
|
return fmt.Sprint(*v)
|
|
}
|
|
|
|
func ordinal(n int) string {
|
|
suffix := "th"
|
|
if n%100 >= 11 && n%100 <= 13 {
|
|
// 11th, 12th, 13th
|
|
} else {
|
|
switch n % 10 {
|
|
case 1:
|
|
suffix = "st"
|
|
case 2:
|
|
suffix = "nd"
|
|
case 3:
|
|
suffix = "rd"
|
|
}
|
|
}
|
|
return fmt.Sprintf("%d%s", n, suffix)
|
|
}
|