Files
wortis_tpe/lib/pages/form_service.dart

2349 lines
83 KiB
Dart

// 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<FormService> {
final ApiService _apiService = ApiService();
Map<String, dynamic>? serviceData;
Map<String, dynamic> formValues = {};
Map<String, TextEditingController> controllers = {};
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
bool isLoading = true;
int currentStep = 0;
Map<String, dynamic>? verificationData;
@override
void initState() {
super.initState();
ConnectivityManager(context).initConnectivity;
fetchServiceFields();
}
@override
void dispose() {
_apiService.cancelOperation();
controllers.forEach((_, controller) => controller.dispose());
super.dispose();
}
Map<String, dynamic> _normalizeApiData(Map<String, dynamic> data) {
try {
if (data['steps'] != null) {
for (var step in data['steps']) {
if (step['fields'] != null) {
List<Map<String, dynamic>> normalizedFields = [];
for (var field in step['fields']) {
normalizedFields.add(_normalizeField(field));
}
step['fields'] = normalizedFields;
}
if (step['api_fields'] != null) {
Map<String, dynamic> 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<Map<String, dynamic>> 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<String, dynamic> _normalizeField(Map<String, dynamic> field) {
Map<String, dynamic> 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<Map<String, String>>((option) {
return {
'value': option['value'] ?? option['valeur'] ?? '',
'label': option['label'] ?? option['étiquette'] ?? '',
};
}).toList();
}
if (field['dependencies'] != null) {
List<Map<String, dynamic>> normalizedDeps = [];
for (var dep in field['dependencies']) {
Map<String, dynamic> normalizedDep = {
'field': dep['field'] ?? dep['champ'] ?? '',
'value': dep['value'] ?? dep['valeur'] ?? '',
};
if (dep['options'] != null) {
normalizedDep['options'] =
dep['options'].map<Map<String, String>>((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<Widget>((entry) {
String fieldName = entry.key;
Map<String, dynamic> 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<void> _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<void> 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<DropdownMenuItem<String>> getDropdownOptions(
Map<String, dynamic> field,
) {
List<DropdownMenuItem<String>> 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<DropdownMenuItem<String>>((option) {
return DropdownMenuItem<String>(
value: option['value'],
child: Text(option['label']),
);
}).toList();
}
break;
}
}
} else if (field['options'] != null) {
items =
field['options'].map<DropdownMenuItem<String>>((option) {
return DropdownMenuItem<String>(
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<String, dynamic> 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<Color>(
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<Color>(
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<void> 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<AuthController>(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<String> 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<String> 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<String> 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<bool>(
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<String, dynamic> 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<void> verifyFirstStep() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
isLoading = true;
});
const operationId = 'verify_first_step';
try {
Map<String, dynamic> 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<String, dynamic> apiFields = nextStep['api_fields'];
apiFields.forEach((fieldName, fieldConfig) {
if (fieldConfig['key'] is List) {
if (fieldConfig['format'] == 'concat') {
List<String> 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<String, dynamic> 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<DropdownMenuItem<String>> 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<String>(
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<String, dynamic> field) {
String fieldName = field['name'];
// Initialisation avec le bon type
if (formValues[fieldName] == null) {
formValues[fieldName] = <Map<String, dynamic>>[];
}
List<Map<String, dynamic>> fieldValues = List<Map<String, dynamic>>.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<Widget>((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<String, dynamic> 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<Widget> buildCurrentStepFields() {
if (serviceData!['steps'] != null) {
var currentStepData = serviceData!['steps'][currentStep];
List<Widget> 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<String, dynamic> 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<String, dynamic> 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<Widget>((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<Widget>((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<Color>(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<Color>(
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<Color>(
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),
],
),
),
),
),
);
},
),
),
);
}
}