Skip to content
This repository was archived by the owner on Oct 15, 2025. It is now read-only.

Commit 82f76ce

Browse files
committed
feat: Email Event, admin and preview page
1 parent 9aa88c9 commit 82f76ce

File tree

9 files changed

+351
-0
lines changed

9 files changed

+351
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Auto-generated Migration: Please modify to your needs!
12+
*/
13+
final class Version20241202060920 extends AbstractMigration
14+
{
15+
public function getDescription(): string
16+
{
17+
return 'Email Event';
18+
}
19+
20+
public function up(Schema $schema): void
21+
{
22+
// this up() migration is auto-generated, please modify it to your needs
23+
$this->addSql(<<<'SQL'
24+
CREATE TABLE email_event (
25+
id UUID NOT NULL,
26+
to_user_id INT DEFAULT NULL,
27+
created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
28+
to_address VARCHAR(512) NOT NULL,
29+
subject VARCHAR(4096) NOT NULL,
30+
content TEXT NOT NULL,
31+
PRIMARY KEY(id)
32+
)
33+
SQL);
34+
$this->addSql(<<<'SQL'
35+
CREATE INDEX IDX_A6E34B2829F6EE60 ON email_event (to_user_id)
36+
SQL);
37+
$this->addSql(<<<'SQL'
38+
COMMENT ON COLUMN email_event.id IS '(DC2Type:ulid)'
39+
SQL);
40+
$this->addSql(<<<'SQL'
41+
COMMENT ON COLUMN email_event.created_at IS '(DC2Type:datetime_immutable)'
42+
SQL);
43+
$this->addSql(<<<'SQL'
44+
ALTER TABLE
45+
email_event
46+
ADD
47+
CONSTRAINT FK_A6E34B2829F6EE60 FOREIGN KEY (to_user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE
48+
SQL);
49+
}
50+
51+
public function down(Schema $schema): void
52+
{
53+
// this down() migration is auto-generated, please modify it to your needs
54+
$this->addSql(<<<'SQL'
55+
ALTER TABLE email_event DROP CONSTRAINT FK_A6E34B2829F6EE60
56+
SQL);
57+
$this->addSql(<<<'SQL'
58+
DROP TABLE email_event
59+
SQL);
60+
}
61+
}

src/Controller/Admin/DashboardController.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use App\Entity\Announcement;
88
use App\Entity\Comment;
99
use App\Entity\CommentLikeEvent;
10+
use App\Entity\EmailEvent;
1011
use App\Entity\Feedback;
1112
use App\Entity\Group;
1213
use App\Entity\HintOpenEvent;
@@ -68,6 +69,7 @@ public function configureMenuItems(): iterable
6869
yield MenuItem::linkToCrud('SolutionVideoEvent', 'fa fa-video', SolutionVideoEvent::class);
6970
yield MenuItem::linkToCrud('HintOpenEvent', 'fa fa-lightbulb', HintOpenEvent::class);
7071
yield MenuItem::linkToCrud('LoginEvent', 'fa fa-right-to-bracket', LoginEvent::class);
72+
yield MenuItem::linkToCrud('EmailEvent', 'fa fa-envelope', EmailEvent::class);
7173

7274
yield MenuItem::section('Feedback');
7375
yield MenuItem::linkToCrud('Feedback', 'fa fa-comments', Feedback::class);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller\Admin;
6+
7+
use App\Entity\EmailEvent;
8+
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
9+
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
10+
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
11+
use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
12+
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
13+
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
14+
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
15+
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
16+
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
17+
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
18+
19+
class EmailEventCrudController extends AbstractCrudController
20+
{
21+
public static function getEntityFqcn(): string
22+
{
23+
return EmailEvent::class;
24+
}
25+
26+
public function configureFields(string $pageName): iterable
27+
{
28+
return [
29+
IdField::new('id')->hideOnIndex()->setDisabled(),
30+
AssociationField::new('toUser'),
31+
TextField::new('toAddress')->hideOnIndex(),
32+
TextField::new('subject'),
33+
TextEditorField::new('content'),
34+
DateTimeField::new('createdAt', 'Created at')->setDisabled(),
35+
];
36+
}
37+
38+
public function configureFilters(Filters $filters): Filters
39+
{
40+
return $filters
41+
->add('toUser')
42+
;
43+
}
44+
45+
public function configureActions(Actions $actions): Actions
46+
{
47+
$previewAction = Action::new('preview', 'Preview', 'fa fa-eye')
48+
->linkToUrl(fn (EmailEvent $entity) => $this->generateUrl(
49+
'app_email_preview',
50+
['id' => $entity->getId()]
51+
));
52+
53+
return $actions
54+
->disable(Action::DELETE, Action::EDIT, Action::NEW)
55+
->add(Crud::PAGE_INDEX, Action::DETAIL)
56+
->add(Crud::PAGE_INDEX, $previewAction)
57+
->add(Crud::PAGE_DETAIL, $previewAction);
58+
}
59+
60+
public function configureCrud(Crud $crud): Crud
61+
{
62+
return $crud->setDefaultSort(['createdAt' => 'DESC']);
63+
}
64+
}

src/Controller/EmailController.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller;
6+
7+
use App\Entity\EmailEvent;
8+
use App\Entity\User;
9+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
10+
use Symfony\Component\HttpFoundation\Response;
11+
use Symfony\Component\Routing\Attribute\Route;
12+
use Symfony\Component\Security\Http\Attribute\CurrentUser;
13+
14+
class EmailController extends AbstractController
15+
{
16+
#[Route('/email/{id}', name: 'app_email_preview')]
17+
public function details(#[CurrentUser] User $user, EmailEvent $emailEvent): Response
18+
{
19+
// if this email is not owned by the current user and the user is not an admin,
20+
// we deny the access.
21+
if ($emailEvent->getToUser() !== $user && !$this->isGranted('ROLE_ADMIN')) {
22+
throw $this->createAccessDeniedException('You are not authorized to access this email.');
23+
}
24+
25+
return $this->render('email/preview.html.twig', [
26+
'email' => $emailEvent,
27+
]);
28+
}
29+
}

src/Entity/EmailEvent.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Entity;
6+
7+
use App\Repository\EmailEventRepository;
8+
use Doctrine\DBAL\Types\Types;
9+
use Doctrine\ORM\Mapping as ORM;
10+
use Symfony\Component\Validator\Constraints\Email;
11+
use Symfony\Component\Validator\Constraints\NotBlank;
12+
13+
#[ORM\Entity(repositoryClass: EmailEventRepository::class)]
14+
class EmailEvent extends BaseEvent
15+
{
16+
#[ORM\ManyToOne(inversedBy: 'emailEvents')]
17+
private ?User $toUser = null;
18+
19+
#[ORM\Column(type: Types::STRING, length: 512)]
20+
#[Email]
21+
private string $toAddress = '';
22+
23+
#[ORM\Column(type: Types::STRING, length: 4096)]
24+
#[NotBlank]
25+
private string $subject = '';
26+
27+
#[ORM\Column(type: Types::TEXT)]
28+
#[NotBlank]
29+
private string $content = '';
30+
31+
public function getToUser(): ?User
32+
{
33+
return $this->toUser;
34+
}
35+
36+
public function setToUser(?User $toUser): static
37+
{
38+
$this->toUser = $toUser;
39+
40+
return $this;
41+
}
42+
43+
public function getToAddress(): ?string
44+
{
45+
return $this->toAddress;
46+
}
47+
48+
public function setToAddress(string $toAddress): static
49+
{
50+
$this->toAddress = $toAddress;
51+
52+
return $this;
53+
}
54+
55+
public function getSubject(): ?string
56+
{
57+
return $this->subject;
58+
}
59+
60+
public function setSubject(string $subject): static
61+
{
62+
$this->subject = $subject;
63+
64+
return $this;
65+
}
66+
67+
public function getContent(): ?string
68+
{
69+
return $this->content;
70+
}
71+
72+
public function setContent(string $content): static
73+
{
74+
$this->content = $content;
75+
76+
return $this;
77+
}
78+
}

src/Entity/User.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
9090
#[ORM\OneToMany(targetEntity: Feedback::class, mappedBy: 'sender')]
9191
private Collection $feedback;
9292

93+
/**
94+
* @var Collection<int, EmailEvent>
95+
*/
96+
#[ORM\OneToMany(targetEntity: EmailEvent::class, mappedBy: 'toUser')]
97+
private Collection $emailEvents;
98+
9399
public function __construct()
94100
{
95101
$this->solutionEvents = new ArrayCollection();
@@ -99,6 +105,7 @@ public function __construct()
99105
$this->hintOpenEvents = new ArrayCollection();
100106
$this->loginEvents = new ArrayCollection();
101107
$this->feedback = new ArrayCollection();
108+
$this->emailEvents = new ArrayCollection();
102109
}
103110

104111
public function getId(): ?int
@@ -419,4 +426,34 @@ public function removeFeedback(Feedback $feedback): static
419426

420427
return $this;
421428
}
429+
430+
/**
431+
* @return Collection<int, EmailEvent>
432+
*/
433+
public function getEmailEvents(): Collection
434+
{
435+
return $this->emailEvents;
436+
}
437+
438+
public function addEmailEvent(EmailEvent $emailEvent): static
439+
{
440+
if (!$this->emailEvents->contains($emailEvent)) {
441+
$this->emailEvents->add($emailEvent);
442+
$emailEvent->setToUser($this);
443+
}
444+
445+
return $this;
446+
}
447+
448+
public function removeEmailEvent(EmailEvent $emailEvent): static
449+
{
450+
if ($this->emailEvents->removeElement($emailEvent)) {
451+
// set the owning side to null (unless already changed)
452+
if ($emailEvent->getToUser() === $this) {
453+
$emailEvent->setToUser(null);
454+
}
455+
}
456+
457+
return $this;
458+
}
422459
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Repository;
6+
7+
use App\Entity\EmailEvent;
8+
use App\Entity\User;
9+
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
10+
use Doctrine\Persistence\ManagerRegistry;
11+
12+
/**
13+
* @extends ServiceEntityRepository<EmailEvent>
14+
*/
15+
class EmailEventRepository extends ServiceEntityRepository
16+
{
17+
public function __construct(ManagerRegistry $registry)
18+
{
19+
parent::__construct($registry, EmailEvent::class);
20+
}
21+
22+
/**
23+
* Find the email target to the user.
24+
*
25+
* @param User $user The user to find the email target
26+
*
27+
* @return list<EmailEvent>
28+
*/
29+
public function findBySendTarget(User $user): array
30+
{
31+
return $this->findBy([
32+
'toUser' => $user,
33+
], orderBy: [
34+
'createdAt' => 'DESC',
35+
]);
36+
}
37+
}

templates/email/preview.html.twig

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{% extends 'app.html.twig' %}
2+
3+
{% block nav %}<twig:Navbar active="" hasUserInfo="false"/>{% endblock %}
4+
{% block title %}信件預覽{% endblock %}
5+
6+
{% block app %}
7+
<main class="app-email-preview">
8+
<div class="row">
9+
<article class="app-email-preview__rendered col-9">
10+
<h2 class="app-email-preview__title mb-4">
11+
<small><i class="bi bi-envelope-fill"></i></small>
12+
{{ email.subject }}
13+
</h2>
14+
15+
<hr>
16+
17+
<section class="app-email-preview__content">
18+
{{ email.content|raw }}
19+
</section>
20+
</article>
21+
22+
<aside class="app-email-preview__meta col-3">
23+
<ul class="app-email-preview__meta__list list-group">
24+
<li class="list-group-item">
25+
<i class="bi bi-envelope-arrow-up-fill"></i>
26+
<span>收件人:</span>
27+
<span>{{ email.toAddress }}</span>
28+
</li>
29+
<li class="list-group-item">
30+
<i class="bi bi-calendar-fill"></i>
31+
<span>發件日期:</span>
32+
<span>{{ email.createdAt|date('Y-m-d H:i:s') }}</span>
33+
</li>
34+
</ul>
35+
</aside>
36+
</div>
37+
</main>
38+
{% endblock %}

translations/messages.zh_TW.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ System Management: 系統管理
5959
Announcement: 公告
6060
URL: 網址
6161
Published: 發布
62+
Preview: 預覽
63+
To User: 收件使用者
64+
To Address: 收件信箱
65+
Subject: 主旨
66+
EmailEvent: 信件事件
6267

6368
result_presenter.tabs.result: 執行結果
6469
result_presenter.tabs.answer: 正確答案

0 commit comments

Comments
 (0)