From a02490ea36e14e96b760f378944cf515a9d75885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 14 Oct 2025 20:30:51 +0200 Subject: [PATCH] wip: add documentation for plan --- REFACTORING_PARTICIPANT_CARDS.md | 1317 ++++++++++++++++++++++++++++++ 1 file changed, 1317 insertions(+) create mode 100644 REFACTORING_PARTICIPANT_CARDS.md diff --git a/REFACTORING_PARTICIPANT_CARDS.md b/REFACTORING_PARTICIPANT_CARDS.md new file mode 100644 index 0000000..a15a36a --- /dev/null +++ b/REFACTORING_PARTICIPANT_CARDS.md @@ -0,0 +1,1317 @@ +# Participant Card View Refactoring Plan + +## Overview + +Transform create (step 2) and edit flows from accordion-style "all forms at once" to a card overview with lazy-loaded individual participant forms. This addresses UX and performance issues with large groups (50+ participants). + +**Status**: Planning Complete - Ready for Implementation + +## Goals + +- **Performance**: Lazy load forms on demand instead of rendering all at once +- **UX**: Cleaner overview with card-based navigation +- **Scalability**: Handle 50+ participant bookings efficiently +- **Code Reuse**: Maximize shared code between create and edit flows +- **Safe Migration**: Keep old flows intact during development + +## Architecture Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| **Form Rendering** | Replace entire content area (Option C) | Best for mobile, prevents concurrency issues | +| **Form Submission** | Explicit "Speichern" button | Not all fields trigger refresh, cleaner UX | +| **Card Content** | Name, room, price only | Minimal, focused information | +| **Validation** | On form submission only | No card-level indicators needed | +| **Card Price Updates** | Full re-render on return to cards | Cards not visible during form editing | +| **Room Assignment** | Keep in participant form | Existing behavior maintained | +| **Migration Strategy** | Separate controllers (-v2 routes) | Safe parallel development, easy comparison | +| **Lazy Loading** | Forms loaded via HTMX on demand | Critical for performance with 50+ participants | + +--- + +## Phase 1: Shared Foundation + +### 1.1 Create `ParticipantCardDataService` + +**File**: `src/Service/ParticipantCardDataService.php` + +**Purpose**: Extract card display data from BookingDto + +**Methods**: + +```php +/** + * Get card data for a single participant + * + * @return array{name: string, roomName: string, price: string} + */ +public function getCardData(BookingDto $bookingDto, int $index): array + +/** + * Get card data for all participants + * + * @return array + */ +public function getAllCardsData(BookingDto $bookingDto): array +``` + +**Implementation Details**: +- **Name**: `$participant->firstName . ' ' . $participant->lastName` or fallback to "Teilnehmer {index+1}" +- **Room**: Look up from `$bookingDto->travel->getRoomById($participant->assignedRoomId)` +- **Price**: Use `BookingPriceCalculatorService::calculateAllParticipantIndividualPrices()`, format as `number_format($price, 2, ',', '.') . ' €'` + +**Dependencies**: +- `BookingPriceCalculatorService` (existing) +- Access to Travel model for room lookups + +**Test Coverage**: +- Unit test: Verify name fallback for participants without names +- Unit test: Verify price formatting +- Unit test: Verify room name lookup +- Unit test: Handle missing room assignment gracefully + +**Todos**: +- [ ] Create service class with DI configuration +- [ ] Implement `getCardData()` method +- [ ] Implement `getAllCardsData()` method +- [ ] Write unit tests +- [ ] Handle edge cases (missing room, missing names) + +--- + +### 1.2 Extend `ParticipantFieldHandlerRegistry` + +**File**: `src/Form/Service/ParticipantFieldHandlerRegistry.php` + +**New Method**: + +```php +/** + * Process field handlers for a single participant + * + * @param array $participantData Submitted data for one participant + * @param BookingDto $bookingDto The booking DTO to update + * @param int $participantIndex Index of participant to process + */ +public function processFieldsForParticipant( + array $participantData, + BookingDto $bookingDto, + int $participantIndex +): void +``` + +**Implementation**: +- Extract loop body from existing `processFields()` method +- Apply all handlers to single participant in dependency order +- Reuse existing handler sorting and execution logic + +**Todos**: +- [ ] Add `processFieldsForParticipant()` method to registry +- [ ] Extract handler execution logic from loop +- [ ] Maintain dependency order execution +- [ ] Test with existing field handlers +- [ ] Verify field sync logic works for single participant + +--- + +### 1.3 Create `ParticipantFormDto` + +**File**: `src/Form/Model/ParticipantFormDto.php` + +**Purpose**: Dedicated DTO for editing a single participant with full booking context + +**Why This Is Needed**: +The existing `BookingParticipantType` uses `getBookingDtoFromForm()` which traverses up the form tree to find a parent form with `BookingDto` as its data. This DTO provides that context while maintaining clean separation between the booking flow state and individual participant editing. + +**Architecture Benefits**: +- ✅ Clean separation: `BookingDto` = full booking flow state, `ParticipantFormDto` = single participant editing context +- ✅ No pollution of BookingDto with editing-specific properties +- ✅ Type-safe and explicit relationships +- ✅ Participant has full booking context for conditional logic +- ✅ Field state providers work unchanged + +**Implementation**: + +```php +/** + * DTO for editing a single participant within a booking context. + * + * This DTO encapsulates the participant being edited along with the full + * booking context needed for field state evaluation, pricing calculations, + * and conditional logic. + */ +class ParticipantFormDto +{ + public function __construct( + public BookingDto $bookingContext, + public ParticipantDto $participant, + ) {} +} +``` + +**Key Points**: +- `bookingContext` provides full booking state for field conditions +- `participant` is the participant being edited +- Participant index available via `$participant->index` (no separate property needed) + +**Todos**: +- [ ] Create DTO class in `src/Form/Model/` +- [ ] Add constructor with type hints +- [ ] Write unit tests for DTO instantiation +- [ ] Test with field state providers + +--- + +### 1.4 Create `ParticipantFormType` + +**File**: `src/Form/ParticipantFormType.php` + +**Purpose**: Form type for editing a single participant with automatic field handler processing + +**Implementation**: + +```php +class ParticipantFormType extends AbstractType +{ + public function __construct( + private readonly ParticipantFieldHandlerRegistry $participantFieldHandlerRegistry, + ) {} + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('participant', BookingParticipantType::class, [ + 'edit_mode' => $options['edit_mode'], + ]) + ->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'onPreSubmit']); + } + + /** + * Process field handlers before form binding and validation. + * + * This follows the same pattern as BookingCreateStep2Type - field handlers + * are called during PRE_SUBMIT to clean and validate data before binding. + */ + public function onPreSubmit(FormEvent $event): void + { + $form = $event->getForm(); + $submittedData = $event->getData(); + + /** @var ParticipantFormDto $participantFormDto */ + $participantFormDto = $form->getData(); + $bookingDto = $participantFormDto->bookingContext; + $participantIndex = $participantFormDto->participant->index; + + // Extract participant data from submitted form + $participantSubmittedData = $submittedData['participant'] ?? []; + + // Process field handlers for this single participant + $this->participantFieldHandlerRegistry->processFieldsForParticipant( + $participantSubmittedData, + $bookingDto, + $participantIndex + ); + + // Note: No need to sync data back - the DTO is updated by reference + // and handleRequest() will bind the updated values + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => ParticipantFormDto::class, + 'edit_mode' => false, + ]); + + $resolver->setAllowedTypes('edit_mode', 'bool'); + } +} +``` + +**Key Points**: +- Field handlers called automatically in PRE_SUBMIT event (matches existing pattern in `BookingCreateStep2Type`) +- Controller doesn't need to call field handlers manually +- Uses `processFieldsForParticipant()` to process only the current participant +- Root form data is `ParticipantFormDto` which provides `BookingDto` context via `bookingContext` property + +**Todos**: +- [ ] Create form type with DI for field handler registry +- [ ] Implement buildForm with participant field +- [ ] Implement onPreSubmit event listener +- [ ] Add proper PHPDoc +- [ ] Test field handler execution +- [ ] Test validation behavior + +--- + +### 1.5 Update `FormTraversalTrait` + +**File**: `src/Form/Service/Trait/FormTraversalTrait.php` + +**Purpose**: Support both `BookingDto` and `ParticipantFormDto` as root form data + +**Modification**: + +```php +trait FormTraversalTrait +{ + /** + * Gets the BookingDto from the root of the form tree. + * + * Supports both direct BookingDto (used in current flows) and + * ParticipantFormDto wrapper (used in card-based flows). + * + * @param FormInterface $form The form to start traversing from + * + * @return BookingDto|null The booking DTO or null if not found + */ + public function getBookingDtoFromForm(FormInterface $form): ?BookingDto + { + // Traverse up the form tree to get the root form's data + $rootForm = $form; + while ($rootForm->getParent()) { + $rootForm = $rootForm->getParent(); + } + + $data = $rootForm->getData(); + + // Direct BookingDto (used in current flows) + if ($data instanceof BookingDto) { + return $data; + } + + // ParticipantFormDto wrapper (used in card-based flows) + if ($data instanceof ParticipantFormDto) { + return $data->bookingContext; + } + + return null; + } +} +``` + +**Why This Works**: +- Existing flows use `BookingDto` directly → no changes needed +- New card flows use `ParticipantFormDto` → extracts `bookingContext` +- Field state providers work with both approaches seamlessly +- `BookingParticipantType` remains completely unchanged + +**Todos**: +- [ ] Add `ParticipantFormDto` support to trait +- [ ] Add `use` statement for `ParticipantFormDto` +- [ ] Test with existing flows (should work unchanged) +- [ ] Test with new card flows +- [ ] Verify field state providers work correctly + +--- + +### 1.6 Create `ParticipantCardFlowTrait` + +**File**: `src/Controller/Booking/ParticipantCardFlowTrait.php` + +**Purpose**: Share common controller logic between create and edit flows + +**Methods**: + +```php +/** + * Load BookingDto from session or throw exception + */ +private function loadBookingDtoOrFail(Request $request, string $mode): BookingDto + +/** + * Generate card data for all participants + */ +private function generateAllCardsData(BookingDto $bookingDto): array + +/** + * Create form for single participant + */ +private function createParticipantForm( + BookingDto $bookingDto, + int $index, + array $options = [] +): FormInterface + +/** + * Calculate summary data (pricing, room counts, etc.) + */ +private function calculateSummaryData(BookingDto $bookingDto): array + +/** + * Process single participant form refresh + */ +private function handleParticipantRefresh( + Request $request, + BookingDto $bookingDto, + int $index, + string $refreshRouteName +): Response +``` + +**Todos**: +- [ ] Create trait file +- [ ] Implement `loadBookingDtoOrFail()` +- [ ] Implement `generateAllCardsData()` using `ParticipantCardDataService` +- [ ] Implement `createParticipantForm()` +- [ ] Implement `calculateSummaryData()` +- [ ] Implement `handleParticipantRefresh()` with OOB swap logic +- [ ] Add type hints and PHPDoc + +--- + +## Phase 2: Create Flow (Step 2 Refactored) + +### 2.1 Create `CreateStep2RefactoredController` + +**File**: `src/Controller/Booking/CreateStep2RefactoredController.php` + +**Routes**: + +| Method | Route | Name | Purpose | +|--------|-------|------|---------| +| GET/POST | `/bookings/create/participants-v2` | `app_booking_create_step_2_v2_cards` | Display card grid | +| GET/POST | `/bookings/create/participants-v2/{index}` | `app_booking_create_step_2_v2_participant` | Show/submit participant form | +| POST | `/bookings/create/participants-v2/{index}/refresh` | `app_booking_create_step_2_v2_participant_refresh` | HTMX refresh without validation | + +**Controller Structure**: + +```php +class CreateStep2RefactoredController extends AbstractController +{ + use BookingCreateTrait; + use BookingExceptionHandlerTrait; + use HxTrait; + use ParticipantCardFlowTrait; + + public function __construct( + private readonly BookingService $bookingService, + private readonly BookingPriceCalculatorService $priceCalculator, + private readonly TravelDataService $travelDataService, + private readonly RoomAssignmentService $roomAssignmentService, + private readonly ParticipantCardDataService $participantCardService, + private readonly ParticipantFieldHandlerRegistry $fieldHandlerRegistry, + ) { + } +} +``` + +**Action: `showCards(Request $request): Response`** + +Implementation steps: +1. Load BookingDto from session (reuse `getOrCreateBookingCreateDto()`) +2. Enrich with fresh availability data +3. Validate step access +4. Ensure correct number of participants +5. Auto-assign rooms if needed +6. Preselect mandatory services +7. Save BookingDto to session +8. Generate all cards data via `ParticipantCardDataService` +9. Calculate summary data +10. Render `create_step_2_v2_cards.html.twig` + +**Action: `editParticipant(int $index, Request $request): Response`** + +Implementation steps: +1. Load BookingDto from session +2. Validate participant index +3. Create form for `$bookingDto->participants[$index]` using `BookingParticipantType` +4. Set HTMX attributes for refresh endpoint +5. `$form->handleRequest($request)` +6. If submitted and valid: + - Extract participant data from submitted form + - Process field handlers via `processFieldsForParticipant()` + - Save BookingDto to session + - HTMX redirect to cards view +7. Calculate summary data for sidebar +8. Render `_participant_form_standalone.html.twig` with form and summary + +**Action: `refreshParticipantForm(int $index, Request $request): Response`** + +Implementation steps: +1. Load BookingDto from session +2. Enrich with fresh availability data +3. Create form with `validation_groups: false` +4. `$form->handleRequest($request)` +5. Extract participant data from submitted form +6. Process field handlers via `processFieldsForParticipant()` +7. Preselect mandatory services +8. Save BookingDto to session +9. Collect notifications from participant DTO +10. Calculate summary data +11. Render form + sidebar using `htmxOobResponse()` with blocks: + - `participant_form` (target) + - `booking_summary` (OOB swap) +12. Add notifications to HX-Trigger header if present + +**Todos**: +- [ ] Create controller class with DI +- [ ] Implement `showCards()` action +- [ ] Implement `editParticipant()` action +- [ ] Implement `refreshParticipantForm()` action +- [ ] Add route annotations +- [ ] Handle edge cases (invalid index, session expiry) +- [ ] Add error handling and logging + +--- + +### 2.2 Templates for Create Flow + +**Main Template: `templates/booking/create_step_2_v2_cards.html.twig`** + +Structure: +```twig +{% extends 'layout.html.twig' %} + +{% block content %} + {% include '_partials/_flashes.html.twig' %} +

