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
|
||||
|
||||
9
backend/migrations/005_guest_players.sql
Normal file
9
backend/migrations/005_guest_players.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- +mixmaker Up
|
||||
ALTER TABLE players
|
||||
ALTER COLUMN account_id DROP NOT NULL;
|
||||
|
||||
-- +mixmaker Down
|
||||
DELETE FROM rsvps WHERE player_id IN (SELECT id FROM players WHERE account_id IS NULL);
|
||||
DELETE FROM players WHERE account_id IS NULL;
|
||||
ALTER TABLE players
|
||||
ALTER COLUMN account_id SET NOT NULL;
|
||||
Reference in New Issue
Block a user