diff --git a/src/Game/Controller/GameAdminFeedbackController.php b/src/Game/Controller/GameAdminFeedbackController.php new file mode 100644 index 0000000..976b7df --- /dev/null +++ b/src/Game/Controller/GameAdminFeedbackController.php @@ -0,0 +1,25 @@ +render('game/admin/feedback/index.html.twig', [ + 'entries' => $feedbackService->getFeedbackEntries(), + 'summary' => $feedbackService->getFeedbackSummary(), + ]); + } +} diff --git a/src/Game/Repository/SessionSettingRepository.php b/src/Game/Repository/SessionSettingRepository.php index 45b788a..d1da111 100644 --- a/src/Game/Repository/SessionSettingRepository.php +++ b/src/Game/Repository/SessionSettingRepository.php @@ -36,4 +36,22 @@ class SessionSettingRepository extends ServiceEntityRepository return $qb->getQuery()->getOneOrNullResult(); } + + /** + * @return SessionSetting[] + */ + public function findFeedbackSettings(): array + { + return $this->createQueryBuilder('s') + ->andWhere('s.name IN (:names)') + ->setParameter('names', [ + SessionSettingType::FEEDBACK_DIFFICULTY->value, + SessionSettingType::FEEDBACK_ENTERTAINING->value, + SessionSettingType::FEEDBACK_THEME->value, + SessionSettingType::FEEDBACK_TEXT->value, + ]) + ->orderBy('s.player', 'DESC') + ->getQuery() + ->getResult(); + } } diff --git a/src/Game/Service/FeedbackService.php b/src/Game/Service/FeedbackService.php new file mode 100644 index 0000000..f440986 --- /dev/null +++ b/src/Game/Service/FeedbackService.php @@ -0,0 +1,84 @@ + + */ + public function getFeedbackEntries(): array + { + $entries = []; + + foreach ($this->sessionSettingRepository->findFeedbackSettings() as $setting) { + $player = $setting->getPlayer(); + if (!$player) { + continue; + } + + $key = $player->getId(); + if (!isset($entries[$key])) { + $entries[$key] = [ + 'session' => $setting->getSession(), + 'player' => $player, + 'difficulty' => null, + 'entertaining' => null, + 'theme' => null, + 'text' => null, + ]; + } + + match ($setting->getName()) { + SessionSettingType::FEEDBACK_DIFFICULTY => $entries[$key]['difficulty'] = (int) $setting->getValue(), + SessionSettingType::FEEDBACK_ENTERTAINING => $entries[$key]['entertaining'] = (int) $setting->getValue(), + SessionSettingType::FEEDBACK_THEME => $entries[$key]['theme'] = (int) $setting->getValue(), + SessionSettingType::FEEDBACK_TEXT => $entries[$key]['text'] = $setting->getValue(), + default => null, + }; + } + + return array_values($entries); + } + + /** + * Aggregate numbers only - free-text comments are left out since they're + * unmoderated player input and this summary is meant for the public + * briefing page. Use getFeedbackEntries() for the admin-only raw view. + * + * @return array{count: int, avgDifficulty: ?float, avgEntertaining: ?float, avgTheme: ?float} + */ + public function getFeedbackSummary(): array + { + $entries = $this->getFeedbackEntries(); + + return [ + 'count' => count($entries), + 'avgDifficulty' => $this->average(array_column($entries, 'difficulty')), + 'avgEntertaining' => $this->average(array_column($entries, 'entertaining')), + 'avgTheme' => $this->average(array_column($entries, 'theme')), + ]; + } + + /** + * @param array $values + */ + private function average(array $values): ?float + { + $values = array_filter($values, static fn ($v) => $v !== null); + + return $values ? round(array_sum($values) / count($values), 1) : null; + } +} diff --git a/src/Website/Controller/HomeController.php b/src/Website/Controller/HomeController.php index 12b6ea5..41a481f 100644 --- a/src/Website/Controller/HomeController.php +++ b/src/Website/Controller/HomeController.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace App\Website\Controller; +use App\Game\Service\FeedbackService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -17,10 +18,11 @@ final class HomeController extends AbstractController } #[Route(path: '/briefing', name: 'website_intro')] - public function intro(): Response + public function intro(FeedbackService $feedbackService): Response { - return $this->render( - 'website/home/intro.html.twig'); + return $this->render('website/home/intro.html.twig', [ + 'feedbackSummary' => $feedbackService->getFeedbackSummary(), + ]); } #[Route(path: '/looking-for-help', name: 'website_help_wanted')] diff --git a/templates/game/admin/base.html.twig b/templates/game/admin/base.html.twig index 0bd26e7..5047459 100644 --- a/templates/game/admin/base.html.twig +++ b/templates/game/admin/base.html.twig @@ -101,6 +101,7 @@ { route: 'game_admin_users', label: 'Users', icon: '👤' }, { route: 'game_admin_games', label: 'Games', icon: '🎮' }, { route: 'game_admin_sessions', label: 'Sessions', icon: '▶' }, + { route: 'game_admin_feedback', label: 'Feedback', icon: '💬' }, { route: 'game_admin_email_log', label: 'Email Log', icon: '✉' }, ] %} diff --git a/templates/game/admin/feedback/index.html.twig b/templates/game/admin/feedback/index.html.twig new file mode 100644 index 0000000..28be7ca --- /dev/null +++ b/templates/game/admin/feedback/index.html.twig @@ -0,0 +1,67 @@ +{% extends 'game/admin/base.html.twig' %} + +{% block title %}Feedback — Admin{% endblock %} + +{% block admin_body %} +
+

