feat: paginate the admin booking list

This commit is contained in:
fromme committed 2026-09-18 10:53:55 +02:00
1 parent ba64033948
commit 2f5d1d14e4
16 files changed
+504 -75

No files matched your search

@@ -9,6 +9,7 @@ use App\Form\Admin\BookingFilterType;
use App\Repository\BookingRepository;
use App\Repository\BookingSeriesRepository;
use App\Security\Role;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -30,9 +31,16 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted(Role::ADMIN)]
final class IndexController extends AbstractController
{
/**
* The window defaults to a year, so the unpaged list grew with the company. 25 rows is about
* one screen of the table without scrolling past the filter form.
*/
private const PER_PAGE = 25;
public function __construct(
private readonly BookingRepository $bookings,
private readonly BookingSeriesRepository $series,
private readonly PaginatorInterface $paginator,
) {
}
@@ -45,10 +53,19 @@ final class IndexController extends AbstractController
// Unfiltered first visits simply keep the constructor's defaults.
$form->handleRequest($request);
// No defaultSortFieldName: one field cannot express the date/start/resource ordering the
// repository already sorts by, and naming one here would replace all three. Unsorted
// requests keep that ordering; a clicked column header takes precedence over it.
$pagination = $this->paginator->paginate(
$this->bookings->createFilteredQueryBuilder($filter),
$request->query->getInt('page', 1),
self::PER_PAGE,
);
return $this->render('admin/bookings.html.twig', [
'form' => $form,
'filter' => $filter,
'bookings' => $this->bookings->findForAdmin($filter),
'pagination' => $pagination,
'series' => $this->series->findActive($filter->fromDate(), $filter->user, $filter->type),
]);
}
+4
View File
@@ -61,6 +61,10 @@ final class BookingFilterType extends AbstractType
'data_class' => BookingFilter::class,
'method' => 'GET',
'csrf_protection' => false,
// The paginator writes page/sort/direction into the same query string this form reads
// back. Without this the form counts them as extra fields, turns invalid, and the
// listing answers 422 to every sorted request.
'allow_extra_fields' => true,
]);
}
+5 -3
View File
@@ -216,9 +216,11 @@ class BookingRepository extends ServiceEntityRepository
* and eager-selects the holder as well, because the admin list names a different person on
* every row and would otherwise fire a query per booking.
*
* @return list<Booking>
* Unexecuted, because the paginator has to add the LIMIT and the ORDER BY of a clicked column
* header itself. Only to-one associations are fetch-joined, so a LIMIT over this query cannot
* truncate a half-hydrated row the way a joined collection would.
*/
public function findForAdmin(BookingFilter $filter): array
public function createFilteredQueryBuilder(BookingFilter $filter): QueryBuilder
{
$qb = $this->createQueryBuilder('b')
->addSelect('u', 'r', 'g', 's')
@@ -241,7 +243,7 @@ class BookingRepository extends ServiceEntityRepository
$qb->andWhere('r.type = :type')->setParameter('type', $filter->type);
}
return $qb->getQuery()->getResult();
return $qb;
}
/**