middleware(function ($request, $next) { if (!Session::has('user_id')) { return redirect()->route('login'); } $userId = Session::get('user_id'); $currentUser = DB::selectOne(" SELECT TOP 1 id, role FROM users WHERE id = ? ", [$userId]); $isAdmin = $currentUser && in_array(($currentUser->role ?? 'user'), ['admin', 'moderator'], true); if (!$isAdmin) { abort(403, 'Acesso negado - Apenas administradores'); } return $next($request); }); } public function index() { return view('Admin.matches'); } public function getActiveMatches() { try { $matches = DB::select(" SELECT tm.id, tm.title, tm.training_type, tm.map_name, tm.status, tm.created_at, tm.started_at, tm.ended_at, tm.max_players, tm.current_players AS player_count, COUNT(CASE WHEN tp.team = 1 AND tp.left_at IS NULL THEN 1 END) AS team_a_count, COUNT(CASE WHEN tp.team = 2 AND tp.left_at IS NULL THEN 1 END) AS team_b_count, MAX(CASE WHEN cap.team = 1 THEN ua.nickname END) AS captain_a, MAX(CASE WHEN cap.team = 2 THEN ub.nickname END) AS captain_b FROM mix_training_matches tm LEFT JOIN mix_training_participants tp ON tm.id = tp.training_match_id LEFT JOIN lobby_match_captains cap ON tm.id = cap.training_match_id LEFT JOIN users ua ON cap.user_id = ua.id AND cap.team = 1 LEFT JOIN users ub ON cap.user_id = ub.id AND cap.team = 2 WHERE tm.status IN ('waiting', 'ready_check', 'veto', 'in_progress') AND tm.ended_at IS NULL AND NOT EXISTS ( SELECT 1 FROM mix_training_history th WHERE th.match_id = tm.id ) GROUP BY tm.id, tm.title, tm.training_type, tm.map_name, tm.status, tm.created_at, tm.started_at, tm.ended_at, tm.max_players, tm.current_players ORDER BY COALESCE(tm.started_at, tm.created_at) DESC "); return response()->json([ 'success' => true, 'matches' => $matches, ]); } catch (\Exception $e) { Log::error('Erro ao buscar partidas ativas: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Erro ao carregar partidas', ], 500); } } public function getMatchDetails($matchId) { try { $match = DB::selectOne(" SELECT tm.*, host.nickname AS host_nickname FROM mix_training_matches tm LEFT JOIN users host ON tm.host_id = host.id WHERE tm.id = ? ", [$matchId]); if (!$match) { return response()->json([ 'success' => false, 'message' => 'Partida não encontrada', ], 404); } // AUTO FIX de partida bugada if ($match && $match->status !== 'finished') { $hasHistory = DB::table('mix_training_history') ->where('match_id', $matchId) ->exists(); if ($hasHistory) { DB::table('mix_training_matches') ->where('id', $matchId) ->update([ 'status' => 'finished', 'ended_at' => $match->ended_at ?? now(), ]); $match->status = 'finished'; } } $players = DB::select(" SELECT tp.id, tp.training_match_id, tp.user_id, tp.team, tp.joined_at, tp.left_at, u.nickname, u.avatar_url, u.skill_level, CASE WHEN cap.user_id IS NOT NULL THEN 1 ELSE 0 END AS is_captain, ISNULL(tps.kills, 0) AS kills, ISNULL(tps.deaths, 0) AS deaths, ISNULL(tps.assists, 0) AS assists, CAST(ISNULL(tps.rating, 0) AS FLOAT) AS rating, ISNULL(tps.is_ace, 0) AS is_ace, ISNULL(tps.is_mvp, 0) AS is_mvp FROM mix_training_participants tp JOIN users u ON tp.user_id = u.id LEFT JOIN lobby_match_captains cap ON cap.training_match_id = tp.training_match_id AND cap.user_id = tp.user_id AND cap.team = tp.team LEFT JOIN mix_training_player_stats tps ON tps.match_id = tp.training_match_id AND tps.user_id = tp.user_id WHERE tp.training_match_id = ? ORDER BY tp.team ASC, CASE WHEN cap.user_id IS NOT NULL THEN 0 ELSE 1 END, tp.joined_at ASC ", [$matchId]); $teamA = array_values(array_filter($players, fn($p) => (int) $p->team === 1)); $teamB = array_values(array_filter($players, fn($p) => (int) $p->team === 2)); return response()->json([ 'success' => true, 'match' => $match, 'team_a' => $teamA, 'team_b' => $teamB, 'players' => $players, ]); } catch (\Exception $e) { Log::error('Erro ao buscar detalhes da partida: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Erro ao carregar detalhes', ], 500); } } public function endMatch(Request $request, $matchId) { $userId = Session::get('user_id'); $winningTeam = (int) $request->input('winning_team'); $aceUserId = $request->input('ace_user_id'); $mvpUserId = $request->input('mvp_user_id'); $resultText = trim((string) $request->input('result_text', '')); $notes = trim((string) $request->input('notes', '')); if (!in_array($winningTeam, [1, 2], true)) { return response()->json([ 'success' => false, 'message' => '❌ Time vencedor inválido', ], 400); } try { DB::beginTransaction(); $match = DB::selectOne(" SELECT * FROM mix_training_matches WHERE id = ? AND status IN ('waiting', 'ready_check', 'veto', 'in_progress') ", [$matchId]); if (!$match) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => '❌ Partida não encontrada ou já finalizada', ], 404); } $players = DB::select(" SELECT tp.*, u.nickname FROM mix_training_participants tp JOIN users u ON tp.user_id = u.id WHERE tp.training_match_id = ? AND tp.left_at IS NULL ORDER BY tp.team, tp.joined_at ", [$matchId]); $winningTeamStr = (string) $winningTeam; $durationSeconds = 0; if (!empty($match->started_at)) { try { $durationSeconds = Carbon::parse($match->started_at)->diffInSeconds(now()); } catch (\Exception $e) { $durationSeconds = 0; } } $gameSlug = $this->resolveMatchGameSlug($match); DB::update(" UPDATE mix_training_matches SET status = 'finished', ended_at = GETDATE() WHERE id = ? ", [$matchId]); DB::table('mix_training_history')->updateOrInsert( ['match_id' => $matchId], [ 'winning_team' => $winningTeamStr, 'map_played' => $match->map_name ?? 'Crossfire', 'completed_at' => now(), ] ); $this->logAdminAction( $userId, 'end_training_match', null, $winningTeam, 'TrainingMatch #' . $matchId . ($notes !== '' ? ' | ' . $notes : '') . ($resultText !== '' ? ' | Resultado: ' . $resultText : '') ); foreach ($players as $player) { $existingStats = DB::selectOne(" SELECT TOP 1 id FROM mix_training_player_stats WHERE match_id = ? AND user_id = ? ", [$matchId, $player->user_id]); if (!$existingStats) { DB::table('mix_training_player_stats')->insert([ 'match_id' => $matchId, 'user_id' => $player->user_id, 'team' => (string) $player->team, 'kills' => 0, 'deaths' => 0, 'assists' => 0, 'hs_percentage' => 0, 'adr' => 0, 'rating' => 0, 'is_ace' => ((string) $aceUserId === (string) $player->user_id) ? 1 : 0, 'is_mvp' => ((string) $mvpUserId === (string) $player->user_id) ? 1 : 0, 'created_at' => now(), ]); } $isWinner = ((int) $player->team === $winningTeam); $pointsChange = $isWinner ? 25 : -15; $rankingQuery = DB::table('mix_ranking_cache') ->where('user_id', $player->user_id); if ($this->rankingHasGameSlug()) { $rankingQuery->where('game_slug', $gameSlug); } $ranking = $rankingQuery->first(); if (!$ranking) { $rankingPayload = [ 'user_id' => $player->user_id, 'total_points' => max(0, 1000 + $pointsChange), 'matches_played' => 1, 'wins' => $isWinner ? 1 : 0, 'losses' => $isWinner ? 0 : 1, 'kills' => 0, 'deaths' => 0, 'assists' => 0, 'aces' => ((string) $aceUserId === (string) $player->user_id) ? 1 : 0, 'mvps' => ((string) $mvpUserId === (string) $player->user_id) ? 1 : 0, 'avg_rating' => 0, 'last_updated' => now(), ]; if ($this->rankingHasGameSlug()) { $rankingPayload['game_slug'] = $gameSlug; } DB::table('mix_ranking_cache')->insert($rankingPayload); } else { $currentPoints = (int) ($ranking->total_points ?? 1000); $rankingUpdateQuery = DB::table('mix_ranking_cache') ->where('user_id', $player->user_id); if ($this->rankingHasGameSlug()) { $rankingUpdateQuery->where('game_slug', $gameSlug); } $rankingUpdateQuery->update([ 'total_points' => max(0, $currentPoints + $pointsChange), 'matches_played' => DB::raw('ISNULL(matches_played, 0) + 1'), 'wins' => DB::raw('ISNULL(wins, 0) + ' . ($isWinner ? '1' : '0')), 'losses' => DB::raw('ISNULL(losses, 0) + ' . ($isWinner ? '0' : '1')), 'aces' => DB::raw('ISNULL(aces, 0) + ' . (((string) $aceUserId === (string) $player->user_id) ? '1' : '0')), 'mvps' => DB::raw('ISNULL(mvps, 0) + ' . (((string) $mvpUserId === (string) $player->user_id) ? '1' : '0')), 'last_updated' => now(), ]); } } DB::table('mix_training_matches') ->where('id', $matchId) ->update([ 'status' => 'finished', 'ended_at' => now(), ]); DB::commit(); // GIGAX: ao encerrar, o provedor ativo move os jogadores primeiro // e somente depois remove as salas temporarias. try { $voiceCleanup = app(\App\Services\VoiceMatchService::class)->finishForMatch((int) $matchId); Log::info('GIGAX voice cleanup apos encerramento administrativo', [ 'match_id' => (int) $matchId, 'result' => $voiceCleanup, ]); } catch (\Throwable $e) { Log::warning('GIGAX voice cleanup falhou apos encerramento administrativo', [ 'match_id' => (int) $matchId, 'error' => $e->getMessage(), ]); } return response()->json([ 'success' => true, 'message' => '✅ Partida encerrada com sucesso!', 'winning_team' => $winningTeam, 'duration_seconds' => $durationSeconds, ]); } catch (\Exception $e) { DB::rollBack(); Log::error('Erro ao encerrar training match: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => '❌ Erro ao encerrar partida: ' . $e->getMessage(), ], 500); } } public function getMatchHistory(Request $request) { $page = max(1, (int) $request->input('page', 1)); $perPage = 20; $offset = ($page - 1) * $perPage; try { $matches = DB::select(" SELECT th.match_id, th.winning_team, th.map_played, DATEDIFF(SECOND, tm.started_at, tm.ended_at) AS duration_seconds, th.completed_at, CAST(NULL AS VARCHAR(100)) AS ended_by_name, ( SELECT COUNT(*) FROM mix_training_player_stats tps WHERE tps.match_id = th.match_id ) AS player_count FROM mix_training_history th LEFT JOIN mix_training_matches tm ON tm.id = th.match_id ORDER BY th.completed_at DESC OFFSET ? ROWS FETCH NEXT ? ROWS ONLY ", [$offset, $perPage]); $totalRow = DB::selectOne("SELECT COUNT(*) AS count FROM mix_training_history"); $total = (int) ($totalRow->count ?? 0); return response()->json([ 'success' => true, 'matches' => $matches, 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'total_pages' => max(1, (int) ceil($total / $perPage)), ]); } catch (\Exception $e) { Log::error('Erro ao buscar histórico training: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Erro ao carregar histórico', ], 500); } } public function getMatchResult($matchId) { try { $history = DB::selectOne(" SELECT TOP 1 * FROM mix_training_history WHERE match_id = ? ORDER BY completed_at DESC ", [$matchId]); $stats = DB::select(" SELECT tps.*, u.nickname FROM mix_training_player_stats tps JOIN users u ON tps.user_id = u.id WHERE tps.match_id = ? ORDER BY CASE WHEN tps.is_mvp = 1 THEN 0 ELSE 1 END, tps.team ASC, tps.kills DESC ", [$matchId]); return response()->json([ 'success' => true, 'result' => [ 'history' => $history, 'stats' => $stats, ], ]); } catch (\Exception $e) { Log::error('Erro ao buscar resultado da training match: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Erro ao carregar resultado', ], 500); } } public function getAdminLogs(Request $request) { $page = max(1, (int) $request->input('page', 1)); $perPage = 50; $offset = ($page - 1) * $perPage; try { $logs = DB::select(" SELECT al.*, admin.nickname AS admin_name FROM admin_action_logs al JOIN users admin ON al.admin_id = admin.id ORDER BY al.created_at DESC OFFSET ? ROWS FETCH NEXT ? ROWS ONLY ", [$offset, $perPage]); $totalRow = DB::selectOne("SELECT COUNT(*) AS count FROM admin_action_logs"); $total = (int) ($totalRow->count ?? 0); return response()->json([ 'success' => true, 'logs' => $logs, 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'total_pages' => max(1, (int) ceil($total / $perPage)), ]); } catch (\Exception $e) { Log::error('Erro ao buscar logs: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Erro ao carregar logs', ], 500); } } /** * Editar dados de um jogador dentro de uma partida. * Mantém a estrutura atual do admin e recalcula o ranking do jogador. */ public function updatePlayerMatchStats(Request $request, $matchId, $userId) { $adminId = Session::get('user_id'); $validated = $request->validate([ 'team' => 'nullable|in:1,2,A,B', 'kills' => 'nullable|integer|min:0|max:999', 'deaths' => 'nullable|integer|min:0|max:999', 'assists' => 'nullable|integer|min:0|max:999', 'hs_percentage' => 'nullable|numeric|min:0|max:100', 'adr' => 'nullable|numeric|min:0|max:9999', 'rating' => 'nullable|numeric|min:0|max:99.99', 'is_ace' => 'nullable|boolean', 'is_mvp' => 'nullable|boolean', 'notes' => 'nullable|string|max:1000', ]); try { DB::beginTransaction(); $match = DB::selectOne(" SELECT TOP 1 id, status FROM mix_training_matches WHERE id = ? ", [$matchId]); if (!$match) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Partida não encontrada' ], 404); } $participant = DB::selectOne(" SELECT TOP 1 training_match_id, user_id, team FROM mix_training_participants WHERE training_match_id = ? AND user_id = ? ", [$matchId, $userId]); if (!$participant) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Jogador não encontrado nesta partida' ], 404); } $teamValue = $validated['team'] ?? $participant->team; if ($teamValue === 'A') { $teamValue = 1; } if ($teamValue === 'B') { $teamValue = 2; } if (array_key_exists('team', $validated)) { DB::update(" UPDATE mix_training_participants SET team = ? WHERE training_match_id = ? AND user_id = ? ", [$teamValue, $matchId, $userId]); } $existingStats = DB::selectOne(" SELECT TOP 1 id FROM mix_training_player_stats WHERE match_id = ? AND user_id = ? ", [$matchId, $userId]); $statsPayload = [ 'team' => (string) $teamValue, 'kills' => (int) ($validated['kills'] ?? 0), 'deaths' => (int) ($validated['deaths'] ?? 0), 'assists' => (int) ($validated['assists'] ?? 0), 'hs_percentage' => (float) ($validated['hs_percentage'] ?? 0), 'adr' => (float) ($validated['adr'] ?? 0), 'rating' => (float) ($validated['rating'] ?? 0), 'is_ace' => !empty($validated['is_ace']) ? 1 : 0, 'is_mvp' => !empty($validated['is_mvp']) ? 1 : 0, ]; if (!empty($validated['is_ace'])) { DB::update(" UPDATE mix_training_player_stats SET is_ace = 0 WHERE match_id = ? ", [$matchId]); } if (!empty($validated['is_mvp'])) { DB::update(" UPDATE mix_training_player_stats SET is_mvp = 0 WHERE match_id = ? ", [$matchId]); } if ($existingStats) { DB::table('mix_training_player_stats') ->where('match_id', $matchId) ->where('user_id', $userId) ->update(array_merge($statsPayload, [ 'updated_at' => now(), ])); } else { DB::table('mix_training_player_stats')->insert(array_merge($statsPayload, [ 'match_id' => $matchId, 'user_id' => $userId, 'created_at' => now(), 'updated_at' => now(), ])); } $this->rebuildPlayerRankingSnapshot($userId, $this->resolveMatchGameSlug($match)); $this->logAdminAction( $adminId, 'edit_player_match_stats', null, null, 'TrainingMatch #' . $matchId . ' | user_id=' . $userId . (!empty($validated['notes']) ? ' | ' . $validated['notes'] : '') ); DB::commit(); return response()->json([ 'success' => true, 'message' => '✅ Dados do jogador atualizados com sucesso' ]); } catch (\Exception $e) { DB::rollBack(); Log::error('Erro ao editar dados do jogador na partida: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => '❌ Erro ao editar dados do jogador: ' . $e->getMessage() ], 500); } } /** * Reajusta partidas encerradas erroneamente. */ public function adjustClosedMatch(Request $request, $matchId) { $adminId = Session::get('user_id'); $validated = $request->validate([ 'winning_team' => 'required|in:1,2', 'map_played' => 'nullable|string|max:120', 'ace_user_id' => 'nullable|integer', 'mvp_user_id' => 'nullable|integer', 'result_text' => 'nullable|string|max:500', 'notes' => 'nullable|string|max:1000', ]); try { DB::beginTransaction(); $match = DB::selectOne(" SELECT TOP 1 id, map_name, status FROM mix_training_matches WHERE id = ? ", [$matchId]); if (!$match) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Partida não encontrada' ], 404); } $history = DB::selectOne(" SELECT TOP 1 match_id, winning_team, map_played FROM mix_training_history WHERE match_id = ? ORDER BY completed_at DESC ", [$matchId]); if (!$history) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Histórico da partida não encontrado' ], 404); } DB::table('mix_training_history') ->where('match_id', $matchId) ->update([ 'winning_team' => (string) $validated['winning_team'], 'map_played' => $validated['map_played'] ?? $history->map_played ?? $match->map_name, 'completed_at' => now(), ]); DB::table('mix_training_matches') ->where('id', $matchId) ->update([ 'status' => 'finished', 'map_name' => $validated['map_played'] ?? $match->map_name, 'ended_at' => now(), ]); if (array_key_exists('ace_user_id', $validated)) { DB::update(" UPDATE mix_training_player_stats SET is_ace = 0 WHERE match_id = ? ", [$matchId]); if (!empty($validated['ace_user_id'])) { DB::update(" UPDATE mix_training_player_stats SET is_ace = 1 WHERE match_id = ? AND user_id = ? ", [$matchId, $validated['ace_user_id']]); } } if (array_key_exists('mvp_user_id', $validated)) { DB::update(" UPDATE mix_training_player_stats SET is_mvp = 0 WHERE match_id = ? ", [$matchId]); if (!empty($validated['mvp_user_id'])) { DB::update(" UPDATE mix_training_player_stats SET is_mvp = 1 WHERE match_id = ? AND user_id = ? ", [$matchId, $validated['mvp_user_id']]); } } $this->rebuildMatchPlayersRanking($matchId); $this->logAdminAction( $adminId, 'adjust_training_match_result', null, (int) $validated['winning_team'], 'TrainingMatch #' . $matchId . ' | vencedor antigo=' . ($history->winning_team ?? '-') . ' | vencedor novo=' . $validated['winning_team'] . (!empty($validated['result_text']) ? ' | resultado=' . $validated['result_text'] : '') . (!empty($validated['notes']) ? ' | ' . $validated['notes'] : '') ); DB::commit(); return response()->json([ 'success' => true, 'message' => '✅ Partida reajustada com sucesso' ]); } catch (\Exception $e) { DB::rollBack(); Log::error('Erro ao reajustar partida fechada: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => '❌ Erro ao reajustar partida: ' . $e->getMessage() ], 500); } } /** * Aplicação de cartão amarelo/vermelho e penalidades. */ public function applyPlayerCard(Request $request, $userId) { $adminId = Session::get('user_id'); $validated = $request->validate([ 'card_type' => 'required|in:yellow,red', 'reason' => 'required|string|max:500', 'match_id' => 'nullable|integer', 'penalty_type' => 'nullable|in:none,warning,points,suspension_matches,suspension_days,temp_ban,perm_ban', 'penalty_value' => 'nullable|integer|min:0|max:9999', 'notes' => 'nullable|string|max:1000', ]); if (!Schema::hasTable('user_penalties')) { return response()->json([ 'success' => false, 'message' => 'Tabela user_penalties não existe. Rode o SQL enviado junto.' ], 500); } try { DB::beginTransaction(); $user = DB::selectOne(" SELECT TOP 1 id, nickname FROM users WHERE id = ? ", [$userId]); if (!$user) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Jogador não encontrado' ], 404); } $penaltyType = $validated['penalty_type'] ?? ($validated['card_type'] === 'red' ? 'suspension_matches' : 'warning'); $penaltyValue = (int) ($validated['penalty_value'] ?? ($validated['card_type'] === 'red' ? 1 : 0)); $expiresAt = null; if ($penaltyType === 'suspension_days' || $penaltyType === 'temp_ban') { $expiresAt = Carbon::now()->addDays(max(1, $penaltyValue)); } DB::table('user_penalties')->insert([ 'user_id' => $userId, 'admin_id' => $adminId, 'match_id' => $validated['match_id'] ?? null, 'card_type' => $validated['card_type'], 'reason' => $validated['reason'], 'penalty_type' => $penaltyType, 'penalty_value' => $penaltyValue, 'status' => 'active', 'notes' => $validated['notes'] ?? null, 'expires_at' => $expiresAt, 'created_at' => now(), 'updated_at' => now(), ]); if (Schema::hasColumn('users', 'status') && in_array($penaltyType, ['temp_ban', 'perm_ban'], true)) { DB::table('users')->where('id', $userId)->update([ 'status' => 'suspended', ]); } if (Schema::hasColumn('users', 'banned_until') && $expiresAt !== null) { DB::table('users')->where('id', $userId)->update([ 'banned_until' => $expiresAt, ]); } if (Schema::hasColumn('users', 'is_banned') && $penaltyType === 'perm_ban') { DB::table('users')->where('id', $userId)->update([ 'is_banned' => 1, ]); } $this->logAdminAction( $adminId, 'apply_player_card', null, null, 'user_id=' . $userId . ' | card=' . $validated['card_type'] . ' | penalty=' . $penaltyType . ' | value=' . $penaltyValue . ' | reason=' . $validated['reason'] . (!empty($validated['notes']) ? ' | ' . $validated['notes'] : '') ); DB::commit(); return response()->json([ 'success' => true, 'message' => '✅ Cartão/penalidade aplicada com sucesso' ]); } catch (\Exception $e) { DB::rollBack(); Log::error('Erro ao aplicar cartão/penalidade: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => '❌ Erro ao aplicar cartão: ' . $e->getMessage() ], 500); } } /** * Edição de perfil do jogador. */ public function updatePlayerProfile(Request $request, $userId) { $adminId = Session::get('user_id'); $validated = $request->validate([ 'nickname' => 'nullable|string|max:100', 'email' => 'nullable|email|max:190', 'avatar_url' => 'nullable|string|max:500', 'skill_level' => 'nullable|integer|min:1|max:99', 'role' => 'nullable|string|max:50', 'status' => 'nullable|string|max:50', 'bio' => 'nullable|string|max:1000', 'discord' => 'nullable|string|max:100', 'clan_id' => 'nullable|integer', 'notes' => 'nullable|string|max:1000', ]); try { DB::beginTransaction(); $user = DB::selectOne(" SELECT TOP 1 * FROM users WHERE id = ? ", [$userId]); if (!$user) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Jogador não encontrado' ], 404); } $updates = []; foreach (['nickname', 'email', 'avatar_url', 'skill_level', 'role', 'status', 'bio', 'discord'] as $field) { if (array_key_exists($field, $validated) && Schema::hasColumn('users', $field)) { $updates[$field] = $validated[$field]; } } if (!empty($updates)) { if (Schema::hasColumn('users', 'updated_at')) { $updates['updated_at'] = now(); } DB::table('users')->where('id', $userId)->update($updates); } if (array_key_exists('clan_id', $validated)) { $this->syncPlayerClan($userId, $validated['clan_id']); } $this->logAdminAction( $adminId, 'edit_player_profile', null, null, 'user_id=' . $userId . (!empty($validated['notes']) ? ' | ' . $validated['notes'] : '') ); DB::commit(); return response()->json([ 'success' => true, 'message' => '✅ Perfil do jogador atualizado com sucesso' ]); } catch (\Exception $e) { DB::rollBack(); Log::error('Erro ao editar perfil do jogador: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => '❌ Erro ao editar perfil: ' . $e->getMessage() ], 500); } } /** * Edição dedicada de clan/membro. */ public function updatePlayerClan(Request $request, $userId) { $adminId = Session::get('user_id'); $validated = $request->validate([ 'clan_id' => 'nullable|integer', 'role_in_clan' => 'nullable|string|max:50', 'notes' => 'nullable|string|max:1000', ]); try { DB::beginTransaction(); $this->syncPlayerClan($userId, $validated['clan_id'] ?? null, $validated['role_in_clan'] ?? 'member'); $this->logAdminAction( $adminId, 'edit_player_clan', null, null, 'user_id=' . $userId . ' | clan_id=' . (($validated['clan_id'] ?? null) === null ? 'NULL' : $validated['clan_id']) . (!empty($validated['notes']) ? ' | ' . $validated['notes'] : '') ); DB::commit(); return response()->json([ 'success' => true, 'message' => '✅ Clan do jogador atualizado com sucesso' ]); } catch (\Exception $e) { DB::rollBack(); Log::error('Erro ao editar clan do jogador: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => '❌ Erro ao editar clan do jogador: ' . $e->getMessage() ], 500); } } private function syncPlayerClan($userId, $clanId = null, $roleInClan = 'member') { if (Schema::hasColumn('users', 'clan_id')) { DB::table('users')->where('id', $userId)->update([ 'clan_id' => $clanId, ]); return; } if (Schema::hasTable('clan_members')) { DB::table('clan_members')->where('user_id', $userId)->delete(); if ($clanId !== null) { $payload = [ 'clan_id' => $clanId, 'user_id' => $userId, 'role' => $roleInClan, ]; if (Schema::hasColumn('clan_members', 'created_at')) { $payload['created_at'] = now(); } if (Schema::hasColumn('clan_members', 'updated_at')) { $payload['updated_at'] = now(); } DB::table('clan_members')->insert($payload); } } } private function rebuildMatchPlayersRanking($matchId) { $players = DB::select(" SELECT DISTINCT user_id FROM mix_training_participants WHERE training_match_id = ? UNION SELECT DISTINCT user_id FROM mix_training_player_stats WHERE match_id = ? ", [$matchId, $matchId]); foreach ($players as $player) { $this->rebuildPlayerRankingSnapshot($player->user_id, $this->resolveMatchGameSlugById($matchId)); } } private function rebuildPlayerRankingSnapshot($userId, ?string $gameSlug = null) { $gameSlug = $gameSlug ?: 'crossfire'; $gameJoinSql = ''; $gameWhereSql = ''; $bindings = [$userId]; if (Schema::hasColumn('mix_training_history', 'game_slug')) { $gameWhereSql = " AND ISNULL(th.game_slug, 'crossfire') = ?"; $bindings[] = $gameSlug; } elseif (Schema::hasColumn('mix_training_matches', 'game_slug')) { $gameJoinSql = ' LEFT JOIN mix_training_matches tm ON tm.id = tps.match_id'; $gameWhereSql = " AND ISNULL(tm.game_slug, 'crossfire') = ?"; $bindings[] = $gameSlug; } $row = DB::selectOne(" SELECT COUNT(*) AS matches_played, SUM(CASE WHEN th.winning_team = tps.team THEN 1 ELSE 0 END) AS wins, SUM(CASE WHEN th.winning_team <> tps.team THEN 1 ELSE 0 END) AS losses, SUM(ISNULL(tps.kills, 0)) AS kills, SUM(ISNULL(tps.deaths, 0)) AS deaths, SUM(ISNULL(tps.assists, 0)) AS assists, AVG(CAST(ISNULL(tps.rating, 0) AS FLOAT)) AS avg_rating, SUM(CASE WHEN ISNULL(tps.is_ace, 0) = 1 THEN 1 ELSE 0 END) AS aces, SUM(CASE WHEN ISNULL(tps.is_mvp, 0) = 1 THEN 1 ELSE 0 END) AS mvps FROM mix_training_player_stats tps INNER JOIN mix_training_history th ON th.match_id = tps.match_id {$gameJoinSql} WHERE tps.user_id = ? {$gameWhereSql} ", $bindings); $matchesPlayed = (int) ($row->matches_played ?? 0); $wins = (int) ($row->wins ?? 0); $losses = (int) ($row->losses ?? 0); $kills = (int) ($row->kills ?? 0); $deaths = (int) ($row->deaths ?? 0); $assists = (int) ($row->assists ?? 0); $avgRating = (float) ($row->avg_rating ?? 0); $aces = (int) ($row->aces ?? 0); $mvps = (int) ($row->mvps ?? 0); $kdRatio = $deaths > 0 ? round($kills / $deaths, 2) : (float) $kills; $totalPoints = max(0, 1000 + ($wins * 25) - ($losses * 15) + ($aces * 10) + ($mvps * 5)); $rankingKeys = ['user_id' => $userId]; $rankingPayload = [ 'total_points' => $totalPoints, 'matches_played' => $matchesPlayed, 'wins' => $wins, 'losses' => $losses, 'kills' => $kills, 'deaths' => $deaths, 'assists' => $assists, 'kd_ratio' => $kdRatio, 'avg_rating' => $avgRating, 'aces' => $aces, 'mvps' => $mvps, 'last_updated' => now(), ]; if ($this->rankingHasGameSlug()) { $rankingKeys['game_slug'] = $gameSlug; $rankingPayload['game_slug'] = $gameSlug; } DB::table('mix_ranking_cache')->updateOrInsert($rankingKeys, $rankingPayload); } private function rankingHasGameSlug(): bool { try { return Schema::hasColumn('mix_ranking_cache', 'game_slug'); } catch (\Throwable $e) { return false; } } private function resolveMatchGameSlugById($matchId): string { try { $match = DB::table('mix_training_matches')->where('id', $matchId)->first(); return $this->resolveMatchGameSlug($match); } catch (\Throwable $e) { return 'crossfire'; } } private function resolveMatchGameSlug($match): string { if (!$match) { return 'crossfire'; } if (isset($match->game_slug) && trim((string) $match->game_slug) !== '') { return $this->normalizeGameSlug((string) $match->game_slug); } foreach (['game', 'game_name', 'title', 'map_played', 'map_name', 'training_type'] as $field) { if (isset($match->{$field}) && trim((string) $match->{$field}) !== '') { $slug = $this->normalizeGameSlug((string) $match->{$field}); if ($slug !== '') { return $slug; } } } return 'crossfire'; } private function normalizeGameSlug(string $value): string { $value = strtolower(trim($value)); $map = [ 'cf' => 'crossfire', 'cross fire' => 'crossfire', 'crossfire' => 'crossfire', 'counter strike 2' => 'cs2', 'counter-strike 2' => 'cs2', 'counter-strike-2' => 'cs2', 'cs 2' => 'cs2', 'cs2' => 'cs2', 'valorant' => 'valorant', 'league of legends' => 'lol', 'league-of-legends' => 'lol', 'lol' => 'lol', 'dota 2' => 'dota2', 'dota-2' => 'dota2', 'dota2' => 'dota2', 'rainbow six siege' => 'rainbow-six-siege', 'rainbow-six-siege' => 'rainbow-six-siege', 'fortnite' => 'fortnite', 'apex legends' => 'apex-legends', 'apex-legends' => 'apex-legends', 'rocket league' => 'rocket-league', 'rocket-league' => 'rocket-league', ]; if (isset($map[$value])) { return $map[$value]; } if (str_contains($value, 'crossfire') || str_contains($value, 'cross fire')) { return 'crossfire'; } if (str_contains($value, 'counter') || str_contains($value, 'cs2')) { return 'cs2'; } if (str_contains($value, 'valorant')) { return 'valorant'; } $slug = preg_replace('/[^a-z0-9]+/', '-', $value); $slug = trim((string) $slug, '-'); return $slug !== '' ? $slug : 'crossfire'; } /** * Deletar partida (admin) */ public function deleteMatch($matchId) { Log::warning('GIGAX DELETE TRACE deleteMatch', [ 'match_id' => $matchId, 'user_id' => session('user_id'), 'ip' => request()->ip(), 'method' => request()->method(), 'url' => request()->fullUrl(), 'referer' => request()->header('referer'), 'user_agent' => request()->userAgent(), 'trace' => collect(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 12)) ->map(fn ($x) => [ 'file' => $x['file'] ?? null, 'line' => $x['line'] ?? null, 'function' => $x['function'] ?? null, 'class' => $x['class'] ?? null, ])->all(), ]); $userId = Session::get('user_id'); try { DB::beginTransaction(); $match = DB::selectOne(" SELECT TOP 1 * FROM mix_training_matches WHERE id = ? ", [$matchId]); if (!$match) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => '❌ Partida não encontrada' ], 404); } DB::delete(" DELETE FROM mix_training_participants WHERE training_match_id = ? ", [$matchId]); DB::delete(" DELETE FROM lobby_match_captains WHERE training_match_id = ? ", [$matchId]); DB::delete(" DELETE FROM mix_training_player_stats WHERE match_id = ? ", [$matchId]); DB::delete(" DELETE FROM mix_training_history WHERE match_id = ? ", [$matchId]); DB::delete(" DELETE FROM mix_training_matches WHERE id = ? ", [$matchId]); $this->logAdminAction( $userId, 'delete_training_match', null, null, 'Deleted TrainingMatch #' . $matchId ); DB::commit(); return response()->json([ 'success' => true, 'message' => '🗑️ Partida deletada com sucesso!' ]); } catch (\Exception $e) { DB::rollBack(); Log::error('Erro ao deletar partida: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => '❌ Erro ao deletar partida' ], 500); } } public function updateVoiceMode(Request $request, int $matchId) { $validated = $request->validate([ 'voice_mode' => ['required', 'in:A,B'], ]); $voiceMode = strtoupper((string) $validated['voice_mode']); $adminId = Session::get('user_id'); try { $match = DB::table('mix_training_matches')->where('id', $matchId)->first(); if (!$match) { return back()->with('error', 'Partida não encontrada.'); } $updatePayload = [ 'voice_mode' => $voiceMode, ]; if (Schema::hasColumn('mix_training_matches', 'voice_status')) { $updatePayload['voice_status'] = 'pending'; } if (Schema::hasColumn('mix_training_matches', 'voice_notes')) { $label = match ($voiceMode) { 'A' => 'TS3', 'B' => 'Discord', default => 'Discord', }; $updatePayload['voice_notes'] = 'Modo de voz atualizado para ' . $label; } DB::table('mix_training_matches') ->where('id', $matchId) ->update($updatePayload); $this->logAdminAction( $adminId, 'update_voice_mode', $this->resolveAdminLogMatchId($matchId), null, 'Modo de voz atualizado para ' . $voiceMode ); $status = (string) ($match->status ?? ''); if (in_array($status, ['veto', 'in_progress'], true)) { try { if (class_exists(\App\Services\VoiceMatchService::class)) { app(\App\Services\VoiceMatchService::class)->startForMatch($matchId); } } catch (\Throwable $voiceError) { Log::warning('Modo de voz salvo, mas a integração não iniciou automaticamente.', [ 'match_id' => $matchId, 'voice_mode' => $voiceMode, 'error' => $voiceError->getMessage(), ]); } } return back()->with('success', 'Modo de voz atualizado com sucesso.'); } catch (\Throwable $e) { Log::error('Erro ao atualizar modo de voz: ' . $e->getMessage(), [ 'match_id' => $matchId, 'voice_mode' => $voiceMode, ]); return back()->with('error', 'Erro ao atualizar modo de voz.'); } } /** * Visão geral de permissões administrativas. * * Mantém compatibilidade com a rota/admin que chama getPermissionOverview() * e evita depender de tabelas opcionais de permissões. */ public function getPermissionOverview() { try { $userId = (int) Session::get('user_id'); $currentUser = DB::table('users') ->select('id', 'nickname', 'role') ->where('id', $userId) ->first(); if (!$currentUser) { return response()->json([ 'success' => false, 'message' => 'Usuário administrativo não encontrado.', ], 404); } $role = strtolower(trim((string) ($currentUser->role ?? 'user'))); $isAdmin = $role === 'admin'; $isModerator = $role === 'moderator'; $hasAdminAccess = $isAdmin || $isModerator; $overview = [ 'admin' => 0, 'moderator' => 0, 'user' => 0, 'total_users' => 0, ]; if (Schema::hasTable('users')) { $rows = DB::table('users') ->select('role', DB::raw('COUNT(*) AS total')) ->groupBy('role') ->get(); foreach ($rows as $row) { $rowRole = strtolower(trim((string) ($row->role ?? 'user'))); $total = (int) ($row->total ?? 0); if ($rowRole === 'admin') { $overview['admin'] += $total; } elseif ($rowRole === 'moderator') { $overview['moderator'] += $total; } else { $overview['user'] += $total; } $overview['total_users'] += $total; } } return response()->json([ 'success' => true, 'user_id' => (int) $currentUser->id, 'nickname' => $currentUser->nickname ?? null, 'role' => $role, 'is_admin' => $isAdmin, 'is_moderator' => $isModerator, 'has_admin_access' => $hasAdminAccess, 'permissions' => [ 'view_admin' => $hasAdminAccess, 'manage_matches' => $hasAdminAccess, 'manage_players' => $hasAdminAccess, 'manage_penalties' => $hasAdminAccess, 'manage_voice' => $hasAdminAccess, 'view_anti_cheat' => $hasAdminAccess, ], 'overview' => $overview, ]); } catch (\Throwable $e) { Log::error('Erro ao carregar visão geral de permissões.', [ 'user_id' => Session::get('user_id'), 'error' => $e->getMessage(), ]); return response()->json([ 'success' => false, 'message' => 'Erro ao carregar permissões.', 'error' => config('app.debug') ? $e->getMessage() : null, ], 500); } } /** * Visão geral das filas do MIX. * * Compatível com a rota admin.queues.overview e com a estrutura atual: * mix_queue_main, mix_queue_players, mix_queue_secondary e mix_queues. */ public function getQueueOverview(Request $request) { try { $mainCount = Schema::hasTable('mix_queue_main') ? (int) DB::table('mix_queue_main')->count() : 0; $secondaryCount = Schema::hasTable('mix_queue_secondary') ? (int) DB::table('mix_queue_secondary')->count() : 0; $queuePlayersCount = Schema::hasTable('mix_queue_players') ? (int) DB::table('mix_queue_players')->count() : 0; $totalQueues = Schema::hasTable('mix_queues') ? (int) DB::table('mix_queues')->count() : 0; $activeQueues = 0; $waitingQueues = 0; $startedQueues = 0; $finishedQueues = 0; $queueStatus = []; $recentQueues = []; if (Schema::hasTable('mix_queues')) { $statusRows = DB::table('mix_queues') ->select('status', DB::raw('COUNT(*) AS total')) ->groupBy('status') ->get(); foreach ($statusRows as $row) { $status = strtolower(trim((string) ($row->status ?? 'unknown'))); $total = (int) ($row->total ?? 0); $queueStatus[$status !== '' ? $status : 'unknown'] = $total; if (in_array($status, ['waiting', 'open', 'searching', 'ready_check', 'forming'], true)) { $waitingQueues += $total; } if (in_array($status, ['started', 'in_progress', 'active', 'veto', 'ready_check'], true)) { $startedQueues += $total; } if (in_array($status, ['finished', 'completed', 'closed', 'cancelled'], true)) { $finishedQueues += $total; } if (!in_array($status, ['finished', 'completed', 'closed', 'cancelled'], true)) { $activeQueues += $total; } } $recentQuery = DB::table('mix_queues') ->orderByDesc('id') ->limit(20); $recentQueues = $recentQuery->get()->map(function ($queue) { return [ 'id' => (int) ($queue->id ?? 0), 'queue_number' => $queue->queue_number ?? null, 'status' => $queue->status ?? null, 'match_id' => isset($queue->match_id) ? (int) $queue->match_id : null, 'game_slug' => $queue->game_slug ?? null, 'queue_mode' => $queue->queue_mode ?? null, 'created_at' => $queue->created_at ?? null, 'started_at' => $queue->started_at ?? null, 'updated_at' => $queue->updated_at ?? null, ]; })->values()->all(); } $mainPlayers = []; if (Schema::hasTable('mix_queue_main')) { $mainPlayers = DB::table('mix_queue_main as q') ->leftJoin('users as u', 'u.id', '=', 'q.user_id') ->select( 'q.id', 'q.user_id', 'q.joined_at', 'u.nickname', 'u.avatar_url' ) ->orderBy('q.joined_at') ->get() ->map(fn ($row) => [ 'id' => (int) ($row->id ?? 0), 'user_id' => (int) ($row->user_id ?? 0), 'nickname' => $row->nickname ?? null, 'avatar_url' => $row->avatar_url ?? null, 'joined_at' => $row->joined_at ?? null, ]) ->values() ->all(); } $secondaryPlayers = []; if (Schema::hasTable('mix_queue_secondary')) { $secondaryPlayers = DB::table('mix_queue_secondary as q') ->leftJoin('users as u', 'u.id', '=', 'q.user_id') ->select( 'q.id', 'q.user_id', 'q.position', 'q.joined_at', 'u.nickname', 'u.avatar_url' ) ->orderBy('q.position') ->orderBy('q.joined_at') ->get() ->map(fn ($row) => [ 'id' => (int) ($row->id ?? 0), 'user_id' => (int) ($row->user_id ?? 0), 'nickname' => $row->nickname ?? null, 'avatar_url' => $row->avatar_url ?? null, 'position' => isset($row->position) ? (int) $row->position : null, 'joined_at' => $row->joined_at ?? null, ]) ->values() ->all(); } $queuePlayersByMode = []; if (Schema::hasTable('mix_queue_players')) { $modeRows = DB::table('mix_queue_players') ->select('queue_mode', DB::raw('COUNT(*) AS total')) ->groupBy('queue_mode') ->get(); foreach ($modeRows as $row) { $mode = strtolower(trim((string) ($row->queue_mode ?? 'default'))); $queuePlayersByMode[$mode !== '' ? $mode : 'default'] = (int) ($row->total ?? 0); } } return response()->json([ 'success' => true, 'overview' => [ 'main_queue_players' => $mainCount, 'secondary_queue_players' => $secondaryCount, 'queue_players' => $queuePlayersCount, 'total_waiting_players' => $mainCount + $secondaryCount, 'total_queues' => $totalQueues, 'active_queues' => $activeQueues, 'waiting_queues' => $waitingQueues, 'started_queues' => $startedQueues, 'finished_queues' => $finishedQueues, ], 'counts' => [ 'main' => $mainCount, 'secondary' => $secondaryCount, 'players' => $queuePlayersCount, 'queues' => $totalQueues, ], 'status' => $queueStatus, 'players_by_mode' => $queuePlayersByMode, 'main_queue' => $mainPlayers, 'secondary_queue' => $secondaryPlayers, 'recent_queues' => $recentQueues, ]); } catch (\Throwable $e) { Log::error('Erro ao carregar visão geral das filas.', [ 'user_id' => Session::get('user_id'), 'error' => $e->getMessage(), ]); return response()->json([ 'success' => false, 'message' => 'Erro ao carregar visão geral das filas.', 'error' => config('app.debug') ? $e->getMessage() : null, ], 500); } } public function getAntiCheatOverview() { try { $devices = 0; $online = 0; $highRisk = 0; if (\Illuminate\Support\Facades\Schema::hasTable('gp_devices')) { $devices = \Illuminate\Support\Facades\DB::table('gp_devices')->count(); if (\Illuminate\Support\Facades\Schema::hasColumn('gp_devices', 'is_online')) { $online = \Illuminate\Support\Facades\DB::table('gp_devices') ->where('is_online', 1) ->count(); } if (\Illuminate\Support\Facades\Schema::hasColumn('gp_devices', 'risk_score')) { $highRisk = \Illuminate\Support\Facades\DB::table('gp_devices') ->where('risk_score', '>=', 40) ->count(); } } return response()->json([ 'success' => true, 'devices' => $devices, 'online' => $online, 'high_risk' => $highRisk, 'heartbeat_ok' => true, 'anti_cheat_status' => 'online', ]); } catch (\Throwable $e) { return response()->json([ 'success' => false, 'message' => 'Erro ao carregar Anti-Cheat.', 'error' => config('app.debug') ? $e->getMessage() : null, ], 500); } } private function logAdminAction($adminId, $actionType, $matchId = null, $winningTeam = null, $notes = null): void { try { $adminId = (int) $adminId; if ($adminId <= 0) { Log::warning('Log administrativo ignorado: admin_id ausente ou inválido.', [ 'admin_id' => $adminId, 'action_type' => $actionType, 'match_id' => $matchId, ]); return; } $adminExists = DB::table('users')->where('id', $adminId)->exists(); if (!$adminExists) { Log::warning('Log administrativo ignorado: admin_id não encontrado em users.', [ 'admin_id' => $adminId, 'action_type' => $actionType, 'match_id' => $matchId, ]); return; } DB::table('admin_action_logs')->insert([ 'admin_id' => $adminId, 'action_type' => $actionType, 'match_id' => $matchId, 'winning_team' => $winningTeam, 'notes' => $notes, 'created_at' => now(), ]); } catch (\Throwable $e) { Log::warning('Falha ao registrar log administrativo sem interromper a ação.', [ 'admin_id' => $adminId, 'action_type' => $actionType, 'match_id' => $matchId, 'error' => $e->getMessage(), ]); } } private function resolveAdminLogMatchId($matchId) { try { $existsInActiveMatches = DB::table('mix_active_matches') ->where('id', $matchId) ->exists(); return $existsInActiveMatches ? $matchId : null; } catch (\Throwable $e) { Log::warning('Não foi possível validar match_id para admin_action_logs.', [ 'match_id' => $matchId, 'error' => $e->getMessage(), ]); return null; } } }