diff --git a/api/app/Http/Controllers/GameController.php b/api/app/Http/Controllers/GameController.php index f4d5cf4..dbaf22f 100644 --- a/api/app/Http/Controllers/GameController.php +++ b/api/app/Http/Controllers/GameController.php @@ -17,6 +17,7 @@ class GameController extends Controller $user = Auth::user(); $query = Game::query()->with(["winner", "player1", "player2"]); + // Se não for Admin, mostra apenas os jogos do utilizador if ($user->type !== 'A') { $query->where(function ($q) use ($user) { $q->where('player1_user_id', $user->id) @@ -40,15 +41,68 @@ class GameController extends Controller $query->where("status", $request->status); } - if ($request->has("sort_direction") && in_array($request->sort_direction, ["asc", "desc"])) { + if ($request->has("sort_direction") && in_array($request->sort_direction, ["asc", "desc"])) { $query->orderBy("began_at", $request->sort_direction); - }else{ + } else { $query->orderBy("began_at", "desc"); } return response()->json($query->paginate(15)); } + // --- MÉTODOS ADICIONADOS --- + + // Host a new multiplayer Game + public function host(Request $request) + { + $request->validate([ + 'type' => 'required|in:3,9' + ]); + + $user = $request->user(); + + // Check for active game + $activeGame = Game::where(function ($q) use ($user) { + $q->where("player1_user_id", $user->id) + ->orWhere("player2_user_id", $user->id); + }) + ->whereIn("status", ["Pending", "Playing"]) + ->exists(); + + if ($activeGame) { + return response()->json(["message" => "You already have an active game."], 400); + } + + $game = Game::create([ + 'type' => $request->type, + 'status' => 'Pending', + 'player1_user_id' => $user->id, + 'player2_user_id' => self::GHOST_ID, // Ghost ID = 1 (Waiting for player) + 'began_at' => now(), + 'player1_points' => 0, + 'player2_points' => 0, + ]); + + return response()->json($game, 201); + } + + // List open games (Available to join) + public function open(Request $request) + { + $type = $request->query('type', '9'); + $GHOST_ID = 1; + + $games = Game::where('status', 'Pending') + ->where('player2_user_id', $GHOST_ID) // Games waiting for P2 + ->where('type', $type) + ->with('player1') + ->orderBy('began_at', 'asc') + ->paginate(10); + + return response()->json($games); + } + // --------------------------- + public function store(Request $request) { $this->authorize('create', Game::class); @@ -78,6 +132,7 @@ class GameController extends Controller $initialStatus = $request->input('status', 'Pending'); + // If specific player 2 is provided, auto-start game if ($player2Id !== self::GHOST_ID && !$request->has('status')) { $initialStatus = 'Playing'; } @@ -104,24 +159,36 @@ class GameController extends Controller public function join(Request $request, Game $game) { - // Prevent joining if already full or started - if ($game->status !== 'Pending' || $game->player2_user_id !== 1) { - return response()->json(['message' => 'Game is not available'], 400); + // Prevent joining if already full or started + if ($game->status !== 'Pending' || $game->player2_user_id !== self::GHOST_ID) { + return response()->json(['message' => 'Game is not available'], 400); + } + + // Prevent joining your own game + if ($game->player1_user_id === $request->user()->id) { + return response()->json(['message' => 'Cannot join your own game'], 400); + } + + // Check if joining user has another active game + $activeGame = Game::where(function ($q) use ($request) { + $q->where("player1_user_id", $request->user()->id) + ->orWhere("player2_user_id", $request->user()->id); + }) + ->whereIn("status", ["Pending", "Playing"]) + ->exists(); + + if ($activeGame) { + return response()->json(["message" => "You already have an active game."], 400); + } + + $game->update([ + 'player2_user_id' => $request->user()->id, + 'status' => 'Playing', + ]); + + return response()->json($game); } - // Prevent joining your own game - if ($game->player1_user_id === $request->user()->id) { - return response()->json(['message' => 'Cannot join your own game'], 400); - } - - $game->update([ - 'player2_user_id' => $request->user()->id, - 'status' => 'Playing', - ]); - - return response()->json($game); -} - public function show(Game $game) { $this->authorize('view', $game); @@ -177,4 +244,4 @@ class GameController extends Controller 'game' => $game->fresh(), ]); } -} +} \ No newline at end of file diff --git a/api/app/Http/Controllers/MatchController.php b/api/app/Http/Controllers/MatchController.php index fe6972f..444ccc9 100644 --- a/api/app/Http/Controllers/MatchController.php +++ b/api/app/Http/Controllers/MatchController.php @@ -3,12 +3,10 @@ namespace App\Http\Controllers; use App\Models\MatchGame; -use App\Models\Game; use App\Models\CoinTransaction; use App\Models\CoinTransactionType; use Illuminate\Http\Request; use App\Http\Requests\StoreMatchRequest; -use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; class MatchController extends Controller @@ -196,4 +194,77 @@ class MatchController extends Controller return response()->json($match); }); } + + public function host(Request $request) + { + $request->validate([ + 'type' => 'required|in:3,9', + 'stake' => 'required|integer|min:0' + ]); + + $user = $request->user(); + $stake = $request->stake; + + if ($user->coins_balance < $stake) { + return response()->json(['message' => 'Insufficient coin balance'], 400); + } + + $activeMatch = MatchGame::where(function ($q) use ($user) { + $q->where('player1_user_id', $user->id) + ->orWhere('player2_user_id', $user->id); + })->whereIn('status', ['Pending', 'Playing'])->exists(); + + if ($activeMatch) { + return response()->json(['message' => 'You already have an active match.'], 400); + } + + return DB::transaction(function () use ($request, $user, $stake) { + $GHOST_ID = 1; + + $user->decrement('coins_balance', $stake); + + $stakeType = CoinTransactionType::firstOrCreate( + ['name' => 'Match stake'], + ['type' => 'D'] // Debit + ); + + $match = MatchGame::create([ + 'type' => $request->type, + 'status' => 'Pending', + 'player1_user_id' => $user->id, + 'player2_user_id' => $GHOST_ID, + 'winner_user_id' => null, + 'loser_user_id' => null, + 'stake' => $stake, + 'began_at' => now(), + 'player1_marks' => 0, 'player2_marks' => 0, + 'player1_points' => 0, 'player2_points' => 0, + ]); + + CoinTransaction::create([ + 'user_id' => $user->id, + 'match_id' => $match->id, + 'coin_transaction_type_id' => $stakeType->id, + 'transaction_datetime' => now(), + 'coins' => -$stake, + ]); + + return response()->json($match, 201); + }); + } + + public function open(Request $request) + { + $type = $request->query('type', '9'); + $GHOST_ID = 1; + + $matches = MatchGame::where('status', 'Pending') + ->where('player2_user_id', $GHOST_ID) + ->where('type', $type) + ->with('player1') + ->orderBy('began_at', 'asc') + ->paginate(10); + + return response()->json($matches); + } } \ No newline at end of file diff --git a/api/app/Http/Controllers/UserController.php b/api/app/Http/Controllers/UserController.php index 98a063d..4e6a608 100644 --- a/api/app/Http/Controllers/UserController.php +++ b/api/app/Http/Controllers/UserController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use App\Models\Game; use App\Models\User; +use App\Models\MatchGame; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -178,72 +179,87 @@ class UserController extends Controller } /** - * GET /api/users/{user}/matches - * Histórico de Jogos Multiplayer do Utilizador + * GET /api/users/{user}/games + * Histórico de Partidas Individuais (Cartas) */ - public function getMatches(Request $request, User $user) + public function getGames(Request $request, User $user) { $this->authorize('view', $user); - $outcomeSql = "CASE - WHEN is_draw = 1 THEN 'DRAW' - WHEN winner_user_id = {$user->id} THEN 'WIN' - ELSE 'LOSS' - END"; - - $query = Game::query() - ->select('*') - ->selectRaw("($outcomeSql) as outcome_text") - ->where(function ($q) use ($user) { - $q->where('player1_user_id', $user->id) - ->orWhere('player2_user_id', $user->id); - }) - ->with(['winner', 'player1', 'player2']); + // Define a query base para GAMES + $query = \App\Models\Game::query() + ->where(function ($q) use ($user) { + $q->where('player1_user_id', $user->id) + ->orWhere('player2_user_id', $user->id); + }) + ->with(['winner', 'player1', 'player2']); if ($request->has('type')) { - $query->where('type', $request->type); + $query->where('type', $request->type); } if ($request->has('status')) { $query->where('status', $request->status); } - if ($request->filled('outcome')) { - switch ($request->outcome) { - case 'win': - $query->where('winner_user_id', $user->id) - ->where('is_draw', 0); - break; - case 'loss': - case 'forfeit': // Forfeit treated as loss for now - $query->where('loser_user_id', $user->id) - ->where('is_draw', 0); - break; - case 'draw': - $query->where('is_draw', 1); - break; - } + $sortDirection = $request->get('sort_direction', 'desc'); + $query->orderBy('began_at', $sortDirection); + + + $games = $query->paginate(10); + + $games->getCollection()->transform(function ($game) use ($user) { + if ($game->winner_user_id == $user->id) { + $game->outcome = 'win'; + } elseif ($game->is_draw) { + $game->outcome = 'draw'; + } else { + $game->outcome = 'loss'; + } + return $game; + }); + + return response()->json($games); } - if ($request->has('date_from')) { - $query->whereDate('began_at', '>=', $request->date_from); + /** + * GET /api/users/{user}/matches + * Histórico de Matches (Apostas / Marcas) + */ + public function getMatches(Request $request, User $user) + { + $this->authorize('view', $user); + + $query = MatchGame::query() + ->where(function ($q) use ($user) { + $q->where('player1_user_id', $user->id) + ->orWhere('player2_user_id', $user->id); + }) + ->with(['winner', 'player1', 'player2']); + + if ($request->has('type')) { + $query->where('type', $request->type); + } + + if ($request->has('status')) { + $query->where('status', $request->status); } - if ($request->has('date_to')) { - $query->whereDate('ended_at', '<=', $request->date_to); - } + $sortDirection = $request->get('sort_direction', 'desc'); + $query->orderBy('began_at', $sortDirection); - $order = $request->get('sort_by', 'began_at'); - $direction = $request->get('sort_direction', 'desc'); + $matches = $query->paginate(10); - if ($order === 'outcome') { - $query->orderBy('outcome_text', $direction); - } else { - $query->orderBy($order, $direction); - } - - $matches = $query->orderBy($order, $direction) - ->paginate(10); + $matches->getCollection()->transform(function ($match) use ($user) { + if ($match->winner_user_id == $user->id) { + $match->outcome = 'win'; + } elseif ($match->winner_user_id && $match->winner_user_id != $user->id) { + $match->outcome = 'loss'; + } else { + $match->outcome = 'interrupted'; // Matches geralmente não empatam (alguém chega a 4 marcas) + } + return $match; + }); return response()->json($matches); } diff --git a/api/routes/api.php b/api/routes/api.php index 1809c01..e4b0a6b 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -40,45 +40,14 @@ Route::prefix('v1')->group(function () { // User Resources Route::prefix('/users')->group(function () { Route::get('/{user}', [UserController::class, 'show']); - Route::get('/{user}/matches', [ - UserController::class, - 'getMatches', - ]); Route::get('/{user}/transactions', [UserController::class, 'getTransactions']); + Route::get('/{user}/games', [UserController::class, 'getGames']); + Route::get('/{user}/matches', [UserController::class, 'getMatches']); }); Route::prefix('games')->group(function () { - //Host a new multiplayer game - MUST come before resource routes - Route::post('host', function (\Illuminate\Http\Request $request) { - $request->validate([ - 'type' => 'required|in:3,9' - ]); - - $game = \App\Models\Game::create([ - 'type' => $request->type, - 'status' => 'Pending', - 'player1_user_id' => $request->user()->id, - 'player2_user_id' => 1, // Ghost player - 'began_at' => now(), - ]); - - return response()->json($game); - }); - - // List open games (available to join) - Route::get('open', function (\Illuminate\Http\Request $request) { - $type = $request->query('type', '9'); - - $games = \App\Models\Game::where('status', 'Pending') - ->where('player2_user_id', 1) // Only games waiting for a second player - ->where('type', $type) - ->with('player1') // Load player1 relationship - ->orderBy('began_at', 'asc') - ->paginate(10); - - return response()->json($games); - }); - + Route::post('host', [GameController::class, 'host']); + Route::get('open', [GameController::class, 'open']); Route::get('/', [GameController::class, 'index']); Route::get('/{game}', [GameController::class, 'show']); Route::post('/', [GameController::class, 'store']); @@ -91,6 +60,8 @@ Route::prefix('v1')->group(function () { Route::prefix('matches')->group(function () { Route::apiResource('/', MatchController::class)->parameters(['' => 'match']); Route::post('/{match}/join', [MatchController::class, 'join']); + Route::post('host', [MatchController::class, 'host']); // <--- NOVA + Route::get('open', [MatchController::class, 'open']); }); // Admin Routes @@ -99,6 +70,8 @@ Route::prefix('v1')->group(function () { ->group(function () { Route::get('/statistics', [StatisticsController::class, 'getAdminStats']); Route::get('/transactions', [TransactionController::class, 'index']); + Route::get('/games', [GameController::class, 'index']); + Route::get('/matches', [MatchController::class, 'index']); Route::prefix('users')->group(function () { Route::get('/', [UserController::class, 'index']); Route::post('/', [UserController::class, 'store']); diff --git a/frontend/src/pages/admin/AdminPage.vue b/frontend/src/pages/admin/AdminPage.vue index b23ee28..1245741 100644 --- a/frontend/src/pages/admin/AdminPage.vue +++ b/frontend/src/pages/admin/AdminPage.vue @@ -1,502 +1,846 @@ \ No newline at end of file + diff --git a/frontend/src/pages/user/UserPage.vue b/frontend/src/pages/user/UserPage.vue index e16057a..66484d6 100644 --- a/frontend/src/pages/user/UserPage.vue +++ b/frontend/src/pages/user/UserPage.vue @@ -1,137 +1,244 @@