From 339997b8c00e075115c5c33b4d8408807db3f97d Mon Sep 17 00:00:00 2001 From: MeisterAdebar Date: Fri, 5 Jun 2026 12:39:53 +0200 Subject: [PATCH 1/9] feat: show reservation history on customer detail view - Add loadAllReservationsForCustomer() to ReservationRepository covers booker and guest role, ordered by startDate DESC - Inject ReservationRepository into getCustomerAction - Display reservation history accordion in customer_form_show modal with dates, nights, apartment, reservation status, invoice number with color-coded payment status and direct link to reservation modal --- src/Controller/CustomerServiceController.php | 18 ++++- src/Repository/ReservationRepository.php | 20 +++++ .../Customers/customer_form_show.html.twig | 15 ++-- .../customer_reservation_history.html.twig | 81 +++++++++++++++++++ translations/Customers/messages.de.xlf | 8 ++ translations/Customers/messages.en.yaml | 2 + 6 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 templates/Customers/customer_reservation_history.html.twig diff --git a/src/Controller/CustomerServiceController.php b/src/Controller/CustomerServiceController.php index 51343df0..0a53a53b 100644 --- a/src/Controller/CustomerServiceController.php +++ b/src/Controller/CustomerServiceController.php @@ -17,6 +17,7 @@ use App\Entity\CustomerAddresses; use App\Entity\Enum\IDCardType; use App\Entity\Template; +use App\Repository\ReservationRepository; use App\Service\CSRFProtectionService; use App\Service\CustomerService; use App\Service\TemplatesService; @@ -100,14 +101,23 @@ public function searchCustomersAction(ManagerRegistry $doctrine, Request $reques } #[Route('/{id}/get', name: 'customers.get.customer', methods: ['GET'], defaults: ['id' => '0'])] - public function getCustomerAction(ManagerRegistry $doctrine, CSRFProtectionService $csrf, $id) - { + public function getCustomerAction( + ManagerRegistry $doctrine, + CSRFProtectionService $csrf, + ReservationRepository $reservationRepo, + $id + ) { $em = $doctrine->getManager(); $customer = $em->getRepository(Customer::class)->find($id); + $reservations = $customer + ? $reservationRepo->loadAllReservationsForCustomer($customer) + : []; + return $this->render('Customers/customer_form_show.html.twig', [ - 'customer' => $customer, - 'token' => $csrf->getCSRFTokenForForm(), + 'customer' => $customer, + 'token' => $csrf->getCSRFTokenForForm(), + 'reservations' => $reservations, ]); } diff --git a/src/Repository/ReservationRepository.php b/src/Repository/ReservationRepository.php index 2674e56f..12f800c9 100644 --- a/src/Repository/ReservationRepository.php +++ b/src/Repository/ReservationRepository.php @@ -513,4 +513,24 @@ public function findImportedWithoutBookerPaginated(int $page, int $perPage): arr ->getQuery() ->getResult(); } + + /** + * Alle Reservierungen eines Gastes – als Buchender (booker) + * oder als Mitreisender – dedupliziert, nach Anreise absteigend. + * + * @return Reservation[] + */ + public function loadAllReservationsForCustomer(\App\Entity\Customer $customer): array + { + return $this->createQueryBuilder('r') + ->select('r', 'a', 'rs') + ->leftJoin('r.appartment', 'a') + ->leftJoin('r.reservationStatus', 'rs') + ->where('r.booker = :customer') + ->orWhere(':customer MEMBER OF r.customers') + ->setParameter('customer', $customer) + ->orderBy('r.startDate', 'DESC') + ->getQuery() + ->getResult(); + } } diff --git a/templates/Customers/customer_form_show.html.twig b/templates/Customers/customer_form_show.html.twig index 5f00eb5c..e70739f1 100644 --- a/templates/Customers/customer_form_show.html.twig +++ b/templates/Customers/customer_form_show.html.twig @@ -12,13 +12,14 @@ - -
- {% include 'Customers/_show_info.html.twig' with {'customer' : customer} %} -
- {% include 'Customers/customer_show_address_fields_short.html.twig' with {'customer' : customer} %} - - + +
+ {% include 'Customers/_show_info.html.twig' with {'customer' : customer} %} +
+ {% include 'Customers/customer_show_address_fields_short.html.twig' with {'customer' : customer} %} + {% include 'Customers/customer_reservation_history.html.twig' with {'reservations': reservations} %} + +
diff --git a/templates/Customers/customer_reservation_history.html.twig b/templates/Customers/customer_reservation_history.html.twig new file mode 100644 index 00000000..23a2ea9d --- /dev/null +++ b/templates/Customers/customer_reservation_history.html.twig @@ -0,0 +1,81 @@ +
+
+
+
+

