# 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`