2349 lines
83 KiB
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),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|