207 lines
8.4 KiB
PHP
207 lines
8.4 KiB
PHP
<?php
|
|
|
|
namespace App\Controller\Admin\Teamer;
|
|
|
|
use App\Controller\Traits\AuthenticatedUserTrait;
|
|
use App\Controller\Traits\ReturnUrlTrait;
|
|
use App\Email\MailBodyRenderer;
|
|
use App\Form\TeamerMailingType;
|
|
use App\Htmx\HxRedirectResponse;
|
|
use App\Message\SendTeamerMailing;
|
|
use App\Model\TeamerMailingDto;
|
|
use App\Service\Common\TeamerFilterHandler;
|
|
use App\Service\Teamer\TeamerMailingDraftHandler;
|
|
use App\Service\Teamer\TeamerMailingService;
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\Form\FormInterface;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Messenger\MessageBusInterface;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
|
|
class MailingController extends AbstractController
|
|
{
|
|
use AuthenticatedUserTrait;
|
|
use ReturnUrlTrait;
|
|
|
|
public function __construct(
|
|
private readonly TeamerFilterHandler $filterHandler,
|
|
private readonly TeamerMailingService $mailingService,
|
|
private readonly TeamerMailingDraftHandler $draftHandler,
|
|
private readonly MailBodyRenderer $mailBodyRenderer,
|
|
private readonly MessageBusInterface $messageBus,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Compose the mailing. A submit of this form sends the preview to the composing admin,
|
|
* the real send goes through the confirmation modal below.
|
|
*/
|
|
#[Route('/admin/teamer/mailing', name: 'app_admin_teamer_mailing')]
|
|
#[IsGranted('ROLE_TEAM_ADMIN')]
|
|
public function index(Request $request): Response
|
|
{
|
|
$form = $this->createMailingForm($request);
|
|
|
|
if ($form->isSubmitted()) {
|
|
$this->draftHandler->saveDraft($form->getData());
|
|
}
|
|
|
|
if ($form->isSubmitted() && $form->isValid()) {
|
|
$this->mailingService->sendPreview($form->getData(), $this->getAuthenticatedUser());
|
|
|
|
$this->addFlash('success', sprintf(
|
|
'Die Vorschau wurde an %s gesendet',
|
|
$this->getUser()->getUserIdentifier()
|
|
));
|
|
}
|
|
|
|
$filterDto = $this->filterHandler->setScope($request->query->get('scope'))->getFilterSettings();
|
|
|
|
// No redirect after the preview mail, the composed mail has to survive it. The
|
|
// preview pane is rendered here so the page arrives complete; from then on the
|
|
// draft route above keeps it up to date.
|
|
return $this->render('admin/teamer/mailing.html.twig', [
|
|
'form' => $form->createView(),
|
|
'preview' => $this->renderPreview($form->getData()),
|
|
'filterDto' => $filterDto,
|
|
'recipients' => $this->mailingService->resolveRecipients($filterDto),
|
|
'placeholders' => TeamerMailingService::PLACEHOLDERS,
|
|
'hasDraft' => $this->draftHandler->hasDraft(),
|
|
'returnUrl' => $this->getReturnUrl($request, $this->getListRoute($request)),
|
|
'scope' => $request->query->get('scope'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Park what has been typed so far, so that a trip to the filter and back does not throw
|
|
* the composed mail away, and answer with the preview of that very draft.
|
|
*
|
|
* Both hang off the same form content, so they are deliberately one request rather than
|
|
* two routes on the same keystroke: the preview can never show wording other than the
|
|
* one that was parked.
|
|
*/
|
|
#[Route('/admin/teamer/mailing/draft', name: 'app_admin_teamer_mailing_draft', methods: ['POST'])]
|
|
#[IsGranted('ROLE_TEAM_ADMIN')]
|
|
public function draft(Request $request): Response
|
|
{
|
|
$mailingDto = $this->createMailingForm($request)->getData();
|
|
|
|
$this->draftHandler->saveDraft($mailingDto);
|
|
|
|
return $this->render('admin/teamer/_mailing_preview.html.twig', $this->renderPreview($mailingDto));
|
|
}
|
|
|
|
#[Route('/admin/teamer/mailing/discard', name: 'app_admin_teamer_mailing_discard')]
|
|
#[IsGranted('ROLE_TEAM_ADMIN')]
|
|
public function discard(Request $request): Response
|
|
{
|
|
$this->draftHandler->resetDraft();
|
|
|
|
return $this->redirectToRoute('app_admin_teamer_mailing', [
|
|
'scope' => $request->query->get('scope'),
|
|
]);
|
|
}
|
|
|
|
#[Route('/admin/teamer/mailing/confirm', name: 'app_admin_teamer_mailing_confirm', methods: ['POST'])]
|
|
#[IsGranted('ROLE_TEAM_ADMIN')]
|
|
public function confirm(Request $request): Response
|
|
{
|
|
$form = $this->createMailingForm($request);
|
|
$filterDto = $this->filterHandler->setScope($request->query->get('scope'))->getFilterSettings();
|
|
|
|
// the composed mail is carried on in hidden fields so no half written mailing has
|
|
// to be parked in the session between the confirmation and the send
|
|
return $this->render('admin/teamer/modal_mailing_confirm.html.twig', [
|
|
'form' => $form->createView(),
|
|
'mailingDto' => $form->getData(),
|
|
'valid' => $form->isSubmitted() && $form->isValid(),
|
|
'recipients' => $this->mailingService->resolveRecipients($filterDto),
|
|
'scope' => $request->query->get('scope'),
|
|
]);
|
|
}
|
|
|
|
#[Route('/admin/teamer/mailing/send', name: 'app_admin_teamer_mailing_send', methods: ['POST'])]
|
|
#[IsGranted('ROLE_TEAM_ADMIN')]
|
|
public function send(Request $request): Response
|
|
{
|
|
$form = $this->createMailingForm($request);
|
|
|
|
if (false === $form->isSubmitted() || false === $form->isValid()) {
|
|
$this->addFlash('error', 'Die Mail konnte nicht gesendet werden, Betreff oder Nachricht fehlen');
|
|
|
|
return new HxRedirectResponse($this->generateUrl('app_admin_teamer_mailing', [
|
|
'scope' => $request->query->get('scope'),
|
|
]));
|
|
}
|
|
|
|
$recipients = $this->mailingService->resolveRecipients(
|
|
$this->filterHandler->setScope($request->query->get('scope'))->getFilterSettings()
|
|
);
|
|
$mailingDto = $form->getData();
|
|
|
|
// the recipients travel with the message, so the mailing reaches exactly the
|
|
// people the confirmation modal counted even though it is sent from a worker
|
|
$this->messageBus->dispatch(SendTeamerMailing::fromRecipients(
|
|
(string) $mailingDto->getSubject(),
|
|
(string) $mailingDto->getMessage(),
|
|
$recipients
|
|
));
|
|
|
|
// it is on its way, the next mailing starts on a blank page
|
|
$this->draftHandler->resetDraft();
|
|
|
|
// deliberately not "wurde gesendet": at this point nothing has been handed to a
|
|
// mail server yet, and saying otherwise would make a failed mailing look fine
|
|
$this->addFlash('success', sprintf(
|
|
'Die Mail wird an %d Teamer:innen gesendet',
|
|
$recipients->getEligibleCount()
|
|
));
|
|
|
|
return new HxRedirectResponse($this->generateUrl($this->getListRoute($request)));
|
|
}
|
|
|
|
/**
|
|
* The list this mailing was started from, so a send returns to it and an unscoped
|
|
* mailing never falls back to the wrong one.
|
|
*/
|
|
private function getListRoute(Request $request): string
|
|
{
|
|
return TeamerFilterHandler::SCOPE_ALL_SEASON === $request->query->get('scope')
|
|
? 'app_administrative_season_teamer_index'
|
|
: 'app_administrative_teamer_index';
|
|
}
|
|
|
|
/**
|
|
* What the composing admin would receive. The placeholders are filled from their own
|
|
* record, the same way sendPreview() fills the test mail, so the pane and the mail that
|
|
* lands in their inbox show the same name.
|
|
*
|
|
* Deliberately rendered whether or not the form validates: a mail that is still missing
|
|
* its subject is exactly the one an admin is looking at while writing it.
|
|
*
|
|
* @return array{subject: string, bodyHtml: string}
|
|
*/
|
|
private function renderPreview(TeamerMailingDto $mailingDto): array
|
|
{
|
|
$values = $this->mailingService->placeholderValuesForUser($this->getAuthenticatedUser());
|
|
|
|
return [
|
|
'subject' => $this->mailingService->render($mailingDto->getSubject(), $values),
|
|
'bodyHtml' => $this->mailBodyRenderer->render($mailingDto->getMessage(), $values),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* A GET starts from the parked draft, a POST always carries the current one itself.
|
|
*/
|
|
private function createMailingForm(Request $request): FormInterface
|
|
{
|
|
$form = $this->createForm(TeamerMailingType::class, $this->draftHandler->getDraft());
|
|
$form->handleRequest($request);
|
|
|
|
return $form;
|
|
}
|
|
}
|