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 arrayFeedback
+ {{ entries|length }} submission{{ entries|length != 1 ? 's' : '' }}
+