Files
myep/docs/AGE_BASED_FIELDS_PLAN.md
T
2026-03-16 11:59:10 +01:00

21 KiB

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
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:

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:

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

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

// Template helper for placeholder cards
public function renderFieldPlaceholder(string $fieldName, array $hint): string
{
    return sprintf('
        <div class="field-placeholder" data-field="%s">
            <div class="placeholder-content">
                <i class="icon-%s"></i>
                <h4>%s verfügbar nach Altersangabe</h4>
                <p>%s</p>
                <button type="button" class="btn btn-sm btn-outline-primary" data-action="focus-date">
                    Geburtsdatum angeben
                </button>
            </div>
        </div>
    ', $fieldName, $hint['icon'], ucfirst($fieldName), $hint['message']);
}

5. Smart Form Guidance

A. Enhanced Date of Birth Field

// 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

// 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

<!-- Add to form template -->
<div data-controller="field-transitions">
    <div class="field-group" data-field-transitions-target="container">
        <!-- Dynamic fields will be inserted here -->
    </div>
</div>
// 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:

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

{# templates/booking/create/step2_participant.html.twig #}

<div class="participant-form" data-controller="age-dependent-fields participant-hints">
    <!-- Personal Data Section -->
    <div class="form-section">
        <h3>Persönliche Daten</h3>
        
        <!-- Date of Birth with enhanced styling -->
        <div class="form-group date-of-birth-group" 
             data-age-dependent-fields-target="dateField">
            {{ form_row(form.dateOfBirth) }}
            <div class="field-counter" data-age-dependent-fields-target="counter"></div>
        </div>
        
        <!-- Other personal fields -->
        {{ form_row(form.firstName) }}
        {{ form_row(form.lastName) }}
        <!-- ... -->
    </div>
    
    <!-- Dynamic Sections with Placeholders -->
    <div class="form-section dynamic-fields" 
         data-age-dependent-fields-target="dynamicContainer">
        
        <!-- Hints Display Area -->
        <div class="field-hints" data-participant-hints-target="container">
            <!-- Hints will be inserted here -->
        </div>
        
        <!-- Placeholders for age-dependent fields -->
        <div class="field-placeholder" data-field="courses" 
             data-age-dependent-fields-target="placeholder">
            <div class="placeholder-content">
                <i class="icon-graduation-cap"></i>
                <h4>Kurse</h4>
                <p>Verfügbar nach Angabe des Geburtsdatums</p>
                <button type="button" class="btn btn-sm btn-outline-primary" 
                        data-action="click->age-dependent-fields#focusDateField">
                    Geburtsdatum angeben
                </button>
            </div>
        </div>
        
        <!-- Dynamic fields will replace placeholders -->
        <div class="dynamic-field-container" data-age-dependent-fields-target="fieldsContainer">
            <!-- Form fields added dynamically -->
        </div>
    </div>
</div>

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

  1. Created DateOfBirthProvidedCondition - Checks if participant has provided birth date
  2. Created ServiceAgeEvaluator - Evaluates service availability for participant age
  3. Enhanced CreateFieldStateProvider - Added field visibility rules to hide age-dependent fields
  4. Enhanced ParticipantFieldOptionsProvider - Integrated age-aware service filtering
  5. 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

  1. Age filtering system - Age-based service filtering working correctly
  2. Service configuration - ServiceAgeEvaluator instantiated directly (no DI needed)
  3. Skipass field implementation - Added single-selection skipass field with age/date validation
  4. Service selection bug fixes - Fixed form submission issues with service ID/object handling
  5. Room-based conditional field system - Added remarksRoom field for specific room types
  6. Field visibility improvements - Fields no longer just hidden but excluded from form entirely
  7. Dynamic field type system - Simplified field configuration with type mapping
  8. Room code normalization - Case-insensitive room code matching (mbz/MBZ/Mbz)

📋 REMAINING IMPLEMENTATION STEPS

  1. Field placeholder system - Add explanatory content for missing age
  2. Enhanced date of birth field - Smart guidance styling
  3. HTMX transitions - Smooth field additions
  4. Stimulus controllers - (age-dependent-fields, field-transitions, participant-hints)
  5. Template enhancements - UX improvements and placeholder system
  6. Service model validation - Ensure all constraint types properly handled
  7. 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

// 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

// 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.