mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-14 12:15:28 +00:00
feat: some fixes to the ui, imlemented user and guest user, ans used the dahboard
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Rooms;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use App\Models\User;
|
||||
use App\Models\Vote;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ClaimGuestRooms
|
||||
{
|
||||
/**
|
||||
* @param array<array-key, mixed> $roomPlayers
|
||||
*/
|
||||
public function handle(User $user, array $roomPlayers): void
|
||||
{
|
||||
foreach ($roomPlayers as $roomId => $playerId) {
|
||||
if (! is_string($roomId) || ! is_numeric($playerId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($user, $roomId, $playerId): void {
|
||||
$room = Room::query()->lockForUpdate()->find($roomId);
|
||||
|
||||
if ($room === null || $room->owner_id !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$moderator = $room->players()
|
||||
->whereKey((int) $playerId)
|
||||
->where('is_moderator', true)
|
||||
->where(function ($query) use ($user): void {
|
||||
$query->whereNull('user_id')->orWhere('user_id', $user->id);
|
||||
})
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($moderator === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existingPlayer = $room->players()
|
||||
->where('user_id', $user->id)
|
||||
->whereKeyNot($moderator->id)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($existingPlayer !== null) {
|
||||
$this->mergePlayerIntoModerator($existingPlayer, $moderator);
|
||||
}
|
||||
|
||||
$moderator->update(['user_id' => $user->id]);
|
||||
$room->update(['owner_id' => $user->id]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function mergePlayerIntoModerator(Player $source, Player $moderator): void
|
||||
{
|
||||
$source->votes()
|
||||
->orderBy('updated_at')
|
||||
->each(function (Vote $vote) use ($moderator): void {
|
||||
$targetVote = $moderator->votes()
|
||||
->where('room_id', $vote->room_id)
|
||||
->where('round', $vote->round)
|
||||
->first();
|
||||
|
||||
if ($targetVote === null || $vote->updated_at->greaterThanOrEqualTo($targetVote->updated_at)) {
|
||||
Vote::updateOrCreate(
|
||||
[
|
||||
'player_id' => $moderator->id,
|
||||
'room_id' => $vote->room_id,
|
||||
'round' => $vote->round,
|
||||
],
|
||||
['value' => $vote->value],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$source->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Room;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$rooms = $request->user()
|
||||
->ownedRooms()
|
||||
->withCount('players')
|
||||
->orderByRaw('closed_at IS NOT NULL')
|
||||
->orderByDesc('last_activity_at')
|
||||
->get()
|
||||
->map(fn (Room $room) => [
|
||||
'id' => $room->id,
|
||||
'title' => $room->title,
|
||||
'join_code' => $room->join_code,
|
||||
'current_round' => $room->current_round,
|
||||
'players_count' => $room->players_count,
|
||||
'last_activity_at' => $room->last_activity_at?->toIso8601String(),
|
||||
'closed_at' => $room->closed_at?->toIso8601String(),
|
||||
]);
|
||||
|
||||
return Inertia::render('Dashboard', [
|
||||
'rooms' => $rooms,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,15 @@ use App\Http\Requests\StoreRoomRequest;
|
||||
use App\Models\Deck;
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class RoomController extends Controller
|
||||
{
|
||||
public function create(Request $request)
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
@@ -37,7 +39,7 @@ class RoomController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request, StoreRoomRequest $roomStoreRequest)
|
||||
public function store(Request $request, StoreRoomRequest $roomStoreRequest): RedirectResponse
|
||||
{
|
||||
$validated = $roomStoreRequest->validated();
|
||||
$user = $request->user();
|
||||
@@ -59,7 +61,9 @@ class RoomController extends Controller
|
||||
$user,
|
||||
) {
|
||||
$room = Room::create([
|
||||
'owner_id' => $user?->id,
|
||||
'title' => $validated['title'] ?? null,
|
||||
'last_activity_at' => now(),
|
||||
'deck_config' => [
|
||||
'id' => $deck->id,
|
||||
'name' => $deck->name,
|
||||
@@ -82,10 +86,19 @@ class RoomController extends Controller
|
||||
$player->id,
|
||||
);
|
||||
|
||||
return to_route('room.show', $room);
|
||||
$redirect = to_route('room.show', $room);
|
||||
|
||||
if ($user === null) {
|
||||
$redirect->with('room_notice', [
|
||||
'guest_retention_days' => config('rooms.guest_retention_days'),
|
||||
'account_retention_days' => config('rooms.account_retention_days'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $redirect;
|
||||
}
|
||||
|
||||
public function show(Room $room, Request $request)
|
||||
public function show(Room $room, Request $request): Response
|
||||
{
|
||||
$currentPlayer = $request->attributes->get('currentPlayer');
|
||||
|
||||
@@ -117,6 +130,9 @@ class RoomController extends Controller
|
||||
->firstWhere('id', $currentPlayer->id)
|
||||
?->votes
|
||||
->first();
|
||||
$statistics = $room->revealed_at !== null
|
||||
? $this->currentRoundStatistics($room)
|
||||
: null;
|
||||
|
||||
return Inertia::render('rooms/Show', [
|
||||
'room' => [
|
||||
@@ -126,6 +142,7 @@ class RoomController extends Controller
|
||||
'current_round' => $room->current_round,
|
||||
'revealed' => $room->revealed_at !== null,
|
||||
'state_version' => $room->state_version,
|
||||
'statistics' => $statistics,
|
||||
'deck' => [
|
||||
'name' => $room->deck_config['name'],
|
||||
'cards' => $room->deck_config['cards'],
|
||||
@@ -141,10 +158,120 @@ class RoomController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function join(Request $request)
|
||||
public function join(Request $request): Response
|
||||
{
|
||||
$joinCode = strtoupper(trim((string) $request->query('join_code')));
|
||||
$room = $joinCode !== ''
|
||||
? Room::query()->where('join_code', $joinCode)->first()
|
||||
: null;
|
||||
$existingPlayer = $room !== null && $request->user() !== null
|
||||
? $room->players()->where('user_id', $request->user()->id)->first()
|
||||
: null;
|
||||
|
||||
return Inertia::render('rooms/Join', [
|
||||
'join_code' => $request->query('join_code'),
|
||||
'join_code' => $joinCode,
|
||||
'defaultPlayerName' => $existingPlayer instanceof Player
|
||||
? $existingPlayer->name
|
||||
: $request->user()?->name,
|
||||
'closedRoom' => $room?->isClosed() ? [
|
||||
'title' => $room->title,
|
||||
'join_code' => $room->join_code,
|
||||
] : null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function reopen(Room $room, Request $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
abort_if($room->trashed(), 404);
|
||||
abort_unless($user !== null && $user->id === $room->owner_id, 403);
|
||||
|
||||
$player = DB::transaction(function () use ($room, $user) {
|
||||
$lockedRoom = Room::query()->lockForUpdate()->findOrFail($room->id);
|
||||
|
||||
abort_unless($user->id === $lockedRoom->owner_id, 403);
|
||||
|
||||
if ($lockedRoom->isClosed()) {
|
||||
$lockedRoom->update([
|
||||
'current_round' => $lockedRoom->current_round + 1,
|
||||
'revealed_at' => null,
|
||||
'closed_at' => null,
|
||||
'last_activity_at' => now(),
|
||||
'state_version' => $lockedRoom->state_version + 1,
|
||||
]);
|
||||
}
|
||||
|
||||
return $lockedRoom->players()
|
||||
->where('user_id', $user->id)
|
||||
->firstOrFail();
|
||||
});
|
||||
|
||||
$request->session()->put("room_players.{$room->id}", $player->id);
|
||||
|
||||
return to_route('room.show', $room);
|
||||
}
|
||||
|
||||
public function destroy(Room $room, Request $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
abort_if($room->trashed(), 404);
|
||||
abort_unless($user !== null && $user->id === $room->owner_id, 403);
|
||||
|
||||
DB::transaction(function () use ($room, $user): void {
|
||||
$lockedRoom = Room::withTrashed()
|
||||
->lockForUpdate()
|
||||
->findOrFail($room->id);
|
||||
|
||||
abort_if($lockedRoom->trashed(), 404);
|
||||
abort_unless($user->id === $lockedRoom->owner_id, 403);
|
||||
|
||||
$lockedRoom->update([
|
||||
'state_version' => $lockedRoom->state_version + 1,
|
||||
]);
|
||||
$lockedRoom->delete();
|
||||
});
|
||||
|
||||
$request->session()->forget("room_players.{$room->id}");
|
||||
Inertia::flash('toast', [
|
||||
'type' => 'success',
|
||||
'message' => 'Der Raum wurde gelöscht.',
|
||||
]);
|
||||
|
||||
return to_route('dashboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{average: string|null, median: string|null}
|
||||
*/
|
||||
private function currentRoundStatistics(Room $room): array
|
||||
{
|
||||
$values = [];
|
||||
|
||||
foreach ($room->players as $player) {
|
||||
$vote = $player->votes->first();
|
||||
|
||||
if ($vote !== null && is_numeric($vote->value)) {
|
||||
$values[] = (float) $vote->value;
|
||||
}
|
||||
}
|
||||
|
||||
sort($values, SORT_NUMERIC);
|
||||
$count = count($values);
|
||||
|
||||
if ($count === 0) {
|
||||
return ['average' => null, 'median' => null];
|
||||
}
|
||||
|
||||
$middle = intdiv($count, 2);
|
||||
$median = $count % 2 === 1
|
||||
? $values[$middle]
|
||||
: ($values[$middle - 1] + $values[$middle]) / 2;
|
||||
|
||||
return [
|
||||
'average' => number_format(array_sum($values) / $count, 2, ',', ''),
|
||||
'median' => number_format($median, 2, ',', ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,51 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\JoinRoomRequest;
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class RoomPlayerController extends Controller
|
||||
{
|
||||
public function join(JoinRoomRequest $request)
|
||||
public function join(JoinRoomRequest $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$user = $request->user();
|
||||
|
||||
$room = Room::where('join_code', '=', $validated['join_code'])->firstOrFail();
|
||||
[$room, $player] = DB::transaction(function () use ($request, $validated, $user): array {
|
||||
$room = Room::query()
|
||||
->where('join_code', $validated['join_code'])
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
|
||||
$player = $room->players()->create([
|
||||
'user_id' => $user?->id,
|
||||
'name' => $validated['player_name'],
|
||||
'is_moderator' => false,
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
if ($room->isClosed()) {
|
||||
throw ValidationException::withMessages([
|
||||
'join_code' => 'Dieser Raum ist geschlossen.',
|
||||
]);
|
||||
}
|
||||
|
||||
$player = $this->existingPlayer($request, $room);
|
||||
|
||||
if ($player === null) {
|
||||
$player = $room->players()->create([
|
||||
'user_id' => $user?->id,
|
||||
'name' => $validated['player_name'],
|
||||
'is_moderator' => false,
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
} else {
|
||||
$player->update(['last_seen_at' => now()]);
|
||||
}
|
||||
|
||||
$room->update([
|
||||
'last_activity_at' => now(),
|
||||
'state_version' => $room->state_version + 1,
|
||||
]);
|
||||
|
||||
return [$room, $player];
|
||||
});
|
||||
|
||||
$request->session()->put(
|
||||
"room_players.{$room->id}",
|
||||
@@ -27,6 +55,20 @@ class RoomPlayerController extends Controller
|
||||
);
|
||||
|
||||
return to_route('room.show', $room);
|
||||
}
|
||||
|
||||
private function existingPlayer(JoinRoomRequest $request, Room $room): ?Player
|
||||
{
|
||||
if ($request->user() !== null) {
|
||||
return $room->players()
|
||||
->where('user_id', $request->user()->id)
|
||||
->first();
|
||||
}
|
||||
|
||||
$playerId = $request->session()->get("room_players.{$room->id}");
|
||||
|
||||
return is_numeric($playerId)
|
||||
? $room->players()->find((int) $playerId)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ class RoundController extends Controller
|
||||
->lockForUpdate()
|
||||
->findOrFail($room->id);
|
||||
|
||||
$this->ensureRoomIsOpen($lockedRoom);
|
||||
|
||||
Vote::query()
|
||||
->where('room_id', $lockedRoom->id)
|
||||
->where('round', $lockedRoom->current_round)
|
||||
@@ -37,6 +39,7 @@ class RoundController extends Controller
|
||||
|
||||
$lockedRoom->update([
|
||||
'revealed_at' => null,
|
||||
'last_activity_at' => now(),
|
||||
'state_version' => $lockedRoom->state_version + 1,
|
||||
]);
|
||||
});
|
||||
@@ -62,6 +65,8 @@ class RoundController extends Controller
|
||||
->lockForUpdate()
|
||||
->findOrFail($room->id);
|
||||
|
||||
$this->ensureRoomIsOpen($lockedRoom);
|
||||
|
||||
if ($lockedRoom->revealed_at !== null) {
|
||||
throw ValidationException::withMessages([
|
||||
'room' => 'Die Karten sind bereits aufgedeckt.',
|
||||
@@ -70,6 +75,7 @@ class RoundController extends Controller
|
||||
|
||||
$lockedRoom->update([
|
||||
'revealed_at' => now(),
|
||||
'last_activity_at' => now(),
|
||||
'state_version' => $lockedRoom->state_version + 1,
|
||||
]);
|
||||
});
|
||||
@@ -95,6 +101,8 @@ class RoundController extends Controller
|
||||
->lockForUpdate()
|
||||
->findOrFail($room->id);
|
||||
|
||||
$this->ensureRoomIsOpen($lockedRoom);
|
||||
|
||||
if ($lockedRoom->revealed_at === null) {
|
||||
throw ValidationException::withMessages([
|
||||
'room' => 'Die aktuelle Runde muss zuerst aufgedeckt werden.',
|
||||
@@ -104,10 +112,20 @@ class RoundController extends Controller
|
||||
$lockedRoom->update([
|
||||
'current_round' => $lockedRoom->current_round + 1,
|
||||
'revealed_at' => null,
|
||||
'last_activity_at' => now(),
|
||||
'state_version' => $lockedRoom->state_version + 1,
|
||||
]);
|
||||
});
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
private function ensureRoomIsOpen(Room $room): void
|
||||
{
|
||||
if ($room->isClosed()) {
|
||||
throw ValidationException::withMessages([
|
||||
'room' => 'Dieser Raum ist geschlossen.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,12 @@ class VoteController extends Controller
|
||||
->lockForUpdate()
|
||||
->findOrFail($room->id);
|
||||
|
||||
if ($lockedRoom->isClosed()) {
|
||||
throw ValidationException::withMessages([
|
||||
'value' => 'Dieser Raum ist geschlossen.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($lockedRoom->revealed_at !== null) {
|
||||
throw ValidationException::withMessages([
|
||||
'value' => 'Die Karten wurden bereits aufgedeckt.',
|
||||
@@ -48,7 +54,10 @@ class VoteController extends Controller
|
||||
],
|
||||
);
|
||||
|
||||
$lockedRoom->increment('state_version');
|
||||
$lockedRoom->update([
|
||||
'last_activity_at' => now(),
|
||||
'state_version' => $lockedRoom->state_version + 1,
|
||||
]);
|
||||
});
|
||||
|
||||
return back();
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Middleware;
|
||||
use App\Models\Room;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureRoomParticipant
|
||||
@@ -23,23 +24,48 @@ class EnsureRoomParticipant
|
||||
}
|
||||
|
||||
$sessionKey = "room_players.{$room->id}";
|
||||
$playerId = $request->session()->get($sessionKey);
|
||||
|
||||
if (! $playerId) {
|
||||
if ($room->trashed()) {
|
||||
$request->session()->forget($sessionKey);
|
||||
Inertia::flash('toast', [
|
||||
'type' => 'info',
|
||||
'message' => 'Dieser Raum wurde gelöscht.',
|
||||
]);
|
||||
|
||||
return to_route('home');
|
||||
}
|
||||
|
||||
if ($room->isClosed()) {
|
||||
return to_route('room.join', [
|
||||
'join_code' => $room->join_code,
|
||||
])->withErrors([
|
||||
'join_code' => 'Du musst diesen Raum erst beitreten.',
|
||||
'join_code' => 'Dieser Raum ist geschlossen.',
|
||||
]);
|
||||
}
|
||||
|
||||
$player = $room->players()->find($playerId);
|
||||
$playerId = $request->session()->get($sessionKey);
|
||||
|
||||
if (! $player) {
|
||||
$player = $playerId ? $room->players()->find($playerId) : null;
|
||||
|
||||
if ($player === null && $playerId) {
|
||||
$request->session()->forget($sessionKey);
|
||||
}
|
||||
|
||||
if ($player === null && $request->user() !== null) {
|
||||
$player = $room->players()
|
||||
->where('user_id', $request->user()->id)
|
||||
->first();
|
||||
|
||||
if ($player !== null) {
|
||||
$request->session()->put($sessionKey, $player->id);
|
||||
}
|
||||
}
|
||||
|
||||
if ($player === null) {
|
||||
return to_route('room.join', [
|
||||
'join_code' => $room->join_code,
|
||||
])->withErrors([
|
||||
'join_code' => 'Du musst diesem Raum erst beitreten.',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ class HandleInertiaRequests extends Middleware
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
],
|
||||
'roomNotice' => fn () => $request->session()->get('room_notice'),
|
||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Models\Room;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class JoinRoomRequest extends FormRequest
|
||||
{
|
||||
@@ -37,9 +38,54 @@ class JoinRoomRequest extends FormRequest
|
||||
->where('join_code', $this->input('join_code'))
|
||||
->value('id');
|
||||
|
||||
$existingPlayerId = null;
|
||||
|
||||
if ($roomId !== null && $this->user() !== null) {
|
||||
$existingPlayerId = Player::query()
|
||||
->where('room_id', $roomId)
|
||||
->where('user_id', $this->user()->id)
|
||||
->value('id');
|
||||
} elseif ($roomId !== null) {
|
||||
$sessionPlayerId = $this->session()->get("room_players.{$roomId}");
|
||||
$existingPlayerId = is_numeric($sessionPlayerId)
|
||||
? Player::query()
|
||||
->where('room_id', $roomId)
|
||||
->whereKey((int) $sessionPlayerId)
|
||||
->value('id')
|
||||
: null;
|
||||
}
|
||||
|
||||
$playerNameRules = ['required', 'string', 'max:50'];
|
||||
|
||||
if ($existingPlayerId === null) {
|
||||
$playerNameRules[] = Rule::unique(Player::class, 'name')->where('room_id', $roomId);
|
||||
}
|
||||
|
||||
return [
|
||||
'join_code' => ['bail', 'string', 'required', 'size:6', Rule::exists(Room::class, 'join_code')],
|
||||
'player_name' => ['required', 'string', 'max:50', Rule::unique(Player::class, 'name')->where('room_id', $roomId)],
|
||||
'join_code' => [
|
||||
'bail',
|
||||
'string',
|
||||
'required',
|
||||
'size:6',
|
||||
Rule::exists(Room::class, 'join_code')->whereNull('deleted_at'),
|
||||
],
|
||||
'player_name' => $playerNameRules,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<callable(Validator): void> */
|
||||
public function after(): array
|
||||
{
|
||||
return [
|
||||
function (Validator $validator): void {
|
||||
$room = Room::query()
|
||||
->where('join_code', $this->input('join_code'))
|
||||
->first();
|
||||
|
||||
if ($room?->isClosed()) {
|
||||
$validator->errors()->add('join_code', 'Dieser Raum ist geschlossen.');
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Room;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -25,6 +26,10 @@ class SubmitVoteRequest extends FormRequest
|
||||
{
|
||||
$room = $this->route('room');
|
||||
|
||||
if (! $room instanceof Room) {
|
||||
return ['value' => ['prohibited']];
|
||||
}
|
||||
|
||||
return [
|
||||
'value' => ['bail', 'required', 'string', Rule::in(array_column($room->deck_config['cards'], 'key'))],
|
||||
];
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Room;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CloseInactiveRooms implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$closedAt = now();
|
||||
$guestCutoff = $closedAt->subDays(config('rooms.guest_retention_days'));
|
||||
$accountCutoff = $closedAt->subDays(config('rooms.account_retention_days'));
|
||||
|
||||
Room::query()
|
||||
->whereNull('closed_at')
|
||||
->where(function ($query) use ($guestCutoff, $accountCutoff): void {
|
||||
$query
|
||||
->where(function ($query) use ($guestCutoff): void {
|
||||
$query->whereNull('owner_id')
|
||||
->where('last_activity_at', '<=', $guestCutoff);
|
||||
})
|
||||
->orWhere(function ($query) use ($accountCutoff): void {
|
||||
$query->whereNotNull('owner_id')
|
||||
->where('last_activity_at', '<=', $accountCutoff);
|
||||
});
|
||||
})
|
||||
->select('id')
|
||||
->chunkById(100, function ($rooms) use ($closedAt, $guestCutoff, $accountCutoff): void {
|
||||
Room::query()
|
||||
->whereKey($rooms->pluck('id'))
|
||||
->whereNull('closed_at')
|
||||
->where(function ($query) use ($guestCutoff, $accountCutoff): void {
|
||||
$query
|
||||
->where(function ($query) use ($guestCutoff): void {
|
||||
$query->whereNull('owner_id')
|
||||
->where('last_activity_at', '<=', $guestCutoff);
|
||||
})
|
||||
->orWhere(function ($query) use ($accountCutoff): void {
|
||||
$query->whereNotNull('owner_id')
|
||||
->where('last_activity_at', '<=', $accountCutoff);
|
||||
});
|
||||
})
|
||||
->update([
|
||||
'closed_at' => $closedAt,
|
||||
'state_version' => DB::raw('state_version + 1'),
|
||||
'updated_at' => $closedAt,
|
||||
]);
|
||||
}, 'id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Actions\Rooms\ClaimGuestRooms;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Login;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ClaimGuestRoomsAfterLogin
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ClaimGuestRooms $claimGuestRooms,
|
||||
private readonly Request $request,
|
||||
) {}
|
||||
|
||||
public function handle(Login $event): void
|
||||
{
|
||||
if (! $event->user instanceof User || ! $this->request->hasSession()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$roomPlayers = $this->request->session()->get('room_players', []);
|
||||
|
||||
if (! is_array($roomPlayers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->claimGuestRooms->handle($event->user, $roomPlayers);
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -5,6 +5,13 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int|null $user_id
|
||||
* @property string $name
|
||||
* @property array<int, array{key: string, label: string}> $cards
|
||||
* @property bool $is_system
|
||||
*/
|
||||
class Deck extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
@@ -12,7 +19,7 @@ class Deck extends Model
|
||||
'cards',
|
||||
];
|
||||
|
||||
protected function casts()
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'cards' => 'array',
|
||||
@@ -20,6 +27,7 @@ class Deck extends Model
|
||||
];
|
||||
}
|
||||
|
||||
/** @return BelongsTo<User, $this> */
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
||||
@@ -5,7 +5,19 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $room_id
|
||||
* @property int|null $user_id
|
||||
* @property string $name
|
||||
* @property bool $is_moderator
|
||||
* @property Carbon|null $last_seen_at
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property-read Room $room
|
||||
*/
|
||||
class Player extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
@@ -24,16 +36,19 @@ class Player extends Model
|
||||
];
|
||||
}
|
||||
|
||||
/** @return BelongsTo<Room, $this> */
|
||||
public function room(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Room::class);
|
||||
}
|
||||
|
||||
/** @return HasMany<Vote, $this> */
|
||||
public function votes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Vote::class);
|
||||
}
|
||||
|
||||
/** @return BelongsTo<User, $this> */
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
||||
+40
-4
@@ -2,20 +2,40 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* @property string $id
|
||||
* @property int|null $owner_id
|
||||
* @property string|null $title
|
||||
* @property array{name: string, cards: array<int, array{key: string, label: string}>, id?: int} $deck_config
|
||||
* @property string $join_code
|
||||
* @property int $current_round
|
||||
* @property CarbonImmutable|null $revealed_at
|
||||
* @property int $state_version
|
||||
* @property CarbonImmutable|null $last_activity_at
|
||||
* @property CarbonImmutable|null $closed_at
|
||||
* @property CarbonImmutable|null $deleted_at
|
||||
* @property int $players_count
|
||||
*/
|
||||
class Room extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
use HasUuids, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'owner_id',
|
||||
'title',
|
||||
'current_round',
|
||||
'deck_config',
|
||||
'revealed_at',
|
||||
'state_version',
|
||||
'last_activity_at',
|
||||
'closed_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
@@ -24,18 +44,21 @@ class Room extends Model
|
||||
'current_round' => 'integer',
|
||||
'state_version' => 'integer',
|
||||
'revealed_at' => 'immutable_datetime',
|
||||
'last_activity_at' => 'immutable_datetime',
|
||||
'closed_at' => 'immutable_datetime',
|
||||
'deck_config' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
protected static function booted()
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function (Room $room) {
|
||||
$room->join_code ??= static::generateJoinCode();
|
||||
$room->last_activity_at ??= now();
|
||||
});
|
||||
}
|
||||
|
||||
private static function generateJoinCode(): string
|
||||
protected static function generateJoinCode(): string
|
||||
{
|
||||
$alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
|
||||
@@ -45,20 +68,33 @@ class Room extends Model
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
$code .= $alphabet[random_int(0, strlen($alphabet) - 1)];
|
||||
}
|
||||
} while (static::where('join_code', $code)->exists());
|
||||
} while (static::withTrashed()->where('join_code', $code)->exists());
|
||||
|
||||
return $code;
|
||||
|
||||
}
|
||||
|
||||
/** @return HasMany<Player, $this> */
|
||||
public function players(): HasMany
|
||||
{
|
||||
return $this->hasMany(Player::class);
|
||||
|
||||
}
|
||||
|
||||
/** @return BelongsTo<User, $this> */
|
||||
public function owner(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'owner_id');
|
||||
}
|
||||
|
||||
/** @return HasMany<Vote, $this> */
|
||||
public function votes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Vote::class);
|
||||
}
|
||||
|
||||
public function isClosed(): bool
|
||||
{
|
||||
return $this->closed_at !== null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,13 +49,21 @@ class User extends Authenticatable implements PasskeyUser
|
||||
];
|
||||
}
|
||||
|
||||
/** @return HasMany<Player, $this> */
|
||||
public function players(): HasMany
|
||||
{
|
||||
return $this->hasMany(Player::class);
|
||||
}
|
||||
|
||||
/** @return HasMany<Deck, $this> */
|
||||
public function decks(): HasMany
|
||||
{
|
||||
return $this->hasMany(Deck::class);
|
||||
}
|
||||
|
||||
/** @return HasMany<Room, $this> */
|
||||
public function ownedRooms(): HasMany
|
||||
{
|
||||
return $this->hasMany(Room::class, 'owner_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,17 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $player_id
|
||||
* @property string $room_id
|
||||
* @property int $round
|
||||
* @property string $value
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
*/
|
||||
class Vote extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
@@ -21,11 +31,13 @@ class Vote extends Model
|
||||
];
|
||||
}
|
||||
|
||||
/** @return BelongsTo<Room, $this> */
|
||||
public function room(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Room::class);
|
||||
}
|
||||
|
||||
/** @return BelongsTo<Player, $this> */
|
||||
public function player(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Player::class);
|
||||
|
||||
Reference in New Issue
Block a user