1318 lines
45 KiB
Markdown
1318 lines
45 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
|
|
|
|
## 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<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 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' %}
|
|
<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);
|
|
|
|
$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`
|