1428 lines
50 KiB
Markdown
1428 lines
50 KiB
Markdown
# 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 (SIMPLIFIED ARCHITECTURE - 2025-01-14)
|
|
|
|
## Architecture Revision (2025-01-14)
|
|
|
|
**Major Simplification**: Removed wrapper forms and DTOs!
|
|
|
|
**What Changed:**
|
|
- ❌ Removed: `ParticipantFormDto` - not needed
|
|
- ❌ Removed: `ParticipantFormType` wrapper - not needed
|
|
- ✅ Simplified: Pass `BookingDto` via form options (`booking_context`)
|
|
- ✅ Simplified: Reuse `BookingParticipantType` directly
|
|
- ✅ Simplified: Field handlers called manually in controller
|
|
|
|
**Why:**
|
|
The initial plan had unnecessary abstraction layers. We realized we can simply pass the `BookingDto` as a form option instead of wrapping everything. This is cleaner, more explicit, and easier to understand.
|
|
|
|
## Implementation Discovery: Field Handler Mode Awareness (2025-01-14)
|
|
|
|
**Problem Encountered:**
|
|
When promoting participant forms to standalone (autonomous) forms in the card flow, field handlers needed to be called from within `BookingParticipantType`'s PRE_SUBMIT event. This created a critical issue in edit mode:
|
|
|
|
- The BPN API **does not return insurance data** for privacy/security reasons
|
|
- In edit mode, the insurance field handler would see missing insurance data in submitted forms
|
|
- The handler would incorrectly interpret this as "user wants to clear insurance"
|
|
- Result: Insurance data would be lost during edit operations
|
|
|
|
**Root Cause:**
|
|
Field handlers had no way to distinguish between:
|
|
1. **Create mode**: Missing data = user didn't select anything (clear it)
|
|
2. **Edit mode**: Missing data = API didn't provide it (preserve existing value)
|
|
|
|
**Solution Implemented:**
|
|
Added mode awareness to the field handler system:
|
|
|
|
```php
|
|
// Interface change
|
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool;
|
|
|
|
// Insurance handler implementation
|
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
|
{
|
|
// Skip processing in edit mode - API doesn't return insurance data
|
|
if ($mode === BookingDto::MODE_EDIT) {
|
|
return false;
|
|
}
|
|
|
|
// Process normally in create mode
|
|
return true;
|
|
}
|
|
```
|
|
|
|
**Architecture Changes:**
|
|
- ✅ Added `$mode` parameter to `ParticipantFieldHandlerInterface::shouldProcess()`
|
|
- ✅ Updated `AbstractParticipantFieldHandler` with mode parameter
|
|
- ✅ Added helper methods: `isEditMode()`, `isCreateMode()` for future use
|
|
- ✅ Updated `ParticipantFieldHandlerRegistry` to pass `$bookingDto->mode` to handlers
|
|
- ✅ Updated all 13+ field handlers to accept mode parameter
|
|
- ✅ Insurance handler skips processing entirely in edit mode
|
|
- ✅ All other handlers continue processing normally in both modes
|
|
|
|
**Why Only Insurance Needs Special Treatment:**
|
|
- **Insurance**: API doesn't return data → must skip processing in edit mode
|
|
- **Services** (ski pass, rentals, courses): API returns data → process normally
|
|
- **Transportation**: API returns data → process normally
|
|
- **Room assignments**: API returns data → process normally
|
|
- **All other fields**: API returns data → process normally
|
|
|
|
**Benefits:**
|
|
- ✅ Insurance data preserved correctly in edit mode
|
|
- ✅ Explicit mode-aware behavior (no implicit assumptions)
|
|
- ✅ Clean separation: only handlers that need it check mode
|
|
- ✅ Future-proof: easy to add mode-specific logic to other handlers if needed
|
|
- ✅ Tests updated to verify edit mode behavior
|
|
|
|
**Implementation Status:** ✅ **COMPLETED** (2025-01-14)
|
|
|
|
## 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 |
|
|
| **BookingDto Context** | Pass via form options | Simple, explicit, no wrappers needed |
|
|
|
|
## Key Simplification
|
|
|
|
**No wrapper forms or DTOs needed!** We pass `BookingDto` directly via form options:
|
|
|
|
```php
|
|
// Controller creates form with explicit context
|
|
$form = $this->createForm(BookingParticipantType::class, $participant, [
|
|
'booking_context' => $bookingDto, // Passed explicitly
|
|
'edit_mode' => false,
|
|
]);
|
|
```
|
|
|
|
**Benefits:**
|
|
- ✅ No `ParticipantFormDto` wrapper
|
|
- ✅ No `ParticipantFormType` wrapper
|
|
- ✅ Reuse existing `BookingParticipantType` directly
|
|
- ✅ Explicit data flow (no "magic" form tree traversal)
|
|
- ✅ Field handlers called manually in controller (like current Step 2)
|
|
- ✅ Clean, maintainable, easy to understand
|
|
|
|
---
|
|
|
|
## 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<int, array{name: string, roomName: string, price: string}>
|
|
*/
|
|
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<string, mixed> $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 Update `BookingParticipantType` to Accept `booking_context` Option
|
|
|
|
**File**: `src/Form/BookingParticipantType.php`
|
|
|
|
**Purpose**: Make form type autonomous - it processes its own field handlers when used standalone
|
|
|
|
**Why This Simplification**:
|
|
In the card flow, there's no parent form to orchestrate field handler execution. The form type becomes **self-contained and autonomous** - it receives `BookingDto` via options and processes its own field handlers in PRE_SUBMIT.
|
|
|
|
**Architecture Benefits**:
|
|
- ✅ No wrapper forms needed
|
|
- ✅ No extra DTOs needed
|
|
- ✅ No parent form needed
|
|
- ✅ Explicit data flow via options
|
|
- ✅ Field handlers called automatically in PRE_SUBMIT
|
|
- ✅ Controller stays thin - just creates form and handles result
|
|
- ✅ Form type is responsible for its own data processing
|
|
|
|
**Key Insight**: We're **promoting the child form to be in charge**. It's no longer a dumb child controlled by a parent - it's a smart, autonomous form that handles everything itself.
|
|
|
|
**Modification to `__construct()` - Add Field Handler Registry**:
|
|
|
|
```php
|
|
public function __construct(
|
|
private readonly FieldOptionsProviderInterface $fieldOptionsProvider,
|
|
private readonly CreateFieldStateProvider $createFieldStateProvider,
|
|
private readonly EditFieldStateProvider $editFieldStateProvider,
|
|
private readonly ParticipantFieldHandlerRegistry $fieldHandlerRegistry, // NEW: for autonomous processing
|
|
) {
|
|
}
|
|
```
|
|
|
|
**Modification to `configureOptions()`**:
|
|
|
|
```php
|
|
public function configureOptions(OptionsResolver $resolver): void
|
|
{
|
|
$resolver->setDefaults([
|
|
'data_class' => ParticipantDto::class,
|
|
'selected_rooms' => [],
|
|
'edit_mode' => false,
|
|
'booking_context' => null, // NEW: Optional BookingDto for card flows
|
|
]);
|
|
|
|
$resolver->setAllowedTypes('selected_rooms', 'array');
|
|
$resolver->setAllowedTypes('edit_mode', 'bool');
|
|
$resolver->setAllowedTypes('booking_context', ['null', BookingDto::class]);
|
|
}
|
|
```
|
|
|
|
**Modification to `buildForm()`**:
|
|
|
|
```php
|
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
|
{
|
|
// Select field state provider based on edit_mode option
|
|
$this->fieldStateProvider = $options['edit_mode']
|
|
? $this->editFieldStateProvider
|
|
: $this->createFieldStateProvider;
|
|
|
|
// Capture booking context for use in event listeners
|
|
$bookingContext = $options['booking_context'];
|
|
|
|
$builder
|
|
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($bookingContext) {
|
|
$this->onPreSetData($event, $bookingContext);
|
|
})
|
|
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($bookingContext) {
|
|
// Process field handlers FIRST (before form binding and validation)
|
|
// This ensures data is cleaned before Symfony processes it
|
|
if (null !== $bookingContext) {
|
|
$this->processFieldHandlers($event, $bookingContext);
|
|
}
|
|
|
|
// Then rebuild fields with updated states
|
|
$this->onPreSubmit($event, $bookingContext);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Process field handlers for this participant.
|
|
*
|
|
* Field handlers are executed in PRE_SUBMIT to clean and transform data
|
|
* before Symfony binds it to the form. This matches the pattern used in
|
|
* the old BookingCreateStep2Type parent form.
|
|
*/
|
|
private function processFieldHandlers(FormEvent $event, BookingDto $bookingContext): void
|
|
{
|
|
$form = $event->getForm();
|
|
$submittedData = $event->getData();
|
|
|
|
if (false === is_array($submittedData)) {
|
|
return;
|
|
}
|
|
|
|
/** @var ParticipantDto $participant */
|
|
$participant = $form->getData();
|
|
|
|
if (null === $participant || false === property_exists($participant, 'index')) {
|
|
return;
|
|
}
|
|
|
|
// Process all field handlers for this participant in dependency order
|
|
$this->fieldHandlerRegistry->processFieldsForParticipant(
|
|
$submittedData,
|
|
$bookingContext,
|
|
$participant->index
|
|
);
|
|
}
|
|
|
|
private function onPreSetData(FormEvent $event, ?BookingDto $bookingContext): void
|
|
{
|
|
/** @var ParticipantDto|null $participantData */
|
|
$participantData = $event->getData();
|
|
$form = $event->getForm();
|
|
|
|
if (null === $participantData) {
|
|
return;
|
|
}
|
|
|
|
// Card flow: BookingDto passed via options
|
|
// Accordion flow (if we had one): traverse form tree
|
|
$bookingDto = $bookingContext ?? $this->fieldStateProvider->getBookingDtoFromForm($form);
|
|
|
|
if (null === $bookingDto) {
|
|
return;
|
|
}
|
|
|
|
// Add base fields with states applied
|
|
$this->addBaseFields($form, $bookingDto, $participantData->index);
|
|
|
|
// Add dynamic fields
|
|
$this->addDynamicFields($form, $bookingDto, $participantData->index);
|
|
}
|
|
|
|
private function onPreSubmit(FormEvent $event, ?BookingDto $bookingContext): void
|
|
{
|
|
$submittedData = $event->getData();
|
|
$form = $event->getForm();
|
|
|
|
if (false === is_array($submittedData)) {
|
|
return;
|
|
}
|
|
|
|
// Card flow: BookingDto passed via options
|
|
// Accordion flow (if we had one): traverse form tree
|
|
$bookingDto = $bookingContext ?? $this->fieldStateProvider->getBookingDtoFromForm($form);
|
|
|
|
if (null === $bookingDto) {
|
|
return;
|
|
}
|
|
|
|
// Get participant index from form data
|
|
$participantData = $form->getData();
|
|
if (null === $participantData || false === property_exists($participantData, 'index')) {
|
|
return;
|
|
}
|
|
|
|
// Rebuild all fields with updated states based on submitted data
|
|
$this->rebuildFieldsWithStates($form, $bookingDto, $participantData->index, $submittedData);
|
|
}
|
|
```
|
|
|
|
**Usage in Controllers**:
|
|
|
|
```php
|
|
// Card flow: Pass booking context explicitly
|
|
$form = $this->createForm(BookingParticipantType::class, $participant, [
|
|
'booking_context' => $bookingDto,
|
|
'edit_mode' => false,
|
|
]);
|
|
```
|
|
|
|
**Todos**:
|
|
- [ ] Add `ParticipantFieldHandlerRegistry` to constructor (DI)
|
|
- [ ] Add `booking_context` option to `configureOptions()`
|
|
- [ ] Update `buildForm()` to capture `booking_context` and add PRE_SUBMIT listener
|
|
- [ ] Add `processFieldHandlers()` private method to call registry
|
|
- [ ] Update `onPreSetData()` to accept and use `$bookingContext` parameter
|
|
- [ ] Update `onPreSubmit()` to accept and use `$bookingContext` parameter
|
|
- [ ] Test with card flow (booking_context provided)
|
|
- [ ] Verify field handlers execute automatically
|
|
- [ ] Verify field state providers work correctly
|
|
|
|
---
|
|
|
|
### 1.4 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` with `booking_context` option:
|
|
```php
|
|
$form = $this->createForm(BookingParticipantType::class, $participant, [
|
|
'booking_context' => $bookingDto,
|
|
'edit_mode' => false,
|
|
'validation_groups' => ['booking_create_step_2'],
|
|
]);
|
|
```
|
|
4. `$form->handleRequest($request)`
|
|
- **Note**: Field handlers are called automatically by the form in PRE_SUBMIT
|
|
5. If submitted and valid:
|
|
- Save BookingDto to session
|
|
- HTMX redirect to cards view
|
|
6. Calculate summary data for sidebar
|
|
7. 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 `booking_context` option and `validation_groups: false`:
|
|
```php
|
|
$form = $this->createForm(BookingParticipantType::class, $participant, [
|
|
'booking_context' => $bookingDto,
|
|
'edit_mode' => false,
|
|
'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' %}
|
|
<h1>Neue Buchung</h1>
|
|
|
|
<div class="grid grid-cols-3 gap-8">
|
|
{# Main content area - cards grid #}
|
|
<div id="main-content" class="col-span-2">
|
|
<h2>Teilnehmer</h2>
|
|
|
|
<div id="participant-cards-grid" class="space-y-4">
|
|
{% for participant in bookingDto.participants %}
|
|
{% include 'booking/_participant_card.html.twig' with {
|
|
'cardData': cardsData[loop.index0],
|
|
'index': loop.index0,
|
|
'mode': 'create'
|
|
} %}
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="flex justify-between mt-8">
|
|
<a href="{{ path('app_booking_create_step_1') }}" class="button bg-button bg-button--secondary">
|
|
Zurück
|
|
</a>
|
|
<button type="submit" class="button bg-button bg-button--secondary">
|
|
Weiter
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{# Sidebar summary #}
|
|
{% block booking_summary %}
|
|
<div id="booking-summary">
|
|
{% include 'booking/_summary.html.twig' with {
|
|
'bookingCreateDto': bookingDto,
|
|
'participantCount': participantsCount,
|
|
'groupedSelectedRooms': groupedSelectedRooms,
|
|
'assignmentCounts': assignmentCounts
|
|
} %}
|
|
</div>
|
|
{% endblock %}
|
|
</div>
|
|
{% 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 #}
|
|
<div id="participant-card-{{ index }}" class="border rounded p-4 flex justify-between items-center">
|
|
<div>
|
|
<h3 class="font-semibold">{{ cardData.name }}</h3>
|
|
<p class="text-sm text-gray-600">{{ cardData.roomName }}</p>
|
|
</div>
|
|
<div class="flex items-center gap-4">
|
|
<span class="font-medium">{{ cardData.price }}</span>
|
|
<button type="button"
|
|
class="button bg-button bg-button--secondary"
|
|
hx-get="{{ path('app_booking_create_step_2_v2_participant', {index: index}) }}"
|
|
hx-target="#main-content"
|
|
hx-swap="innerHTML">
|
|
Bearbeiten
|
|
</button>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
**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) #}
|
|
<div id="participant-form-view">
|
|
<h2>{{ participantIndex == 0 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1) }}</h2>
|
|
|
|
{{ 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 %}
|
|
<div id="participant-form" class="space-y-4">
|
|
{# Personal data section #}
|
|
<div class="grid grid-cols-2 gap-4">
|
|
{{ 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) }}
|
|
</div>
|
|
|
|
{# Contact information #}
|
|
<div class="grid grid-cols-2 gap-4">
|
|
{{ form_row(form.email) }}
|
|
{{ form_row(form.mobile) }}
|
|
</div>
|
|
|
|
{# Address #}
|
|
{% if form.address is defined %}
|
|
<div class="grid grid-cols-2 gap-4">
|
|
{{ form_row(form.address.street) }}
|
|
{{ form_row(form.address.postCode) }}
|
|
{{ form_row(form.address.city) }}
|
|
{{ form_row(form.address.country) }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Body dimensions #}
|
|
{% if form.bodyDimensions is defined %}
|
|
<div class="grid grid-cols-2 gap-4">
|
|
{{ form_row(form.bodyDimensions.height) }}
|
|
{{ form_row(form.bodyDimensions.shoeSize) }}
|
|
{{ form_row(form.bodyDimensions.weight) }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Room assignment #}
|
|
<div class="grid grid-cols-2 gap-4">
|
|
{{ 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 %}
|
|
</div>
|
|
|
|
{# Service selection - same structure as current templates #}
|
|
{# Age eligibility checks, conditional field rendering, etc. #}
|
|
{# Transportation, insurance, etc. #}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
<div class="flex gap-4 mt-8">
|
|
<button type="button"
|
|
class="button bg-button bg-button--secondary"
|
|
hx-get="{{ path('app_booking_create_step_2_v2_cards') }}"
|
|
hx-target="#main-content"
|
|
hx-swap="innerHTML">
|
|
Abbrechen
|
|
</button>
|
|
<button type="submit" class="button bg-button bg-button--secondary">
|
|
Speicheren
|
|
</button>
|
|
</div>
|
|
|
|
{{ form_rest(form) }}
|
|
{{ form_end(form) }}
|
|
</div>
|
|
```
|
|
|
|
**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);
|
|
$participant = $bookingDto->participants[$index] ?? null;
|
|
|
|
if (null === $participant) {
|
|
throw new \InvalidArgumentException('Invalid participant index');
|
|
}
|
|
|
|
// Create form with booking context passed explicitly
|
|
$form = $this->createForm(BookingParticipantType::class, $participant, [
|
|
'booking_context' => $bookingDto,
|
|
'edit_mode' => false,
|
|
'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`
|