From 894a38bb27ff24e1662c2524d5f40340f4158391 Mon Sep 17 00:00:00 2001 From: Tiago Ramos <2222055@my.ipleiria.pt> Date: Sat, 3 Jan 2026 00:41:24 +0000 Subject: [PATCH] Added corrections to user profile and admin match and game history --- api/app/Http/Controllers/GameController.php | 105 +- api/app/Http/Controllers/MatchController.php | 75 +- api/routes/api.php | 37 +- frontend/src/pages/admin/AdminPage.vue | 1687 +++++++++++------- frontend/src/pages/user/UserPage.vue | 24 +- frontend/src/stores/api.js | 18 +- 6 files changed, 1240 insertions(+), 706 deletions(-) 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/routes/api.php b/api/routes/api.php index a1c3ffd..e4b0a6b 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -46,37 +46,8 @@ Route::prefix('v1')->group(function () { }); 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']); @@ -89,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 @@ -97,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 0ad8ad3..66484d6 100644 --- a/frontend/src/pages/user/UserPage.vue +++ b/frontend/src/pages/user/UserPage.vue @@ -1199,7 +1199,6 @@ const fetchData = async () => { }) games.value.push(...newData) } - if (isMatch) { const response = await apiStore.getCurrentUserMatches(gameMatchParams) const apiMatches = response.data.data @@ -1212,21 +1211,30 @@ const fetchData = async () => { if (game.is_draw) outcome = 'draw' else if (game.winner_user_id === authStore.currentUser.id) outcome = 'win' - let coinAmount = 0 - if (game.total_reward) { - coinAmount = parseFloat(game.total_reward) + // LÓGICA CORRIGIDA PARA COINS + // A API devolve 'stake' (o valor que cada um apostou) + let stakeAmount = 0 + if (game.stake) { + stakeAmount = parseFloat(game.stake) } - const displayAmount = outcome === 'loss' ? coinAmount / 2 : coinAmount + // Se Ganhou: O lucro é o valor da stake (ex: apostou 10, recebeu 20, lucro +10) + // Se Perdeu: O prejuízo é o valor da stake (ex: -10) + // Se Empate: Normalmente devolve-se a aposta, por isso é 0 (ou ajusta conforme a tua regra) + let displayAmount = 0 + + if (outcome === 'win') { + displayAmount = stakeAmount // Ganhou a aposta do outro + } else if (outcome === 'loss') { + displayAmount = stakeAmount // Perdeu a sua aposta (o sinal negativo é adicionado no HTML) + } return { id: game.id, variant: `Type ${game.type}`, outcome: outcome, opponent: opponent?.nickname || 'Unknown Player', - - amount: displayAmount, - + amount: displayAmount, // Valor corrigido duration: `${Math.floor(game.total_time / 60)}m ${game.total_time % 60}s`, began_at: game.began_at, details: { diff --git a/frontend/src/stores/api.js b/frontend/src/stores/api.js index 695aab6..ebfc4dd 100644 --- a/frontend/src/stores/api.js +++ b/frontend/src/stores/api.js @@ -174,9 +174,24 @@ export const useAPIStore = defineStore('api', () => { ...(params.sort_direction == 'asc' && { sort_direction: 'asc' }), ...(params.type && { type: params.type }), }).toString() + return axios.get(`${API_BASE_URL}/games?${queryParams}`) } + const getAdminMatches = (params = {}) => { + const queryParams = new URLSearchParams({ + page: params.page || 1, + ...(params.type && { type: params.type }), + ...(params.outcome && { outcome: params.outcome }), + ...(params.date_from && { date_from: params.date_from }), + ...(params.date_to && { date_to: params.date_to }), + sort_by: params.sort_by || 'began_at', + sort_direction: params.sort_direction || 'desc', + }).toString() + + return axios.get(`${API_BASE_URL}/matches?${queryParams}`) + } + const postAdmin = async (credentials) => { return axios.post(`${API_BASE_URL}/admin/users`, credentials) } @@ -199,7 +214,8 @@ export const useAPIStore = defineStore('api', () => { deleteUser, getAdminTransactions, getAdminGames, + getAdminMatches, postAdmin, gameQueryParameters, } -}) \ No newline at end of file +})