+ +

+
+
+ {% if reservations is empty %} +

{{ 'customer.reservation.history.empty'|trans }}

+ {% else %} +
+ + + + + + + + + + + + + + {% for res in reservations %} + + + + + + + + + + {% endfor %} + +
{{ 'housekeeping.summary.arrive'|trans({}, 'Housekeeping') }}{{ 'housekeeping.summary.depart'|trans({}, 'Housekeeping') }}{{ 'registrationbook.stays'|trans }}{{ 'statistics.tourism.room_label'|trans }}{{ 'reservation.status'|trans }}{{ 'invoice.number.short'|trans }}
{{ res.startDate|date('d.m.y') }}{{ res.endDate|date('d.m.y') }}{{ res.amount }}{{ res.appartment ? res.appartment.number ~ ' ' ~ res.appartment.description : '–' }}{{ res.reservationStatus ? res.reservationStatus.name : '–' }} + {% if res.invoices is empty %} + + {% else %} + {% for invoice in res.invoices %} + + {{ invoice.number ? invoice.number : '–' }} + {% if not loop.last %}
{% endif %} + {% endfor %} + {% endif %} +
+ + + +
+
+ {% endif %} +
+
+
+
+
+
diff --git a/translations/Customers/messages.de.xlf b/translations/Customers/messages.de.xlf index fb89f7db..5e26a042 100644 --- a/translations/Customers/messages.de.xlf +++ b/translations/Customers/messages.de.xlf @@ -214,6 +214,14 @@ customer.mandateReference.label Mandatsreferenznr. + + customer.reservation.history + Reservierungshistorie + + + customer.reservation.history.empty + Keine Reservierungen vorhanden. + diff --git a/translations/Customers/messages.en.yaml b/translations/Customers/messages.en.yaml index d5e2237f..9a10941f 100644 --- a/translations/Customers/messages.en.yaml +++ b/translations/Customers/messages.en.yaml @@ -59,3 +59,5 @@ customer.accountIBAN.label: IBAN customer.accountIBAN.hint: >- The IBAN and mandate reference number must be filled in for the direct debit payment method. customer.mandateReference.label: Mandate Reference Number +customer.reservation.history: "Reservation history" +customer.reservation.history.empty: "No reservations found." From c5e7639b65cda784ef6021572aa3ca1c1fc9bd09 Mon Sep 17 00:00:00 2001 From: MeisterAdebar Date: Fri, 5 Jun 2026 13:17:21 +0200 Subject: [PATCH 2/9] fix: grey out only cancelled reservations in history view --- templates/Customers/customer_form_show.html.twig | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/Customers/customer_form_show.html.twig b/templates/Customers/customer_form_show.html.twig index e70739f1..0a8d4c4b 100644 --- a/templates/Customers/customer_form_show.html.twig +++ b/templates/Customers/customer_form_show.html.twig @@ -16,7 +16,6 @@
{% include 'Customers/_show_info.html.twig' with {'customer' : customer} %}
- {% include 'Customers/customer_show_address_fields_short.html.twig' with {'customer' : customer} %} {% include 'Customers/customer_reservation_history.html.twig' with {'reservations': reservations} %} From b84f3aae1e0e7ada5c489afebe12ad8370d7d3fc Mon Sep 17 00:00:00 2001 From: MeisterAdebar Date: Fri, 5 Jun 2026 13:23:29 +0200 Subject: [PATCH 3/9] feat: add clickable invoice link in reservation history --- templates/Customers/customer_form_show.html.twig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/Customers/customer_form_show.html.twig b/templates/Customers/customer_form_show.html.twig index 0a8d4c4b..c1f36ecd 100644 --- a/templates/Customers/customer_form_show.html.twig +++ b/templates/Customers/customer_form_show.html.twig @@ -16,7 +16,9 @@
{% include 'Customers/_show_info.html.twig' with {'customer' : customer} %}
- {% include 'Customers/customer_reservation_history.html.twig' with {'reservations': reservations} %} + {% include 'Customers/customer_show_address_fields_short.html.twig' with {'customer' : customer} %} + {% include 'Customers/customer_reservation_history.html.twig' with {'reservations': reservations} %} + Date: Sat, 6 Jun 2026 01:08:11 +0200 Subject: [PATCH 4/9] feat: add client-side pagination for reservation history - initReservationHistoryPagination() in customers_controller.js triggered via onSuccess callback after modal load - shows 10 reservations per page, pagination rendered in accordion without reloading the modal --- assets/controllers/customers_controller.js | 65 ++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/assets/controllers/customers_controller.js b/assets/controllers/customers_controller.js index 45829e3c..cadc8962 100644 --- a/assets/controllers/customers_controller.js +++ b/assets/controllers/customers_controller.js @@ -63,6 +63,10 @@ export default class extends Controller { url, method: 'GET', target, + onSuccess: (html) => { + target.innerHTML = html; + initReservationHistoryPagination(); + } }); } @@ -89,3 +93,64 @@ export default class extends Controller { }); } } + +function initReservationHistoryPagination() { + const perPage = 10; + const tbody = document.querySelector('#reservationHistoryCollapse tbody'); + if (!tbody) return; + const rows = Array.from(tbody.querySelectorAll('tr')); + if (rows.length <= perPage) return; + + let currentPage = 1; + const pages = Math.ceil(rows.length / perPage); + + function showPage(page) { + currentPage = page; + rows.forEach((row, i) => { + row.style.display = (i >= (page-1)*perPage && i < page*perPage) ? '' : 'none'; + }); + renderPager(); + } + + function renderPager() { + let pager = document.getElementById('res-history-pager'); + if (!pager) { + pager = document.createElement('div'); + pager.id = 'res-history-pager'; + pager.className = 'px-3 py-2'; + document.querySelector('#reservationHistoryCollapse .accordion-body').appendChild(pager); + } + pager.innerHTML = ''; + const ul = document.createElement('ul'); + ul.className = 'pagination pagination-sm mb-0'; + + const prev = document.createElement('li'); + prev.className = 'page-item' + (currentPage === 1 ? ' disabled' : ''); + prev.innerHTML = '«'; + if (currentPage > 1) prev.querySelector('a').addEventListener('click', e => { e.preventDefault(); showPage(currentPage-1); }); + ul.appendChild(prev); + + for (let i = 1; i <= pages; i++) { + const li = document.createElement('li'); + li.className = 'page-item' + (i === currentPage ? ' active' : ''); + const p = i; + li.innerHTML = '' + i + ''; + li.querySelector('a').addEventListener('click', e => { e.preventDefault(); showPage(p); }); + ul.appendChild(li); + } + + const next = document.createElement('li'); + next.className = 'page-item' + (currentPage === pages ? ' disabled' : ''); + next.innerHTML = '»'; + if (currentPage < pages) next.querySelector('a').addEventListener('click', e => { e.preventDefault(); showPage(currentPage+1); }); + ul.appendChild(next); + pager.appendChild(ul); + } + + const collapse = document.getElementById('reservationHistoryCollapse'); + if (collapse) { + collapse.addEventListener('shown.bs.collapse', () => showPage(1), { once: true }); + if (collapse.classList.contains('show')) showPage(1); + } +} + From f308f19f648cc3237187de6a6cfe11fb5843466d Mon Sep 17 00:00:00 2001 From: MeisterAdebar Date: Sat, 6 Jun 2026 01:35:09 +0200 Subject: [PATCH 5/9] fix: correct modal title and client-side pagination for reservation history --- assets/controllers/customers_controller.js | 2 +- templates/Customers/customer_reservation_history.html.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/controllers/customers_controller.js b/assets/controllers/customers_controller.js index cadc8962..b28e1b7c 100644 --- a/assets/controllers/customers_controller.js +++ b/assets/controllers/customers_controller.js @@ -56,7 +56,7 @@ export default class extends Controller { if (!url) { return; } - setModalTitle(title); + if (title) setModalTitle(title); const target = document.getElementById('modal-content-ajax'); httpRequest({ diff --git a/templates/Customers/customer_reservation_history.html.twig b/templates/Customers/customer_reservation_history.html.twig index 23a2ea9d..ff33f6ab 100644 --- a/templates/Customers/customer_reservation_history.html.twig +++ b/templates/Customers/customer_reservation_history.html.twig @@ -61,7 +61,7 @@ From 92249587ed94990fdcf3ca1861b2667738bdd323 Mon Sep 17 00:00:00 2001 From: MeisterAdebar Date: Sat, 6 Jun 2026 13:12:22 +0200 Subject: [PATCH 6/9] fix: deduplizierung, invoices N+1 und pagination reinit nach AJAX - ReservationRepository: distinct() + leftJoin invoices eager-loading - customers_controller: requestAnimationFrame vor initReservationHistoryPagination - customers_controller: alten #res-history-pager bei Modal-Reinjektion entfernen --- assets/controllers/customers_controller.js | 9 ++++++++- src/Repository/ReservationRepository.php | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/assets/controllers/customers_controller.js b/assets/controllers/customers_controller.js index b28e1b7c..a45230df 100644 --- a/assets/controllers/customers_controller.js +++ b/assets/controllers/customers_controller.js @@ -65,7 +65,9 @@ export default class extends Controller { target, onSuccess: (html) => { target.innerHTML = html; - initReservationHistoryPagination(); + // rAF stellt sicher, dass Bootstrap das neue DOM + // registriert hat, bevor Listener angehängt werden + requestAnimationFrame(() => initReservationHistoryPagination()); } }); } @@ -99,6 +101,11 @@ function initReservationHistoryPagination() { const tbody = document.querySelector('#reservationHistoryCollapse tbody'); if (!tbody) return; const rows = Array.from(tbody.querySelectorAll('tr')); + + // Alten Pager aus vorherigem Modal-Aufruf entfernen + const oldPager = document.getElementById('res-history-pager'); + if (oldPager) oldPager.remove(); + if (rows.length <= perPage) return; let currentPage = 1; diff --git a/src/Repository/ReservationRepository.php b/src/Repository/ReservationRepository.php index 12f800c9..300058b3 100644 --- a/src/Repository/ReservationRepository.php +++ b/src/Repository/ReservationRepository.php @@ -523,13 +523,15 @@ public function findImportedWithoutBookerPaginated(int $page, int $perPage): arr public function loadAllReservationsForCustomer(\App\Entity\Customer $customer): array { return $this->createQueryBuilder('r') - ->select('r', 'a', 'rs') + ->select('r', 'a', 'rs', 'i') ->leftJoin('r.appartment', 'a') ->leftJoin('r.reservationStatus', 'rs') + ->leftJoin('r.invoices', 'i') ->where('r.booker = :customer') ->orWhere(':customer MEMBER OF r.customers') ->setParameter('customer', $customer) ->orderBy('r.startDate', 'DESC') + ->distinct() ->getQuery() ->getResult(); } From 900ef3bb7828d0c0b1f6516a3e386e7593da309e Mon Sep 17 00:00:00 2001 From: MeisterAdebar Date: Sat, 6 Jun 2026 17:40:33 +0200 Subject: [PATCH 7/9] Change data-title from invoice.number to translated invoice.details --- templates/Customers/customer_reservation_history.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/Customers/customer_reservation_history.html.twig b/templates/Customers/customer_reservation_history.html.twig index ff33f6ab..3f0d58bf 100644 --- a/templates/Customers/customer_reservation_history.html.twig +++ b/templates/Customers/customer_reservation_history.html.twig @@ -49,7 +49,7 @@ {{ invoice.number ? invoice.number : '–' }} From e8ff4e0b7c9aa0f0f48b1825ffbc22e4b86dd05f Mon Sep 17 00:00:00 2001 From: MeisterAdebar Date: Sat, 6 Jun 2026 18:20:49 +0200 Subject: [PATCH 8/9] fix: Remove redundant title guard - setModalTitle already handles empty strings --- assets/controllers/customers_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/controllers/customers_controller.js b/assets/controllers/customers_controller.js index a45230df..f6de2781 100644 --- a/assets/controllers/customers_controller.js +++ b/assets/controllers/customers_controller.js @@ -56,7 +56,7 @@ export default class extends Controller { if (!url) { return; } - if (title) setModalTitle(title); + setModalTitle(title); const target = document.getElementById('modal-content-ajax'); httpRequest({ From ac606c884635255d41800d904342a4ddd25f2a99 Mon Sep 17 00:00:00 2001 From: MeisterAdebar Date: Sat, 13 Jun 2026 16:41:00 +0200 Subject: [PATCH 9/9] fix: improve reservation history template - use reservationStatus.code == 'canceled_noshow' instead of id == 3 for stable cancelled status detection across different installations - remove mt-3 from outer row div - add mt-3 to accordion-body for better inner spacing --- templates/Customers/customer_reservation_history.html.twig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/Customers/customer_reservation_history.html.twig b/templates/Customers/customer_reservation_history.html.twig index 3f0d58bf..330c23b5 100644 --- a/templates/Customers/customer_reservation_history.html.twig +++ b/templates/Customers/customer_reservation_history.html.twig @@ -1,4 +1,4 @@ -
+
@@ -16,7 +16,7 @@ class="accordion-collapse collapse" aria-labelledby="reservationHistoryHeading" data-bs-parent="#reservationHistoryAccordion"> -
+
{% if reservations is empty %}

{{ 'customer.reservation.history.empty'|trans }}

{% else %} @@ -35,7 +35,7 @@ {% for res in reservations %} - + {{ res.startDate|date('d.m.y') }} {{ res.endDate|date('d.m.y') }} {{ res.amount }}