Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Instead of storing only the latest match score, it stores each match action as a

The core domain includes:

- Championships and games
- Competitions and games
- Teams, players, staff, and officials assigned to a game
- An event stream (`game_events`) for everything that happens during a match
- Materialized state snapshots (`game_state_snapshots`) for fast reads
Expand Down
54 changes: 54 additions & 0 deletions app/Livewire/Competition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace App\Livewire;

use App\Models\Competition as CompetitionModel;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;

#[Title('Competition')]
class Competition extends Component
{
public string $name = '';

public bool $saved = false;

public function mount(): void
{
$competition = CompetitionModel::current();

$this->name = $competition !== null
? $competition->name
: config('competition.name');
}

public function save(): void
{
$this->name = trim($this->name);

$validated = $this->validate([
'name' => ['required', 'string', 'max:255'],
]);

$competition = CompetitionModel::ensureSingleton();
$competition->forceFill([
'name' => trim((string) $validated['name']),
])->save();

$this->name = $competition->name;
$this->saved = true;
}

public function updatedName(): void
{
$this->saved = false;
}

public function render(): View
{
return view('livewire.competition');
}
}
82 changes: 78 additions & 4 deletions app/Livewire/MatchSetup.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Enums\StaffRole;
use App\Enums\TeamAB;
use App\Exceptions\InvalidGameEventTransition;
use App\Models\Competition;
use App\Models\Game;
use App\Models\Official;
use App\Models\Player;
Expand All @@ -16,7 +17,6 @@
use App\Services\CurrentMatchResolver;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Livewire\Component;

Expand All @@ -26,6 +26,10 @@ class MatchSetup extends Component

public string $step = 'missing-match';

public string $competitionName = '';

public bool $editingCompetition = false;

public string $matchNumber = '';

public string $matchCountryCode = '';
Expand Down Expand Up @@ -71,11 +75,46 @@ class MatchSetup extends Component

public function mount(CurrentMatchResolver $currentMatchResolver): void
{
$this->synchronizeCompetitionState();
$this->synchronizeState($currentMatchResolver->current());
}

public function saveCompetition(): void
{
$this->competitionName = trim($this->competitionName);

$validated = $this->validate([
'competitionName' => ['required', 'string', 'max:255'],
]);

$competition = Competition::ensureSingleton();
$competition->forceFill([
'name' => trim((string) $validated['competitionName']),
])->save();

$this->editingCompetition = false;
$this->synchronizeCompetitionState();
$this->synchronizeState($this->game?->fresh(['homeTeam', 'awayTeam', 'officials']));
}

public function editCompetition(): void
{
if (! Competition::setupComplete()) {
return;
}

$this->editingCompetition = true;
$this->step = 'competition';
}

