- TableService with CRUD, AssignSeat, AutoAssignSeat (fills evenly), MoveSeat, SwapSeats - Dealer button tracking with SetDealerButton and AdvanceDealerButton (skips empty seats) - Hand-for-hand mode with per-table completion tracking and clock integration - BlueprintService with CRUD, SaveBlueprintFromTournament, CreateTablesFromBlueprint - Migration 006 adds hand_for_hand_hand_number and hand_completed columns Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
227 lines
6.3 KiB
Go
227 lines
6.3 KiB
Go
package seating
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// Blueprint is a venue-level saved table layout configuration.
|
|
type Blueprint struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
TableConfigs []BlueprintTableConfig `json:"table_configs"`
|
|
}
|
|
|
|
// BlueprintTableConfig is a single table spec within a blueprint.
|
|
type BlueprintTableConfig struct {
|
|
Name string `json:"name"`
|
|
SeatCount int `json:"seat_count"`
|
|
}
|
|
|
|
// BlueprintService provides CRUD operations for table blueprints.
|
|
type BlueprintService struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewBlueprintService creates a new BlueprintService.
|
|
func NewBlueprintService(db *sql.DB) *BlueprintService {
|
|
return &BlueprintService{db: db}
|
|
}
|
|
|
|
// CreateBlueprint creates a new blueprint.
|
|
func (s *BlueprintService) CreateBlueprint(ctx context.Context, name string, configs []BlueprintTableConfig) (*Blueprint, error) {
|
|
if name == "" {
|
|
return nil, fmt.Errorf("blueprint name is required")
|
|
}
|
|
if len(configs) == 0 {
|
|
return nil, fmt.Errorf("at least one table config is required")
|
|
}
|
|
for i, c := range configs {
|
|
if c.SeatCount < 6 || c.SeatCount > 10 {
|
|
return nil, fmt.Errorf("table config %d: seat count must be between 6 and 10", i+1)
|
|
}
|
|
if c.Name == "" {
|
|
return nil, fmt.Errorf("table config %d: name is required", i+1)
|
|
}
|
|
}
|
|
|
|
configsJSON, err := json.Marshal(configs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal configs: %w", err)
|
|
}
|
|
|
|
result, err := s.db.ExecContext(ctx,
|
|
`INSERT INTO table_blueprints (name, table_configs) VALUES (?, ?)`,
|
|
name, string(configsJSON),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create blueprint: %w", err)
|
|
}
|
|
|
|
id, _ := result.LastInsertId()
|
|
return &Blueprint{
|
|
ID: int(id),
|
|
Name: name,
|
|
TableConfigs: configs,
|
|
}, nil
|
|
}
|
|
|
|
// GetBlueprint returns a blueprint by ID.
|
|
func (s *BlueprintService) GetBlueprint(ctx context.Context, id int) (*Blueprint, error) {
|
|
var bp Blueprint
|
|
var configsJSON string
|
|
err := s.db.QueryRowContext(ctx,
|
|
`SELECT id, name, table_configs FROM table_blueprints WHERE id = ?`, id,
|
|
).Scan(&bp.ID, &bp.Name, &configsJSON)
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("blueprint not found")
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get blueprint: %w", err)
|
|
}
|
|
if err := json.Unmarshal([]byte(configsJSON), &bp.TableConfigs); err != nil {
|
|
return nil, fmt.Errorf("unmarshal configs: %w", err)
|
|
}
|
|
return &bp, nil
|
|
}
|
|
|
|
// ListBlueprints returns all blueprints.
|
|
func (s *BlueprintService) ListBlueprints(ctx context.Context) ([]Blueprint, error) {
|
|
rows, err := s.db.QueryContext(ctx,
|
|
`SELECT id, name, table_configs FROM table_blueprints ORDER BY name`,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list blueprints: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var blueprints []Blueprint
|
|
for rows.Next() {
|
|
var bp Blueprint
|
|
var configsJSON string
|
|
if err := rows.Scan(&bp.ID, &bp.Name, &configsJSON); err != nil {
|
|
return nil, fmt.Errorf("scan blueprint: %w", err)
|
|
}
|
|
if err := json.Unmarshal([]byte(configsJSON), &bp.TableConfigs); err != nil {
|
|
return nil, fmt.Errorf("unmarshal configs: %w", err)
|
|
}
|
|
blueprints = append(blueprints, bp)
|
|
}
|
|
return blueprints, rows.Err()
|
|
}
|
|
|
|
// UpdateBlueprint updates a blueprint.
|
|
func (s *BlueprintService) UpdateBlueprint(ctx context.Context, bp Blueprint) error {
|
|
if bp.Name == "" {
|
|
return fmt.Errorf("blueprint name is required")
|
|
}
|
|
for i, c := range bp.TableConfigs {
|
|
if c.SeatCount < 6 || c.SeatCount > 10 {
|
|
return fmt.Errorf("table config %d: seat count must be between 6 and 10", i+1)
|
|
}
|
|
if c.Name == "" {
|
|
return fmt.Errorf("table config %d: name is required", i+1)
|
|
}
|
|
}
|
|
|
|
configsJSON, err := json.Marshal(bp.TableConfigs)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal configs: %w", err)
|
|
}
|
|
|
|
result, err := s.db.ExecContext(ctx,
|
|
`UPDATE table_blueprints SET name = ?, table_configs = ?, updated_at = unixepoch()
|
|
WHERE id = ?`,
|
|
bp.Name, string(configsJSON), bp.ID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("update blueprint: %w", err)
|
|
}
|
|
n, _ := result.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("blueprint not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteBlueprint deletes a blueprint.
|
|
func (s *BlueprintService) DeleteBlueprint(ctx context.Context, id int) error {
|
|
result, err := s.db.ExecContext(ctx,
|
|
`DELETE FROM table_blueprints WHERE id = ?`, id,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("delete blueprint: %w", err)
|
|
}
|
|
n, _ := result.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("blueprint not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SaveBlueprintFromTournament creates a blueprint from the current tables in a tournament.
|
|
func (s *BlueprintService) SaveBlueprintFromTournament(ctx context.Context, db *sql.DB, tournamentID, name string) (*Blueprint, error) {
|
|
if name == "" {
|
|
return nil, fmt.Errorf("blueprint name is required")
|
|
}
|
|
|
|
rows, err := db.QueryContext(ctx,
|
|
`SELECT name, seat_count FROM tables
|
|
WHERE tournament_id = ? AND is_active = 1
|
|
ORDER BY name`,
|
|
tournamentID,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get tournament tables: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var configs []BlueprintTableConfig
|
|
for rows.Next() {
|
|
var c BlueprintTableConfig
|
|
if err := rows.Scan(&c.Name, &c.SeatCount); err != nil {
|
|
return nil, fmt.Errorf("scan table: %w", err)
|
|
}
|
|
configs = append(configs, c)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(configs) == 0 {
|
|
return nil, fmt.Errorf("no active tables in tournament")
|
|
}
|
|
|
|
return s.CreateBlueprint(ctx, name, configs)
|
|
}
|
|
|
|
// CreateTablesFromBlueprint creates tables in a tournament from a blueprint.
|
|
func (s *BlueprintService) CreateTablesFromBlueprint(ctx context.Context, db *sql.DB, tournamentID string, blueprintID int) ([]Table, error) {
|
|
bp, err := s.GetBlueprint(ctx, blueprintID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var tables []Table
|
|
for _, cfg := range bp.TableConfigs {
|
|
result, err := db.ExecContext(ctx,
|
|
`INSERT INTO tables (tournament_id, name, seat_count, is_active)
|
|
VALUES (?, ?, ?, 1)`,
|
|
tournamentID, cfg.Name, cfg.SeatCount,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create table from blueprint: %w", err)
|
|
}
|
|
id, _ := result.LastInsertId()
|
|
tables = append(tables, Table{
|
|
ID: int(id),
|
|
TournamentID: tournamentID,
|
|
Name: cfg.Name,
|
|
SeatCount: cfg.SeatCount,
|
|
IsActive: true,
|
|
})
|
|
}
|
|
return tables, nil
|
|
}
|