// ignore_for_file: unused_local_variable, avoid_print, use_build_context_synchronously, sized_box_for_whitespace, unnecessary_to_list_in_spreads, deprecated_member_use import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:wtpe/main.dart'; import 'package:wtpe/pages/class.dart'; import 'package:wtpe/services/wortis_api_service.dart'; import 'package:wtpe/widgets/pin_verification_dialog.dart'; // Constantes de style class FormStyles { static const primaryColor = Color(0xFF006699); static const secondaryColor = Color(0xFF0088CC); static const backgroundColor = Color(0xFFF5F7FA); static const textColor = Color(0xFF2C3E50); static const errorColor = Color(0xFFE74C3C); static const successColor = Color(0xFF2ECC71); static final cardDecoration = BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( // ignore: duplicate_ignore // ignore: deprecated_member_use color: Colors.black.withOpacity(0.05), spreadRadius: 0, blurRadius: 10, offset: const Offset(0, 4), ), ], ); static final inputDecoration = InputDecorationTheme( filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFE0E7FF), width: 1), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFE0E7FF), width: 1), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: primaryColor, width: 2), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: errorColor, width: 1), ), labelStyle: const TextStyle( color: Color(0xFF64748B), fontSize: 14, fontWeight: FontWeight.w500, ), hintStyle: TextStyle(color: textColor.withOpacity(0.5), fontSize: 14), errorStyle: const TextStyle( color: errorColor, fontSize: 12, fontWeight: FontWeight.w500, ), ); static final elevatedButtonStyle = ElevatedButton.styleFrom( backgroundColor: primaryColor, foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), textStyle: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ); static final outlinedButtonStyle = OutlinedButton.styleFrom( foregroundColor: primaryColor, side: const BorderSide(color: primaryColor, width: 1.5), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), textStyle: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ); } class FormService extends StatefulWidget { final String serviceName; const FormService({super.key, required this.serviceName}); @override _FormServiceState createState() => _FormServiceState(); } class _FormServiceState extends State { final ApiService _apiService = ApiService(); Map? serviceData; Map formValues = {}; Map controllers = {}; final GlobalKey _formKey = GlobalKey(); bool isLoading = true; int currentStep = 0; Map? verificationData; @override void initState() { super.initState(); ConnectivityManager(context).initConnectivity; fetchServiceFields(); } @override void dispose() { _apiService.cancelOperation(); controllers.forEach((_, controller) => controller.dispose()); super.dispose(); } Map _normalizeApiData(Map data) { try { if (data['steps'] != null) { for (var step in data['steps']) { if (step['fields'] != null) { List> normalizedFields = []; for (var field in step['fields']) { normalizedFields.add(_normalizeField(field)); } step['fields'] = normalizedFields; } if (step['api_fields'] != null) { Map normalizedApiFields = {}; step['api_fields'].forEach((key, fieldConfig) { normalizedApiFields[key] = { 'type': fieldConfig['type'] ?? 'text', 'label': fieldConfig['label'] ?? key, 'key': fieldConfig['key'], 'readonly': fieldConfig['readonly'] ?? true, 'required': fieldConfig['required'] ?? true, 'format': fieldConfig['format'], }; }); step['api_fields'] = normalizedApiFields; } } } else if (data['fields'] != null) { List> normalizedFields = []; for (var field in data['fields']) { normalizedFields.add(_normalizeField(field)); } data['fields'] = normalizedFields; } return data; } catch (e) { print('Erreur lors de la normalisation des données : $e'); return data; } } Map _normalizeField(Map field) { Map normalizedField = {}; normalizedField['name'] = field['nom'] ?? field['name'] ?? ''; normalizedField['type'] = _normalizeFieldType(field['type']); normalizedField['required'] = field['required'] ?? true; normalizedField['label'] = field['label'] ?? field['nom'] ?? field['name'] ?? ''; normalizedField['readonly'] = field['readonly'] ?? false; if (field['options'] != null) { normalizedField['options'] = field['options'].map>((option) { return { 'value': option['value'] ?? option['valeur'] ?? '', 'label': option['label'] ?? option['étiquette'] ?? '', }; }).toList(); } if (field['dependencies'] != null) { List> normalizedDeps = []; for (var dep in field['dependencies']) { Map normalizedDep = { 'field': dep['field'] ?? dep['champ'] ?? '', 'value': dep['value'] ?? dep['valeur'] ?? '', }; if (dep['options'] != null) { normalizedDep['options'] = dep['options'].map>((option) { return { 'value': option['value'] ?? option['valeur'] ?? '', 'label': option['label'] ?? option['étiquette'] ?? '', }; }).toList(); } normalizedDeps.add(normalizedDep); } normalizedField['dependencies'] = normalizedDeps; } return normalizedField; } String _normalizeFieldType(String type) { final typeMap = { 'numéro': 'number', 'sélecteur': 'selecteur', 'texte': 'text', }; return typeMap[type.toLowerCase()] ?? type.toLowerCase(); } Widget buildApiFieldGroup() { if (serviceData == null) return const SizedBox(); var currentStepData = serviceData!['steps'][currentStep]; if (currentStepData['api_fields'] == null) return const SizedBox(); return Container( margin: const EdgeInsets.only(bottom: 24), decoration: FormStyles.cardDecoration, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.person, color: FormStyles.primaryColor, size: 24), SizedBox(width: 8), Text( 'Informations récupérées', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: FormStyles.textColor, ), ), ], ), const SizedBox(height: 16), ...currentStepData['api_fields'].entries.map((entry) { String fieldName = entry.key; Map fieldConfig = entry.value; // Assurez-vous que le contrôleur existe if (!controllers.containsKey(fieldName)) { controllers[fieldName] = TextEditingController( text: formValues[fieldName]?.toString() ?? '', ); } // Déterminer l'icône en fonction du type de champ IconData fieldIcon = Icons.info_outline; switch (fieldConfig['type']?.toString().toLowerCase()) { case 'number': fieldIcon = Icons.numbers; break; case 'date': fieldIcon = Icons.calendar_today; break; case 'time': fieldIcon = Icons.access_time; break; case 'datetime': fieldIcon = Icons.event; break; case 'selecteur': fieldIcon = Icons.arrow_drop_down_circle; break; default: fieldIcon = Icons.text_fields; } return Container( margin: const EdgeInsets.only(bottom: 16), child: TextFormField( controller: controllers[fieldName], readOnly: fieldConfig['readonly'] ?? true, decoration: InputDecoration( labelText: fieldConfig['label'] ?? fieldName, filled: true, fillColor: Colors.white, prefixIcon: Icon( fieldIcon, color: FormStyles.primaryColor.withOpacity(0.7), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFE0E7FF)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFE0E7FF)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide( color: FormStyles.primaryColor, width: 2, ), ), ), style: TextStyle( fontSize: 16, color: fieldConfig['readonly'] ?? true ? FormStyles.textColor.withOpacity(0.7) : FormStyles.textColor, ), validator: (value) { if (fieldConfig['required'] == true && (value?.isEmpty ?? true)) { return 'Ce champ est requis'; } return null; }, ), ); }).toList(), ], ), ), ); } // ignore: unused_element Future _selectDate(BuildContext context, String fieldName) async { final DateTime currentDate = DateTime.now(); DateTime initialDate; try { String? currentValue = controllers[fieldName]?.text; if (currentValue!.isNotEmpty) { initialDate = DateFormat('yyyy-MM-dd').parse(currentValue); } else { initialDate = currentDate; } } catch (e) { initialDate = currentDate; } final DateTime? picked = await showDatePicker( context: context, initialDate: initialDate, firstDate: DateTime(2000), lastDate: DateTime(2101), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: const ColorScheme.light( primary: FormStyles.primaryColor, onPrimary: Colors.white, surface: Colors.white, onSurface: FormStyles.textColor, ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( foregroundColor: FormStyles.primaryColor, ), ), dialogTheme: DialogThemeData(backgroundColor: Colors.white), ), child: child!, ); }, helpText: 'Sélectionner une date', cancelText: 'Annuler', confirmText: 'Confirmer', errorFormatText: 'Format de date invalide', errorInvalidText: 'Date invalide', fieldLabelText: 'Date', fieldHintText: 'JJ/MM/AAAA', ); if (picked != null && mounted) { String formattedDate = DateFormat('yyyy-MM-dd').format(picked); setState(() { controllers[fieldName]?.value = TextEditingValue(text: formattedDate); updateFormValue(fieldName, formattedDate); }); } } Future fetchServiceFields() async { try { final responseData = await _apiService.fetchServiceFields( widget.serviceName, ); setState(() { serviceData = _normalizeApiData(responseData['service']); initializeFormValues(); isLoading = false; }); } catch (e) { print('Erreur : $e'); if (mounted) { CustomOverlay.showError( context, message: 'Erreur lors de la récupération des données du formulaire', ); } } } List> getDropdownOptions( Map field, ) { List> items = []; try { if (field['dependencies'] != null) { for (var dependency in field['dependencies']) { String dependentField = dependency['field']; String expectedValue = dependency['value']; if (formValues[dependentField] == expectedValue) { var optionsToUse = dependency['options'] ?? field['options']; if (optionsToUse != null) { items = optionsToUse.map>((option) { return DropdownMenuItem( value: option['value'], child: Text(option['label']), ); }).toList(); } break; } } } else if (field['options'] != null) { items = field['options'].map>((option) { return DropdownMenuItem( value: option['value'], child: Text(option['label']), ); }).toList(); } } catch (e) { print('Erreur lors de la création des options du dropdown : $e'); } return items; } void updateFormValue(String fieldName, dynamic value) { setState(() { formValues[fieldName] = value; var fields = serviceData!['steps'] != null ? serviceData!['steps'][currentStep]['fields'] : serviceData!['fields']; var field = fields.firstWhere( (f) => f['name'] == fieldName, orElse: () => {'type': ''}, ); if (!['text', 'number'].contains(field['type']) && controllers.containsKey(fieldName)) { controllers[fieldName]?.value = TextEditingValue( text: value?.toString() ?? '', ); } if (fields != null) { for (var field in fields) { if (field['dependencies'] != null) { bool shouldReset = field['dependencies'].any( (dependency) => dependency['field'] == fieldName && dependency['value'] != value, ); if (shouldReset) { String dependentFieldName = field['name']; formValues[dependentFieldName] = null; if (controllers.containsKey(dependentFieldName)) { controllers[dependentFieldName]?.clear(); } } } } } }); } bool shouldShowField(Map field) { if (field['dependencies'] == null) return true; return field['dependencies'].any((dependency) { String dependentField = dependency['field']; String expectedValue = dependency['value']; return formValues[dependentField] == expectedValue; }); } Widget buildProgressIndicator() { if (serviceData!['steps'] == null) return const SizedBox(); return Container( margin: const EdgeInsets.only(bottom: 24), child: Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: (currentStep + 1) / serviceData!['steps'].length, backgroundColor: Colors.grey[200], valueColor: const AlwaysStoppedAnimation( FormStyles.primaryColor, ), minHeight: 8, ), ), const SizedBox(height: 12), // Text( // 'Étape ${currentStep + 1}/${serviceData!['steps'].length}', // style: const TextStyle( // fontSize: 16, // fontWeight: FontWeight.w600, // color: FormStyles.textColor, // ), // ), ], ), ); } // Ajoutez cette méthode dans la classe _FormServiceState void _showPaymentInstructionsModal() { showDialog( context: context, barrierColor: Colors.black54, builder: (BuildContext context) { // Obtenir les dimensions de l'écran final size = MediaQuery.of(context).size; final isSmallScreen = size.width < 360; final isLandscape = size.width > size.height; // Calculer les dimensions optimales final dialogWidth = size.width * (isSmallScreen ? 0.95 : 0.9); final maxHeight = size.height * (isLandscape ? 0.9 : 0.7); final padding = isSmallScreen ? 16.0 : 24.0; final iconSize = isSmallScreen ? 18.0 : 20.0; return Dialog( insetPadding: EdgeInsets.symmetric( horizontal: (size.width - dialogWidth) / 2, vertical: size.height * 0.05, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), elevation: 0, backgroundColor: Colors.white, child: Container( constraints: BoxConstraints( maxHeight: maxHeight, maxWidth: dialogWidth, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête Padding( padding: EdgeInsets.all(padding), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Row( children: [ Container( padding: EdgeInsets.all(isSmallScreen ? 6 : 8), decoration: BoxDecoration( color: FormStyles.primaryColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.payments_outlined, color: FormStyles.primaryColor, size: iconSize, ), ), SizedBox(width: isSmallScreen ? 8 : 12), Flexible( child: Text( 'Comment ça marche', style: TextStyle( fontSize: isSmallScreen ? 18 : 20, fontWeight: FontWeight.w600, color: Colors.black87, ), overflow: TextOverflow.ellipsis, ), ), ], ), ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: Icon( Icons.close, color: Colors.black54, size: iconSize, ), padding: EdgeInsets.zero, constraints: BoxConstraints( minWidth: iconSize * 1.5, minHeight: iconSize * 1.5, ), splashRadius: iconSize, ), ], ), ), // Contenu scrollable Flexible( child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Padding( padding: EdgeInsets.symmetric( horizontal: padding, vertical: isSmallScreen ? 8 : 12, ), child: Text( serviceData!['comment_payer'] ?? 'Instructions non disponibles', style: TextStyle( fontSize: isSmallScreen ? 14 : 15, color: Colors.black87, height: 1.6, letterSpacing: 0.3, ), ), ), ), ), // Bouton de fermeture Padding( padding: EdgeInsets.all(padding), child: SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.of(context).pop(), style: ElevatedButton.styleFrom( backgroundColor: FormStyles.primaryColor, foregroundColor: Colors.white, padding: EdgeInsets.symmetric( vertical: isSmallScreen ? 12 : 14, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 0, ), child: Text( 'Compris', style: TextStyle( fontSize: isSmallScreen ? 14 : 16, fontWeight: FontWeight.w500, ), ), ), ), ), ], ), ), ); }, ); } // Remplacez la méthode buildBanner existante par celle-ci Widget buildBanner() { if (serviceData == null) { return const SizedBox.shrink(); } return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), spreadRadius: 0, blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Image banner // Image banner if (serviceData!['banner'] != null) ClipRRect( borderRadius: const BorderRadius.vertical( top: Radius.circular(12), ), child: Image.network( serviceData!['banner'], width: double.infinity, fit: BoxFit.fitWidth, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Container( color: Colors.white, child: Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, valueColor: const AlwaysStoppedAnimation( FormStyles.primaryColor, ), ), ), ); }, errorBuilder: (context, error, stackTrace) { return Container( color: Colors.grey[200], child: const Center( child: Icon( Icons.error_outline, color: FormStyles.errorColor, size: 32, ), ), ); }, ), ), // Description // Dans la méthode buildBanner, remplacez la section Description par ceci: // Description if (serviceData!['description'] != null) Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Colors.grey.withOpacity(0.2), width: 1, ), ), ), padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ const Icon( Icons.info_outline, color: FormStyles.primaryColor, size: 20, ), const SizedBox(width: 8), Text( serviceData!['tite_description'] ?? 'Description', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: FormStyles.textColor, ), ), ], ), ), const SizedBox(height: 12), Text( serviceData!['description'], style: const TextStyle( fontSize: 14, color: FormStyles.textColor, height: 1.5, ), ), const SizedBox(height: 16), if (serviceData!['comment_payer'] != null) Container( decoration: BoxDecoration( border: Border( top: BorderSide( color: Colors.grey.withOpacity(0.2), width: 1, ), ), ), padding: const EdgeInsets.only(top: 12), child: Row( children: [ const Icon( Icons.help_outline, color: FormStyles.primaryColor, size: 20, ), const SizedBox(width: 8), InkWell( onTap: _showPaymentInstructionsModal, child: Row( children: const [ Text( 'Comment ça marche ?', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: FormStyles.primaryColor, ), ), ], ), ), ], ), ), ], ), ), ], ), ); } bool validateAllFields() { bool isValid = true; String missingFields = ''; // Vérifier les champs simples if (serviceData!['fields'] != null) { for (var field in serviceData!['fields']) { String fieldName = field['name']; bool isRequired = field['required'] == true; // Vérifier si le champ doit être affiché (dépendances) if (shouldShowField(field) && isRequired) { var value = formValues[fieldName]; bool fieldIsEmpty = false; switch (field['type']) { case 'selecteur': fieldIsEmpty = value == null || value.toString().isEmpty; break; case 'number': fieldIsEmpty = value == null || value.toString().trim().isEmpty; if (!fieldIsEmpty && double.tryParse(value.toString()) == null) { fieldIsEmpty = true; } break; default: // text et autres fieldIsEmpty = value == null || value.toString().trim().isEmpty; } // Si le champ est vide et que c'est un champ requis actuellement visible if (fieldIsEmpty) { isValid = false; missingFields += '${field['label'] ?? fieldName}, '; // Mettre en évidence le champ manquant if (controllers.containsKey(fieldName)) { controllers[fieldName]?.text = controllers[fieldName]?.text ?? ''; } } } } } // Si des champs sont manquants, afficher un message spécifique if (!isValid) { missingFields = missingFields.replaceAll(RegExp(r', $'), ''); CustomOverlay.showError( context, message: 'Veuillez remplir les champs obligatoires : $missingFields', ); } print('Validation du formulaire : ${isValid ? 'Valide' : 'Invalide'}'); // Afficher les valeurs actuelles pour le débogage print('Valeurs du formulaire : $formValues'); return isValid; } // ===== MODIFICATION DU FORMSERVICE ===== // Dans votre FormService, modifiez la méthode submitForm() : // Méthode submitForm() corrigée pour afficher correctement les montants // Méthode submitForm() corrigée Future submitForm() async { if (!validateAllFields()) { return; } if (!_formKey.currentState!.validate()) { return; } if (serviceData!['steps'] != null && currentStep == 0) { await verifyFirstStep(); return; } // Vérification de l'agent connecté final authController = Provider.of(context, listen: false); final String? agentId = authController.agentId; final String? agentName = authController.agentName; // Utiliser le solde actif (entreprise ou personnel selon le type de compte) final double agentBalance = authController.activeBalance; final bool isEnterprise = authController.isEnterpriseMember; final String accountType = authController.accountType; if (agentId == null || agentId.isEmpty) { CustomOverlay.showError( context, message: 'Agent non connecté. Veuillez vous reconnecter.', ); return; } // ===== CALCUL DES MONTANTS CORRIGÉ ===== double baseAmount = 0.0; double fees = 0.0; double commission = 0.0; double totalAmount = 0.0; // 1. Récupérer le montant de base depuis le champ "montantinitial" que l'utilisateur a saisi if (formValues['montantinitial'] != null) { baseAmount = double.tryParse(formValues['montantinitial'].toString()) ?? 0.0; } // 2. Si pas trouvé dans formValues, chercher dans les controllers if (baseAmount == 0.0 && controllers.containsKey('montantinitial')) { String controllerValue = controllers['montantinitial']?.text ?? ''; if (controllerValue.isNotEmpty) { baseAmount = double.tryParse( controllerValue.replaceAll(RegExp(r'[^\d.]'), ''), ) ?? 0.0; } } // 3. Chercher d'autres champs de montant possibles if (baseAmount == 0.0) { List possibleAmountKeys = [ 'montant', 'montant à payer', 'Prix de l\'Abonnement', 'Dette Totale', ]; for (String key in possibleAmountKeys) { // Chercher dans formValues if (formValues[key] != null) { double? parsed = double.tryParse( formValues[key].toString().replaceAll(RegExp(r'[^\d.]'), ''), ); if (parsed != null && parsed > 0) { baseAmount = parsed; break; } } // Chercher dans controllers if (baseAmount == 0.0 && controllers.containsKey(key)) { String controllerValue = controllers[key]?.text ?? ''; if (controllerValue.isNotEmpty) { double? parsed = double.tryParse( controllerValue.replaceAll(RegExp(r'[^\d.]'), ''), ); if (parsed != null && parsed > 0) { baseAmount = parsed; break; } } } // Chercher dans verificationData if (baseAmount == 0.0 && verificationData != null && verificationData![key] != null) { double? parsed = double.tryParse( verificationData![key].toString().replaceAll(RegExp(r'[^\d.]'), ''), ); if (parsed != null && parsed > 0) { baseAmount = parsed; break; } } } } // 4. Calculer les frais automatiquement (2% du montant de base) if (baseAmount > 0) { fees = baseAmount * 0.02; // 2% du montant de base totalAmount = baseAmount + fees; } // 5. Vérifier si des frais sont déjà définis explicitement List possibleFeeKeys = [ 'frais', 'Frais de Service', 'Frais', 'commission', 'Commission Agent', 'Commission', ]; for (String key in possibleFeeKeys) { double? explicitFees; // Chercher dans formValues if (formValues[key] != null) { explicitFees = double.tryParse( formValues[key].toString().replaceAll(RegExp(r'[^\d.]'), ''), ); } // Chercher dans controllers if (explicitFees == null && controllers.containsKey(key)) { String controllerValue = controllers[key]?.text ?? ''; if (controllerValue.isNotEmpty) { explicitFees = double.tryParse( controllerValue.replaceAll(RegExp(r'[^\d.]'), ''), ); } } // Chercher dans verificationData if (explicitFees == null && verificationData != null && verificationData![key] != null) { explicitFees = double.tryParse( verificationData![key].toString().replaceAll(RegExp(r'[^\d.]'), ''), ); } if (explicitFees != null && explicitFees > 0) { if (key.toLowerCase().contains('commission')) { commission = explicitFees; } else { fees = explicitFees; } break; } } // 6. Recalculer le total si des frais explicites ont été trouvés if (fees > 0 || commission > 0) { totalAmount = baseAmount + fees + commission; } // 7. Chercher un total explicite si défini List possibleTotalKeys = [ 'total', 'Total', 'Montant à Débiter', 'montant_total', ]; for (String key in possibleTotalKeys) { double? explicitTotal; // Chercher dans formValues if (formValues[key] != null) { explicitTotal = double.tryParse( formValues[key].toString().replaceAll(RegExp(r'[^\d.]'), ''), ); } // Chercher dans controllers if (explicitTotal == null && controllers.containsKey(key)) { String controllerValue = controllers[key]?.text ?? ''; if (controllerValue.isNotEmpty) { explicitTotal = double.tryParse( controllerValue.replaceAll(RegExp(r'[^\d.]'), ''), ); } } // Chercher dans verificationData if (explicitTotal == null && verificationData != null && verificationData![key] != null) { explicitTotal = double.tryParse( verificationData![key].toString().replaceAll(RegExp(r'[^\d.]'), ''), ); } if (explicitTotal != null && explicitTotal > 0) { totalAmount = explicitTotal; break; } } // Logs détaillés pour debugging print('🔍 Debug des montants (Service: ${widget.serviceName}):'); print(' FormValues: $formValues'); print(' VerificationData: $verificationData'); print(' Controllers values:'); controllers.forEach((key, controller) { if (controller.text.isNotEmpty) { print(' $key: ${controller.text}'); } }); print('💰 Résultats calculés:'); print(' Montant de base: $baseAmount FCFA'); print(' Frais (2%): $fees FCFA'); print(' Commission: $commission FCFA'); print(' Total: $totalAmount FCFA'); // Vérification finale if (totalAmount <= 0 && baseAmount <= 0) { print('⚠️ Aucun montant détecté !'); CustomOverlay.showError( context, message: 'Impossible de déterminer le montant à payer. Veuillez vérifier que vous avez bien saisi le montant.', ); return; } // Si seul le montant de base est disponible, calculer le total if (totalAmount <= 0 && baseAmount > 0) { fees = baseAmount * 0.02; // 2% par défaut totalAmount = baseAmount + fees; print( '💡 Total calculé automatiquement: $totalAmount FCFA (base: $baseAmount + frais: $fees)', ); } // Afficher le dialog PIN avec les montants corrects final bool? pinConfirmed = await showDialog( context: context, barrierDismissible: false, builder: (context) => PinVerificationDialog( agentName: agentName ?? 'Agent $agentId', serviceName: widget.serviceName, baseAmount: baseAmount, fees: fees, totalAmount: totalAmount, agentBalance: agentBalance, onPinConfirmed: (pin) async { Navigator.of(context).pop(true); }, onCancel: () { Navigator.of(context).pop(false); }, ), ); if (pinConfirmed != true) { return; } // Continuer avec la soumission... try { setState(() { isLoading = true; }); print( '🔐 Agent connecté: $agentName ($agentId) - Type: ${isEnterprise ? "Entreprise" : "Individuel"} - Solde: $agentBalance FCFA', ); Map requestBody = {}; String url; bool isCardPayment = false; // Ajouter les informations de l'agent et du type de compte requestBody['agent_id'] = agentId; requestBody['account_type'] = accountType; // Ajouter l'ID entreprise si c'est un compte entreprise if (isEnterprise && authController.enterprise?.enterpriseId != null) { requestBody['enterprise_id'] = authController.enterprise!.enterpriseId; requestBody['balance_source'] = 'enterprise'; print('🏢 Transaction entreprise: ${authController.enterprise!.nomEntreprise}'); } else { requestBody['balance_source'] = 'personal'; } if (serviceData!['steps'] != null) { var finalStep = serviceData!['steps'][1]; finalStep['body'].forEach((key, value) { requestBody[key] = formValues[value]; }); if (finalStep['link_momo_dependencies'] != null) { String dependentField = finalStep['link_momo_dependencies']['field']; String expectedValue = finalStep['link_momo_dependencies']['value']; isCardPayment = formValues[dependentField] != expectedValue; url = isCardPayment ? finalStep['link_cb'] ?? finalStep['link_momo'] : finalStep['link_momo']; } else { url = finalStep['link_momo']; } } else { serviceData!['body'].forEach((key, value) { requestBody[key] = formValues[value]; }); if (serviceData!['link_momo_dependencies'] != null) { String dependentField = serviceData!['link_momo_dependencies']['field']; String expectedValue = serviceData!['link_momo_dependencies']['value']; isCardPayment = formValues[dependentField] != expectedValue; url = isCardPayment ? serviceData!['link_cb'] ?? serviceData!['link_momo'] : serviceData!['link_momo']; } else { url = serviceData!['link_momo']; } } print('📤 Soumission vers: $url'); print('📊 Données envoyées: $requestBody'); print('👤 Agent: $agentId'); print( '💳 Type de paiement: ${isCardPayment ? "Carte bancaire" : "Solde Agent"}', ); await _apiService.submitFormData( context, url, requestBody, serviceData, null, isCardPayment, ); if (mounted) { setState(() { currentStep = 0; formValues.clear(); verificationData = null; initializeFormValues(); }); } } catch (e) { print('❌ Erreur lors de la soumission : $e'); if (mounted) { CustomOverlay.showError( context, message: 'Erreur lors de la soumission : $e', ); } } finally { if (mounted) { setState(() { isLoading = false; }); } } } Future verifyFirstStep() async { if (!_formKey.currentState!.validate()) return; setState(() { isLoading = true; }); const operationId = 'verify_first_step'; try { Map requestBody = {}; var currentStepData = serviceData!['steps'][currentStep]; currentStepData['body'].forEach((key, value) { if (value is String) { requestBody[key] = formValues[value]; } }); if (currentStepData['body']['additional_params'] != null) { requestBody.addAll(currentStepData['body']['additional_params']); } final requestMethod = currentStepData['request']?.toString().toUpperCase() ?? 'GET'; try { final response = requestMethod == 'POST' ? await _apiService.verifyDataPost( currentStepData['link'], requestBody, operationId, ) : await _apiService.verifyDataGet( currentStepData['link'], requestBody, ); // Cacher l'overlay de chargement CustomOverlay.hide(); setState(() { verificationData = response; currentStep = 1; var nextStep = serviceData!['steps'][1]; if (nextStep['api_fields'] != null) { Map apiFields = nextStep['api_fields']; apiFields.forEach((fieldName, fieldConfig) { if (fieldConfig['key'] is List) { if (fieldConfig['format'] == 'concat') { List values = []; for (String key in fieldConfig['key']) { if (response[key] != null) { values.add(response[key].toString()); } } formValues[fieldName] = values.join(' ').trim(); } } else { String apiKey = fieldConfig['key']; if (response[apiKey] != null) { formValues[fieldName] = response[apiKey]; } } if (controllers[fieldName] == null) { controllers[fieldName] = TextEditingController( text: formValues[fieldName]?.toString() ?? '', ); } else { controllers[fieldName]?.value = TextEditingValue( text: formValues[fieldName]?.toString() ?? '', ); } }); } if (nextStep['preserve_fields'] != null) { var preserveFields = nextStep['preserve_fields']; if (preserveFields['source'] != null && preserveFields['target'] != null) { formValues[preserveFields['target']] = formValues[preserveFields['source']]; } } isLoading = false; }); if (mounted) { CustomOverlay.showSuccess( context, message: 'Informations récupérées avec succès', ); } } catch (e) { // Si l'erreur est liée à un code HTTP > 300 ou une réponse non trouvée throw Exception('not_found'); } } catch (e) { setState(() { isLoading = false; }); // Cacher l'overlay de chargement CustomOverlay.hide(); if (mounted) { CustomOverlay.showError(context, message: 'Informations non trouvées'); } } } Widget buildFormField(Map field) { if (!shouldShowField(field)) return const SizedBox(); String fieldName = field['name']; String fieldType = field['type']; bool isReadOnly = field['readonly'] ?? false; switch (fieldType) { case 'text': case 'number': return Container( margin: const EdgeInsets.symmetric(vertical: 8.0), child: TextFormField( controller: controllers[fieldName], keyboardType: fieldType == 'number' ? TextInputType.number : TextInputType.text, decoration: InputDecoration( labelText: field['label'] ?? fieldName, filled: true, fillColor: Colors.white, prefixIcon: Icon( fieldType == 'number' ? Icons.numbers : Icons.text_fields, color: FormStyles.primaryColor.withOpacity(0.7), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFE0E7FF)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFE0E7FF)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide( color: FormStyles.primaryColor, width: 2, ), ), ), readOnly: isReadOnly, onChanged: (value) => updateFormValue(fieldName, value), validator: (value) { if (field['required'] == true && (value?.isEmpty ?? true)) { return 'Ce champ est requis'; } if (fieldType == 'number' && value != null && value.isNotEmpty) { if (double.tryParse(value) == null) { return 'Veuillez entrer un nombre valide'; } } return null; }, ), ); case 'selecteur': List> items = getDropdownOptions(field); String? currentValue = formValues[fieldName]; if (currentValue != null && !items.any((item) => item.value == currentValue)) { currentValue = null; } return Container( margin: const EdgeInsets.symmetric(vertical: 8.0), child: DropdownButtonFormField( value: currentValue, items: items, decoration: InputDecoration( labelText: field['label'] ?? fieldName, filled: true, fillColor: Colors.white, prefixIcon: Icon( Icons.arrow_drop_down_circle, color: FormStyles.primaryColor.withOpacity(0.7), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFE0E7FF)), ), ), onChanged: isReadOnly ? null : (value) => updateFormValue(fieldName, value), validator: (value) { if (field['required'] == true && value == null) { return 'Ce champ est requis'; } return null; }, ), ); case 'date': return Container( margin: const EdgeInsets.symmetric(vertical: 8.0), child: TextFormField( controller: controllers[fieldName], decoration: InputDecoration( labelText: field['label'] ?? fieldName, filled: true, fillColor: Colors.white, prefixIcon: Icon( Icons.calendar_today, color: FormStyles.primaryColor.withOpacity(0.7), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFE0E7FF)), ), ), readOnly: true, onTap: isReadOnly ? null : () async { final now = DateTime.now(); final DateTime? picked = await showDatePicker( context: context, initialDate: now, firstDate: now, // La date de début est aujourd'hui lastDate: DateTime(2101), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: const ColorScheme.light( primary: FormStyles.primaryColor, onPrimary: Colors.white, surface: Colors.white, onSurface: FormStyles.textColor, ), ), child: child!, ); }, helpText: 'Sélectionner une date', cancelText: 'Annuler', confirmText: 'Confirmer', errorFormatText: 'Format de date invalide', errorInvalidText: 'Les dates passées ne sont pas autorisées', fieldLabelText: field['label'] ?? fieldName, fieldHintText: 'JJ/MM/AAAA', ); if (picked != null && mounted) { setState(() { String formattedDate = DateFormat( 'yyyy-MM-dd', ).format(picked); controllers[fieldName]?.value = TextEditingValue( text: formattedDate, ); updateFormValue(fieldName, formattedDate); }); } }, validator: (value) { if (field['required'] == true && (value?.isEmpty ?? true)) { return 'Ce champ est requis'; } // Validation supplémentaire pour les dates if (value != null && value.isNotEmpty) { try { final selectedDate = DateFormat('yyyy-MM-dd').parse(value); if (selectedDate.isBefore( DateTime.now().subtract(const Duration(days: 1)), )) { return 'La date doit être aujourd\'hui ou future'; } } catch (e) { return 'Format de date invalide'; } } return null; }, ), ); case 'time': return Container( margin: const EdgeInsets.symmetric(vertical: 8.0), child: TextFormField( controller: controllers[fieldName], decoration: InputDecoration( labelText: field['label'] ?? fieldName, filled: true, fillColor: Colors.white, prefixIcon: Icon( Icons.access_time, color: FormStyles.primaryColor.withOpacity(0.7), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFE0E7FF)), ), ), readOnly: true, onTap: isReadOnly ? null : () async { final TimeOfDay? picked = await showTimePicker( context: context, initialTime: TimeOfDay.now(), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: const ColorScheme.light( primary: FormStyles.primaryColor, onPrimary: Colors.white, surface: Colors.white, onSurface: FormStyles.textColor, ), ), child: child!, ); }, ); if (picked != null) { setState(() { controllers[fieldName]?.value = TextEditingValue( text: picked.format(context), ); updateFormValue(fieldName, picked.format(context)); }); } }, validator: (value) { if (field['required'] == true && (value?.isEmpty ?? true)) { return 'Ce champ est requis'; } return null; }, ), ); case 'list': return buildDynamicListField(field); case 'datetime': return Container( margin: const EdgeInsets.symmetric(vertical: 8.0), child: TextFormField( controller: controllers[fieldName], decoration: InputDecoration( labelText: field['label'] ?? fieldName, filled: true, fillColor: Colors.white, prefixIcon: Icon( Icons.event, color: FormStyles.primaryColor.withOpacity(0.7), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFE0E7FF)), ), ), readOnly: true, onTap: isReadOnly ? null : () async { final DateTime? date = await showDatePicker( context: context, initialDate: DateTime.now(), firstDate: DateTime(2000), lastDate: DateTime(2101), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: const ColorScheme.light( primary: FormStyles.primaryColor, onPrimary: Colors.white, surface: Colors.white, onSurface: FormStyles.textColor, ), ), child: child!, ); }, ); if (date != null) { final TimeOfDay? time = await showTimePicker( context: context, initialTime: TimeOfDay.now(), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: const ColorScheme.light( primary: FormStyles.primaryColor, onPrimary: Colors.white, surface: Colors.white, onSurface: FormStyles.textColor, ), ), child: child!, ); }, ); if (time != null && mounted) { final DateTime dateTime = DateTime( date.year, date.month, date.day, time.hour, time.minute, ); setState(() { String formattedDateTime = DateFormat( 'yyyy-MM-dd HH:mm', ).format(dateTime); controllers[fieldName]?.value = TextEditingValue( text: formattedDateTime, ); updateFormValue(fieldName, formattedDateTime); }); } } }, validator: (value) { if (field['required'] == true && (value?.isEmpty ?? true)) { return 'Ce champ est requis'; } return null; }, ), ); default: return const SizedBox(); } } // ignore: unused_element InputDecoration _buildInputDecoration( String label, IconData icon, bool isRequired, ) { return InputDecoration( labelText: isRequired ? '$label *' : label, filled: true, fillColor: Colors.white, prefixIcon: Icon(icon, color: FormStyles.primaryColor.withOpacity(0.7)), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFE0E7FF)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFFE0E7FF)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: FormStyles.primaryColor, width: 2), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: FormStyles.errorColor), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: FormStyles.errorColor, width: 2), ), ); } Widget buildDynamicListField(Map field) { String fieldName = field['name']; // Initialisation avec le bon type if (formValues[fieldName] == null) { formValues[fieldName] = >[]; } List> fieldValues = List>.from( formValues[fieldName], ); return Container( margin: const EdgeInsets.only(bottom: 16), decoration: FormStyles.cardDecoration, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ const Icon(Icons.list_alt, color: FormStyles.primaryColor), const SizedBox(width: 8), Text( field['label'] ?? fieldName, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: FormStyles.textColor, ), ), ], ), const SizedBox(height: 16), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: fieldValues.length, itemBuilder: (context, index) { return Card( elevation: 2, margin: const EdgeInsets.only(bottom: 16), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Élément ${index + 1}', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: FormStyles.textColor, ), ), IconButton( icon: const Icon( Icons.delete, color: FormStyles.errorColor, ), onPressed: () { setState(() { fieldValues.removeAt(index); formValues[fieldName] = fieldValues; }); }, ), ], ), const Divider(height: 24), ...(field['options'] as List).map((option) { String optionName = option['value'].toString(); return Padding( padding: const EdgeInsets.only(bottom: 16), child: TextFormField( initialValue: fieldValues[index][optionName]?.toString() ?? '', decoration: InputDecoration( labelText: option['label'], filled: true, fillColor: Colors.white, prefixIcon: const Icon( Icons.edit, color: FormStyles.primaryColor, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide( color: Color(0xFFE0E7FF), ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide( color: Color(0xFFE0E7FF), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide( color: FormStyles.primaryColor, width: 2, ), ), ), onChanged: (value) { setState(() { fieldValues[index][optionName] = value; formValues[fieldName] = fieldValues; }); }, validator: (value) => value!.isEmpty ? 'Ce champ est requis' : null, ), ); }), ], ), ), ); }, ), const SizedBox(height: 16), ElevatedButton.icon( onPressed: () { setState(() { // Création d'un nouvel élément avec le bon typage Map newItem = {}; for (var option in (field['options'] as List)) { newItem[option['value'].toString()] = ''; } fieldValues.add(newItem); formValues[fieldName] = fieldValues; }); }, style: ElevatedButton.styleFrom( backgroundColor: FormStyles.primaryColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), icon: const Icon(Icons.add), label: Text( serviceData!['bouton_list'] ?? 'Ajouter un élément', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ), ), ), ], ), ), ); } // Modifiez également la méthode initializeFormValues pour bien initialiser les listes void initializeFormValues() { if (serviceData == null) return; if (serviceData!['steps'] != null) { var currentStepData = serviceData!['steps'][currentStep]; if (currentStepData['fields'] != null) { for (var field in currentStepData['fields']) { String fieldName = field['name']; if (field['type'] == 'list') { formValues[fieldName] = []; // Initialisation explicite de la liste vide } else { formValues[fieldName] = null; } if ([ 'text', 'number', 'date', 'time', 'datetime', ].contains(field['type'])) { controllers[fieldName] = TextEditingController(); } } } // Rest of the method remains the same... } else { if (serviceData!['fields'] == null) return; for (var field in serviceData!['fields']) { String fieldName = field['name']; if (field['type'] == 'list') { formValues[fieldName] = []; // Initialisation explicite de la liste vide } else { formValues[fieldName] = null; } if ([ 'text', 'number', 'date', 'time', 'datetime', ].contains(field['type'])) { controllers[fieldName] = TextEditingController(); } } } } List buildCurrentStepFields() { if (serviceData!['steps'] != null) { var currentStepData = serviceData!['steps'][currentStep]; List fields = []; // On n'affiche les informations détaillées que pour l'étape 1 if (currentStep == 1) { // Section des champs API (informations récupérées) if (currentStepData['api_fields'] != null) { Map apiFields = currentStepData['api_fields']; // Titre de la section des informations récupérées fields.add( Container( margin: const EdgeInsets.only(bottom: 16), child: const Row( children: [ Icon( Icons.info_outline, color: FormStyles.primaryColor, size: 20, ), SizedBox(width: 8), Text( 'Informations détaillées', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: FormStyles.textColor, ), ), ], ), ), ); // Ajout des champs API apiFields.forEach((fieldName, fieldConfig) { Map field = { 'name': fieldName, 'type': fieldConfig['type'] ?? 'text', 'label': fieldConfig['label'] ?? fieldName, 'readonly': fieldConfig['readonly'] ?? true, 'required': fieldConfig['required'] ?? true, }; fields.add(buildFormField(field)); }); // Ajout d'un séparateur entre les sections fields.add(const Divider(height: 32)); } // Section des champs de paiement if (currentStepData['fields'] != null && currentStepData['fields'].isNotEmpty) { fields.add( Container( margin: const EdgeInsets.only(bottom: 16, top: 8), child: const Row( children: [ Icon(Icons.payment, color: FormStyles.primaryColor, size: 20), SizedBox(width: 8), Text( 'Informations de paiement', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: FormStyles.textColor, ), ), ], ), ), ); } } // Ajout des champs normaux if (currentStepData['fields'] != null) { fields.addAll( currentStepData['fields'] .map((field) => buildFormField(field)) .toList(), ); } return [ Container( decoration: FormStyles.cardDecoration, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: fields, ), ), ), ]; } else { return serviceData!['fields'] .map((field) => buildFormField(field)) .toList(); } } Widget buildNavigationButtons() { if (serviceData!['steps'] == null) { return Container( margin: const EdgeInsets.only(top: 24), height: 48, width: double.infinity, child: ElevatedButton( onPressed: isLoading ? null : submitForm, style: FormStyles.elevatedButtonStyle, child: isLoading ? const SizedBox( height: 24, width: 24, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Text(serviceData!['button_service'] ?? 'Soumettre'), ), ); } // Récupérer le titre du bouton pour le step actuel var currentStepData = serviceData!['steps'][currentStep]; String buttonTitle = currentStepData['title_button'] ?? (currentStep == 0 ? 'Vérifier' : 'Payer'); return Container( margin: const EdgeInsets.only(top: 24), child: Row( children: [ if (currentStep == 1) Expanded( child: Padding( padding: const EdgeInsets.only(right: 8.0), child: SizedBox( height: 48, child: OutlinedButton( onPressed: isLoading ? null : () { _apiService.cancelOperation(); setState(() { currentStep = 0; verificationData = null; var step2Fields = serviceData!['steps'][1]; if (step2Fields['api_fields'] != null) { step2Fields['api_fields'].forEach(( fieldName, _, ) { formValues.remove(fieldName); controllers[fieldName]?.clear(); }); } }); }, style: FormStyles.outlinedButtonStyle, child: const Text('Retour'), ), ), ), ), Expanded( child: SizedBox( height: 48, child: ElevatedButton( onPressed: isLoading ? null : submitForm, style: FormStyles.elevatedButtonStyle, child: isLoading ? const SizedBox( height: 24, width: 24, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Colors.white, ), ), ) : Text( buttonTitle, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), ), ], ), ); } @override Widget build(BuildContext context) { // Obtenir les dimensions de l'écran final size = MediaQuery.of(context).size; final padding = MediaQuery.of(context).padding; final isSmallScreen = size.width < 360; // Pour les très petits écrans return Theme( data: Theme.of( context, ).copyWith(inputDecorationTheme: FormStyles.inputDecoration), child: Scaffold( backgroundColor: FormStyles.backgroundColor, appBar: AppBar( elevation: 0, centerTitle: true, title: Text( widget.serviceName, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white, letterSpacing: 0.5, ), ), leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () { Navigator.pop(context); }, ), backgroundColor: FormStyles.primaryColor, ), resizeToAvoidBottomInset: false, // Empêche le redimensionnement automatique body: LayoutBuilder( builder: (context, constraints) { // Calculer les dimensions adaptatives final maxWidth = constraints.maxWidth; final maxHeight = constraints.maxHeight; final horizontalPadding = maxWidth * 0.05; // 5% de la largeur final verticalPadding = maxHeight * 0.02; // 2% de la hauteur final fieldSpacing = maxHeight * 0.015; // Espacement entre les champs return isLoading ? const Center(child: CircularProgressIndicator()) : serviceData == null ? const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation( FormStyles.primaryColor, ), ), ) : serviceData == null ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.error_outline, size: 48, color: FormStyles.errorColor, ), const SizedBox(height: 16), const Text( 'Erreur de chargement des données', style: TextStyle( fontSize: 18, color: FormStyles.textColor, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 24), ElevatedButton( onPressed: fetchServiceFields, style: FormStyles.elevatedButtonStyle, child: const Text('Réessayer'), ), ], ), ) : SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: GestureDetector( // Ajout du GestureDetector ici onTap: () { // Fermer le clavier quand on clique n'importe où FocusScope.of(context).unfocus(); }, child: Padding( padding: EdgeInsets.only( left: horizontalPadding, right: horizontalPadding, top: verticalPadding, bottom: MediaQuery.of(context).viewInsets.bottom + maxHeight * 0.1, ), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ buildBanner(), Container( width: maxWidth * 0.9, child: buildProgressIndicator(), ), SizedBox(height: fieldSpacing), ...buildCurrentStepFields() .map( (field) => Container( width: maxWidth * 0.9, margin: EdgeInsets.only( bottom: fieldSpacing, ), child: field, ), ) .toList(), Container( width: maxWidth * 0.9, margin: EdgeInsets.only( top: fieldSpacing * 2, bottom: fieldSpacing, ), child: buildNavigationButtons(), ), SizedBox(height: maxHeight * 0.05), ], ), ), ), ), ); }, ), ), ); } }