public function createMatch(): void
{
if (! Competition::setupComplete()) {
$this->step = 'competition';

return;
}

if ($this->game !== null) {
$this->synchronizeState($this->game->fresh(['homeTeam', 'awayTeam', 'officials']));

Expand All @@ -87,7 +126,13 @@ public function createMatch(): void

public function openStep(string $step): void
{
if (! in_array($step, ['match-details', 'rosters', 'officials', 'ready'], true)) {
if (! in_array($step, ['competition', 'match-details', 'rosters', 'officials', 'ready'], true)) {
return;
}

if ($step === 'competition') {
$this->step = 'competition';

return;
}

Expand All @@ -102,7 +147,15 @@ public function openStep(string $step): void
}

$requiredStep = $this->resolver()->nextStep($this->game);
$availableSteps = ['match-details'];
$availableSteps = ['competition'];

if (! Competition::setupComplete()) {
$this->step = 'competition';

return;
}

$availableSteps[] = 'match-details';

if ($this->game->hasCompleteMatchDetails()) {
$availableSteps[] = 'rosters';
Expand Down Expand Up @@ -290,11 +343,13 @@ public function saveOfficials(): void
public function render(): View
{
return view('livewire.match-setup', [
'competitionName' => $this->game?->competitionName() ?? Config::string('competition.name'),
'currentCompetitionName' => $this->game?->competitionName() ?? $this->competitionName,
'competitionConfigured' => Competition::setupComplete(),
'currentRequiredStep' => $this->resolver()->nextStep($this->game),
'isSetupComplete' => $this->game !== null && $this->resolver()->isSetupComplete($this->game),
'isSetupLocked' => $this->setupLocked(),
'setupSteps' => [
'competition' => 'Competition',
'match-details' => 'Match details',
'rosters' => 'Team rosters',
'officials' => 'Officials',
Expand All @@ -307,6 +362,16 @@ private function synchronizeState(?Game $game): void
{
$this->game = $game;

if (! Competition::setupComplete()) {
$this->step = 'competition';

if ($game === null) {
$this->resetFormState();
}

return;
}

if ($game === null) {
$this->step = 'missing-match';
$this->resetFormState();
Expand Down Expand Up @@ -363,6 +428,15 @@ private function resetFormState(): void
$this->officialRows = $this->emptyOfficialRows();
}

private function synchronizeCompetitionState(): void
{
$competition = Competition::current();

$this->competitionName = $competition !== null
? $competition->name
: '';
}

/**
* @param array<int, array{first_name: string, last_name: string, number: int, is_captain: bool, is_libero: bool}> $players
* @param array<int, array{role: StaffRole, first_name: string, last_name: string}> $staffRows
Expand Down
70 changes: 70 additions & 0 deletions app/Models/Competition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace App\Models;

use Carbon\CarbonImmutable;
use Database\Factories\CompetitionFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\MultipleRecordsFoundException;
use LogicException;

/**
* @property string $name
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
*/
#[Fillable(['name'])]
class Competition extends Model
{
/** @use HasFactory<CompetitionFactory> */
use HasFactory;

/**
* @return array<string, string>
*/
#[\Override]
protected function casts(): array
{
return [
'created_at' => 'immutable_datetime',
'updated_at' => 'immutable_datetime',
];
}

public static function current(): ?self
{
try {
/** @var self */
return static::query()->sole();
} catch (ModelNotFoundException) {
return null;
} catch (MultipleRecordsFoundException $exception) {
throw new LogicException('The single-competition application found multiple competition records.', previous: $exception);
}
}

public static function ensureSingleton(): self
{
$competition = static::current();

if ($competition !== null) {
return $competition;
}

return static::query()->create([
'name' => config('competition.name'),
]);
}

public static function setupComplete(): bool
{
$competition = static::current();

return $competition !== null && trim($competition->name) !== '';
}
}
6 changes: 5 additions & 1 deletion app/Models/Game.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,11 @@ public static function ensureSingleton(
*/
public function competitionName(): string
{
return Config::string('competition.name');
$competition = Competition::current();

return $competition !== null
? $competition->name
: Config::string('competition.name');
}

public function resetForSetup(): void
Expand Down
13 changes: 12 additions & 1 deletion app/Services/CurrentMatchResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Services;

use App\Enums\MatchPhase;
use App\Models\Competition;
use App\Models\Game;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\MultipleRecordsFoundException;
Expand Down Expand Up @@ -40,6 +41,10 @@ public function currentOrFail(): Game

public function landingRouteName(): string
{
if (! Competition::setupComplete()) {
return 'match.setup';
}

$currentGame = $this->current();

if ($currentGame === null) {
Expand All @@ -53,6 +58,10 @@ public function landingRouteName(): string

public function nextStep(?Game $game = null): string
{
if (! Competition::setupComplete()) {
return 'competition';
}

$currentGame = $game ?? $this->current();

if ($currentGame === null) {
Expand Down Expand Up @@ -82,6 +91,8 @@ public function isSetupComplete(?Game $game = null): bool
{
$currentGame = $game ?? $this->current();

return $currentGame !== null && $currentGame->status !== MatchPhase::Setup;
return Competition::setupComplete()
&& $currentGame !== null
&& $currentGame->status !== MatchPhase::Setup;
}
}
31 changes: 31 additions & 0 deletions database/factories/CompetitionFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Database\Factories;

use App\Models\Competition;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
* @extends Factory<Competition>
*/
class CompetitionFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->unique()->company(),
];
}

public function named(string $name): static
{
return $this->state(fn (array $attributes): array => [
'name' => $name,
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/
public function up(): void
{
Schema::create('championships', function (Blueprint $table) {
Schema::create('competitions', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public function up(): void
{
Schema::create('games', function (Blueprint $table) {
$table->id();
$table->foreignId('championship_id')->constrained()->cascadeOnDelete();
$table->foreignId('competition_id')->constrained()->cascadeOnDelete();
$table->foreignId('home_team_id')->constrained('teams')->cascadeOnDelete();
$table->foreignId('away_team_id')->constrained('teams')->cascadeOnDelete();
$table->unsignedTinyInteger('number');
Expand Down
Loading