# Age-Based Field Visibility with Enhanced UX Implementation Plan
## Overview
Extend the existing conditional field system to hide fields when date of birth is not provided and filter options based on age groups, with comprehensive UX improvements to guide users through the progressive disclosure process.
## Architecture Analysis
**Existing Infrastructure to Leverage:**
- `AgeRangeCondition` already handles age calculations and returns `false` when `dateOfBirth` is null
- Field state providers can hide fields using `'hidden'` state conditions
- `CompositeCondition` allows complex condition combinations
- Field options providers support dynamic choice filtering
- Form events already trigger rebuilds when form data changes
## Implementation Strategy: Hybrid Approach with Enhanced UX
### 1. Create New Condition Classes
**A. DateOfBirthProvidedCondition**
- Simple condition that checks if participant has `dateOfBirth` set
- Returns `true` only when date of birth exists
- Used to show/hide entire fields
```php
class DateOfBirthProvidedCondition implements FieldConditionInterface
{
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
{
$participant = $bookingDto->getParticipant($participantIndex);
return null !== $participant && null !== $participant->dateOfBirth;
}
public function getDependentFields(): array
{
return ['dateOfBirth'];
}
}
```
**B. Extend AgeRangeCondition Usage**
- Leverage existing age calculation logic
- Create specific age range instances for different field requirements
### 2. Extend CreateFieldStateProvider
**Add Field Visibility Rules:**
```php
protected function registerFieldStateConditions(): void
{
// Existing rental condition logic...
// Hide age-restricted fields when no date of birth provided
$this->fieldStateConditions['courses'] = [
'hidden' => CompositeCondition::not(new DateOfBirthProvidedCondition()),
];
$this->fieldStateConditions['additionalServices'] = [
'hidden' => CompositeCondition::not(new DateOfBirthProvidedCondition()),
];
// Hide fields for specific age groups (example: alcohol services)
$this->fieldStateConditions['alcoholicBeverages'] = [
'hidden' => CompositeCondition::or(
CompositeCondition::not(new DateOfBirthProvidedCondition()),
CompositeCondition::not(new AgeRangeCondition(18, null))
),
];
// Youth-specific fields (example: under 16 only)
$this->fieldStateConditions['youthActivities'] = [
'hidden' => CompositeCondition::or(
CompositeCondition::not(new DateOfBirthProvidedCondition()),
CompositeCondition::not(new AgeRangeCondition(null, 15))
),
];
}
```
### 3. Enhance ParticipantFieldOptionsProvider
**Add Age-Aware Filtering:**
```php
protected function registerFieldOptionProviders(): void
{
// Existing providers...
// Enhanced courses provider with age filtering
$this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
'label' => 'Kurse',
'multiple' => true,
'expanded' => true,
'required' => false,
'choices' => $this->filterChoicesByAge(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES),
$bookingDto,
$participantIndex
),
'choice_label' => 'label',
];
}
private function filterChoicesByAge(array $services, BookingDtoInterface $bookingDto, int $participantIndex): array
{
$participant = $bookingDto->getParticipant($participantIndex);
if (null === $participant || null === $participant->dateOfBirth) {
return []; // No services available without age
}
$age = $this->calculateAge($participant->dateOfBirth);
return array_filter($services, function ($service) use ($age) {
return $this->isServiceAvailableForAge($service, $age);
});
}
private function isServiceAvailableForAge($service, int $age): bool
{
// Check service age restrictions (could be metadata on Service model)
if (isset($service->minAge) && $age < $service->minAge) {
return false;
}
if (isset($service->maxAge) && $age > $service->maxAge) {
return false;
}
return true;
}
```
### 4. UX Enhancement System
**A. Age Requirement Hints Provider**
```php
class ParticipantFieldHintsProvider
{
public function getFieldHints(BookingDtoInterface $bookingDto, int $participantIndex): array
{
$hints = [];
$participant = $bookingDto->getParticipant($participantIndex);
if (null === $participant || null === $participant->dateOfBirth) {
$hints[] = [
'type' => 'info',
'message' => 'Geben Sie Ihr Geburtsdatum an, um alle verfügbaren Optionen zu sehen.',
'icon' => 'birthday-cake',
'fields_affected' => $this->getAgeRestrictedFields(),
'action' => 'focus_date_field'
];
} else {
$age = $this->calculateAge($participant->dateOfBirth);
$restrictedFields = $this->getRestrictedFieldsForAge($age);
if (!empty($restrictedFields)) {
$hints[] = [
'type' => 'warning',
'message' => sprintf('Einige Optionen sind für Ihr Alter (%d Jahre) nicht verfügbar.', $age),
'icon' => 'info-circle',
'fields_affected' => $restrictedFields
];
}
}
return $hints;
}
}
```
**B. Field Placeholder System**
```php
// Template helper for placeholder cards
public function renderFieldPlaceholder(string $fieldName, array $hint): string
{
return sprintf('
%s verfügbar nach Altersangabe
%s
', $fieldName, $hint['icon'], ucfirst($fieldName), $hint['message']);
}
```
### 5. Smart Form Guidance
**A. Enhanced Date of Birth Field**
```php
// Add to BookingCreateParticipantType
->add('dateOfBirth', BirthdayType::class, [
'label' => 'Geburtsdatum',
'html5' => true,
'widget' => 'single_text',
'input' => 'datetime_immutable',
'attr' => [
'class' => 'age-dependent-trigger',
'data-controller' => 'age-dependent-fields',
'data-age-dependent-fields-participant-index-value' => $participantIndex,
'placeholder' => 'TT.MM.JJJJ - Für altersgerechte Optionen erforderlich'
],
'help' => 'Benötigt für die Anzeige aller verfügbaren Kurse und Services'
])
```
**B. Field Counter Display**
```javascript
// Stimulus controller: age-dependent-fields
connect() {
this.updateFieldCounter();
this.showPlaceholders();
}
updateFieldCounter() {
const hiddenFieldCount = this.getHiddenFieldCount();
if (hiddenFieldCount > 0) {
this.showFieldCounter(hiddenFieldCount);
}
}
showFieldCounter(count) {
const counter = this.element.querySelector('.field-counter');
if (counter) {
counter.textContent = `👤 ${count} weitere Optionen verfügbar nach Altersangabe`;
counter.classList.add('visible');
}
}
```
### 6. HTMX Integration Enhancements
**A. Smooth Transitions**
```html
```
```javascript
// Stimulus controller: field-transitions
showField(fieldElement) {
fieldElement.style.opacity = '0';
fieldElement.style.transform = 'translateY(-10px)';
// Animate in
fieldElement.addEventListener('transitionend', () => {
fieldElement.classList.add('field-visible');
}, { once: true });
requestAnimationFrame(() => {
fieldElement.style.opacity = '1';
fieldElement.style.transform = 'translateY(0)';
});
}
```
### 7. Update hasValidFieldOptions Method
**Enhanced Validation Logic:**
```php
private function hasValidFieldOptions(array $fieldOptions): bool
{
// Existing validation...
// Check if choices array exists and is not empty
if (false === empty($fieldOptions['choices'])) {
return true;
}
// Check if choice_loader exists and is not null
if (isset($fieldOptions['choice_loader'])) {
return true;
}
// NEW: Check if field has age restrictions that prevent display
if (isset($fieldOptions['age_restricted']) && true === $fieldOptions['age_restricted']) {
return false; // Field should not be added if age-restricted
}
// Check for other valid choice sources
if (isset($fieldOptions['choice_list'])) {
return true;
}
return false;
}
```
### 8. Template Enhancements
**A. Add UX Components to Form Template**
```twig
{# templates/booking/create/step2_participant.html.twig #}
```
## Implementation Status
### ✅ COMPLETED PHASE 1: XML Parsing & Age Constraints
1. **✅ Extended Service Model** - Added age constraint properties (`ageFrom`, `ageTo`, `birthYearFrom`, `birthYearTo`, `ageConstraintType`)
2. **✅ Created AgeConstraintResult DTO** - Structured data for parsed age constraints
3. **✅ Created AgeConstraintParserInterface** - Extensible parser contract for future constraint types
4. **✅ Implemented BirthYearConstraintParser** - Parses `JG:YYYY-YYYY` format constraints from XML
5. **✅ Created AgeConstraintParserRegistry** - Manages multiple parsers, handles semicolon-separated constraints
6. **✅ Enhanced TravelParser** - Integrated age constraint parsing for both absolute age and extensible formats
7. **✅ Successfully tested XML parsing** - Confirmed correct extraction of age constraints from real BPN data
### ✅ COMPLETED PHASE 2: Form Integration & Age-Based Filtering
8. **✅ Created DateOfBirthProvidedCondition** - Checks if participant has provided birth date
9. **✅ Created ServiceAgeEvaluator** - Evaluates service availability for participant age
10. **✅ Enhanced CreateFieldStateProvider** - Added field visibility rules to hide age-dependent fields
11. **✅ Enhanced ParticipantFieldOptionsProvider** - Integrated age-aware service filtering
12. **✅ Enhanced hasValidFieldOptions method** - Handles empty choices from age filtering
### ✅ RESOLVED: Age Filtering Implementation Complete
**Resolution:** Age-based service filtering is now working correctly - services with age constraints are properly filtered based on participant birth date.
### ✅ COMPLETED PHASE 3: Form Field Management & Room-Based Fields
13. **✅ Age filtering system** - Age-based service filtering working correctly
14. **✅ Service configuration** - ServiceAgeEvaluator instantiated directly (no DI needed)
15. **✅ Skipass field implementation** - Added single-selection skipass field with age/date validation
16. **✅ Service selection bug fixes** - Fixed form submission issues with service ID/object handling
17. **✅ Room-based conditional field system** - Added remarksRoom field for specific room types
18. **✅ Field visibility improvements** - Fields no longer just hidden but excluded from form entirely
19. **✅ Dynamic field type system** - Simplified field configuration with type mapping
20. **✅ Room code normalization** - Case-insensitive room code matching (mbz/MBZ/Mbz)
### 📋 REMAINING IMPLEMENTATION STEPS
21. **⏳ Field placeholder system** - Add explanatory content for missing age
22. **⏳ Enhanced date of birth field** - Smart guidance styling
23. **⏳ HTMX transitions** - Smooth field additions
24. **⏳ Stimulus controllers** - (`age-dependent-fields`, `field-transitions`, `participant-hints`)
25. **⏳ Template enhancements** - UX improvements and placeholder system
26. **⏳ Service model validation** - Ensure all constraint types properly handled
27. **⏳ Comprehensive testing** - UX flows and edge cases
### ✅ NEW FEATURES ADDED
#### Skipass Field System
- **Field Type**: Single-selection radio buttons (mandatory when available)
- **Age Constraints**: Automatic filtering via existing ServiceAgeEvaluator
- **Date Validation**: Travel date range validation for service availability
- **Form Integration**: Proper ID/object handling with choice_value configuration
- **Files Modified**:
- `ParticipantFieldOptionsProvider.php` - Added skipass field provider
- `ParticipantSkiPassFieldHandler.php` - Created field handler
- `ParticipantDto.php` - Changed from array to single Service object
- `BookingEditDto.php` - Conversion logic for existing bookings
- `BookingCreateParticipantType.php` - Added to dynamic fields
- `services.yaml` - Registered handler
#### Room-Based Conditional Fields
- **remarksRoom Field**: Textarea field for room-specific comments
- **Conditional Logic**: Only appears when room with code 'mbz' is selected
- **Case-Insensitive**: Room code matching normalized to lowercase
- **Form Integration**: Uses TextareaType with XSS protection
- **Files Created**:
- `RoomSelectionCondition.php` - Flexible room-based condition
- `ParticipantRemarksRoomFieldHandler.php` - Field processing
- **Files Modified**:
- `CreateFieldStateProvider.php` - Added room selection condition
- `ParticipantDto.php` - Added remarksRoom property
#### Improved Form Field Management
- **Problem**: Fields were added to form but hidden with CSS
- **Solution**: Fields now excluded from form entirely when conditions not met
- **Benefits**: Cleaner HTML, better performance, no hidden field clutter
- **Logic**: Check field state conditions BEFORE adding fields to form
- **Choice Field Optimization**: Only validate choices for ChoiceType fields
- **Field Type System**: Simple field name → form type mapping
#### Service Selection Bug Fixes & HTMX Improvements
- **HTMX Trigger Issue**: HTMX requests were not triggered for service fields
- **Root Cause**: HTMX attributes placed on container elements instead of individual form inputs
- **Solution**: Moved HTMX triggers to individual checkbox/radio inputs for expanded choice fields
- **Affected Fields**: board, skipass, courses, additionalServices, rentals
- **Result**: Real-time dynamic updates now work reliably for all service fields
- **Field Handler Data Issue**: Field handlers were storing service IDs instead of Service objects
- **Root Cause**: Form submission processing returned IDs rather than full Service entities
- **Solution**: Updated all field handlers to retrieve and store complete Service objects
- **Impact**: Pricing calculator now has access to service price data
- **Affected Handlers**: ParticipantBoardFieldHandler, ParticipantSkipassFieldHandler, etc.
- **Form Submission Reset Issue**: Service selections were lost on form submission
- **Root Cause**: Inconsistent choice_value configuration between service types
- **Solution**: Added 'choice_value' => 'id' to all service field providers
- **Affected Fields**: courses, additionalServices, board, rentals, skipass
- **Result**: Consistent form submission behavior across all service types
**Legend:** ✅ Completed | 🔄 In Progress | 🔍 Investigating | ⏳ Pending
## Key Benefits
### Technical Benefits
- **Leverages existing architecture** - no major refactoring needed
- **Consistent patterns** - follows current field state and options provider patterns
- **Dynamic updates** - automatically works with existing HTMX integration
- **Flexible configuration** - easy to add new age-based rules
- **Performance optimized** - fields not computed when not visible
### UX Benefits
- **Clear Expectations** - Users know what to expect after providing age
- **Progressive Disclosure** - Information revealed at the right time
- **Visual Continuity** - Placeholders maintain form layout consistency
- **Contextual Help** - Specific guidance for different age scenarios
- **Smooth Interactions** - Animated transitions for dynamic changes
- **Accessibility** - Screen reader friendly field announcements
- **Mobile Optimized** - Touch-friendly interactions and responsive design
## UX Components to Add
1. **Field Hint Cards** - Informational cards explaining missing fields
2. **Age Requirement Badges** - Visual indicators on age-dependent sections
3. **Progress Indicators** - Show completion status including hidden fields
4. **Interactive Placeholders** - Clickable areas that focus date field
5. **Transition Animations** - Smooth field appearance/disappearance
6. **Contextual Tooltips** - Explain specific age requirements
7. **Smart Form Validation** - Guide users to complete prerequisites first
## Testing Scenarios
### Functional Testing
1. **Initial Load** - Proper placeholders and guidance shown when no date of birth
2. **Progressive Disclosure** - Fields appear correctly as age is entered
3. **Age Restrictions** - Correct fields hidden/shown based on age ranges
4. **Dynamic Updates** - HTMX updates work seamlessly with field state changes
5. **Form Validation** - Proper validation with conditional field visibility
6. **Field Dependencies** - Complex conditions work correctly
### UX Testing
1. **Visual Feedback** - Clear indication of why fields are missing
2. **Interaction Flow** - Smooth user journey through form completion
3. **Accessibility** - Screen readers announce field changes properly
4. **Mobile Experience** - Touch interactions work on mobile devices
5. **Performance** - No lag during dynamic field updates
6. **Error Handling** - Graceful handling of invalid dates and edge cases
## Configuration Examples
### Service Age Restrictions
```php
// Example: Add age restrictions to Service model or metadata
$alcoholicBeverages = [
'minAge' => 18,
'description' => 'Nur für Erwachsene verfügbar'
];
$youthActivities = [
'maxAge' => 15,
'description' => 'Nur für Jugendliche unter 16'
];
$seniorServices = [
'minAge' => 65,
'description' => 'Spezielle Services für Senioren'
];
```
### Field Age Requirements
```php
// Configuration for which fields require age verification
private const AGE_RESTRICTED_FIELDS = [
'courses' => ['requireAge' => true, 'reason' => 'Altersgerechte Kursauswahl'],
'additionalServices' => ['requireAge' => true, 'reason' => 'Altersspezifische Services'],
'alcoholicBeverages' => ['minAge' => 18, 'reason' => 'Jugendschutz'],
'youthActivities' => ['maxAge' => 15, 'reason' => 'Nur für Jugendliche'],
'seniorServices' => ['minAge' => 65, 'reason' => 'Seniorenspezifisch'],
];
```
This comprehensive plan provides both robust technical implementation and excellent user experience for age-based field management, building seamlessly on the existing sophisticated form architecture.