feat: admin list filtering and search
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Admin\Filter;
|
||||
|
||||
use App\Form\Model\Filter\AbstractListFilterDto;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SearchType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Base for every admin list filter form.
|
||||
*
|
||||
* The form submits as GET straight to the list route, so the filter it produces *is* the URL.
|
||||
* Its block prefix is deliberately empty: that gives readable parameters (`?q=meier&status[]=open`
|
||||
* instead of `?filter[q]=…`) and lets handleRequest() bind the whole query string in one go —
|
||||
* which in turn is why extra fields have to be tolerated, as page, limit, sort, direction, the
|
||||
* return URL and the filter marker all travel in that same query string.
|
||||
*
|
||||
* @template T of AbstractListFilterDto
|
||||
*
|
||||
* @extends AbstractType<T>
|
||||
*/
|
||||
abstract class AbstractListFilterType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @param FormBuilderInterface<T|null> $builder
|
||||
*/
|
||||
final public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$fields = [
|
||||
// The marker has to be a field of this form, not just a query parameter: an unnamed
|
||||
// form is only auto-submitted when the query string contains at least one of its own
|
||||
// fields. Without it, removing the last active filter would leave `?f=1` alone, the
|
||||
// form would never bind, and the list would snap back to its defaults instead of
|
||||
// widening.
|
||||
AbstractListFilterDto::MARKER => [HiddenType::class, [
|
||||
'mapped' => false,
|
||||
'data' => '1',
|
||||
]],
|
||||
'q' => [SearchType::class, [
|
||||
'label' => 'Suche',
|
||||
'required' => false,
|
||||
'attr' => ['placeholder' => $this->searchPlaceholder()],
|
||||
]],
|
||||
'dateFrom' => [DateType::class, [
|
||||
'label' => $this->dateFromLabel(),
|
||||
'widget' => 'single_text',
|
||||
'input' => 'datetime_immutable',
|
||||
'required' => false,
|
||||
]],
|
||||
'dateTo' => [DateType::class, [
|
||||
'label' => $this->dateToLabel(),
|
||||
'widget' => 'single_text',
|
||||
'input' => 'datetime_immutable',
|
||||
'required' => false,
|
||||
]],
|
||||
];
|
||||
|
||||
foreach ($fields + $this->filterFields($options) as $name => [$type, $fieldOptions]) {
|
||||
$builder->add($name, $type, $fieldOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The fields that only make sense for one entity, as name => [type, options].
|
||||
*
|
||||
* Declaring them instead of adding them keeps the search term and the date range first in
|
||||
* every filter, and keeps buildForm() — which is final for that reason — the only place a
|
||||
* form builder is touched.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*
|
||||
* @return array<string, array{class-string, array<string, mixed>}>
|
||||
*/
|
||||
protected function filterFields(array $options): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'method' => 'GET',
|
||||
'csrf_protection' => false,
|
||||
'allow_extra_fields' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function searchPlaceholder(): string
|
||||
{
|
||||
return 'Suchen…';
|
||||
}
|
||||
|
||||
protected function dateFromLabel(): string
|
||||
{
|
||||
return 'Zeitraum ab';
|
||||
}
|
||||
|
||||
protected function dateToLabel(): string
|
||||
{
|
||||
return 'Zeitraum bis';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Admin\Filter;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\Groups\AccommodationBookingRepository;
|
||||
use App\Repository\Groups\AccommodationRepository;
|
||||
use App\Repository\UserRepository;
|
||||
|
||||
/**
|
||||
* The choice lists for the accommodation booking filter form.
|
||||
*
|
||||
* The list view and the filter modal have to configure the form identically: the modal writes
|
||||
* the query string, the list hydrates the very same form from it, and a field missing on either
|
||||
* side would silently drop that filter.
|
||||
*/
|
||||
final readonly class AccommodationBookingFilterOptionsProvider
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private AccommodationBookingRepository $bookingRepository,
|
||||
private AccommodationRepository $accommodationRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $seesAllBookings whether the current user may look past their own bookings
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function formOptions(bool $seesAllBookings): array
|
||||
{
|
||||
return [
|
||||
'can_filter_by_manager' => $seesAllBookings,
|
||||
'managers' => $seesAllBookings ? $this->managers() : [],
|
||||
'accommodations' => $this->accommodationRepository->findBy([], ['name' => 'ASC']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Everyone worth filtering by: the staff a booking can be assigned to, plus whoever is
|
||||
* currently assigned to one.
|
||||
*
|
||||
* The second half matters because the two lists drift apart — somebody assigned last season
|
||||
* may have lost the groups role since, and their bookings would otherwise be impossible to
|
||||
* find. The edit form keeps the current value in its choices for the same reason.
|
||||
*
|
||||
* @return User[]
|
||||
*/
|
||||
private function managers(): array
|
||||
{
|
||||
$managers = [];
|
||||
|
||||
foreach ([...$this->userRepository->findGroupsStaff(), ...$this->bookingRepository->findAssignedManagers()] as $manager) {
|
||||
$managers[(int) $manager->getId()] = $manager;
|
||||
}
|
||||
|
||||
uasort($managers, static fn (User $a, User $b) => strcasecmp((string) $a->getEmail(), (string) $b->getEmail()));
|
||||
|
||||
return array_values($managers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Admin\Filter;
|
||||
|
||||
use App\Entity\Groups\Accommodation;
|
||||
use App\Entity\User;
|
||||
use App\Enum\Groups\AccommodationBookingStatus;
|
||||
use App\Enum\Groups\AccommodationBookingType;
|
||||
use App\Form\Model\Filter\AccommodationBookingFilterDto;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractListFilterType<AccommodationBookingFilterDto>
|
||||
*/
|
||||
class AccommodationBookingFilterType extends AbstractListFilterType
|
||||
{
|
||||
protected function filterFields(array $options): array
|
||||
{
|
||||
$fields = [
|
||||
'status' => [EnumType::class, [
|
||||
'label' => 'Status',
|
||||
'class' => AccommodationBookingStatus::class,
|
||||
'choice_label' => static fn (AccommodationBookingStatus $status) => $status->label(),
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
]],
|
||||
'type' => [EnumType::class, [
|
||||
'label' => 'Art',
|
||||
'class' => AccommodationBookingType::class,
|
||||
'choice_label' => static fn (AccommodationBookingType $type) => $type->label(),
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
]],
|
||||
'accommodation' => [EntityType::class, [
|
||||
'label' => 'Gruppenhaus',
|
||||
'class' => Accommodation::class,
|
||||
'choices' => $options['accommodations'],
|
||||
'choice_label' => 'name',
|
||||
'placeholder' => 'alle',
|
||||
'required' => false,
|
||||
]],
|
||||
];
|
||||
|
||||
// Mirrors the edit form: only group admins get to look at other people's bookings, so
|
||||
// for everyone else the fields simply do not exist and the scope is pinned server-side.
|
||||
if (true === $options['can_filter_by_manager']) {
|
||||
// Triage does not depend on there being anyone to choose from, so this stays even
|
||||
// when nobody carries a groups role yet.
|
||||
$fields['unassigned'] = [CheckboxType::class, [
|
||||
'label' => 'nur ohne Zuordnung',
|
||||
'required' => false,
|
||||
]];
|
||||
|
||||
if ([] !== $options['managers']) {
|
||||
$fields['managedBy'] = [EntityType::class, [
|
||||
'label' => 'Betreuer:in',
|
||||
'class' => User::class,
|
||||
'choices' => $options['managers'],
|
||||
'choice_label' => 'email',
|
||||
'placeholder' => 'alle',
|
||||
'required' => false,
|
||||
]];
|
||||
}
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'data_class' => AccommodationBookingFilterDto::class,
|
||||
'can_filter_by_manager' => false,
|
||||
'managers' => [],
|
||||
'accommodations' => [],
|
||||
]);
|
||||
$resolver->setAllowedTypes('can_filter_by_manager', 'bool');
|
||||
$resolver->setAllowedTypes('managers', User::class.'[]');
|
||||
$resolver->setAllowedTypes('accommodations', Accommodation::class.'[]');
|
||||
}
|
||||
|
||||
protected function searchPlaceholder(): string
|
||||
{
|
||||
return 'Gruppe, Name, Ort, E-Mail…';
|
||||
}
|
||||
|
||||
protected function dateFromLabel(): string
|
||||
{
|
||||
return 'Aufenthalt ab';
|
||||
}
|
||||
|
||||
protected function dateToLabel(): string
|
||||
{
|
||||
return 'Aufenthalt bis';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Admin\Filter;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\Model\Filter\BookingEditDraftFilterDto;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractListFilterType<BookingEditDraftFilterDto>
|
||||
*/
|
||||
class BookingEditDraftFilterType extends AbstractListFilterType
|
||||
{
|
||||
protected function filterFields(array $options): array
|
||||
{
|
||||
return [
|
||||
'user' => [EntityType::class, [
|
||||
'label' => 'Kundenaccount',
|
||||
'class' => User::class,
|
||||
'choices' => $options['users'],
|
||||
'choice_label' => 'email',
|
||||
'placeholder' => 'alle',
|
||||
'required' => false,
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'data_class' => BookingEditDraftFilterDto::class,
|
||||
'users' => [],
|
||||
]);
|
||||
$resolver->setAllowedTypes('users', User::class.'[]');
|
||||
}
|
||||
|
||||
protected function searchPlaceholder(): string
|
||||
{
|
||||
return 'Vorgang, E-Mail…';
|
||||
}
|
||||
|
||||
protected function dateFromLabel(): string
|
||||
{
|
||||
return 'Erstellt ab';
|
||||
}
|
||||
|
||||
protected function dateToLabel(): string
|
||||
{
|
||||
return 'Erstellt bis';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Admin\Filter;
|
||||
|
||||
use App\Form\Model\Filter\LogEntryFilterDto;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractListFilterType<LogEntryFilterDto>
|
||||
*/
|
||||
class LogEntryFilterType extends AbstractListFilterType
|
||||
{
|
||||
protected function filterFields(array $options): array
|
||||
{
|
||||
return [
|
||||
'channel' => [ChoiceType::class, [
|
||||
'label' => 'Kanal',
|
||||
'choices' => array_combine($options['channels'], $options['channels']),
|
||||
'placeholder' => 'alle',
|
||||
'required' => false,
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'data_class' => LogEntryFilterDto::class,
|
||||
'channels' => [],
|
||||
]);
|
||||
$resolver->setAllowedTypes('channels', 'string[]');
|
||||
}
|
||||
|
||||
protected function searchPlaceholder(): string
|
||||
{
|
||||
return 'Meldung, Fehlercode…';
|
||||
}
|
||||
|
||||
protected function dateFromLabel(): string
|
||||
{
|
||||
return 'Datum ab';
|
||||
}
|
||||
|
||||
protected function dateToLabel(): string
|
||||
{
|
||||
return 'Datum bis';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Admin\Filter;
|
||||
|
||||
use App\Form\Model\Filter\UserFilterDto;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractListFilterType<UserFilterDto>
|
||||
*/
|
||||
class UserFilterType extends AbstractListFilterType
|
||||
{
|
||||
/**
|
||||
* The roles that are actually assigned to accounts. ROLE_USER is left out because every
|
||||
* account has it implicitly and filtering by it would match everything.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public const ROLES = [
|
||||
'ROLE_ADMIN',
|
||||
'ROLE_MANAGER',
|
||||
'ROLE_TEAMER',
|
||||
'ROLE_CUSTOMER',
|
||||
'ROLE_HOUSE_MANAGER',
|
||||
'ROLE_GROUPS_ADMIN',
|
||||
'ROLE_GROUPS_MANAGER',
|
||||
];
|
||||
|
||||
protected function filterFields(array $options): array
|
||||
{
|
||||
return [
|
||||
'role' => [ChoiceType::class, [
|
||||
'label' => 'Rolle',
|
||||
'choices' => array_combine($options['roles'], $options['roles']),
|
||||
'placeholder' => 'alle',
|
||||
'required' => false,
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'data_class' => UserFilterDto::class,
|
||||
'roles' => [],
|
||||
]);
|
||||
$resolver->setAllowedTypes('roles', 'string[]');
|
||||
}
|
||||
|
||||
protected function searchPlaceholder(): string
|
||||
{
|
||||
return 'E-Mail, Adress-Id, Personen-Id…';
|
||||
}
|
||||
|
||||
protected function dateFromLabel(): string
|
||||
{
|
||||
return 'Letzter Login ab';
|
||||
}
|
||||
|
||||
protected function dateToLabel(): string
|
||||
{
|
||||
return 'Letzter Login bis';
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,6 @@ class AccommodationBookingCreateType extends AbstractType
|
||||
'choices' => [
|
||||
AccommodationBookingStatus::Draft,
|
||||
AccommodationBookingStatus::Open,
|
||||
AccommodationBookingStatus::Accepted,
|
||||
],
|
||||
'choice_label' => fn (AccommodationBookingStatus $status) => $status->label(),
|
||||
])
|
||||
|
||||
@@ -14,22 +14,36 @@ class ModalSubmitExtension extends AbstractTypeExtension
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'hx_post' => null,
|
||||
'hx_get' => null,
|
||||
'hx_target' => '#htmx-modal',
|
||||
'hx_swap' => 'outerHTML',
|
||||
'hx_push_url' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
if (null !== $options['hx_post']) {
|
||||
$attr = [
|
||||
'hx-post' => $options['hx_post'],
|
||||
'hx-target' => $options['hx_target'],
|
||||
'hx-swap' => $options['hx_swap'],
|
||||
'hx-indicator' => '#htmx-modal-indicator',
|
||||
];
|
||||
$view->vars['attr'] = array_merge($view->vars['attr'], $attr);
|
||||
// hx_post keeps the form inside the modal (so validation errors re-render in place);
|
||||
// hx_get is for read-only forms such as list filters, which navigate the list instead
|
||||
// and therefore usually target the body and push the resulting URL.
|
||||
$method = null !== $options['hx_post'] ? 'hx-post' : (null !== $options['hx_get'] ? 'hx-get' : null);
|
||||
|
||||
if (null === $method) {
|
||||
return;
|
||||
}
|
||||
|
||||
$attr = [
|
||||
$method => $options['hx-post' === $method ? 'hx_post' : 'hx_get'],
|
||||
'hx-target' => $options['hx_target'],
|
||||
'hx-swap' => $options['hx_swap'],
|
||||
'hx-indicator' => '#htmx-modal-indicator',
|
||||
];
|
||||
|
||||
if (false !== $options['hx_push_url']) {
|
||||
$attr['hx-push-url'] = true === $options['hx_push_url'] ? 'true' : $options['hx_push_url'];
|
||||
}
|
||||
|
||||
$view->vars['attr'] = array_merge($view->vars['attr'], $attr);
|
||||
}
|
||||
|
||||
public static function getExtendedTypes(): iterable
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Model\Filter;
|
||||
|
||||
use App\Model\ListFilterChip;
|
||||
|
||||
/**
|
||||
* Shared state of every admin list filter: a generic search term and a date range.
|
||||
*
|
||||
* The whole filter lives in the query string, so a filtered list is bookmarkable, survives the
|
||||
* back button and is carried through pagination and sorting by Knp's `query|merge`.
|
||||
*/
|
||||
abstract class AbstractListFilterDto
|
||||
{
|
||||
/**
|
||||
* Query key marking a request as explicitly filtered by the user.
|
||||
*
|
||||
* Its presence is what separates "show me the defaults" from "show me exactly what the URL
|
||||
* says". Without it an empty filter would be indistinguishable from a fresh visit, and there
|
||||
* would be no way to say "no status filter at all" once a default status exists.
|
||||
*/
|
||||
public const MARKER = 'f';
|
||||
|
||||
public ?string $q = null;
|
||||
|
||||
public ?\DateTimeImmutable $dateFrom = null;
|
||||
|
||||
public ?\DateTimeImmutable $dateTo = null;
|
||||
|
||||
/**
|
||||
* The search term, normalised — null when the user submitted nothing but whitespace.
|
||||
*/
|
||||
public function searchTerm(): ?string
|
||||
{
|
||||
$term = trim((string) $this->q);
|
||||
|
||||
return '' === $term ? null : $term;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ListFilterChip[]
|
||||
*/
|
||||
public function activeFilters(): array
|
||||
{
|
||||
$chips = [];
|
||||
|
||||
if (null !== $this->searchTerm()) {
|
||||
$chips[] = new ListFilterChip('Suche', (string) $this->searchTerm(), ['q']);
|
||||
}
|
||||
|
||||
if (null !== $this->dateFrom) {
|
||||
$chips[] = new ListFilterChip($this->dateFromLabel(), $this->dateFrom->format('d.m.Y'), ['dateFrom']);
|
||||
}
|
||||
|
||||
if (null !== $this->dateTo) {
|
||||
$chips[] = new ListFilterChip($this->dateToLabel(), $this->dateTo->format('d.m.Y'), ['dateTo']);
|
||||
}
|
||||
|
||||
return $chips;
|
||||
}
|
||||
|
||||
public function activeCount(): int
|
||||
{
|
||||
return \count($this->activeFilters());
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return [] !== $this->activeFilters();
|
||||
}
|
||||
|
||||
protected function dateFromLabel(): string
|
||||
{
|
||||
return 'Zeitraum ab';
|
||||
}
|
||||
|
||||
protected function dateToLabel(): string
|
||||
{
|
||||
return 'Zeitraum bis';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Model\Filter;
|
||||
|
||||
use App\Entity\Groups\Accommodation;
|
||||
use App\Entity\User;
|
||||
use App\Enum\Groups\AccommodationBookingStatus;
|
||||
use App\Enum\Groups\AccommodationBookingType;
|
||||
use App\Model\ListFilterChip;
|
||||
|
||||
class AccommodationBookingFilterDto extends AbstractListFilterDto
|
||||
{
|
||||
/** @var AccommodationBookingStatus[] */
|
||||
public array $status = [];
|
||||
|
||||
/** @var AccommodationBookingType[] */
|
||||
public array $type = [];
|
||||
|
||||
public ?User $managedBy = null;
|
||||
|
||||
/**
|
||||
* Bookings nobody is responsible for yet — the triage case. Takes precedence over
|
||||
* $managedBy, which cannot be true at the same time without contradicting itself.
|
||||
*/
|
||||
public bool $unassigned = false;
|
||||
|
||||
public ?Accommodation $accommodation = null;
|
||||
|
||||
/**
|
||||
* Whether the manager scope is imposed rather than chosen. A group manager without admin
|
||||
* rights only ever sees their own bookings, so the chip has to be shown — otherwise the
|
||||
* short list looks like a bug — but must not offer a way out that would not work.
|
||||
*/
|
||||
public bool $managedByLocked = false;
|
||||
|
||||
/**
|
||||
* The list as it presents itself to someone who has not filtered yet.
|
||||
*
|
||||
* The still-live statuses — drafts and open bookings, i.e. everything that may still need
|
||||
* work — nothing whose stay is already over, and, for staff who are not group admins, only
|
||||
* the bookings they are responsible for.
|
||||
*/
|
||||
public static function defaults(bool $seesAllBookings, ?User $currentUser): self
|
||||
{
|
||||
$filter = new self();
|
||||
$filter->status = [AccommodationBookingStatus::Draft, AccommodationBookingStatus::Open];
|
||||
$filter->dateFrom = new \DateTimeImmutable('today');
|
||||
|
||||
if (!$seesAllBookings) {
|
||||
$filter->managedBy = $currentUser;
|
||||
$filter->managedByLocked = true;
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
public function activeFilters(): array
|
||||
{
|
||||
$chips = parent::activeFilters();
|
||||
|
||||
if ([] !== $this->status) {
|
||||
$chips[] = new ListFilterChip(
|
||||
'Status',
|
||||
implode(', ', array_map(static fn (AccommodationBookingStatus $s) => $s->label(), $this->status)),
|
||||
['status'],
|
||||
);
|
||||
}
|
||||
|
||||
if ([] !== $this->type) {
|
||||
$chips[] = new ListFilterChip(
|
||||
'Art',
|
||||
implode(', ', array_map(static fn (AccommodationBookingType $t) => $t->label(), $this->type)),
|
||||
['type'],
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->unassigned) {
|
||||
$chips[] = new ListFilterChip('Betreuer:in', 'keine Zuordnung', ['unassigned']);
|
||||
} elseif (null !== $this->managedBy) {
|
||||
$chips[] = new ListFilterChip(
|
||||
'Betreuer:in',
|
||||
(string) $this->managedBy->getEmail(),
|
||||
$this->managedByLocked ? [] : ['managedBy'],
|
||||
);
|
||||
}
|
||||
|
||||
if (null !== $this->accommodation) {
|
||||
$chips[] = new ListFilterChip('Gruppenhaus', (string) $this->accommodation->getName(), ['accommodation']);
|
||||
}
|
||||
|
||||
return $chips;
|
||||
}
|
||||
|
||||
protected function dateFromLabel(): string
|
||||
{
|
||||
return 'Aufenthalt ab';
|
||||
}
|
||||
|
||||
protected function dateToLabel(): string
|
||||
{
|
||||
return 'Aufenthalt bis';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Model\Filter;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\ListFilterChip;
|
||||
|
||||
class BookingEditDraftFilterDto extends AbstractListFilterDto
|
||||
{
|
||||
public ?User $user = null;
|
||||
|
||||
public function activeFilters(): array
|
||||
{
|
||||
$chips = parent::activeFilters();
|
||||
|
||||
if (null !== $this->user) {
|
||||
$chips[] = new ListFilterChip('Kundenaccount', (string) $this->user->getEmail(), ['user']);
|
||||
}
|
||||
|
||||
return $chips;
|
||||
}
|
||||
|
||||
protected function dateFromLabel(): string
|
||||
{
|
||||
return 'Erstellt ab';
|
||||
}
|
||||
|
||||
protected function dateToLabel(): string
|
||||
{
|
||||
return 'Erstellt bis';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Model\Filter;
|
||||
|
||||
use App\Model\ListFilterChip;
|
||||
|
||||
class LogEntryFilterDto extends AbstractListFilterDto
|
||||
{
|
||||
public ?string $channel = null;
|
||||
|
||||
public function activeFilters(): array
|
||||
{
|
||||
$chips = parent::activeFilters();
|
||||
|
||||
if (null !== $this->channel && '' !== $this->channel) {
|
||||
$chips[] = new ListFilterChip('Kanal', $this->channel, ['channel']);
|
||||
}
|
||||
|
||||
return $chips;
|
||||
}
|
||||
|
||||
protected function dateFromLabel(): string
|
||||
{
|
||||
return 'Datum ab';
|
||||
}
|
||||
|
||||
protected function dateToLabel(): string
|
||||
{
|
||||
return 'Datum bis';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Model\Filter;
|
||||
|
||||
use App\Model\ListFilterChip;
|
||||
|
||||
class UserFilterDto extends AbstractListFilterDto
|
||||
{
|
||||
public ?string $role = null;
|
||||
|
||||
public function activeFilters(): array
|
||||
{
|
||||
$chips = parent::activeFilters();
|
||||
|
||||
if (null !== $this->role && '' !== $this->role) {
|
||||
$chips[] = new ListFilterChip('Rolle', $this->role, ['role']);
|
||||
}
|
||||
|
||||
return $chips;
|
||||
}
|
||||
|
||||
protected function dateFromLabel(): string
|
||||
{
|
||||
return 'Letzter Login ab';
|
||||
}
|
||||
|
||||
protected function dateToLabel(): string
|
||||
{
|
||||
return 'Letzter Login bis';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user