Add participant creation functionality to event API, including backend service and database integration. Update frontend to support participant registration and enhance UI for displaying registered players.
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / compose (push) Has been cancelled

This commit is contained in:
2026-07-19 01:21:00 +03:00
parent d85d5ffdc0
commit 59c0bdb345
9 changed files with 217 additions and 6 deletions

View File

@@ -71,6 +71,7 @@ func New(service *application.Service, store application.Store, hub *realtime.Hu
api.Put("/api/events/{eventID}", s.updateEvent)
api.Get("/api/events/{eventID}/tournament", s.getEventTournament)
api.Get("/api/events/{eventID}/rsvps", s.rsvps)
api.Post("/api/events/{eventID}/participants", s.createParticipant)
api.Put("/api/events/{eventID}/rsvps/{playerID}", s.setRSVP)
api.Post("/api/events/{eventID}/balance", s.balance)
api.Put("/api/events/{eventID}/teams", s.selectBalance)
@@ -277,6 +278,19 @@ func (s *Server) rsvps(w http.ResponseWriter, r *http.Request) {
respond(w, out, err, 200)
}
func (s *Server) createParticipant(w http.ResponseWriter, r *http.Request) {
var in struct {
DisplayName string `json:"displayName"`
Ratings domain.Ratings `json:"ratings"`
Status domain.RSVPStatus `json:"status"`
}
if !decode(w, r, &in) {
return
}
player, rsvp, err := s.service.CreateParticipant(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.DisplayName, in.Ratings, in.Status)
respond(w, map[string]any{"player": player, "rsvp": rsvp}, err, http.StatusCreated)
}
func (s *Server) setRSVP(w http.ResponseWriter, r *http.Request) {
var in struct {
Status domain.RSVPStatus `json:"status"`

View File

@@ -122,7 +122,7 @@ func (s *Store) UpdatePlayer(ctx context.Context, p domain.Player) (domain.Playe
}
func (s *Store) ListPlayers(ctx context.Context) ([]domain.Player, error) {
rows, err := s.pool.Query(ctx, `SELECT id,account_id,display_name,tank_rating,damage_rating,support_rating,
rows, err := s.pool.Query(ctx, `SELECT id,COALESCE(account_id,''),display_name,tank_rating,damage_rating,support_rating,
preferred_roles,preferred_player_ids,created_at,updated_at FROM players ORDER BY id`)
if err != nil {
return nil, err
@@ -147,6 +147,40 @@ func (s *Store) ListPlayers(ctx context.Context) ([]domain.Player, error) {
return out, rows.Err()
}
func (s *Store) CreateParticipant(ctx context.Context, p domain.Player, r domain.RSVP) (domain.Player, domain.RSVP, error) {
tx, err := s.pool.Begin(ctx)
if err != nil {
return domain.Player{}, domain.RSVP{}, err
}
defer tx.Rollback(ctx)
var preferredRoles, preferredPlayers []byte
err = tx.QueryRow(ctx, `INSERT INTO players(id,account_id,display_name,tank_rating,damage_rating,support_rating,created_at,updated_at)
VALUES($1,NULL,$2,$3,$4,$5,$6,$7)
RETURNING id,COALESCE(account_id,''),display_name,tank_rating,damage_rating,support_rating,preferred_roles,preferred_player_ids,created_at,updated_at`,
p.ID, p.DisplayName, p.Ratings.Tank, p.Ratings.Damage, p.Ratings.Support, p.CreatedAt, p.UpdatedAt).
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &p.CreatedAt, &p.UpdatedAt)
if err == nil {
err = json.Unmarshal(preferredRoles, &p.PreferredRoles)
}
if err == nil {
err = json.Unmarshal(preferredPlayers, &p.PreferredPlayerIDs)
}
if err == nil {
err = tx.QueryRow(ctx, `INSERT INTO rsvps(event_id,player_id,status,actor_account_id,source,updated_at)
VALUES($1,$2,$3,$4,$5,$6)
RETURNING event_id,player_id,status,actor_account_id,source,updated_at`,
r.EventID, r.PlayerID, r.Status, r.ActorAccountID, r.Source, r.UpdatedAt).
Scan(&r.EventID, &r.PlayerID, &r.Status, &r.ActorAccountID, &r.Source, &r.UpdatedAt)
}
if err == nil {
err = tx.Commit(ctx)
}
if err != nil {
return domain.Player{}, domain.RSVP{}, mapError(err)
}
return p, r, nil
}
func (s *Store) CreateEvent(ctx context.Context, e domain.Event) (domain.Event, error) {
err := s.pool.QueryRow(ctx, `INSERT INTO events(id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)

View File

@@ -5,6 +5,7 @@ import (
"crypto/rand"
"encoding/hex"
"fmt"
"strings"
"time"
"mixmaker/backend/internal/domain"
@@ -18,6 +19,7 @@ type Store interface {
DeleteSession(context.Context, string) error
UpdatePlayer(context.Context, domain.Player) (domain.Player, error)
ListPlayers(context.Context) ([]domain.Player, error)
CreateParticipant(context.Context, domain.Player, domain.RSVP) (domain.Player, domain.RSVP, error)
CreateEvent(context.Context, domain.Event) (domain.Event, error)
GetEvent(context.Context, string) (domain.Event, error)
UpdateEvent(context.Context, domain.Event) (domain.Event, error)
@@ -174,6 +176,47 @@ func (s *Service) SetRSVP(ctx context.Context, actor domain.Account, actorPlayer
return out, err
}
func (s *Service) CreateParticipant(ctx context.Context, actor domain.Account, eventID, displayName string, ratings domain.Ratings, status domain.RSVPStatus) (domain.Player, domain.RSVP, error) {
if !actor.IsAdmin() {
return domain.Player{}, domain.RSVP{}, domain.ErrForbidden
}
displayName = strings.TrimSpace(displayName)
if displayName == "" {
return domain.Player{}, domain.RSVP{}, fmt.Errorf("%w: display name is required", domain.ErrInvalid)
}
if err := ratings.Validate(); err != nil {
return domain.Player{}, domain.RSVP{}, err
}
if _, err := s.Store.GetEvent(ctx, eventID); err != nil {
return domain.Player{}, domain.RSVP{}, err
}
now := s.Now()
player := domain.Player{
ID: NewID(),
DisplayName: displayName,
Ratings: ratings,
CreatedAt: now,
UpdatedAt: now,
}
rsvp := domain.RSVP{
EventID: eventID,
PlayerID: player.ID,
ActorAccountID: actor.ID,
Status: status,
Source: domain.SourceAdmin,
UpdatedAt: now,
}
if err := rsvp.Validate(); err != nil {
return domain.Player{}, domain.RSVP{}, err
}
player, rsvp, err := s.Store.CreateParticipant(ctx, player, rsvp)
if err == nil {
_ = s.Store.AppendAudit(ctx, actor.ID, "participant.created", eventID, map[string]any{"player": player, "rsvp": rsvp})
s.Bus.Publish("event:"+eventID, rsvp)
}
return player, rsvp, err
}
func (s *Service) Balance(ctx context.Context, actor domain.Account, eventID string) ([]domain.BalanceCandidate, error) {
if !actor.IsAdmin() {
return nil, domain.ErrForbidden