feat: admin dashboard widgets
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dashboard\Widget;
|
||||
|
||||
use App\Dashboard\Contract\DashboardWidgetProviderInterface;
|
||||
use App\Entity\Groups\AccommodationBooking;
|
||||
use App\Entity\User;
|
||||
use App\Form\Model\Filter\AccommodationBookingFilterDto;
|
||||
use App\Model\DashboardWidget;
|
||||
use App\Model\DashboardWidgetEntry;
|
||||
use App\Repository\Groups\AccommodationBookingRepository;
|
||||
use App\Security\Role;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* The group bookings that still need work, as an excerpt of the admin list.
|
||||
*
|
||||
* The excerpt is deliberately built from the list's own default filter rather than a second
|
||||
* definition of "open": that keeps the statuses, the "nothing already over" date floor and the
|
||||
* rule that a group manager sees only their own bookings in one place. Widen the list filter and
|
||||
* this widget follows.
|
||||
*/
|
||||
class OpenGroupBookingsWidgetProvider implements DashboardWidgetProviderInterface
|
||||
{
|
||||
private const LIMIT = 5;
|
||||
|
||||
public function __construct(
|
||||
private readonly AccommodationBookingRepository $bookingRepository,
|
||||
private readonly Security $security,
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRequiredRole(): string
|
||||
{
|
||||
return Role::GROUPS_MANAGER;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
public function build(): ?DashboardWidget
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
$filter = AccommodationBookingFilterDto::defaults(
|
||||
$this->security->isGranted(Role::GROUPS_ADMIN),
|
||||
$user instanceof User ? $user : null,
|
||||
);
|
||||
|
||||
/** @var AccommodationBooking[] $bookings */
|
||||
$bookings = $this->bookingRepository
|
||||
->createFilteredQueryBuilder($filter)
|
||||
->orderBy('booking.dateFrom', 'ASC')
|
||||
->setMaxResults(self::LIMIT)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
|
||||
return new DashboardWidget(
|
||||
'Offene Gruppenbuchungen',
|
||||
array_map(fn (AccommodationBooking $booking): DashboardWidgetEntry => new DashboardWidgetEntry(
|
||||
$this->label($booking),
|
||||
$this->urlGenerator->generate('app_admin_accommodationbooking_show', ['id' => $booking->getId()]),
|
||||
'house',
|
||||
), $bookings),
|
||||
'Keine offenen Gruppenbuchungen.',
|
||||
$this->urlGenerator->generate('app_admin_accommodationbooking'),
|
||||
'Alle Buchungen',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The group, or whoever booked when it has no name yet, plus the stay it is about.
|
||||
*/
|
||||
private function label(AccommodationBooking $booking): string
|
||||
{
|
||||
$name = $booking->getGroupName() ?? $booking->getLastName() ?? 'Ohne Namen';
|
||||
|
||||
$dateFrom = $booking->getDateFrom()?->format('d.m.Y');
|
||||
$dateTo = $booking->getDateTo()?->format('d.m.Y');
|
||||
|
||||
if (null === $dateFrom || null === $dateTo) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
return sprintf('%s (%s–%s)', $name, $dateFrom, $dateTo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dashboard\Widget;
|
||||
|
||||
use App\Dashboard\Contract\DashboardWidgetProviderInterface;
|
||||
use App\Entity\User;
|
||||
use App\Form\Model\Filter\AbstractListFilterDto;
|
||||
use App\Model\DashboardWidget;
|
||||
use App\Model\DashboardWidgetEntry;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Security\Role;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* The accounts waiting on an administrator to act.
|
||||
*
|
||||
* A nomination grants nothing until it is approved, and nothing else in the application
|
||||
* announces that one is waiting — without this widget an administrator only finds out by
|
||||
* opening the user list.
|
||||
*/
|
||||
class PendingRoleApprovalsWidgetProvider implements DashboardWidgetProviderInterface
|
||||
{
|
||||
private const LIMIT = 5;
|
||||
|
||||
public function __construct(
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRequiredRole(): string
|
||||
{
|
||||
return Role::ADMIN;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 90;
|
||||
}
|
||||
|
||||
public function build(): ?DashboardWidget
|
||||
{
|
||||
return new DashboardWidget(
|
||||
'Offene Rollenfreigaben',
|
||||
array_map(fn (User $user): DashboardWidgetEntry => new DashboardWidgetEntry(
|
||||
$this->label($user),
|
||||
$this->approvalUrl($user),
|
||||
'user',
|
||||
), $this->userRepository->findWithPendingRoles(self::LIMIT)),
|
||||
'Keine offenen Rollenfreigaben.',
|
||||
$this->urlGenerator->generate('app_admin_user'),
|
||||
'Alle Benutzer',
|
||||
);
|
||||
}
|
||||
|
||||
private function label(User $user): string
|
||||
{
|
||||
$nominated = Role::nominatedFrom($user->getRoles());
|
||||
|
||||
if ([] === $nominated) {
|
||||
return $user->getDisplayName();
|
||||
}
|
||||
|
||||
return sprintf('%s: %s', $user->getDisplayName(), implode(', ', $nominated));
|
||||
}
|
||||
|
||||
/**
|
||||
* The user list, filtered down to this account.
|
||||
*
|
||||
* Approving happens in a modal that app_admin_user_permissions renders as a bare fragment,
|
||||
* so it cannot be linked to directly. The filtered list is one click away from it and shows
|
||||
* the nomination badge on the way. The query string is flat because the list filter forms
|
||||
* declare no block prefix.
|
||||
*/
|
||||
private function approvalUrl(User $user): string
|
||||
{
|
||||
return $this->urlGenerator->generate('app_admin_user', [
|
||||
AbstractListFilterDto::MARKER => 1,
|
||||
'q' => $user->getEmail(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dashboard\Widget;
|
||||
|
||||
use App\Dashboard\Contract\DashboardWidgetProviderInterface;
|
||||
use App\Entity\LogEntry;
|
||||
use App\Model\DashboardWidget;
|
||||
use App\Model\DashboardWidgetEntry;
|
||||
use App\Repository\LogEntryRepository;
|
||||
use App\Security\Role;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* What the application has been up to lately.
|
||||
*
|
||||
* The entries themselves are display only: a log line is a report, not somewhere to go, and
|
||||
* everything worth drilling into lives behind the log list's own filters — which is where the
|
||||
* card's header link leads.
|
||||
*/
|
||||
class RecentLogEntriesWidgetProvider implements DashboardWidgetProviderInterface
|
||||
{
|
||||
private const LIMIT = 5;
|
||||
|
||||
/**
|
||||
* Log messages have no length limit worth relying on, and the widget renders one line per
|
||||
* entry, so the message is cut to something a card can hold.
|
||||
*/
|
||||
private const MESSAGE_LENGTH = 60;
|
||||
|
||||
public function __construct(
|
||||
private readonly LogEntryRepository $logEntryRepository,
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRequiredRole(): string
|
||||
{
|
||||
return Role::ADMIN;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 80;
|
||||
}
|
||||
|
||||
public function build(): ?DashboardWidget
|
||||
{
|
||||
return new DashboardWidget(
|
||||
'Neueste Logeinträge',
|
||||
array_map(static fn (LogEntry $entry): DashboardWidgetEntry => new DashboardWidgetEntry(
|
||||
self::label($entry),
|
||||
null,
|
||||
'document',
|
||||
), $this->logEntryRepository->findLatest(self::LIMIT)),
|
||||
'Keine Logeinträge vorhanden.',
|
||||
$this->urlGenerator->generate('app_admin_log'),
|
||||
'Alle Logeinträge',
|
||||
);
|
||||
}
|
||||
|
||||
private static function label(LogEntry $entry): string
|
||||
{
|
||||
$message = (string) $entry->getMessage();
|
||||
|
||||
if (mb_strlen($message) > self::MESSAGE_LENGTH) {
|
||||
$message = mb_substr($message, 0, self::MESSAGE_LENGTH).'…';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'%s %s: %s',
|
||||
$entry->getCreatedAt()?->format('d.m.Y H:i') ?? '',
|
||||
(string) $entry->getChannel(),
|
||||
$message,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user