Neue Buchung

+ +
+ {# Main content area - cards grid #} +
+

Teilnehmer

+ +
+ {% for participant in bookingDto.participants %} + {% include 'booking/_participant_card.html.twig' with { + 'cardData': cardsData[loop.index0], + 'index': loop.index0, + 'mode': 'create' + } %} + {% endfor %} +
+ +
+ + Zurück + + +
+
+ + {# Sidebar summary #} + {% block booking_summary %} +
+ {% include 'booking/_summary.html.twig' with { + 'bookingCreateDto': bookingDto, + 'participantCount': participantsCount, + 'groupedSelectedRooms': groupedSelectedRooms, + 'assignmentCounts': assignmentCounts + } %} +
+ {% endblock %} +
+{% endblock %} +``` + +**Todos**: +- [ ] Create main template file +- [ ] Add grid layout structure +- [ ] Include card loop +- [ ] Add navigation buttons +- [ ] Include sidebar summary + +--- + +**Card Partial: `templates/booking/_participant_card.html.twig`** + +Structure: +```twig +{# Compact participant card with name, room, price, and edit button #} +
+
+

{{ cardData.name }}

+

{{ cardData.roomName }}

+
+
+ {{ cardData.price }} + +
+
+``` + +**Features**: +- Compact layout +- Name (or "Teilnehmer X" fallback) +- Room name +- Price formatted as "450,00 €" +- "Bearbeiten" button with HTMX attributes +- Target replaces entire `#main-content` area + +**Todos**: +- [ ] Create card template +- [ ] Add HTMX attributes for navigation +- [ ] Style with TailwindCSS +- [ ] Test responsive layout +- [ ] Handle long names gracefully + +--- + +**Form Standalone: `templates/booking/_participant_form_standalone.html.twig`** + +Structure: +```twig +{# Standalone participant form view (replaces main content area) #} +
+

{{ participantIndex == 0 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1) }}

+ + {{ form_start(form, { + 'attr': { + 'novalidate': 'novalidate', + 'hx-post': path('app_booking_create_step_2_v2_participant', {index: participantIndex}), + 'hx-target': '#main-content', + 'hx-swap': 'innerHTML' + } + }) }} + + {% block participant_form %} +
+ {# Personal data section #} +
+ {{ form_row(form.firstName) }} + {{ form_row(form.lastName) }} + {{ form_row(form.dateOfBirth, { + 'attr': { + 'hx-trigger': 'change', + 'hx-post': path('app_booking_create_step_2_v2_participant_refresh', {index: participantIndex}), + 'hx-swap': 'none' + } + }) }} + {{ form_row(form.gender) }} + {{ form_row(form.nationality) }} +
+ + {# Contact information #} +
+ {{ form_row(form.email) }} + {{ form_row(form.mobile) }} +
+ + {# Address #} + {% if form.address is defined %} +
+ {{ form_row(form.address.street) }} + {{ form_row(form.address.postCode) }} + {{ form_row(form.address.city) }} + {{ form_row(form.address.country) }} +
+ {% endif %} + + {# Body dimensions #} + {% if form.bodyDimensions is defined %} +
+ {{ form_row(form.bodyDimensions.height) }} + {{ form_row(form.bodyDimensions.shoeSize) }} + {{ form_row(form.bodyDimensions.weight) }} +
+ {% endif %} + + {# Room assignment #} +
+ {{ form_row(form.assignedRoomId, { + 'attr': { + 'hx-trigger': 'change', + 'hx-post': path('app_booking_create_step_2_v2_participant_refresh', {index: participantIndex}), + 'hx-swap': 'none' + } + }) }} + {% if form.remarksRoom is defined %} + {{ form_row(form.remarksRoom) }} + {% endif %} +
+ + {# Service selection - same structure as current templates #} + {# Age eligibility checks, conditional field rendering, etc. #} + {# Transportation, insurance, etc. #} +
+ {% endblock %} + +
+ + +
+ + {{ form_rest(form) }} + {{ form_end(form) }} +
+``` + +**Key Features**: +- Reuses existing form field structure from `create_step_2.html.twig` +- HTMX attributes on fields that trigger refresh +- "Abbrechen" returns to cards (GET request, no save) +- "Speichern" submits form (POST request, validation, save on success) +- Form action posts to same route (Symfony convention) + +**Todos**: +- [ ] Create form template +- [ ] Copy form field structure from current template +- [ ] Add HTMX attributes for refresh triggers +- [ ] Add navigation buttons (Abbrechen/Speichern) +- [ ] Test all conditional field logic +- [ ] Ensure age eligibility checks work +- [ ] Verify field state providers work correctly + +--- + +## Phase 3: Edit Flow Refactored + +### 3.1 Create `EditRefactoredController` + +**File**: `src/Controller/Booking/EditRefactoredController.php` + +**Routes**: + +| Method | Route | Name | Purpose | +|--------|-------|------|---------| +| GET/POST | `/bookings/{id}/edit-v2` | `app_booking_edit_v2_cards` | Display card grid | +| GET/POST | `/bookings/{id}/edit-v2/participants/{index}` | `app_booking_edit_v2_participant` | Show/submit participant form | +| POST | `/bookings/{id}/edit-v2/participants/{index}/refresh` | `app_booking_edit_v2_participant_refresh` | HTMX refresh | +| POST | `/bookings/{id}/edit-v2/reload` | `app_booking_edit_v2_reload` | Discard changes, reload from API | + +**Differences from Create Flow**: +- Load booking data from API on first visit (via `BookingDataTrait`) +- Store in session with `MODE_EDIT` +- Handle canceled participants (status 'S'): + - Show "storniert" badge on card + - Disable "Bearbeiten" button + - Show surcharges info when clicked +- Final submission calls `ApiClient::updateBooking()` instead of navigating to step 3 +- Apply mutability constraints via `EditFieldStateProvider` + +**Controller Structure**: + +```php +class EditRefactoredController extends AbstractController +{ + use BookingDataTrait; + use HxTrait; + use ParticipantCardFlowTrait; + + public function __construct( + private readonly ApiClient $apiClient, + private readonly BookingDataProcessor $bookingDataProcessor, + private readonly TravelDataService $travelDataService, + private readonly BookingService $bookingService, + private readonly BookingPriceCalculatorService $priceCalculator, + private readonly ParticipantCardDataService $participantCardService, + private readonly ParticipantFieldHandlerRegistry $fieldHandlerRegistry, + private readonly CacheInterface $cache, + private readonly Security $security, + private readonly Crypt $crypt, + private readonly LoggerInterface $logger, + ) { + } +} +``` + +**Action Implementations**: Similar to create flow, with edit-specific logic + +**Todos**: +- [ ] Create controller class with DI +- [ ] Implement `showCards()` action (load from API on first visit) +- [ ] Implement `editParticipant()` action (handle canceled participants) +- [ ] Implement `refreshParticipantForm()` action (apply mutability constraints) +- [ ] Implement `reloadFromApi()` action (discard session changes) +- [ ] Add route annotations +- [ ] Handle edit-specific edge cases +- [ ] Test API integration + +--- + +### 3.2 Templates for Edit Flow + +**Main Template: `templates/booking/edit_v2_cards.html.twig`** + +Similar to create flow, with differences: +- Header shows "Buchung bearbeiten" +- "Reload" button to discard changes +- Different navigation (Zurück to bookings list, Aktualisieren to save) +- Final submit button calls API update + +**Card Template**: Reuse `_participant_card.html.twig` with mode parameter +- Add "storniert" badge for canceled participants +- Disable button for canceled participants + +**Form Template**: Reuse `_participant_form_standalone.html.twig` with mode parameter +- EditFieldStateProvider applies mutability constraints +- Show applicant address placeholders + +**Todos**: +- [ ] Create main template for edit +- [ ] Add reload button with confirmation +- [ ] Modify card template to handle canceled participants +- [ ] Test form template with edit mode +- [ ] Verify mutability constraints applied correctly + +--- + +## Phase 4: Integration & Testing + +### 4.1 Functional Testing Checklist + +**Card View**: +- [ ] Card grid renders all participants correctly +- [ ] Card names show participant names or "Teilnehmer X" fallback +- [ ] Card room names display correctly +- [ ] Card prices formatted as "450,00 €" +- [ ] "Bearbeiten" button on each card +- [ ] Clicking "Bearbeiten" loads participant form +- [ ] Navigation buttons work (Zurück/Weiter or Zurück/Aktualisieren) + +**Participant Form**: +- [ ] Form loads for correct participant +- [ ] All form fields render correctly +- [ ] Field states applied (readonly, disabled, hidden, required) +- [ ] Conditional fields appear/disappear based on data +- [ ] Age eligibility checks work +- [ ] Service fields populated correctly + +**HTMX Interactions**: +- [ ] Field changes trigger refresh endpoint +- [ ] Form refreshes with updated data +- [ ] Sidebar summary updates (OOB swap) +- [ ] Notifications display via toast +- [ ] "Abbrechen" returns to cards without saving +- [ ] "Speichern" submits form with validation +- [ ] Valid submission redirects to cards +- [ ] Invalid submission shows validation errors + +**Field Handlers**: +- [ ] Date of birth enables age-dependent services +- [ ] Room assignment updates counts +- [ ] Ski pass selection filters rentals by duration +- [ ] Rental selection shows insurance checkbox +- [ ] Bulk insurance updates dependent participants +- [ ] Transportation selection shows/hides pickup/parking +- [ ] License plate field appears with parking selection +- [ ] Insurance auto-reassignment on price changes +- [ ] Rental clearing when ski pass changes + +**Data Consistency**: +- [ ] Session DTO updated correctly after form save +- [ ] Session DTO updated correctly after refresh +- [ ] Card data matches DTO after returning from form +- [ ] Prices recalculated correctly +- [ ] Room assignments preserved +- [ ] Service selections preserved + +**Edit Flow Specific**: +- [ ] Booking data loads from API on first visit +- [ ] Session stores data correctly (MODE_EDIT) +- [ ] Canceled participants show badge +- [ ] Canceled participants have disabled button +- [ ] Mutability constraints applied +- [ ] Final submission calls API correctly +- [ ] Reload discards session changes + +--- + +### 4.2 Performance Testing + +**Metrics to Measure**: +- [ ] Card grid load time with 50 participants: < 1 second +- [ ] Participant form load time: < 200ms (lazy loading benefit) +- [ ] Field refresh response time: < 500ms +- [ ] Memory usage with 50 participants: reasonable +- [ ] No N+1 queries in card data generation +- [ ] Session size doesn't grow excessively + +**Performance Tests**: +- [ ] Create booking with 2-3 participants (baseline) +- [ ] Create booking with 10 participants +- [ ] Create booking with 25 participants +- [ ] Create booking with 50 participants +- [ ] Create booking with 100 participants (stress test) +- [ ] Measure database queries per action +- [ ] Measure memory usage per action +- [ ] Profile with Symfony profiler + +--- + +### 4.3 Comparison Testing + +**Compare Old vs New Flow**: +- [ ] Session DTO structure identical +- [ ] Final booking API payload identical +- [ ] Pricing calculations identical +- [ ] Field handler behavior identical +- [ ] Validation rules identical +- [ ] Service selection logic identical +- [ ] Room assignment logic identical +- [ ] Insurance matching logic identical + +**Test Scenarios**: +- [ ] Simple booking (2 adults, no services) +- [ ] Family booking (2 adults, 2 children, services) +- [ ] Complex booking (mixed ages, all services, insurance) +- [ ] Large group (50 participants) +- [ ] Edge cases (age limits, mandatory services, mutability) + +--- + +## Phase 5: Code Sharing Summary + +### Shared Components (100%) + +**Services**: +- `ParticipantCardDataService` (new) +- `BookingService` (existing) +- `BookingPriceCalculatorService` (existing) +- `TravelDataService` (existing) +- `RoomAssignmentService` (existing) +- `ParticipantFieldHandlerRegistry` (existing, extended) +- All field handlers (existing) + +**Form Types**: +- `BookingParticipantType` (existing) +- `AddressType`, `BodyDimensionsType`, etc. (existing) +- All field types (existing) + +**Templates**: +- `_participant_card.html.twig` (new, shared) +- `_participant_form_standalone.html.twig` (new, shared) +- `_summary.html.twig` (existing, shared) + +**Traits**: +- `ParticipantCardFlowTrait` (new, shared) +- `HxTrait` (existing, shared) + +### Controller-Specific (~20%) + +**Create Flow**: +- `CreateStep2RefactoredController` routes +- Initial step validation +- Navigation to step 3 on success +- Template: `create_step_2_v2_cards.html.twig` + +**Edit Flow**: +- `EditRefactoredController` routes +- Initial API data loading +- API update on success +- Canceled participant handling +- Reload functionality +- Template: `edit_v2_cards.html.twig` + +--- + +## Technical Details + +### Form Handling Pattern + +Standard Symfony pattern - single action for both GET and POST: + +```php +public function editParticipant(int $index, Request $request): Response +{ + $bookingDto = $this->loadBookingDtoOrFail($request, BookingDto::MODE_CREATE); + + $form = $this->createParticipantForm($bookingDto, $index, [ + 'validation_groups' => ['booking_create_step_2'], + ]); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // Extract submitted data + $submittedData = $request->request->all(); + $participantData = $submittedData['booking_participant'] ?? []; + + // Process field handlers for this participant + $this->fieldHandlerRegistry->processFieldsForParticipant( + $participantData, + $bookingDto, + $index + ); + + // Save to session + $this->bookingService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE); + + // Redirect back to cards + return $this->hxRedirect($request, $this->generateUrl('app_booking_create_step_2_v2_cards')); + } + + // Render form (GET or invalid POST) + return $this->render('booking/_participant_form_standalone.html.twig', [ + 'form' => $form, + 'participantIndex' => $index, + 'bookingDto' => $bookingDto, + // ... summary data + ]); +} +``` + +### Card Data Structure + +```php +[ + 'name' => 'Max Mustermann', // or "Teilnehmer 1" + 'roomName' => 'Doppelzimmer', + 'price' => '450,00 €' +] +``` + +### HTMX Flow Diagram + +``` +┌─────────────────────────────────────────────┐ +│ CARDS VIEW (GET /participants-v2) │ +│ │ +│ [Teilnehmer 1 | Doppelzimmer | 450,00 €] │ +│ [Teilnehmer 2 | Einzelzimmer | 520,00 €] │ +│ [Teilnehmer 3 | Doppelzimmer | 450,00 €] │ +│ │ +│ [Zurück] [Weiter] │ +└─────────────────────────────────────────────┘ + │ + │ Click "Bearbeiten" + │ hx-get="/participants-v2/0" + │ hx-target="#main-content" + ▼ +┌─────────────────────────────────────────────┐ +│ FORM VIEW (GET /participants-v2/0) │ +│ │ +│ Teilnehmer 1 │ +│ │ +│ [Vorname] [Nachname] │ +│ [Geburtsdatum] [Geschlecht] │ +│ ... │ +│ [Skipass ▼] ◄────────────┐ │ +│ [Kurse ▼] │ │ +│ │ Field change │ +│ [Abbrechen] [Speichern] │ POST refresh │ +└───────────────────────────┼─────────────────┘ + │ + │ hx-post="/participants-v2/0/refresh" + │ validation_groups: false + │ Returns: form + sidebar OOB swap + ▼ +┌─────────────────────────────────────────────┐ +│ FORM VIEW (refreshed) │ +│ - Form updated with new field states │ +│ - Sidebar summary updated (OOB) │ +│ - Notifications shown via toast │ +└─────────────────────────────────────────────┘ + │ + │ Click "Speichern" + │ POST /participants-v2/0 + │ validation_groups: ['booking_create_step_2'] + ▼ + ┌──────┴──────┐ + │ Validation │ + └──────┬──────┘ + │ + ┌───────┴────────┐ + │ │ + Valid Invalid + │ │ + ▼ ▼ + HX-Redirect Return form + to cards with errors + │ + ▼ +┌─────────────────────────────────────────────┐ +│ CARDS VIEW (all re-rendered) │ +│ │ +│ [Max Mustermann | Doppelzimmer | 580,00 €] │ ← Updated! +│ [Teilnehmer 2 | Einzelzimmer | 520,00 €] │ +│ [Teilnehmer 3 | Doppelzimmer | 450,00 €] │ +└─────────────────────────────────────────────┘ +``` + +### Session Flow + +1. **Cards View**: Load DTO from session, render cards +2. **Form View**: Load DTO from session, create form for participant[index] +3. **Form Refresh**: Load DTO, update with field changes (no validation), save to session +4. **Form Submit**: Load DTO, validate, run handlers, save to session, redirect +5. **Back to Cards**: Load DTO from session (with all changes), render cards + +**Key**: DTO is single source of truth in session, updated incrementally + +--- + +## Implementation Sequence + +### Week 1: Foundation & Create Flow + +**Day 1-2**: Foundation +- [ ] Create `ParticipantCardDataService` with tests +- [ ] Add `processFieldsForParticipant()` to registry with tests +- [ ] Create `ParticipantCardFlowTrait` + +**Day 3**: Templates +- [ ] Create `_participant_card.html.twig` +- [ ] Create `_participant_form_standalone.html.twig` +- [ ] Create `create_step_2_v2_cards.html.twig` + +**Day 4-5**: Create Flow Controller +- [ ] Implement `CreateStep2RefactoredController` +- [ ] All three routes/actions +- [ ] Test with small bookings (2-3 participants) +- [ ] Test field refresh logic +- [ ] Test validation + +### Week 2: Edit Flow & Testing + +**Day 6-7**: Edit Flow +- [ ] Implement `EditRefactoredController` +- [ ] Reuse trait methods +- [ ] Handle canceled participants +- [ ] Create edit templates +- [ ] Test API integration + +**Day 8-9**: Integration Testing +- [ ] Compare outputs (old vs new flows) +- [ ] Test all field handlers +- [ ] Test bulk insurance +- [ ] Test edge cases +- [ ] Performance test with 50+ participants + +**Day 10**: Cleanup & Documentation +- [ ] Code review +- [ ] Update documentation +- [ ] Test both flows side by side +- [ ] Mark as ready for production consideration + +--- + +## Migration Strategy + +### Current State (Before Refactoring) +- Old routes: `/bookings/create/participants` and `/bookings/{id}/edit` +- Old controllers: `CreateStep2Controller`, `EditController` +- Old templates: `create_step_2.html.twig`, `edit.html.twig` + +### Development State (During Refactoring) +- **Old routes remain functional and unchanged** +- New routes: `/bookings/create/participants-v2` and `/bookings/{id}/edit-v2` +- New controllers: `CreateStep2RefactoredController`, `EditRefactoredController` +- New templates: `create_step_2_v2_cards.html.twig`, `edit_v2_cards.html.twig` +- Both flows coexist - **critical for testing and comparison** + +### Transition State (Switching to New Flow) + +**Phase 1: Testing & Validation (During Development)** +- [ ] Deploy both flows to production (both accessible via their routes) +- [ ] Test NEW flow manually in production environment +- [ ] OLD routes remain the default in all navigation +- [ ] NEW routes accessible directly for testing (e.g., via direct URL) +- [ ] Compare bookings created through both flows +- [ ] Verify session DTO structure is identical +- [ ] Confirm pricing calculations match exactly + +**Phase 2: Navigation Switch (Single Deployment)** +- [ ] Update all navigation links to point to NEW routes +- [ ] Update internal redirects to use NEW routes +- [ ] OLD routes remain accessible (direct URL access works) +- [ ] Deploy navigation changes +- [ ] Monitor error rates and booking success rates closely +- [ ] **If issues found**: Deploy rollback (revert navigation to OLD routes) + +**Phase 3: Stabilization (2-4 weeks)** +- [ ] Monitor NEW flow in production with all traffic +- [ ] OLD routes still accessible but deprecated +- [ ] No new features added to OLD flow +- [ ] Document any issues found and resolved + +**Phase 4: Cleanup (After Confidence Established)** +- [ ] Remove OLD controllers (`CreateStep2Controller`, `EditController`) +- [ ] Remove OLD templates (`create_step_2.html.twig`, `edit.html.twig`) +- [ ] Remove OLD routes from routing configuration +- [ ] (Optional) Remove `-v2` suffix from route names and paths + +### Critical Transition Checklist + +**Before Switching Navigation**: +- [ ] All functional tests pass for NEW flow +- [ ] Performance tests show acceptable load times +- [ ] Comparison tests confirm identical DTO structure +- [ ] Field handlers produce identical results +- [ ] Pricing calculations match exactly +- [ ] Session compatibility verified +- [ ] HTMX refresh logic tested extensively +- [ ] Bulk insurance functionality verified +- [ ] All age-dependent service filtering works +- [ ] Transportation service conditional logic works +- [ ] Insurance auto-reassignment tested +- [ ] Edit flow API integration verified +- [ ] Large booking tests (50+ participants) pass + +**Session Compatibility**: +- ✅ Both flows use identical `BookingDto` structure +- ✅ Session keys are mode-specific (`MODE_CREATE`, `MODE_EDIT`) +- ✅ Users can switch between OLD and NEW flows without losing data +- ✅ No session migration needed + +**Rollback Plan**: +If critical issues discovered: +1. **Immediate**: Deploy navigation changes to revert links to OLD routes +2. **Investigation**: Analyze logs, compare booking data, identify root cause +3. **Fix**: Apply fixes to NEW flow controllers/templates +4. **Retest**: Verify fixes in development environment +5. **Redeploy**: Update navigation to NEW routes again + +### Monitoring During Transition + +**Key Metrics to Track**: +- [ ] Booking creation success rate (OLD vs NEW) +- [ ] Average page load time for card grid +- [ ] Average form load time (lazy loading) +- [ ] Field refresh response times +- [ ] Validation error rates +- [ ] Session expiry rates +- [ ] Browser console errors +- [ ] Server error logs +- [ ] Database query counts +- [ ] Memory usage per request + +**Logging Strategy**: +Add structured logging to NEW flow controllers: +```php +$this->logger->info('Refactored flow: Card view loaded', [ + 'participant_count' => count($bookingDto->participants), + 'load_time_ms' => $loadTime, + 'user_id' => $user->getId(), +]); + +$this->logger->info('Refactored flow: Participant form saved', [ + 'participant_index' => $index, + 'had_validation_errors' => !$form->isValid(), + 'field_handler_execution_time_ms' => $handlerTime, +]); +``` + +**Comparison Analysis**: +Since bookings are created via BPN API (not database), compare OLD vs NEW flows by: +- Analyzing application logs for booking creation success/failure rates +- Comparing API response times between OLD and NEW flows +- Monitoring Symfony profiler data for both flows +- Tracking error rates in application logs + +### Post-Transition Cleanup + +After successful transition (2+ months with no issues): + +**Step 1: Remove OLD Controllers and Templates** +- [ ] Delete `CreateStep2Controller.php` +- [ ] Delete `EditController.php` +- [ ] Delete `create_step_2.html.twig` +- [ ] Delete `edit.html.twig` +- [ ] Remove OLD routes from routing configuration + +**Step 2: (Optional) Rename Routes** +- [ ] Rename `app_booking_create_step_2_v2_cards` → `app_booking_create_step_2` +- [ ] Rename `app_booking_create_step_2_v2_participant` → `app_booking_create_step_2_participant` +- [ ] Rename `app_booking_edit_v2_cards` → `app_booking_edit` +- [ ] Update all navigation links and redirects +- [ ] Test thoroughly after rename + +**Step 3: Verify Navigation** +- [ ] Ensure all links point to correct routes (after cleanup) +- [ ] Check for any hardcoded OLD route references +- [ ] Test navigation flow end-to-end + +**Step 4: Documentation Updates** +- [ ] Update developer documentation +- [ ] Remove references to "refactored" or "v2" terminology +- [ ] Update onboarding guides for new developers + +### Common Pitfalls to Avoid + +**❌ DON'T**: +- Switch navigation to NEW flow without thorough testing in production first +- Remove OLD controllers/routes immediately after navigation switch +- Ignore session compatibility between flows +- Switch without monitoring tools in place +- Forget to update ALL navigation links (easy to miss some) + +**✅ DO**: +- Test NEW flow extensively via direct URL access before navigation switch +- Keep OLD flow accessible for weeks/months as safety net +- Monitor metrics closely after navigation switch +- Have rollback plan ready (revert navigation links) +- Document all navigation link locations before switch + +--- + +--- + +## Success Criteria + +### Performance +- [ ] Card grid loads in <1 second with 50 participants +- [ ] Form loads instantly (lazy loading) +- [ ] Field refresh responds in <500ms +- [ ] Memory usage reasonable with large bookings + +### Functionality +- [ ] All field handlers work identically to old flow +- [ ] Validation rules applied correctly +- [ ] Session DTO structure matches old flow +- [ ] Final booking creation/update produces identical results + +### Code Quality +- [ ] 80%+ code reuse between create and edit flows +- [ ] Full test coverage for new services +- [ ] Clean separation of concerns +- [ ] Documented and maintainable + +--- + +## Notes & Considerations + +### Why No OOB Swaps for Cards? +When editing a participant form, the cards view is not rendered - the entire #main-content is replaced with the form. When returning to cards (via "Speichern" or "Abbrechen"), the entire cards view is re-rendered fresh from the session DTO. Therefore, no need for OOB swaps of individual card prices. + +### Why Lazy Loading? +With 50+ participants, rendering all forms at once (current accordion approach) causes: +- Long initial page load time +- High memory usage +- Heavy DOM with hundreds of form fields +Lazy loading loads only the form for the participant being edited, dramatically improving performance. + +### Why Separate Controllers? +Keeps old implementation intact for: +- Comparison testing during development +- Fallback if issues discovered +- Easier code review (clear diff between old and new) +- Lower risk deployment + +### Field Handler Registry Change +Adding `processFieldsForParticipant()` allows processing a single participant's data instead of looping through all participants. This is necessary because the new flow processes participants individually, not all at once. + +--- + +## Open Questions + +- [ ] Should we add a progress indicator showing X/Y participants completed? +- [ ] Should we add keyboard shortcuts (e.g., Ctrl+S to save, Esc to cancel)? +- [ ] Should we add a "Save & Next" button to quickly move through participants? +- [ ] Should we store completion state per participant to show visual progress? + +--- + +## References + +- Current Implementation: `src/Controller/Booking/CreateStep2Controller.php` +- Current Template: `templates/booking/create_step_2.html.twig` +- Field Handlers: `src/Form/Service/` +- HTMX Trait: `src/Htmx/HxTrait.php`