feat: surface stuck booking edits to customer experts

This commit is contained in:
2026-09-15 15:13:47 +02:00
parent a1d1fdde14
commit 019de4e705
4 changed files with 215 additions and 0 deletions
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Dashboard\Widget;
use App\Dashboard\Contract\DashboardWidgetProviderInterface;
use App\Entity\BookingEditDraft;
use App\Model\DashboardWidget;
use App\Model\DashboardWidgetEntry;
use App\Repository\BookingEditDraftRepository;
use App\Security\Role;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Customers whose changes cannot be saved.
*
* A draft is deleted as soon as an update succeeds, so one that has been around for days belongs to
* somebody BusPro keeps refusing — and nothing else tells anyone. One booking accumulated
* thirty-seven failed attempts across two months before it was noticed by accident; the customer
* had entered seventy-nine participants, none of which ever reached BusPro. The point of this card
* is that the next one gets noticed in the first week instead.
*
* Each entry links to the draft, since the age alone does not say what is going wrong.
*/
class StuckBookingDraftsWidgetProvider implements DashboardWidgetProviderInterface
{
/**
* How long a draft must have survived to be worth reporting.
*
* Short enough to catch a customer inside their first week of trying, long enough that an edit
* somebody merely abandoned over a weekend does not fill the card.
*/
private const MIN_AGE_DAYS = 7;
private const LIMIT = 10;
public function __construct(
private readonly BookingEditDraftRepository $draftRepository,
private readonly UrlGeneratorInterface $urlGenerator,
) {
}
public function getRequiredRole(): string
{
return Role::CUSTOMER_EXPERT;
}
public function getPriority(): int
{
return 95;
}
public function build(): ?DashboardWidget
{
$drafts = $this->draftRepository->findStuck(self::MIN_AGE_DAYS, self::LIMIT);
return new DashboardWidget(
'Festhängende Buchungsänderungen',
array_map(fn (BookingEditDraft $draft): DashboardWidgetEntry => new DashboardWidgetEntry(
$this->label($draft),
$this->urlGenerator->generate('app_admin_bookingeditdraft_show', ['id' => $draft->getId()]),
'edit',
), $drafts),
'Keine festhängenden Buchungsänderungen.',
$this->urlGenerator->generate('app_admin_bookingeditdraft'),
'Alle Buchungsentwürfe',
);
}
/**
* Names the booking, how long it has been stuck, and when the customer last tried.
*
* The gap between the two is what distinguishes an abandoned edit from somebody still trying
* every few days and getting nowhere.
*/
private function label(BookingEditDraft $draft): string
{
$days = $draft->getCreatedAt()->diff(new \DateTimeImmutable())->days ?? 0;
return sprintf(
'Vorgang %s (%s) — seit %d Tagen, zuletzt %s',
$draft->getBookingNumber() ?? $draft->getBookingId(),
(string) $draft->getUser()->getEmail(),
$days,
$draft->getUpdatedAt()->format('d.m.Y'),
);
}
}