feat: mailing to filtered teamer list

addresses #869dv9bur
This commit is contained in:
Björn Fromme
2026-08-11 16:25:53 +02:00
parent 48cd2d6b67
commit 6511f821ac
18 changed files with 1223 additions and 7 deletions
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace App\Model;
use Symfony\Component\Validator\Constraints as Assert;
class TeamerMailingDto
{
#[Assert\NotBlank(message: 'Bitte gib einen Betreff an')]
private ?string $subject = null;
#[Assert\NotBlank(message: 'Bitte gib eine Nachricht ein')]
private ?string $message = null;
public function getSubject(): ?string
{
return $this->subject;
}
public function setSubject(?string $subject): static
{
$this->subject = $subject;
return $this;
}
public function getMessage(): ?string
{
return $this->message;
}
public function setMessage(?string $message): static
{
$this->message = $message;
return $this;
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
namespace App\Model;
/**
* A single candidate for a mailing, projected straight out of the database.
*
* A mailing needs a name and an address per person and nothing else, so hydrating the
* teamer entities - with their availabilities, licenses, photo and user account - would
* cost a lot of time and memory for data that is never read.
*/
class TeamerMailingRecipient
{
public function __construct(
private readonly int $id,
private readonly ?string $firstName,
private readonly ?string $lastName,
private readonly ?string $email,
private readonly ?\DateTimeImmutable $deletedAt,
private readonly ?\DateTimeImmutable $disabledAt,
) {
}
/**
* @param array{
* id: int,
* firstName: ?string,
* lastName: ?string,
* email: ?string,
* deletedAt: ?\DateTimeImmutable,
* disabledAt: ?\DateTimeImmutable,
* } $row a row of TeamerRepository::getMailingRecipients()
*/
public static function fromRow(array $row): self
{
return new self(
$row['id'],
$row['firstName'],
$row['lastName'],
$row['email'],
$row['deletedAt'],
$row['disabledAt']
);
}
public function getId(): int
{
return $this->id;
}
public function getFirstName(): string
{
return (string) $this->firstName;
}
public function getLastName(): string
{
return (string) $this->lastName;
}
/**
* Built the same way as Teamer::getFullName(), so a mailing reads like every other
* place the app spells a teamer out.
*/
public function getFullName(): string
{
return sprintf('%s %s', $this->firstName, $this->lastName);
}
public function getEmail(): ?string
{
if (null === $this->email || '' === trim($this->email)) {
return null;
}
return $this->email;
}
public function isDeleted(): bool
{
return null !== $this->deletedAt;
}
public function isDisabled(): bool
{
return null !== $this->disabledAt;
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace App\Model;
/**
* The outcome of matching a teamer filter against the mailing rules, so that the compose
* page, the confirmation modal and the send itself all report the very same numbers.
*/
class TeamerMailingRecipientsDto
{
/**
* @var TeamerMailingRecipient[]
*/
private array $eligible = [];
private int $skippedNoEmail = 0;
private int $skippedDeleted = 0;
private int $skippedDisabled = 0;
public function addEligible(TeamerMailingRecipient $recipient): static
{
$this->eligible[] = $recipient;
return $this;
}
public function addSkippedNoEmail(): static
{
++$this->skippedNoEmail;
return $this;
}
public function addSkippedDeleted(): static
{
++$this->skippedDeleted;
return $this;
}
public function addSkippedDisabled(): static
{
++$this->skippedDisabled;
return $this;
}
/**
* @return TeamerMailingRecipient[]
*/
public function getEligible(): array
{
return $this->eligible;
}
public function getEligibleCount(): int
{
return count($this->eligible);
}
public function getSkippedNoEmail(): int
{
return $this->skippedNoEmail;
}
public function getSkippedDeleted(): int
{
return $this->skippedDeleted;
}
public function getSkippedDisabled(): int
{
return $this->skippedDisabled;
}
public function getSkippedCount(): int
{
return $this->skippedNoEmail + $this->skippedDeleted + $this->skippedDisabled;
}
public function getTotal(): int
{
return $this->getEligibleCount() + $this->getSkippedCount();
}
public function hasSkipped(): bool
{
return 0 < $this->getSkippedCount();
}
}