import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'dart:math' as math; import 'dart:io'; import '../models/am_registration_data.dart' show kAmCapaciteAccueilMax; import '../models/card_assets.dart'; import '../config/display_config.dart'; import '../utils/nir_utils.dart'; import '../utils/name_format_utils.dart'; import 'custom_app_text_field.dart'; import 'nir_text_field.dart'; import 'form_field_wrapper.dart'; import 'app_custom_checkbox.dart'; import 'hover_relief_widget.dart'; import 'custom_navigation_button.dart'; import 'registration_photo_slot.dart'; /// Données pour le formulaire d'informations professionnelles class ProfessionalInfoData { final String? photoPath; final File? photoFile; final Uint8List? photoBytes; final String? photoFilename; final bool photoConsent; final DateTime? dateOfBirth; final String birthCity; final String birthCountry; final String nir; final String agrementNumber; /// Date d'obtention de l'agrément (obligatoire, API `date_agrement`). final DateTime? agreementDate; final int? capacity; final int? placesAvailable; ProfessionalInfoData({ this.photoPath, this.photoFile, this.photoBytes, this.photoFilename, this.photoConsent = false, this.dateOfBirth, this.birthCity = '', this.birthCountry = '', this.nir = '', this.agrementNumber = '', this.agreementDate, this.capacity, this.placesAvailable, }); } /// Widget générique pour le formulaire d'informations professionnelles /// Utilisé pour l'inscription des Assistantes Maternelles /// Supporte mode éditable et readonly, responsive mobile/desktop class ProfessionalInfoFormScreen extends StatefulWidget { final DisplayMode mode; final String stepText; final String title; final CardColorHorizontal cardColor; final ProfessionalInfoData? initialData; final String previousRoute; final Function(ProfessionalInfoData) onSubmit; final Future Function()? onPickPhoto; final bool embedContentOnly; final VoidCallback? onEdit; const ProfessionalInfoFormScreen({ super.key, this.mode = DisplayMode.editable, required this.stepText, required this.title, required this.cardColor, this.initialData, required this.previousRoute, required this.onSubmit, this.onPickPhoto, this.embedContentOnly = false, this.onEdit, }); @override State createState() => _ProfessionalInfoFormScreenState(); } class _ProfessionalInfoFormScreenState extends State { final _formKey = GlobalKey(); /// Ordre Tab explicite : sans cela, la zone photo (InkWell) peut s’insérer entre pays et NIR. static const double _kTabPhotoPick = 1; static const double _kTabDateBirth = 2; static const double _kTabBirthCity = 3; static const double _kTabBirthCountry = 4; static const double _kTabNir = 5; static const double _kTabAgrement = 6; static const double _kTabAgreementDate = 7; static const double _kTabCapacity = 8; static const double _kTabPlaces = 9; static const double _kTabChevronNext = 10; static const double _kTabChevronPrev = 11; final _dateOfBirthController = TextEditingController(); final _birthCityController = TextEditingController(); final _birthCountryController = TextEditingController(); final _nirController = TextEditingController(); final _agrementController = TextEditingController(); final _agreementDateController = TextEditingController(); final _capacityController = TextEditingController(); final _placesAvailableController = TextEditingController(); FocusNode? _birthCityFocus; FocusNode? _birthCountryFocus; FocusNode? _nirFocus; DateTime? _selectedDate; DateTime? _selectedAgreementDate; String? _photoPathFramework; File? _photoFile; Uint8List? _photoBytes; String? _photoFilename; bool _photoConsent = false; bool get _hasRealPhoto => (_photoBytes != null && _photoBytes!.isNotEmpty) || _photoFile != null || (_photoPathFramework != null && _photoPathFramework!.isNotEmpty && !_photoPathFramework!.startsWith('assets/')); @override void initState() { super.initState(); final data = widget.initialData; if (data != null) { _selectedDate = data.dateOfBirth; _dateOfBirthController.text = data.dateOfBirth != null ? DateFormat('dd/MM/yyyy').format(data.dateOfBirth!) : ''; _birthCityController.text = data.birthCity; _birthCountryController.text = data.birthCountry; final nirRaw = nirToRaw(data.nir); _nirController.text = nirRaw.length == 15 ? formatNir(nirRaw) : data.nir; _agrementController.text = data.agrementNumber; _selectedAgreementDate = data.agreementDate; _agreementDateController.text = data.agreementDate != null ? DateFormat('dd/MM/yyyy').format(data.agreementDate!) : ''; _capacityController.text = data.capacity?.toString() ?? ''; _placesAvailableController.text = data.placesAvailable?.toString() ?? ''; _photoPathFramework = data.photoPath; _photoFile = data.photoFile; _photoBytes = data.photoBytes; _photoFilename = data.photoFilename; _photoConsent = data.photoConsent; } if (widget.mode == DisplayMode.editable) { _birthCityFocus = FocusNode(); _birthCountryFocus = FocusNode(); _nirFocus = FocusNode(); _birthCityFocus!.addListener(_onBirthCityFocusChange); _birthCountryFocus!.addListener(_onBirthCountryFocusChange); } } void _onBirthCityFocusChange() { if (_birthCityFocus == null || _birthCityFocus!.hasFocus) return; _applyPlaceNameFormat(_birthCityController); } void _onBirthCountryFocusChange() { if (_birthCountryFocus == null || _birthCountryFocus!.hasFocus) return; _applyPlaceNameFormat(_birthCountryController); } void _applyPlaceNameFormat(TextEditingController controller) { final formatted = formatPersonNameCase(controller.text); if (formatted == controller.text) return; controller.value = TextEditingValue( text: formatted, selection: TextSelection.collapsed(offset: formatted.length), ); } @override void dispose() { _birthCityFocus?.removeListener(_onBirthCityFocusChange); _birthCountryFocus?.removeListener(_onBirthCountryFocusChange); _birthCityFocus?.dispose(); _birthCountryFocus?.dispose(); _nirFocus?.dispose(); _dateOfBirthController.dispose(); _birthCityController.dispose(); _birthCountryController.dispose(); _nirController.dispose(); _agrementController.dispose(); _agreementDateController.dispose(); _capacityController.dispose(); _placesAvailableController.dispose(); super.dispose(); } String? _validateBirthCity(String? v) { final t = (v ?? '').trim(); if (t.isEmpty) return 'Ville requise'; if (t.length < 2) return 'Au moins 2 caractères'; return null; } String? _validateBirthCountry(String? v) { final t = (v ?? '').trim(); if (t.isEmpty) return 'Pays requis'; if (t.length < 2) return 'Au moins 2 caractères'; return null; } String? _validatePlacesAvailable(String? v) { if (v == null || v.isEmpty) return 'Places disponibles requises'; final n = int.tryParse(v); if (n == null || n < 0) return 'Nombre entre 0 et la capacité'; if (n > kAmCapaciteAccueilMax) return 'Maximum $kAmCapaciteAccueilMax'; final cap = int.tryParse(_capacityController.text); if (cap != null && n > cap) return 'Ne peut pas dépasser la capacité'; return null; } Future _selectDate(BuildContext context) async { final DateTime? picked = await showDatePicker( context: context, initialDate: _selectedDate ?? DateTime.now().subtract(const Duration(days: 365 * 25)), firstDate: DateTime(1920, 1), lastDate: DateTime.now().subtract(const Duration(days: 365 * 18)), locale: const Locale('fr', 'FR'), ); if (picked != null && picked != _selectedDate) { setState(() { _selectedDate = picked; _dateOfBirthController.text = DateFormat('dd/MM/yyyy').format(picked); }); } } Future _selectAgreementDate(BuildContext context) async { final DateTime? picked = await showDatePicker( context: context, initialDate: _selectedAgreementDate ?? DateTime.now().subtract(const Duration(days: 365 * 5)), firstDate: DateTime(1970, 1), lastDate: DateTime.now(), locale: const Locale('fr', 'FR'), ); if (picked != null) { setState(() { _selectedAgreementDate = picked; _agreementDateController.text = DateFormat('dd/MM/yyyy').format(picked); }); } } Future _pickPhoto() async { if (widget.onPickPhoto != null) { await widget.onPickPhoto!(); return; } final picker = ImagePicker(); try { final XFile? picked = await picker.pickImage( source: ImageSource.gallery, imageQuality: 75, maxWidth: 1200, maxHeight: 1200, ); if (picked == null) return; final bytes = await picked.readAsBytes(); if (bytes.isEmpty) return; File? file; if (!kIsWeb) { try { final f = File(picked.path); if (await f.exists()) file = f; } catch (_) {} } final name = picked.name.trim(); setState(() { _photoBytes = bytes; _photoFile = file; _photoPathFramework = null; _photoFilename = name.isNotEmpty ? name : 'photo_am.jpg'; }); } catch (_) {} } void _clearRegistrationPhoto() { setState(() { _photoBytes = null; _photoFile = null; _photoPathFramework = null; _photoFilename = null; _photoConsent = false; }); } void _submitForm() { _applyPlaceNameFormat(_birthCityController); _applyPlaceNameFormat(_birthCountryController); if (_formKey.currentState!.validate()) { if (!_hasRealPhoto) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Veuillez ajouter une photo de profil.')), ); return; } if (!_photoConsent) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Veuillez accepter le consentement photo pour continuer.')), ); return; } final data = ProfessionalInfoData( photoPath: _photoBytes != null ? null : _photoPathFramework, photoFile: _photoFile, photoBytes: _photoBytes, photoFilename: _photoFilename, photoConsent: _photoConsent, dateOfBirth: _selectedDate, birthCity: _birthCityController.text.trim(), birthCountry: _birthCountryController.text.trim(), nir: normalizeNir(_nirController.text), agrementNumber: _agrementController.text, agreementDate: _selectedAgreementDate, capacity: int.tryParse(_capacityController.text), placesAvailable: int.tryParse(_placesAvailableController.text), ); widget.onSubmit(data); } } @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; final config = DisplayConfig.fromContext(context, mode: widget.mode); if (widget.embedContentOnly) { return _buildCard(context, config, screenSize); } return FocusTraversalGroup( policy: OrderedTraversalPolicy(), child: Scaffold( body: Stack( children: [ Positioned.fill( child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat), ), Center( child: SingleChildScrollView( padding: EdgeInsets.symmetric(vertical: config.isMobile ? 40.0 : 28.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( widget.stepText, style: GoogleFonts.merienda( fontSize: config.isMobile ? 13 : 16, color: Colors.black54, ), ), SizedBox(height: config.isMobile ? 6 : 10), Text( widget.title, style: GoogleFonts.merienda( fontSize: config.isMobile ? 18 : 24, fontWeight: FontWeight.bold, color: Colors.black87, ), textAlign: TextAlign.center, ), SizedBox(height: config.isMobile ? 16 : 20), _buildCard(context, config, screenSize), // Boutons mobile sous la carte if (config.isMobile) ...[ const SizedBox(height: 20), _buildMobileButtons(context, config, screenSize), const SizedBox(height: 10), ], ], ), ), ), // Chevrons desktop : visuellement sur les côtés ; ordre Tab 10–11 = après « Places disponibles ». if (!config.isMobile) ...[ Positioned( top: screenSize.height / 2 - 20, left: 40, child: FocusTraversalOrder( order: const NumericFocusOrder(_kTabChevronPrev), child: IconButton( icon: Transform( alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40), ), onPressed: () { if (context.canPop()) { context.pop(); } else { context.go(widget.previousRoute); } }, tooltip: 'Précédent', ), ), ), Positioned( top: screenSize.height / 2 - 20, right: 40, child: FocusTraversalOrder( order: const NumericFocusOrder(_kTabChevronNext), child: IconButton( icon: Image.asset('assets/images/chevron_right.png', height: 40), onPressed: _submitForm, tooltip: 'Suivant', ), ), ), ], ], ), ), ); } Widget _buildCard(BuildContext context, DisplayConfig config, Size screenSize) { // Si mode Readonly Desktop : Layout spécial "Vintage" horizontal if (config.isReadonly && !config.isMobile && widget.embedContentOnly) { return _buildReadonlyDesktopCard(context, config, screenSize); } // Si mode Readonly Mobile : Layout spécial "Vintage" vertical if (config.isReadonly && config.isMobile && widget.embedContentOnly) { return Padding( padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05), child: _buildMobileReadonlyCard(context, config, screenSize), ); } return Stack( clipBehavior: Clip.none, children: [ Container( width: config.isMobile ? screenSize.width * 0.9 : screenSize.width * 0.6, padding: EdgeInsets.symmetric( vertical: config.isMobile ? 20 : (config.isReadonly ? 24 : 28), horizontal: config.isMobile ? 24 : 50, ), decoration: BoxDecoration( image: DecorationImage( image: AssetImage( config.isMobile ? _getVerticalCardAsset() : widget.cardColor.path ), fit: BoxFit.fill, ), ), child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ if (widget.embedContentOnly) ...[ Text( widget.title, style: GoogleFonts.merienda( fontSize: config.isMobile ? 18 : 22, fontWeight: FontWeight.bold, color: Colors.black87, ), textAlign: TextAlign.center, ), SizedBox(height: config.isMobile ? 20.0 : 14.0), ], config.isMobile ? _buildMobileFields(context, config) : _buildDesktopFields(context, config), ], ), ), ), if (config.isReadonly && widget.onEdit != null) Positioned( top: 10, right: 10, child: IconButton( icon: const Icon(Icons.edit, color: Colors.black54), onPressed: widget.onEdit, tooltip: 'Modifier', ), ), ], ); } /// Carte en mode readonly MOBILE avec hauteur adaptative Widget _buildMobileReadonlyCard(BuildContext context, DisplayConfig config, Size screenSize) { return Container( width: double.infinity, // Pas de height fixe padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 24.0), decoration: BoxDecoration( image: DecorationImage( image: AssetImage(_getVerticalCardAsset()), fit: BoxFit.fill, // Fill pour s'adapter ), borderRadius: BorderRadius.circular(15), ), child: Stack( children: [ Column( mainAxisSize: MainAxisSize.min, children: [ // Titre + Edit Button Row( children: [ Expanded( child: Text( widget.title, style: GoogleFonts.merienda( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87, ), textAlign: TextAlign.center, ), ), if (widget.onEdit != null) const SizedBox(width: 28), ], ), // Contenu aligné en haut Padding( padding: const EdgeInsets.only(top: 20.0), child: _buildMobileFields(context, config), ), ], ), if (widget.onEdit != null) Positioned( top: 0, right: 0, child: IconButton( padding: EdgeInsets.zero, constraints: const BoxConstraints(), icon: const Icon(Icons.edit, color: Colors.black54, size: 24), onPressed: widget.onEdit, tooltip: 'Modifier', ), ), ], ), ); } /// Carte en mode readonly desktop avec AspectRatio 2:1 Widget _buildReadonlyDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) { final cardWidth = screenSize.width / 2.0; return SizedBox( width: cardWidth, child: AspectRatio( aspectRatio: 2.0, child: Container( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0), decoration: BoxDecoration( image: DecorationImage( image: AssetImage(widget.cardColor.path), fit: BoxFit.cover, ), borderRadius: BorderRadius.circular(15), ), child: Column( children: [ // Titre + Edit Button Row( children: [ Expanded( child: Text( widget.title, style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center, ), ), if (widget.onEdit != null) IconButton( icon: const Icon(Icons.edit, color: Colors.black54, size: 28), onPressed: widget.onEdit, tooltip: 'Modifier', ), ], ), const SizedBox(height: 10), // Contenu Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // PHOTO (1/3) Expanded( flex: 1, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ AspectRatio( aspectRatio: 1, child: LayoutBuilder( builder: (context, constraints) { final side = constraints.biggest.shortestSide; return RegistrationPhotoSlot( side: side, imageBytes: _photoBytes, imageFile: _photoFile, imagePathOrAsset: _photoPathFramework, baseShadowColor: Colors.green.shade300, ); }, ), ), const SizedBox(height: 5), AppCustomCheckbox( label: 'J\'accepte l\'utilisation\nde ma photo.', value: _photoConsent, onChanged: (_) {}, // Readonly checkboxSize: 22.0, fontSize: 14.0, ), ], ), ), const SizedBox(width: 24), // CHAMPS (2/3) - Layout optimisé compact Expanded( flex: 2, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Ligne 1 : Date de naissance + Ville Row( children: [ Expanded(flex: 2, child: _buildReadonlyField('Date de naissance', _dateOfBirthController.text)), const SizedBox(width: 16), Expanded(flex: 3, child: _buildReadonlyField('Ville de naissance', _birthCityController.text)), ], ), const SizedBox(height: 5), // Ligne 2 : Pays + NIR Row( children: [ Expanded(flex: 2, child: _buildReadonlyField('Pays de naissance', _birthCountryController.text)), const SizedBox(width: 16), Expanded(flex: 3, child: _buildReadonlyField('NIR', _formatNirForDisplay(_nirController.text))), ], ), const SizedBox(height: 5), // Ligne 3 : N° agrément + Date d'obtention Row( children: [ Expanded( flex: 3, child: _buildReadonlyField('N° d\'agrément', _agrementController.text), ), const SizedBox(width: 16), Expanded( flex: 2, child: _buildReadonlyField( 'Date d\'obtention de l\'agrément', _agreementDateController.text.isNotEmpty ? _agreementDateController.text : '—', ), ), ], ), const SizedBox(height: 5), Row( children: [ Expanded( child: _buildReadonlyField('Capacité d\'accueil', _capacityController.text), ), const SizedBox(width: 16), Expanded( child: _buildReadonlyField( 'Places disponibles', _placesAvailableController.text, ), ), ], ), ], ), ), ], ), ), ], ), ), ), ); } /// NIR formaté pour affichage (1 12 34 56 789 012 - 34 ou 2A pour la Corse). String _formatNirForDisplay(String value) { final raw = nirToRaw(value); return raw.length == 15 ? formatNir(raw) : value; } /// Helper pour champ Readonly style "Beige" Widget _buildReadonlyField(String label, String value) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: GoogleFonts.merienda(fontSize: 16.0, fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), const SizedBox(height: 3), Container( width: double.infinity, height: 45.0, // Hauteur réduite pour compacter padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0), decoration: BoxDecoration( image: const DecorationImage( image: AssetImage('assets/images/bg_beige.png'), fit: BoxFit.fill, ), borderRadius: BorderRadius.circular(8), ), child: Text( value.isNotEmpty ? value : '-', style: GoogleFonts.merienda(fontSize: 14.0), overflow: TextOverflow.ellipsis, ), ), ], ); } /// Layout DESKTOP : Photo à gauche, champs à droite Widget _buildDesktopFields(BuildContext context, DisplayConfig config) { final double verticalSpacing = config.isReadonly ? 8.0 : 14.0; return Column( mainAxisSize: MainAxisSize.min, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Photo + Checkbox à gauche SizedBox( width: 300, child: _buildPhotoSection(context, config), ), const SizedBox(width: 22), // Champs à droite Expanded( child: Column( children: [ _buildField( config: config, label: 'Date de naissance', controller: _dateOfBirthController, hint: 'JJ/MM/AAAA', readOnly: true, onTap: () => _selectDate(context), suffixIcon: Icons.calendar_today, validator: (v) => _selectedDate == null ? 'Date requise' : null, focusTraversalOrder: _kTabDateBirth, ), SizedBox(height: verticalSpacing), _buildField( config: config, label: 'Ville de naissance', controller: _birthCityController, hint: 'Ex. Ajaccio, Paris, Casablanca…', validator: _validateBirthCity, focusNode: _birthCityFocus, focusTraversalOrder: _kTabBirthCity, ), SizedBox(height: verticalSpacing), _buildField( config: config, label: 'Pays de naissance', controller: _birthCountryController, hint: 'Ex. France, Maroc…', validator: _validateBirthCountry, focusNode: _birthCountryFocus, focusTraversalOrder: _kTabBirthCountry, textInputAction: TextInputAction.next, onFieldSubmitted: (_) => _nirFocus?.requestFocus(), onEditingComplete: () => _nirFocus?.requestFocus(), ), ], ), ), ], ), SizedBox(height: verticalSpacing), NirTextField( controller: _nirController, focusNode: _nirFocus, focusTraversalOrder: _nirFocus != null ? _kTabNir : null, fieldWidth: double.infinity, fieldHeight: config.isMobile ? 45.0 : 53.0, labelFontSize: config.isMobile ? 15.0 : 20.0, inputFontSize: config.isMobile ? 14.0 : 18.0, labelFieldSpacing: config.isMobile ? 6.0 : 3.0, ), SizedBox(height: verticalSpacing), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _buildField( config: config, label: 'N° d\'agrément', controller: _agrementController, hint: 'Votre numéro d\'agrément', validator: (v) => v!.isEmpty ? 'Agrément requis' : null, focusTraversalOrder: _kTabAgrement, ), ), const SizedBox(width: 14), Expanded( child: _buildField( config: config, label: 'Date d\'obtention de l\'agrément', controller: _agreementDateController, hint: 'JJ/MM/AAAA', readOnly: true, onTap: () => _selectAgreementDate(context), suffixIcon: Icons.calendar_today, validator: (_) => _selectedAgreementDate == null ? 'Date d\'obtention requise' : null, focusTraversalOrder: _kTabAgreementDate, ), ), ], ), SizedBox(height: verticalSpacing), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _buildField( config: config, label: 'Capacité d\'accueil', controller: _capacityController, hint: 'De 1 à $kAmCapaciteAccueilMax enfants', keyboardType: TextInputType.number, validator: (v) { if (v == null || v.isEmpty) return 'Capacité requise'; final n = int.tryParse(v); if (n == null || n < 1) { return 'Entrez un nombre entre 1 et $kAmCapaciteAccueilMax'; } if (n > kAmCapaciteAccueilMax) { return 'Maximum $kAmCapaciteAccueilMax'; } return null; }, focusTraversalOrder: _kTabCapacity, ), ), const SizedBox(width: 14), Expanded( child: _buildField( config: config, label: 'Places disponibles', controller: _placesAvailableController, hint: 'Entre 0 et la capacité', keyboardType: TextInputType.number, validator: _validatePlacesAvailable, focusTraversalOrder: _kTabPlaces, ), ), ], ), ], ); } /// Layout MOBILE : Tout empilé verticalement Widget _buildMobileFields(BuildContext context, DisplayConfig config) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Photo + Checkbox en premier _buildPhotoSection(context, config), const SizedBox(height: 20), _buildField( config: config, label: 'Date de naissance', controller: _dateOfBirthController, hint: 'JJ/MM/AAAA', readOnly: true, onTap: () => _selectDate(context), suffixIcon: Icons.calendar_today, validator: (v) => _selectedDate == null ? 'Date requise' : null, focusTraversalOrder: _kTabDateBirth, ), const SizedBox(height: 12), _buildField( config: config, label: 'Ville de naissance', controller: _birthCityController, hint: 'Ex. Ajaccio, Paris, Casablanca…', validator: _validateBirthCity, focusNode: _birthCityFocus, focusTraversalOrder: _kTabBirthCity, ), const SizedBox(height: 12), _buildField( config: config, label: 'Pays de naissance', controller: _birthCountryController, hint: 'Ex. France, Maroc…', validator: _validateBirthCountry, focusNode: _birthCountryFocus, focusTraversalOrder: _kTabBirthCountry, textInputAction: TextInputAction.next, onFieldSubmitted: (_) => _nirFocus?.requestFocus(), onEditingComplete: () => _nirFocus?.requestFocus(), ), const SizedBox(height: 12), NirTextField( controller: _nirController, focusNode: _nirFocus, focusTraversalOrder: _nirFocus != null ? _kTabNir : null, fieldWidth: double.infinity, fieldHeight: 45.0, labelFontSize: 15.0, inputFontSize: 14.0, ), const SizedBox(height: 12), _buildField( config: config, label: 'N° d\'agrément', controller: _agrementController, hint: 'Votre numéro d\'agrément', validator: (v) => v!.isEmpty ? 'Agrément requis' : null, focusTraversalOrder: _kTabAgrement, ), const SizedBox(height: 12), _buildField( config: config, label: 'Date d\'obtention de l\'agrément', controller: _agreementDateController, hint: 'JJ/MM/AAAA', readOnly: true, onTap: () => _selectAgreementDate(context), suffixIcon: Icons.calendar_today, validator: (_) => _selectedAgreementDate == null ? 'Date d\'obtention requise' : null, focusTraversalOrder: _kTabAgreementDate, ), const SizedBox(height: 12), _buildField( config: config, label: 'Capacité d\'accueil', controller: _capacityController, hint: 'De 1 à $kAmCapaciteAccueilMax enfants', keyboardType: TextInputType.number, validator: (v) { if (v == null || v.isEmpty) return 'Capacité requise'; final n = int.tryParse(v); if (n == null || n < 1) { return 'Entrez un nombre entre 1 et $kAmCapaciteAccueilMax'; } if (n > kAmCapaciteAccueilMax) { return 'Maximum $kAmCapaciteAccueilMax'; } return null; }, focusTraversalOrder: _kTabCapacity, ), const SizedBox(height: 12), _buildField( config: config, label: 'Places disponibles', controller: _placesAvailableController, hint: 'Entre 0 et la capacité', keyboardType: TextInputType.number, validator: _validatePlacesAvailable, focusTraversalOrder: _kTabPlaces, ), ], ); } /// Section photo + checkbox (même logique visuelle que les enfants : cadre, croix). Widget _buildPhotoSection(BuildContext context, DisplayConfig config) { final photoSize = config.isMobile ? 200.0 : 270.0; final scaleFactor = config.isMobile ? 0.9 : 1.1; final slot = RegistrationPhotoSlot( side: photoSize, scaleFactor: scaleFactor, imageBytes: _photoBytes, imageFile: _photoFile, imagePathOrAsset: _photoPathFramework, onTapPick: config.isReadonly ? null : _pickPhoto, onClear: config.isReadonly ? null : _clearRegistrationPhoto, baseShadowColor: Colors.green.shade300, isMobile: config.isMobile, ); return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ if (config.isReadonly) slot else FocusTraversalOrder( order: const NumericFocusOrder(_kTabPhotoPick), child: slot, ), SizedBox(height: config.isMobile ? 10.0 : 6.0), AppCustomCheckbox( label: 'J\'accepte l\'utilisation\nde ma photo.', value: _photoConsent, onChanged: (val) { if (config.isReadonly) return; setState(() => _photoConsent = val == true); }, ), ], ); } /// Construit un champ individuel Widget _buildField({ required DisplayConfig config, required String label, required TextEditingController controller, String? hint, TextInputType? keyboardType, bool readOnly = false, VoidCallback? onTap, IconData? suffixIcon, String? Function(String?)? validator, List? inputFormatters, FocusNode? focusNode, double? focusTraversalOrder, TextInputAction? textInputAction, ValueChanged? onFieldSubmitted, VoidCallback? onEditingComplete, }) { if (config.isReadonly) { return FormFieldWrapper( config: config, label: label, value: controller.text, ); } else { final field = CustomAppTextField( controller: controller, focusNode: focusNode, labelText: label, hintText: hint ?? label, fieldWidth: double.infinity, fieldHeight: config.isMobile ? 45.0 : 53.0, labelFontSize: config.isMobile ? 15.0 : 20.0, inputFontSize: config.isMobile ? 14.0 : 18.0, labelFieldSpacing: config.isMobile ? 6.0 : 3.0, keyboardType: keyboardType ?? TextInputType.text, readOnly: readOnly, onTap: onTap, suffixIcon: suffixIcon, validator: validator, inputFormatters: inputFormatters, textInputAction: textInputAction, onFieldSubmitted: onFieldSubmitted, onEditingComplete: onEditingComplete, ); final o = focusTraversalOrder; if (o != null) { return FocusTraversalOrder( order: NumericFocusOrder(o), child: field, ); } return field; } } /// Boutons mobile Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) { return Padding( padding: EdgeInsets.symmetric( horizontal: screenSize.width * 0.05, ), child: Row( children: [ Expanded( child: HoverReliefWidget( child: CustomNavigationButton( text: 'Précédent', style: NavigationButtonStyle.purple, onPressed: () { if (context.canPop()) { context.pop(); } else { context.go(widget.previousRoute); } }, width: double.infinity, height: 50, fontSize: 16, ), ), ), const SizedBox(width: 16), Expanded( child: HoverReliefWidget( child: CustomNavigationButton( text: 'Suivant', style: NavigationButtonStyle.green, onPressed: _submitForm, width: double.infinity, height: 50, fontSize: 16, ), ), ), ], ), ); } /// Retourne l'asset de carte vertical correspondant à la couleur String _getVerticalCardAsset() { switch (widget.cardColor) { case CardColorHorizontal.blue: return CardColorVertical.blue.path; case CardColorHorizontal.green: return CardColorVertical.green.path; case CardColorHorizontal.lavender: return CardColorVertical.lavender.path; case CardColorHorizontal.lime: return CardColorVertical.lime.path; case CardColorHorizontal.peach: return CardColorVertical.peach.path; case CardColorHorizontal.pink: return CardColorVertical.pink.path; case CardColorHorizontal.red: return CardColorVertical.red.path; } } }