Feedback

+ {{ entries|length }} submission{{ entries|length != 1 ? 's' : '' }} +
+ +
+ {% set stats = [ + { label: 'Submissions', value: summary.count, color: '#64748b' }, + { label: 'Avg. Difficulty', value: summary.avgDifficulty is not null ? summary.avgDifficulty ~ '/10' : '—', color: '#3b82f6' }, + { label: 'Avg. Entertainment', value: summary.avgEntertaining is not null ? summary.avgEntertaining ~ '/10' : '—', color: '#8b5cf6' }, + { label: 'Avg. Theme', value: summary.avgTheme is not null ? summary.avgTheme ~ '/10' : '—', color: '#10b981' }, + ] %} + + {% for stat in stats %} +
+
{{ stat.value }}
+
{{ stat.label }}
+
+ {% endfor %} +
+ +
+ + + + + + + + + + + + + + {% for entry in entries %} + + + + + + + + + + {% else %} + + + + {% endfor %} + +
GameSessionPlayerDifficultyEntertainmentThemeComment
{{ entry.session.game.name }} + #{{ entry.session.id }} + {{ entry.player.user.username }}{{ entry.difficulty ?? '—' }}{{ entry.entertaining ?? '—' }}{{ entry.theme ?? '—' }}{{ entry.text ?? '' }}
No feedback submitted yet.
+
+{% endblock %} diff --git a/templates/website/home/intro.html.twig b/templates/website/home/intro.html.twig index fc72133..7ae9acd 100644 --- a/templates/website/home/intro.html.twig +++ b/templates/website/home/intro.html.twig @@ -30,6 +30,17 @@ color: var(--brand-teal-light, #6fb8d1); padding-right: .1em; } + .briefing-feedback { + background: rgba(111, 184, 209, .08); + border: 1px solid #1c5a70; + border-radius: .5rem; + padding: 1.25rem; + } + .briefing-feedback-stat { + font-size: 1.75rem; + font-weight: 700; + color: #6fb8d1; + } .dev-watermark-wrap { position: relative; display: inline-block; @@ -101,6 +112,34 @@

+ {% if feedbackSummary.count > 0 %} +
+
+ Field Reports from {{ feedbackSummary.count }} Agent{{ feedbackSummary.count != 1 ? 's' : '' }} +
+
+ {% if feedbackSummary.avgDifficulty is not null %} +
+
{{ feedbackSummary.avgDifficulty }}/10
+
Difficulty
+
+ {% endif %} + {% if feedbackSummary.avgEntertaining is not null %} +
+
{{ feedbackSummary.avgEntertaining }}/10
+
Entertainment
+
+ {% endif %} + {% if feedbackSummary.avgTheme is not null %} +
+
{{ feedbackSummary.avgTheme }}/10
+
Theme
+
+ {% endif %} +
+
+ {% endif %} +