Merge branch 'forfeits' into development
This commit is contained in:
@@ -31,6 +31,12 @@ type FixtureResult struct {
|
|||||||
TamperingDetected bool `bun:",default:false"`
|
TamperingDetected bool `bun:",default:false"`
|
||||||
TamperingReason *string
|
TamperingReason *string
|
||||||
|
|
||||||
|
// Forfeit-related fields
|
||||||
|
IsForfeit bool `bun:"is_forfeit,default:false"`
|
||||||
|
ForfeitType *string `bun:"forfeit_type"` // "mutual" or "outright"
|
||||||
|
ForfeitTeam *string `bun:"forfeit_team"` // "home" or "away" (nil for mutual)
|
||||||
|
ForfeitReason *string `bun:"forfeit_reason"` // User-provided reason
|
||||||
|
|
||||||
Fixture *Fixture `bun:"rel:belongs-to,join:fixture_id=id"`
|
Fixture *Fixture `bun:"rel:belongs-to,join:fixture_id=id"`
|
||||||
UploadedBy *User `bun:"rel:belongs-to,join:uploaded_by_user_id=id"`
|
UploadedBy *User `bun:"rel:belongs-to,join:uploaded_by_user_id=id"`
|
||||||
PlayerStats []*FixtureResultPlayerStats `bun:"rel:has-many,join:id=fixture_result_id"`
|
PlayerStats []*FixtureResultPlayerStats `bun:"rel:has-many,join:id=fixture_result_id"`
|
||||||
@@ -95,6 +101,106 @@ type PlayerWithPlayStatus struct {
|
|||||||
Stats *FixtureResultPlayerStats // Period 3 (final cumulative) stats, nil if no result
|
Stats *FixtureResultPlayerStats // Period 3 (final cumulative) stats, nil if no result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forfeit type constants
|
||||||
|
const (
|
||||||
|
ForfeitTypeMutual = "mutual"
|
||||||
|
ForfeitTypeOutright = "outright"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateForfeitResult creates a finalized forfeit result for a fixture.
|
||||||
|
// For outright forfeits, forfeitTeam specifies which team ("home" or "away") forfeited.
|
||||||
|
// For mutual forfeits, forfeitTeam should be empty.
|
||||||
|
func CreateForfeitResult(
|
||||||
|
ctx context.Context,
|
||||||
|
tx bun.Tx,
|
||||||
|
fixture *Fixture,
|
||||||
|
forfeitType string,
|
||||||
|
forfeitTeam string,
|
||||||
|
reason string,
|
||||||
|
userID int,
|
||||||
|
audit *AuditMeta,
|
||||||
|
) (*FixtureResult, error) {
|
||||||
|
if fixture == nil {
|
||||||
|
return nil, errors.New("fixture cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate forfeit type
|
||||||
|
if forfeitType != ForfeitTypeMutual && forfeitType != ForfeitTypeOutright {
|
||||||
|
return nil, BadRequest("invalid forfeit type: must be 'mutual' or 'outright'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate forfeit team for outright forfeits
|
||||||
|
if forfeitType == ForfeitTypeOutright {
|
||||||
|
if forfeitTeam != "home" && forfeitTeam != "away" {
|
||||||
|
return nil, BadRequest("outright forfeit requires a team: must be 'home' or 'away'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine winner and scores based on forfeit type
|
||||||
|
var winner string
|
||||||
|
var homeScore, awayScore int
|
||||||
|
var endReason string
|
||||||
|
var forfeitTeamPtr *string
|
||||||
|
|
||||||
|
switch forfeitType {
|
||||||
|
case ForfeitTypeMutual:
|
||||||
|
// Mutual forfeit: both teams get an OT loss, no winner
|
||||||
|
// Use "draw" as winner to signal mutual loss
|
||||||
|
winner = "draw"
|
||||||
|
homeScore = 0
|
||||||
|
awayScore = 0
|
||||||
|
endReason = "Forfeit"
|
||||||
|
case ForfeitTypeOutright:
|
||||||
|
// Outright forfeit: forfeiting team loses, opponent wins
|
||||||
|
forfeitTeamPtr = &forfeitTeam
|
||||||
|
if forfeitTeam == "home" {
|
||||||
|
winner = "away"
|
||||||
|
} else {
|
||||||
|
winner = "home"
|
||||||
|
}
|
||||||
|
homeScore = 0
|
||||||
|
awayScore = 0
|
||||||
|
endReason = "Forfeit"
|
||||||
|
}
|
||||||
|
|
||||||
|
var reasonPtr *string
|
||||||
|
if reason != "" {
|
||||||
|
reasonPtr = &reason
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &FixtureResult{
|
||||||
|
FixtureID: fixture.ID,
|
||||||
|
Winner: winner,
|
||||||
|
HomeScore: homeScore,
|
||||||
|
AwayScore: awayScore,
|
||||||
|
EndReason: endReason,
|
||||||
|
UploadedByUserID: userID,
|
||||||
|
Finalized: true, // Forfeits are immediately finalized
|
||||||
|
IsForfeit: true,
|
||||||
|
ForfeitType: &forfeitType,
|
||||||
|
ForfeitTeam: forfeitTeamPtr,
|
||||||
|
ForfeitReason: reasonPtr,
|
||||||
|
CreatedAt: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Insert(tx, result).WithAudit(audit, &AuditInfo{
|
||||||
|
Action: "fixture_results.forfeit",
|
||||||
|
ResourceType: "fixture_result",
|
||||||
|
ResourceID: nil,
|
||||||
|
Details: map[string]any{
|
||||||
|
"fixture_id": fixture.ID,
|
||||||
|
"forfeit_type": forfeitType,
|
||||||
|
"forfeit_team": forfeitTeam,
|
||||||
|
"reason": reason,
|
||||||
|
},
|
||||||
|
}).Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Insert forfeit result")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// InsertFixtureResult stores a new match result with all player stats in a single transaction.
|
// InsertFixtureResult stores a new match result with all player stats in a single transaction.
|
||||||
func InsertFixtureResult(
|
func InsertFixtureResult(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@@ -352,6 +458,7 @@ const (
|
|||||||
|
|
||||||
// ComputeTeamRecord calculates W-OTW-OTL-L, GF/GA, and points from fixtures and results.
|
// ComputeTeamRecord calculates W-OTW-OTL-L, GF/GA, and points from fixtures and results.
|
||||||
// Points: Win=3, OT Win=2, OT Loss=1, Loss=0.
|
// Points: Win=3, OT Win=2, OT Loss=1, Loss=0.
|
||||||
|
// Forfeits: Outright = Win(3)/Loss(0), Mutual = OT Loss(1) for both teams.
|
||||||
func ComputeTeamRecord(teamID int, fixtures []*Fixture, resultMap map[int]*FixtureResult) *TeamRecord {
|
func ComputeTeamRecord(teamID int, fixtures []*Fixture, resultMap map[int]*FixtureResult) *TeamRecord {
|
||||||
rec := &TeamRecord{}
|
rec := &TeamRecord{}
|
||||||
for _, f := range fixtures {
|
for _, f := range fixtures {
|
||||||
@@ -361,6 +468,34 @@ func ComputeTeamRecord(teamID int, fixtures []*Fixture, resultMap map[int]*Fixtu
|
|||||||
}
|
}
|
||||||
rec.Played++
|
rec.Played++
|
||||||
isHome := f.HomeTeamID == teamID
|
isHome := f.HomeTeamID == teamID
|
||||||
|
|
||||||
|
// Handle forfeits separately
|
||||||
|
if res.IsForfeit {
|
||||||
|
// Forfeits have 0-0 score, no goal impact
|
||||||
|
if res.ForfeitType != nil && *res.ForfeitType == ForfeitTypeMutual {
|
||||||
|
// Mutual forfeit: both teams get OT loss (1 point)
|
||||||
|
rec.OvertimeLosses++
|
||||||
|
rec.Points += PointsOvertimeLoss
|
||||||
|
} else if res.ForfeitType != nil && *res.ForfeitType == ForfeitTypeOutright {
|
||||||
|
// Outright forfeit: check if this team forfeited
|
||||||
|
thisSide := "away"
|
||||||
|
if isHome {
|
||||||
|
thisSide = "home"
|
||||||
|
}
|
||||||
|
if res.ForfeitTeam != nil && *res.ForfeitTeam == thisSide {
|
||||||
|
// This team forfeited - loss
|
||||||
|
rec.Losses++
|
||||||
|
rec.Points += PointsLoss
|
||||||
|
} else {
|
||||||
|
// Opponent forfeited - win
|
||||||
|
rec.Wins++
|
||||||
|
rec.Points += PointsWin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal match handling
|
||||||
if isHome {
|
if isHome {
|
||||||
rec.GoalsFor += res.HomeScore
|
rec.GoalsFor += res.HomeScore
|
||||||
rec.GoalsAgainst += res.AwayScore
|
rec.GoalsAgainst += res.AwayScore
|
||||||
|
|||||||
89
internal/db/migrations/20260305140000_add_forfeit_support.go
Normal file
89
internal/db/migrations/20260305140000_add_forfeit_support.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.haelnorr.com/h/oslstats/internal/db"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Migrations.MustRegister(
|
||||||
|
// UP migration
|
||||||
|
func(ctx context.Context, conn *bun.DB) error {
|
||||||
|
// Add is_forfeit column
|
||||||
|
_, err := conn.NewAddColumn().
|
||||||
|
Model((*db.FixtureResult)(nil)).
|
||||||
|
ColumnExpr("is_forfeit BOOLEAN NOT NULL DEFAULT false").
|
||||||
|
IfNotExists().
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add forfeit_type column
|
||||||
|
_, err = conn.NewAddColumn().
|
||||||
|
Model((*db.FixtureResult)(nil)).
|
||||||
|
ColumnExpr("forfeit_type VARCHAR").
|
||||||
|
IfNotExists().
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add forfeit_team column
|
||||||
|
_, err = conn.NewAddColumn().
|
||||||
|
Model((*db.FixtureResult)(nil)).
|
||||||
|
ColumnExpr("forfeit_team VARCHAR").
|
||||||
|
IfNotExists().
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add forfeit_reason column
|
||||||
|
_, err = conn.NewAddColumn().
|
||||||
|
Model((*db.FixtureResult)(nil)).
|
||||||
|
ColumnExpr("forfeit_reason VARCHAR").
|
||||||
|
IfNotExists().
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
// DOWN migration
|
||||||
|
func(ctx context.Context, conn *bun.DB) error {
|
||||||
|
_, err := conn.NewDropColumn().
|
||||||
|
Model((*db.FixtureResult)(nil)).
|
||||||
|
ColumnExpr("forfeit_reason").
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conn.NewDropColumn().
|
||||||
|
Model((*db.FixtureResult)(nil)).
|
||||||
|
ColumnExpr("forfeit_team").
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conn.NewDropColumn().
|
||||||
|
Model((*db.FixtureResult)(nil)).
|
||||||
|
ColumnExpr("forfeit_type").
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conn.NewDropColumn().
|
||||||
|
Model((*db.FixtureResult)(nil)).
|
||||||
|
ColumnExpr("is_forfeit").
|
||||||
|
Exec(ctx)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -906,6 +906,12 @@
|
|||||||
.border-overlay0 {
|
.border-overlay0 {
|
||||||
border-color: var(--overlay0);
|
border-color: var(--overlay0);
|
||||||
}
|
}
|
||||||
|
.border-peach\/50 {
|
||||||
|
border-color: var(--peach);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border-color: color-mix(in oklab, var(--peach) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
.border-red {
|
.border-red {
|
||||||
border-color: var(--red);
|
border-color: var(--red);
|
||||||
}
|
}
|
||||||
@@ -915,6 +921,12 @@
|
|||||||
border-color: color-mix(in oklab, var(--red) 30%, transparent);
|
border-color: color-mix(in oklab, var(--red) 30%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.border-red\/50 {
|
||||||
|
border-color: var(--red);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border-color: color-mix(in oklab, var(--red) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
.border-surface0 {
|
.border-surface0 {
|
||||||
border-color: var(--surface0);
|
border-color: var(--surface0);
|
||||||
}
|
}
|
||||||
@@ -1008,6 +1020,12 @@
|
|||||||
.bg-peach {
|
.bg-peach {
|
||||||
background-color: var(--peach);
|
background-color: var(--peach);
|
||||||
}
|
}
|
||||||
|
.bg-peach\/5 {
|
||||||
|
background-color: var(--peach);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--peach) 5%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
.bg-peach\/20 {
|
.bg-peach\/20 {
|
||||||
background-color: var(--peach);
|
background-color: var(--peach);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
@@ -1017,6 +1035,12 @@
|
|||||||
.bg-red {
|
.bg-red {
|
||||||
background-color: var(--red);
|
background-color: var(--red);
|
||||||
}
|
}
|
||||||
|
.bg-red\/5 {
|
||||||
|
background-color: var(--red);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--red) 5%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
.bg-red\/10 {
|
.bg-red\/10 {
|
||||||
background-color: var(--red);
|
background-color: var(--red);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
@@ -1848,6 +1872,16 @@
|
|||||||
--tw-ring-color: var(--mauve);
|
--tw-ring-color: var(--mauve);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.focus\:ring-peach {
|
||||||
|
&:focus {
|
||||||
|
--tw-ring-color: var(--peach);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:ring-red {
|
||||||
|
&:focus {
|
||||||
|
--tw-ring-color: var(--red);
|
||||||
|
}
|
||||||
|
}
|
||||||
.focus\:outline-none {
|
.focus\:outline-none {
|
||||||
&:focus {
|
&:focus {
|
||||||
--tw-outline-style: none;
|
--tw-outline-style: none;
|
||||||
|
|||||||
95
internal/handlers/forfeit.go
Normal file
95
internal/handlers/forfeit.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.haelnorr.com/h/golib/hws"
|
||||||
|
"git.haelnorr.com/h/oslstats/internal/db"
|
||||||
|
"git.haelnorr.com/h/oslstats/internal/notify"
|
||||||
|
"git.haelnorr.com/h/oslstats/internal/respond"
|
||||||
|
"git.haelnorr.com/h/oslstats/internal/throw"
|
||||||
|
"git.haelnorr.com/h/oslstats/internal/validation"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ForfeitFixture handles POST /fixtures/{fixture_id}/forfeit
|
||||||
|
// Creates a finalized forfeit result for the fixture. Requires fixtures.manage permission.
|
||||||
|
func ForfeitFixture(
|
||||||
|
s *hws.Server,
|
||||||
|
conn *db.DB,
|
||||||
|
) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fixtureID, err := strconv.Atoi(r.PathValue("fixture_id"))
|
||||||
|
if err != nil {
|
||||||
|
throw.BadRequest(s, w, r, "Invalid fixture ID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getter, ok := validation.ParseFormOrNotify(s, w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
forfeitType := getter.String("forfeit_type").TrimSpace().Required().Value
|
||||||
|
forfeitTeam := getter.String("forfeit_team").TrimSpace().Value
|
||||||
|
forfeitReason := getter.String("forfeit_reason").TrimSpace().Value
|
||||||
|
if !getter.ValidateAndNotify(s, w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate forfeit type
|
||||||
|
if forfeitType != db.ForfeitTypeMutual && forfeitType != db.ForfeitTypeOutright {
|
||||||
|
notify.Warn(s, w, r, "Invalid Forfeit Type", "Forfeit type must be 'mutual' or 'outright'.", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate forfeit team for outright forfeits
|
||||||
|
if forfeitType == db.ForfeitTypeOutright {
|
||||||
|
if forfeitTeam != "home" && forfeitTeam != "away" {
|
||||||
|
notify.Warn(s, w, r, "Missing Team", "An outright forfeit requires specifying which team forfeited.", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
||||||
|
fixture, err := db.GetFixture(ctx, tx, fixtureID)
|
||||||
|
if err != nil {
|
||||||
|
if db.IsBadRequest(err) {
|
||||||
|
respond.NotFound(w, errors.Wrap(err, "db.GetFixture"))
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, errors.Wrap(err, "db.GetFixture")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a result already exists
|
||||||
|
existing, err := db.GetFixtureResult(ctx, tx, fixtureID)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "db.GetFixtureResult")
|
||||||
|
}
|
||||||
|
if existing != nil {
|
||||||
|
notify.Warn(s, w, r, "Result Exists",
|
||||||
|
"A result already exists for this fixture. Discard it first to record a forfeit.", nil)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user := db.CurrentUser(ctx)
|
||||||
|
|
||||||
|
_, err = db.CreateForfeitResult(ctx, tx, fixture, forfeitType, forfeitTeam, forfeitReason, user.ID, db.NewAuditFromRequest(r))
|
||||||
|
if err != nil {
|
||||||
|
if db.IsBadRequest(err) {
|
||||||
|
notify.Warn(s, w, r, "Cannot Forfeit", err.Error(), nil)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, errors.Wrap(err, "db.CreateForfeitResult")
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notify.SuccessWithDelay(s, w, r, "Forfeit Recorded", "The forfeit has been recorded and finalized.", nil)
|
||||||
|
respond.HXRedirect(w, "/fixtures/%d", fixtureID)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -287,6 +287,12 @@ func addRoutes(
|
|||||||
Method: hws.MethodPOST,
|
Method: hws.MethodPOST,
|
||||||
Handler: perms.RequirePermission(s, permissions.FixturesManage)(handlers.DiscardMatchResult(s, conn)),
|
Handler: perms.RequirePermission(s, permissions.FixturesManage)(handlers.DiscardMatchResult(s, conn)),
|
||||||
},
|
},
|
||||||
|
// Forfeit route
|
||||||
|
{
|
||||||
|
Path: "/fixtures/{fixture_id}/forfeit",
|
||||||
|
Method: hws.MethodPOST,
|
||||||
|
Handler: perms.RequirePermission(s, permissions.FixturesManage)(handlers.ForfeitFixture(s, conn)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
teamRoutes := []hws.Route{
|
teamRoutes := []hws.Route{
|
||||||
|
|||||||
@@ -220,11 +220,28 @@ templ fixtureResultDisplay(fixture *db.Fixture, result *db.FixtureResult) {
|
|||||||
isOT := strings.EqualFold(result.EndReason, "Overtime")
|
isOT := strings.EqualFold(result.EndReason, "Overtime")
|
||||||
homeWon := result.Winner == "home"
|
homeWon := result.Winner == "home"
|
||||||
awayWon := result.Winner == "away"
|
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-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">
|
<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>
|
<h2 class="text-lg font-bold text-text">Match Result</h2>
|
||||||
<div class="flex items-center gap-2">
|
<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 {
|
if result.Finalized {
|
||||||
<span class="px-2 py-0.5 bg-green/20 text-green rounded text-xs font-medium">
|
<span class="px-2 py-0.5 bg-green/20 text-green rounded text-xs font-medium">
|
||||||
Finalized
|
Finalized
|
||||||
@@ -263,55 +280,154 @@ templ fixtureResultDisplay(fixture *db.Fixture, result *db.FixtureResult) {
|
|||||||
<p class="text-red/80 text-xs">{ *result.TamperingReason }</p>
|
<p class="text-red/80 text-xs">{ *result.TamperingReason }</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<!-- Score Display -->
|
if isForfeit {
|
||||||
<div class="flex items-center justify-center gap-6 py-4">
|
<!-- Forfeit Display -->
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex flex-col items-center py-4 space-y-4">
|
||||||
if homeWon {
|
if isMutualForfeit {
|
||||||
<span class="text-2xl">🏆</span>
|
<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">🏆</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">🏆</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-subtext0">
|
||||||
|
{ forfeitTeamName } forfeited the match
|
||||||
|
</p>
|
||||||
}
|
}
|
||||||
if fixture.HomeTeam.Color != "" {
|
if result.ForfeitReason != nil && *result.ForfeitReason != "" {
|
||||||
<span
|
<div class="bg-surface0 border border-surface1 rounded-lg p-3 max-w-md w-full">
|
||||||
class="px-2.5 py-1 rounded text-sm font-bold"
|
<p class="text-xs text-subtext1 font-medium mb-1">Reason</p>
|
||||||
style={ fmt.Sprintf("background-color: %s22; color: %s; border: 1px solid %s44",
|
<p class="text-sm text-subtext0">{ *result.ForfeitReason }</p>
|
||||||
fixture.HomeTeam.Color, fixture.HomeTeam.Color, fixture.HomeTeam.Color) }
|
</div>
|
||||||
>
|
|
||||||
{ 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>
|
||||||
<div class="flex items-center gap-3">
|
} else {
|
||||||
<span class="text-4xl font-bold text-text">{ fmt.Sprint(result.AwayScore) }</span>
|
<!-- Normal Score Display -->
|
||||||
if fixture.AwayTeam.Color != "" {
|
<div class="flex items-center justify-center gap-6 py-4">
|
||||||
<span
|
<div class="flex items-center gap-3">
|
||||||
class="px-2.5 py-1 rounded text-sm font-bold"
|
if homeWon {
|
||||||
style={ fmt.Sprintf("background-color: %s22; color: %s; border: 1px solid %s44",
|
<span class="text-2xl">🏆</span>
|
||||||
fixture.AwayTeam.Color, fixture.AwayTeam.Color, fixture.AwayTeam.Color) }
|
}
|
||||||
>
|
if fixture.HomeTeam.Color != "" {
|
||||||
{ fixture.AwayTeam.ShortName }
|
<span
|
||||||
</span>
|
class="px-2.5 py-1 rounded text-sm font-bold"
|
||||||
} else {
|
style={ fmt.Sprintf("background-color: %s22; color: %s; border: 1px solid %s44",
|
||||||
<span class="px-2.5 py-1 bg-surface1 text-text rounded text-sm font-bold">
|
fixture.HomeTeam.Color, fixture.HomeTeam.Color, fixture.HomeTeam.Color) }
|
||||||
{ fixture.AwayTeam.ShortName }
|
>
|
||||||
</span>
|
{ fixture.HomeTeam.ShortName }
|
||||||
}
|
</span>
|
||||||
if awayWon {
|
} else {
|
||||||
<span class="text-2xl">🏆</span>
|
<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">🏆</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -321,13 +437,169 @@ templ fixtureUploadPrompt(fixture *db.Fixture) {
|
|||||||
<div class="text-4xl mb-3">📋</div>
|
<div class="text-4xl mb-3">📋</div>
|
||||||
<p class="text-lg text-text font-medium mb-2">No Result Uploaded</p>
|
<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>
|
<p class="text-sm text-subtext1 mb-4">Upload match log files to record the result of this fixture.</p>
|
||||||
<a
|
<div class="flex items-center justify-center gap-3">
|
||||||
href={ templ.SafeURL(fmt.Sprintf("/fixtures/%d/results/upload", fixture.ID)) }
|
<a
|
||||||
class="inline-block px-4 py-2 bg-blue hover:bg-blue/80 text-mantle rounded-lg
|
href={ templ.SafeURL(fmt.Sprintf("/fixtures/%d/results/upload", fixture.ID)) }
|
||||||
font-medium transition hover:cursor-pointer"
|
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>
|
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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,21 +159,35 @@ templ fixtureListItem(fixture *db.Fixture, sched *db.FixtureSchedule, hasSchedul
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
if hasResult {
|
if hasResult {
|
||||||
<span class="flex items-center gap-2">
|
if res.IsForfeit {
|
||||||
if res.Winner == "home" {
|
<span class="flex items-center gap-2">
|
||||||
<span class="text-sm font-bold text-text">{ fmt.Sprint(res.HomeScore) }</span>
|
if res.ForfeitType != nil && *res.ForfeitType == "mutual" {
|
||||||
<span class="text-xs text-subtext0">–</span>
|
<span class="px-2 py-0.5 bg-peach/20 text-peach rounded text-xs font-medium">
|
||||||
<span class="text-sm text-subtext0">{ fmt.Sprint(res.AwayScore) }</span>
|
Mutual Forfeit
|
||||||
} else if res.Winner == "away" {
|
</span>
|
||||||
<span class="text-sm text-subtext0">{ fmt.Sprint(res.HomeScore) }</span>
|
} else {
|
||||||
<span class="text-xs text-subtext0">–</span>
|
<span class="px-2 py-0.5 bg-red/20 text-red rounded text-xs font-medium">
|
||||||
<span class="text-sm font-bold text-text">{ fmt.Sprint(res.AwayScore) }</span>
|
Forfeit
|
||||||
} else {
|
</span>
|
||||||
<span class="text-sm text-text">{ fmt.Sprint(res.HomeScore) }</span>
|
}
|
||||||
<span class="text-xs text-subtext0">–</span>
|
</span>
|
||||||
<span class="text-sm text-text">{ fmt.Sprint(res.AwayScore) }</span>
|
} else {
|
||||||
}
|
<span class="flex items-center gap-2">
|
||||||
</span>
|
if res.Winner == "home" {
|
||||||
|
<span class="text-sm font-bold text-text">{ fmt.Sprint(res.HomeScore) }</span>
|
||||||
|
<span class="text-xs text-subtext0">–</span>
|
||||||
|
<span class="text-sm text-subtext0">{ fmt.Sprint(res.AwayScore) }</span>
|
||||||
|
} else if res.Winner == "away" {
|
||||||
|
<span class="text-sm text-subtext0">{ fmt.Sprint(res.HomeScore) }</span>
|
||||||
|
<span class="text-xs text-subtext0">–</span>
|
||||||
|
<span class="text-sm font-bold text-text">{ fmt.Sprint(res.AwayScore) }</span>
|
||||||
|
} else {
|
||||||
|
<span class="text-sm text-text">{ fmt.Sprint(res.HomeScore) }</span>
|
||||||
|
<span class="text-xs text-subtext0">–</span>
|
||||||
|
<span class="text-sm text-text">{ fmt.Sprint(res.AwayScore) }</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
} else if hasSchedule && sched.ScheduledTime != nil {
|
} else if hasSchedule && sched.ScheduledTime != nil {
|
||||||
<span class="text-xs text-green font-medium">
|
<span class="text-xs text-green font-medium">
|
||||||
@localtime(sched.ScheduledTime, "short")
|
@localtime(sched.ScheduledTime, "short")
|
||||||
|
|||||||
@@ -520,13 +520,17 @@ templ teamResultRow(team *db.Team, fixture *db.Fixture, resultMap map[int]*db.Fi
|
|||||||
won := (isHome && res.Winner == "home") || (!isHome && res.Winner == "away")
|
won := (isHome && res.Winner == "home") || (!isHome && res.Winner == "away")
|
||||||
lost := (isHome && res.Winner == "away") || (!isHome && res.Winner == "home")
|
lost := (isHome && res.Winner == "away") || (!isHome && res.Winner == "home")
|
||||||
_ = lost
|
_ = lost
|
||||||
|
isForfeit := res.IsForfeit
|
||||||
|
isMutualForfeit := isForfeit && res.ForfeitType != nil && *res.ForfeitType == "mutual"
|
||||||
}}
|
}}
|
||||||
<a
|
<a
|
||||||
href={ templ.SafeURL(fmt.Sprintf("/fixtures/%d", fixture.ID)) }
|
href={ templ.SafeURL(fmt.Sprintf("/fixtures/%d", fixture.ID)) }
|
||||||
class="px-4 py-3 flex items-center justify-between gap-3 hover:bg-surface1 transition hover:cursor-pointer block"
|
class="px-4 py-3 flex items-center justify-between gap-3 hover:bg-surface1 transition hover:cursor-pointer block"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
if won {
|
if isMutualForfeit {
|
||||||
|
<span class="text-xs font-bold px-2 py-0.5 bg-peach/20 text-peach rounded shrink-0">FF</span>
|
||||||
|
} else if won {
|
||||||
<span class="text-xs font-bold px-2 py-0.5 bg-green/20 text-green rounded shrink-0">W</span>
|
<span class="text-xs font-bold px-2 py-0.5 bg-green/20 text-green rounded shrink-0">W</span>
|
||||||
} else if lost {
|
} else if lost {
|
||||||
<span class="text-xs font-bold px-2 py-0.5 bg-red/20 text-red rounded shrink-0">L</span>
|
<span class="text-xs font-bold px-2 py-0.5 bg-red/20 text-red rounded shrink-0">L</span>
|
||||||
@@ -550,21 +554,35 @@ templ teamResultRow(team *db.Team, fixture *db.Fixture, resultMap map[int]*db.Fi
|
|||||||
{ opponent }
|
{ opponent }
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="flex items-center gap-2 shrink-0">
|
if isForfeit {
|
||||||
if res.Winner == "home" {
|
<span class="flex items-center gap-2 shrink-0">
|
||||||
<span class="text-sm font-bold text-text">{ fmt.Sprint(res.HomeScore) }</span>
|
if isMutualForfeit {
|
||||||
<span class="text-xs text-subtext0">–</span>
|
<span class="px-2 py-0.5 bg-peach/20 text-peach rounded text-xs font-medium">
|
||||||
<span class="text-sm text-subtext0">{ fmt.Sprint(res.AwayScore) }</span>
|
Mutual Forfeit
|
||||||
} else if res.Winner == "away" {
|
</span>
|
||||||
<span class="text-sm text-subtext0">{ fmt.Sprint(res.HomeScore) }</span>
|
} else {
|
||||||
<span class="text-xs text-subtext0">–</span>
|
<span class="px-2 py-0.5 bg-red/20 text-red rounded text-xs font-medium">
|
||||||
<span class="text-sm font-bold text-text">{ fmt.Sprint(res.AwayScore) }</span>
|
Forfeit
|
||||||
} else {
|
</span>
|
||||||
<span class="text-sm text-text">{ fmt.Sprint(res.HomeScore) }</span>
|
}
|
||||||
<span class="text-xs text-subtext0">–</span>
|
</span>
|
||||||
<span class="text-sm text-text">{ fmt.Sprint(res.AwayScore) }</span>
|
} else {
|
||||||
}
|
<span class="flex items-center gap-2 shrink-0">
|
||||||
</span>
|
if res.Winner == "home" {
|
||||||
|
<span class="text-sm font-bold text-text">{ fmt.Sprint(res.HomeScore) }</span>
|
||||||
|
<span class="text-xs text-subtext0">–</span>
|
||||||
|
<span class="text-sm text-subtext0">{ fmt.Sprint(res.AwayScore) }</span>
|
||||||
|
} else if res.Winner == "away" {
|
||||||
|
<span class="text-sm text-subtext0">{ fmt.Sprint(res.HomeScore) }</span>
|
||||||
|
<span class="text-xs text-subtext0">–</span>
|
||||||
|
<span class="text-sm font-bold text-text">{ fmt.Sprint(res.AwayScore) }</span>
|
||||||
|
} else {
|
||||||
|
<span class="text-sm text-text">{ fmt.Sprint(res.HomeScore) }</span>
|
||||||
|
<span class="text-xs text-subtext0">–</span>
|
||||||
|
<span class="text-sm text-text">{ fmt.Sprint(res.AwayScore) }</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user