diff --git a/src/Game/Controller/GameAdminController.php b/src/Game/Controller/GameAdminController.php index aaac8ec..763f671 100644 --- a/src/Game/Controller/GameAdminController.php +++ b/src/Game/Controller/GameAdminController.php @@ -1,9 +1,12 @@ findByRole('ROLE_PLAYER'); - $sessions = $sessionRepository->findAll(); + $allUsers = $userRepository->findAll(); + $allSessions = $sessionRepository->findAll(); + $activeSessions = array_filter( + $allSessions, + fn(Session $s) => in_array($s->getStatus(), [SessionStatus::CREATED, SessionStatus::READY, SessionStatus::PLAYING]) + ); return $this->render('game/admin/index.html.twig', [ - 'players' => $players, - 'sessions' => $sessions, + 'totalUsers' => count($allUsers), + 'totalPlayers' => count($userRepository->findByRole('ROLE_PLAYER')), + 'totalAdmins' => count($userRepository->findByRole('ROLE_ADMIN')), + 'totalGames' => count($gameRepository->findAll()), + 'totalSessions' => count($allSessions), + 'activeSessions' => count($activeSessions), ]); } @@ -44,18 +56,13 @@ final class GameAdminController extends AbstractController $username = $player->getUser()->getUsername(); $logFile = $this->projectDir . '/var/log/sessions/' . $session->getId() . '/' . $username . '.txt'; - $logs = ''; - if (file_exists($logFile)) { - $logs = file_get_contents($logFile); - } - $playersLogs[] = [ 'username' => $username, - 'logs' => $logs, + 'logs' => file_exists($logFile) ? file_get_contents($logFile) : '', ]; } - return $this->render('game/admin/session.html.twig', [ + return $this->render('game/admin/sessions/view.html.twig', [ 'session' => $session, 'playersLogs' => $playersLogs, ]); diff --git a/src/Game/Controller/GameAdminEmailLogController.php b/src/Game/Controller/GameAdminEmailLogController.php new file mode 100644 index 0000000..a634ef6 --- /dev/null +++ b/src/Game/Controller/GameAdminEmailLogController.php @@ -0,0 +1,24 @@ +render('game/admin/email_log/index.html.twig', [ + 'logs' => $emailLogRepository->findBy([], ['sentAt' => 'DESC']), + ]); + } +} diff --git a/src/Game/Controller/GameAdminGameController.php b/src/Game/Controller/GameAdminGameController.php new file mode 100644 index 0000000..55a5df3 --- /dev/null +++ b/src/Game/Controller/GameAdminGameController.php @@ -0,0 +1,70 @@ +render('game/admin/games/index.html.twig', [ + 'games' => $gameRepository->findBy([], ['id' => 'ASC']), + ]); + } + + #[Route('/{id}/edit', name: 'game_admin_game_edit', methods: ['GET', 'POST'])] + public function edit( + Game $game, + Request $request, + EntityManagerInterface $em, + GameSettingRepository $gameSettingRepository, + ): Response { + $totalTimeSetting = $gameSettingRepository->getSetting($game, GameSettingType::TOTAL_TIME); + + $form = $this->createForm(AdminGameType::class, $game, [ + 'total_time' => $totalTimeSetting?->getValue(), + ]); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $totalTime = $form->get('totalTime')->getData(); + + if ($totalTime !== null) { + if (!$totalTimeSetting) { + $totalTimeSetting = new GameSetting(); + $totalTimeSetting->setGame($game); + $totalTimeSetting->setName(GameSettingType::TOTAL_TIME); + } + $totalTimeSetting->setValue((string) $totalTime); + $em->persist($totalTimeSetting); + } + + $em->flush(); + $this->addFlash('success', sprintf('Game "%s" updated.', $game->getName())); + + return $this->redirectToRoute('game_admin_games'); + } + + return $this->render('game/admin/games/edit.html.twig', [ + 'game' => $game, + 'form' => $form, + ]); + } +} diff --git a/src/Game/Controller/GameAdminSessionController.php b/src/Game/Controller/GameAdminSessionController.php new file mode 100644 index 0000000..75cfee9 --- /dev/null +++ b/src/Game/Controller/GameAdminSessionController.php @@ -0,0 +1,44 @@ +render('game/admin/sessions/index.html.twig', [ + 'sessions' => $sessionRepository->findBy([], ['created' => 'DESC']), + ]); + } + + #[Route('/{id}/close', name: 'game_admin_session_close', methods: ['POST'])] + public function close(Session $session, Request $request, EntityManagerInterface $em): Response + { + if (!$this->isCsrfTokenValid('close_session_' . $session->getId(), $request->request->get('_token'))) { + $this->addFlash('danger', 'Invalid CSRF token.'); + return $this->redirectToRoute('game_admin_sessions'); + } + + $session->setStatus(SessionStatus::LOST); + $em->flush(); + + $this->addFlash('success', sprintf('Session #%d closed.', $session->getId())); + + return $this->redirectToRoute('game_admin_sessions'); + } +} diff --git a/src/Game/Controller/GameAdminUserController.php b/src/Game/Controller/GameAdminUserController.php new file mode 100644 index 0000000..1030149 --- /dev/null +++ b/src/Game/Controller/GameAdminUserController.php @@ -0,0 +1,74 @@ +render('game/admin/users/index.html.twig', [ + 'users' => $userRepository->findBy([], ['id' => 'ASC']), + ]); + } + + #[Route('/{id}/edit', name: 'game_admin_user_edit', methods: ['GET', 'POST'])] + public function edit( + User $user, + Request $request, + EntityManagerInterface $em, + UserPasswordHasherInterface $passwordHasher, + ): Response { + $form = $this->createForm(AdminUserType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $plainPassword = $form->get('plainPassword')->getData(); + if ($plainPassword) { + $user->setPassword($passwordHasher->hashPassword($user, $plainPassword)); + } + + $em->flush(); + $this->addFlash('success', sprintf('User "%s" updated.', $user->getUsername())); + + return $this->redirectToRoute('game_admin_users'); + } + + return $this->render('game/admin/users/edit.html.twig', [ + 'user' => $user, + 'form' => $form, + ]); + } + + #[Route('/{id}/delete', name: 'game_admin_user_delete', methods: ['POST'])] + public function delete(User $user, Request $request, EntityManagerInterface $em): Response + { + if (!$this->isCsrfTokenValid('delete_user_' . $user->getId(), $request->request->get('_token'))) { + $this->addFlash('danger', 'Invalid CSRF token.'); + return $this->redirectToRoute('game_admin_users'); + } + + $username = $user->getUsername(); + $em->remove($user); + $em->flush(); + + $this->addFlash('success', sprintf('User "%s" deleted.', $username)); + + return $this->redirectToRoute('game_admin_users'); + } +} diff --git a/src/Game/Form/AdminGameType.php b/src/Game/Form/AdminGameType.php new file mode 100644 index 0000000..3512d98 --- /dev/null +++ b/src/Game/Form/AdminGameType.php @@ -0,0 +1,55 @@ +add('name', TextType::class, [ + 'constraints' => [new NotBlank()], + ]) + ->add('numberOfPlayers', IntegerType::class, [ + 'label' => 'Number of players', + 'constraints' => [new NotBlank(), new Positive()], + ]) + ->add('status', ChoiceType::class, [ + 'choices' => [ + 'In development' => GameStatus::IN_DEVELOPMENT, + 'Locked' => GameStatus::LOCKED, + 'Open' => GameStatus::OPEN, + ], + ]) + ->add('totalTime', IntegerType::class, [ + 'mapped' => false, + 'required' => false, + 'label' => 'Total time (seconds)', + 'data' => $options['total_time'], + 'attr' => ['placeholder' => 'e.g. 3600 for 1 hour'], + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Game::class, + 'total_time' => null, + ]); + + $resolver->setAllowedTypes('total_time', ['null', 'string', 'int']); + } +} diff --git a/src/Tech/Form/AdminUserType.php b/src/Tech/Form/AdminUserType.php new file mode 100644 index 0000000..d701d87 --- /dev/null +++ b/src/Tech/Form/AdminUserType.php @@ -0,0 +1,58 @@ +add('email', EmailType::class, [ + 'constraints' => [new NotBlank(), new Email()], + ]) + ->add('username', TextType::class, [ + 'constraints' => [new NotBlank(), new Length(min: 2, max: 180)], + ]) + ->add('plainPassword', PasswordType::class, [ + 'mapped' => false, + 'required' => false, + 'label' => 'New password', + 'attr' => ['autocomplete' => 'new-password', 'placeholder' => 'Leave blank to keep current'], + ]) + ->add('roles', ChoiceType::class, [ + 'choices' => [ + 'Player' => 'ROLE_PLAYER', + 'Admin' => 'ROLE_ADMIN', + ], + 'multiple' => true, + 'expanded' => true, + 'label' => 'Roles', + ]) + ->add('isVerified', CheckboxType::class, [ + 'required' => false, + 'label' => 'Email verified', + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/templates/base.html.twig b/templates/base.html.twig index f53ebfc..c9a9e34 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -23,6 +23,7 @@ {% endif %} +{% block main %}
{% for label, messages in app.flashes %} {% for message in messages %} @@ -33,6 +34,7 @@ {% endfor %} {% block body %}{% endblock %}
+{% endblock %} diff --git a/templates/game/admin/base.html.twig b/templates/game/admin/base.html.twig new file mode 100644 index 0000000..3940254 --- /dev/null +++ b/templates/game/admin/base.html.twig @@ -0,0 +1,78 @@ +{% extends 'base.html.twig' %} + +{% block main %} +
+ + {# ── Sidebar ──────────────────────────────────────────────────────── #} + + + {# ── Content ──────────────────────────────────────────────────────── #} +
+ {% for label, messages in app.flashes %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endfor %} + + {% block admin_body %}{% endblock %} +
+ +
+{% endblock %} diff --git a/templates/game/admin/email_log/index.html.twig b/templates/game/admin/email_log/index.html.twig new file mode 100644 index 0000000..780b10c --- /dev/null +++ b/templates/game/admin/email_log/index.html.twig @@ -0,0 +1,49 @@ +{% extends 'game/admin/base.html.twig' %} + +{% block title %}Email Log — Admin{% endblock %} + +{% block admin_body %} +
+

Email Log

+ {{ logs|length }} entries +
+ +
+ + + + + + + + + + + + {% for log in logs %} + + + + + + + + {% else %} + + + + {% endfor %} + +
IDUserEmailTypeSent at
{{ log.id }}{{ log.user.username }}{{ log.user.email }} + {{ log.emailIdentifier }} + {{ log.sentAt|date('Y-m-d H:i:s') }}
No emails logged yet.
+
+{% endblock %} diff --git a/templates/game/admin/games/edit.html.twig b/templates/game/admin/games/edit.html.twig new file mode 100644 index 0000000..badafd9 --- /dev/null +++ b/templates/game/admin/games/edit.html.twig @@ -0,0 +1,64 @@ +{% extends 'game/admin/base.html.twig' %} + +{% block title %}Edit Game — Admin{% endblock %} + +{% block admin_body %} +
+ ← Games +
+ +

Edit game: {{ game.name }}

+ +
+ {{ form_start(form, {attr: {style: 'display: flex; flex-direction: column; gap: 1.25rem;'}}) }} + +
+ {{ form_label(form.name, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }} + {{ form_widget(form.name, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }} + {{ form_errors(form.name) }} +
+ +
+ {{ form_label(form.numberOfPlayers, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }} + {{ form_widget(form.numberOfPlayers, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }} + {{ form_errors(form.numberOfPlayers) }} +
+ +
+ {{ form_label(form.status, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }} + {{ form_widget(form.status, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }} + {{ form_errors(form.status) }} +
+ +
+ {{ form_label(form.totalTime, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }} + {{ form_widget(form.totalTime, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }} + In seconds — e.g. 3600 = 1 hour + {{ form_errors(form.totalTime) }} +
+ +
+ + Cancel +
+ + {{ form_end(form) }} +
+{% endblock %} diff --git a/templates/game/admin/games/index.html.twig b/templates/game/admin/games/index.html.twig new file mode 100644 index 0000000..15fbbae --- /dev/null +++ b/templates/game/admin/games/index.html.twig @@ -0,0 +1,60 @@ +{% extends 'game/admin/base.html.twig' %} + +{% block title %}Games — Admin{% endblock %} + +{% block admin_body %} +
+

Games

+ {{ games|length }} total +
+ +
+ + + + + + + + + + + + + {% for game in games %} + {% set statusColor = { + 'inDevelopment': '#fef3c7:#92400e', + 'locked': '#fee2e2:#991b1b', + 'open': '#dcfce7:#166534', + } %} + {% set colors = (statusColor[game.status.value] ?? '#f1f5f9:#475569')|split(':') %} + + + + + + + + + + {% else %} + + + + {% endfor %} + +
IDNamePlayersStatusSessionsActions
{{ game.id }}{{ game.name }}{{ game.numberOfPlayers }} + {{ game.status.value }} + {{ game.sessions|length }} + Edit +
No games found.
+
+{% endblock %} diff --git a/templates/game/admin/index.html.twig b/templates/game/admin/index.html.twig index 8378e8f..7bfe1d4 100644 --- a/templates/game/admin/index.html.twig +++ b/templates/game/admin/index.html.twig @@ -1,82 +1,90 @@ -{% extends 'base.html.twig' %} +{% extends 'game/admin/base.html.twig' %} -{% block title %}Game Admin Dashboard{% endblock %} +{% block title %}Admin Dashboard{% endblock %} -{% block body %} -

Game Admin Dashboard

+{% block admin_body %} +

Dashboard

-
-
-

All Players ({{ players|length }})

- - - - - - - - - - - - {% for player in players %} - - - - - - - - {% else %} - - - - {% endfor %} - -
IDUsernameEmailRolesVerified
{{ player.id }}{{ player.username }}{{ player.email }}{{ player.roles|join(', ') }}{{ player.isVerified ? 'Yes' : 'No' }}
No players found.
-
+
+ {% set stats = [ + { label: 'Total Users', value: totalUsers, color: '#3b82f6' }, + { label: 'Players', value: totalPlayers, color: '#8b5cf6' }, + { label: 'Admins', value: totalAdmins, color: '#f59e0b' }, + { label: 'Games', value: totalGames, color: '#10b981' }, + { label: 'Active Sessions', value: activeSessions, color: '#ef4444' }, + { label: 'Total Sessions', value: totalSessions, color: '#64748b' }, + ] %} -
-

All Sessions ({{ sessions|length }})

- - - - - - - - - - - - - {% for session in sessions %} - - - - - - - - - {% else %} - - - - {% endfor %} - -
IDGameStatusPlayers JoinedCreated AtActions
{{ session.id }}{{ session.game.name }}{{ session.status.value }} -
    - {% for p in session.players %} -
  • {{ p.user.username }} (Screen: {{ p.screen ?? 'N/A' }})
  • - {% else %} -
  • No players
  • - {% endfor %} -
- ({{ session.players|length }} / {{ session.game.numberOfPlayers }}) -
{{ session.created|date('Y-m-d H:i') }} - View Game Logs -
No sessions found.
-
+ {% for stat in stats %} +
+
{{ stat.value }}
+
{{ stat.label }}
+
+ {% endfor %} +
+ +
+ + 👤 Manage Users + + + 🎮 Manage Games + + + View Sessions + + + Email Log +
{% endblock %} diff --git a/templates/game/admin/sessions/index.html.twig b/templates/game/admin/sessions/index.html.twig new file mode 100644 index 0000000..9bd5c26 --- /dev/null +++ b/templates/game/admin/sessions/index.html.twig @@ -0,0 +1,79 @@ +{% extends 'game/admin/base.html.twig' %} + +{% block title %}Sessions — Admin{% endblock %} + +{% block admin_body %} +
+

Sessions

+ {{ sessions|length }} total +
+ +
+ + + + + + + + + + + + + {% for session in sessions %} + {% set statusColors = { + 'created': '#ede9fe:#5b21b6', + 'ready': '#fef3c7:#92400e', + 'playing': '#dbeafe:#1e40af', + 'won': '#dcfce7:#166534', + 'lost': '#fee2e2:#991b1b', + } %} + {% set colors = (statusColors[session.status.value] ?? '#f1f5f9:#475569')|split(':') %} + + + + + + + + + + {% else %} + + + + {% endfor %} + +
IDGameStatusPlayersCreatedActions
{{ session.id }}{{ session.game.name }} + {{ session.status.value }} + + {{ session.players|map(p => p.user.username)|join(', ') ?: '—' }} + ({{ session.players|length }}/{{ session.game.numberOfPlayers }}) + {{ session.created|date('Y-m-d H:i') }} + View logs + + {% if session.status.value not in ['won', 'lost'] %} +
+ + +
+ {% endif %} +
No sessions found.
+
+{% endblock %} diff --git a/templates/game/admin/sessions/view.html.twig b/templates/game/admin/sessions/view.html.twig new file mode 100644 index 0000000..f7030fe --- /dev/null +++ b/templates/game/admin/sessions/view.html.twig @@ -0,0 +1,83 @@ +{% extends 'game/admin/base.html.twig' %} + +{% block title %}Session #{{ session.id }} logs — Admin{% endblock %} + +{% block admin_body %} +
+ ← Sessions +
+ +

{{ session.game.name }} — Session #{{ session.id }}

+

{{ session.status.value }} · {{ session.players|length }} player(s) · Created {{ session.created|date('Y-m-d H:i') }}

+ + {% if playersLogs is empty %} +
+ No players in this session. +
+ {% else %} + {# Tab buttons #} +
+ {% for playerLog in playersLogs %} + + {% endfor %} +
+ + {# Tab content #} + {% for playerLog in playersLogs %} +
+
{{ playerLog.logs ?: 'No logs found for this player.' }}
+
+ {% endfor %} + {% endif %} + + +{% endblock %} diff --git a/templates/game/admin/users/edit.html.twig b/templates/game/admin/users/edit.html.twig new file mode 100644 index 0000000..7942238 --- /dev/null +++ b/templates/game/admin/users/edit.html.twig @@ -0,0 +1,71 @@ +{% extends 'game/admin/base.html.twig' %} + +{% block title %}Edit User — Admin{% endblock %} + +{% block admin_body %} +
+ ← Users +
+ +

Edit user: {{ user.username }}

+ +
+ {{ form_start(form, {attr: {style: 'display: flex; flex-direction: column; gap: 1.25rem;'}}) }} + +
+ {{ form_label(form.email, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }} + {{ form_widget(form.email, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }} + {{ form_errors(form.email) }} +
+ +
+ {{ form_label(form.username, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }} + {{ form_widget(form.username, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }} + {{ form_errors(form.username) }} +
+ +
+ {{ form_label(form.plainPassword, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }} + {{ form_widget(form.plainPassword, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }} + {{ form_errors(form.plainPassword) }} +
+ +
+ {{ form_label(form.roles, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.5rem; color: #374151; font-size: 0.875rem;'}}) }} +
+ {{ form_widget(form.roles) }} +
+ {{ form_errors(form.roles) }} +
+ +
+ {{ form_widget(form.isVerified) }} + {{ form_label(form.isVerified, null, {label_attr: {style: 'font-weight: 600; color: #374151; font-size: 0.875rem; cursor: pointer;'}}) }} + {{ form_errors(form.isVerified) }} +
+ +
+ + Cancel +
+ + {{ form_end(form) }} +
+{% endblock %} diff --git a/templates/game/admin/users/index.html.twig b/templates/game/admin/users/index.html.twig new file mode 100644 index 0000000..db650c9 --- /dev/null +++ b/templates/game/admin/users/index.html.twig @@ -0,0 +1,83 @@ +{% extends 'game/admin/base.html.twig' %} + +{% block title %}Users — Admin{% endblock %} + +{% block admin_body %} +
+

Users

+ {{ users|length }} total +
+ +
+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
IDUsernameEmailRolesVerifiedActions
{{ user.id }}{{ user.username }}{{ user.email }} + {% for role in user.roles %} + {% if role != 'ROLE_USER' %} + {{ role }} + {% endif %} + {% endfor %} + + {% if user.verified %} + ✓ Yes + {% else %} + ✗ No + {% endif %} + + Edit + + {% if user != app.user %} +
+ + +
+ {% endif %} +
No users found.
+
+{% endblock %}