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.
This commit is contained in:
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user