From e470a9848d5f4413f5dadae4cf70d09b08060599 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 29 Aug 2026 11:19:54 +0200 Subject: [PATCH] Add read-tracking and soft-delete to contact messages Admin can now open a message via a per-row "View" page (which stamps read_at the first time it's opened), see a sortable Read column on the list instead of the raw message text, and soft-delete messages via a Delete button on both the list and detail view. Deleted messages are excluded from the default list so it doesn't pile up over time. --- migrations/Version20260829130000.php | 26 +++++++++ migrations/Version20260829140000.php | 26 +++++++++ .../Controller/GameAdminContactController.php | 36 ++++++++++++- src/Website/Entity/ContactMessage.php | 40 ++++++++++++++ .../Repository/ContactMessageRepository.php | 8 +++ templates/game/admin/contact/index.html.twig | 21 ++++++-- templates/game/admin/contact/show.html.twig | 53 +++++++++++++++++++ 7 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 migrations/Version20260829130000.php create mode 100644 migrations/Version20260829140000.php create mode 100644 templates/game/admin/contact/show.html.twig diff --git a/migrations/Version20260829130000.php b/migrations/Version20260829130000.php new file mode 100644 index 0000000..8890f31 --- /dev/null +++ b/migrations/Version20260829130000.php @@ -0,0 +1,26 @@ +addSql('ALTER TABLE contact_message ADD read_at DATETIME DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE contact_message DROP read_at'); + } +} diff --git a/migrations/Version20260829140000.php b/migrations/Version20260829140000.php new file mode 100644 index 0000000..077ea19 --- /dev/null +++ b/migrations/Version20260829140000.php @@ -0,0 +1,26 @@ +addSql('ALTER TABLE contact_message ADD deleted_at DATETIME DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE contact_message DROP deleted_at'); + } +} diff --git a/src/Game/Controller/GameAdminContactController.php b/src/Game/Controller/GameAdminContactController.php index 69b7252..c7cce47 100644 --- a/src/Game/Controller/GameAdminContactController.php +++ b/src/Game/Controller/GameAdminContactController.php @@ -4,8 +4,11 @@ declare(strict_types=1); namespace App\Game\Controller; +use App\Website\Entity\ContactMessage; use App\Website\Repository\ContactMessageRepository; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -18,7 +21,38 @@ final class GameAdminContactController extends AbstractController public function index(ContactMessageRepository $contactMessageRepository): Response { return $this->render('game/admin/contact/index.html.twig', [ - 'messages' => $contactMessageRepository->findBy([], ['createdAt' => 'DESC']), + 'messages' => $contactMessageRepository->findActive(), ]); } + + #[Route('/{id}', name: 'game_admin_contact_show', methods: ['GET'])] + public function show(ContactMessage $contactMessage, EntityManagerInterface $entityManager): Response + { + if (!$contactMessage->isRead()) { + $contactMessage->setReadAt(new \DateTimeImmutable()); + $entityManager->flush(); + } + + return $this->render('game/admin/contact/show.html.twig', [ + 'message' => $contactMessage, + ]); + } + + #[Route('/{id}/delete', name: 'game_admin_contact_delete', methods: ['POST'])] + public function delete(ContactMessage $contactMessage, Request $request, EntityManagerInterface $entityManager): Response + { + if (!$this->isCsrfTokenValid('delete_contact_' . $contactMessage->getId(), $request->request->get('_token'))) { + $this->addFlash('danger', 'Invalid CSRF token.'); + return $this->redirectToRoute('game_admin_contact'); + } + + if (!$contactMessage->isDeleted()) { + $contactMessage->setDeletedAt(new \DateTimeImmutable()); + $entityManager->flush(); + } + + $this->addFlash('success', sprintf('Message from "%s" deleted.', $contactMessage->getName())); + + return $this->redirectToRoute('game_admin_contact'); + } } diff --git a/src/Website/Entity/ContactMessage.php b/src/Website/Entity/ContactMessage.php index 1aca114..1292e3f 100644 --- a/src/Website/Entity/ContactMessage.php +++ b/src/Website/Entity/ContactMessage.php @@ -27,6 +27,12 @@ class ContactMessage #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] private ?\DateTimeImmutable $createdAt = null; + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + private ?\DateTimeImmutable $readAt = null; + + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + private ?\DateTimeImmutable $deletedAt = null; + public function __construct() { $this->createdAt = new \DateTimeImmutable(); @@ -84,4 +90,38 @@ class ContactMessage return $this; } + + public function getReadAt(): ?\DateTimeImmutable + { + return $this->readAt; + } + + public function setReadAt(?\DateTimeImmutable $readAt): static + { + $this->readAt = $readAt; + + return $this; + } + + public function isRead(): bool + { + return $this->readAt !== null; + } + + public function getDeletedAt(): ?\DateTimeImmutable + { + return $this->deletedAt; + } + + public function setDeletedAt(?\DateTimeImmutable $deletedAt): static + { + $this->deletedAt = $deletedAt; + + return $this; + } + + public function isDeleted(): bool + { + return $this->deletedAt !== null; + } } diff --git a/src/Website/Repository/ContactMessageRepository.php b/src/Website/Repository/ContactMessageRepository.php index 2634367..f4dbe4e 100644 --- a/src/Website/Repository/ContactMessageRepository.php +++ b/src/Website/Repository/ContactMessageRepository.php @@ -15,4 +15,12 @@ class ContactMessageRepository extends ServiceEntityRepository { parent::__construct($registry, ContactMessage::class); } + + /** + * @return ContactMessage[] + */ + public function findActive(): array + { + return $this->findBy(['deletedAt' => null], ['createdAt' => 'DESC']); + } } diff --git a/templates/game/admin/contact/index.html.twig b/templates/game/admin/contact/index.html.twig index 504dd69..9b694eb 100644 --- a/templates/game/admin/contact/index.html.twig +++ b/templates/game/admin/contact/index.html.twig @@ -15,7 +15,8 @@ Received Name Email - Message + Read + Actions @@ -26,11 +27,25 @@ {{ entry.email }} - {{ entry.message }} + {{ entry.read ? '✓' : '' }} + + View +
+ + +
+ {% else %} - No messages yet. + No messages yet. {% endfor %} diff --git a/templates/game/admin/contact/show.html.twig b/templates/game/admin/contact/show.html.twig new file mode 100644 index 0000000..e161dc3 --- /dev/null +++ b/templates/game/admin/contact/show.html.twig @@ -0,0 +1,53 @@ +{% extends 'game/admin/base.html.twig' %} + +{% block title %}Message from {{ message.name }} — Admin{% endblock %} + +{% block admin_body %} +
+

Message from {{ message.name }}

+ ← Back to Contact Messages +
+ +
+
+
Name
+
{{ message.name }}
+ +
Email
+
+ {{ message.email }} +
+ +
Received
+
{{ message.createdAt|date('Y-m-d H:i:s') }}
+ +
Read at
+
+ {% if message.readAt %} + {{ message.readAt|date('Y-m-d H:i:s') }} + {% else %} + Just now + {% endif %} +
+
+ +
+ +
{{ message.message }}
+ +
+ +
+ + +
+
+{% endblock %}