- Added ParentRegisterStep4Screen for capturing motivation and CGU acceptance. - Integrated custom checkbox and text field widgets for better UI. - Implemented modal dialog for displaying CGU text. - Created ParentRegisterStep5Screen for summarizing registration data. - Added functionality to display parent and child details with edit options. - Included confirmation modal upon submission of the registration request.
487 lines
21 KiB
Dart
487 lines
21 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'dart:math' as math; // Pour la rotation du chevron
|
|
import 'package:flutter/gestures.dart'; // Pour PointerDeviceKind
|
|
import '../../../widgets/hover_relief_widget.dart'; // Import du nouveau widget
|
|
import 'package:image_picker/image_picker.dart';
|
|
// import 'package:image_cropper/image_cropper.dart'; // Supprimé
|
|
import 'dart:io' show File, Platform; // Ajout de Platform
|
|
import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb
|
|
import '../../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField
|
|
import '../../../widgets/app_custom_checkbox.dart'; // Import du nouveau widget Checkbox
|
|
import '../../../models/user_registration_data.dart'; // Import du modèle de données
|
|
import '../../../utils/data_generator.dart'; // Import du générateur
|
|
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
|
|
|
// La classe _ChildFormData est supprimée car on utilise ChildData du modèle
|
|
|
|
class ParentRegisterStep3Screen extends StatefulWidget {
|
|
final UserRegistrationData registrationData; // Accepte les données
|
|
|
|
const ParentRegisterStep3Screen({super.key, required this.registrationData});
|
|
|
|
@override
|
|
State<ParentRegisterStep3Screen> createState() => _ParentRegisterStep3ScreenState();
|
|
}
|
|
|
|
class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|
late UserRegistrationData _registrationData; // Stocke l'état complet
|
|
final ScrollController _scrollController = ScrollController(); // Pour le défilement horizontal
|
|
bool _isScrollable = false;
|
|
bool _showLeftFade = false;
|
|
bool _showRightFade = false;
|
|
static const double _fadeExtent = 0.05; // Pourcentage de fondu
|
|
|
|
// Liste ordonnée des couleurs de cartes pour les enfants
|
|
static const List<CardColorVertical> _childCardColors = [
|
|
CardColorVertical.lavender, // Premier enfant toujours lavande
|
|
CardColorVertical.pink,
|
|
CardColorVertical.peach,
|
|
CardColorVertical.lime,
|
|
CardColorVertical.red,
|
|
CardColorVertical.green,
|
|
CardColorVertical.blue,
|
|
];
|
|
|
|
// Garder une trace des couleurs déjà utilisées
|
|
final Set<CardColorVertical> _usedColors = {};
|
|
|
|
// Utilisation de GlobalKey pour les cartes enfants si validation complexe future
|
|
// Map<int, GlobalKey<FormState>> _childFormKeys = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_registrationData = widget.registrationData;
|
|
// Initialiser les couleurs utilisées avec les enfants existants
|
|
for (var child in _registrationData.children) {
|
|
_usedColors.add(child.cardColor);
|
|
}
|
|
// S'il n'y a pas d'enfant, en ajouter un automatiquement avec des données générées
|
|
if (_registrationData.children.isEmpty) {
|
|
_addChild();
|
|
}
|
|
_scrollController.addListener(_scrollListener);
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.removeListener(_scrollListener);
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _scrollListener() {
|
|
if (!_scrollController.hasClients) return;
|
|
final position = _scrollController.position;
|
|
final newIsScrollable = position.maxScrollExtent > 0.0;
|
|
final newShowLeftFade = newIsScrollable && position.pixels > (position.viewportDimension * _fadeExtent / 2);
|
|
final newShowRightFade = newIsScrollable && position.pixels < (position.maxScrollExtent - (position.viewportDimension * _fadeExtent / 2));
|
|
if (newIsScrollable != _isScrollable || newShowLeftFade != _showLeftFade || newShowRightFade != _showRightFade) {
|
|
setState(() {
|
|
_isScrollable = newIsScrollable;
|
|
_showLeftFade = newShowLeftFade;
|
|
_showRightFade = newShowRightFade;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _addChild() {
|
|
setState(() {
|
|
bool isUnborn = DataGenerator.boolean();
|
|
|
|
// Trouver la première couleur non utilisée
|
|
CardColorVertical cardColor = _childCardColors.firstWhere(
|
|
(color) => !_usedColors.contains(color),
|
|
orElse: () => _childCardColors[0], // Fallback sur la première couleur si toutes sont utilisées
|
|
);
|
|
|
|
final newChild = ChildData(
|
|
lastName: _registrationData.parent1.lastName,
|
|
firstName: DataGenerator.firstName(),
|
|
dob: DataGenerator.dob(isUnborn: isUnborn),
|
|
isUnbornChild: isUnborn,
|
|
photoConsent: DataGenerator.boolean(),
|
|
multipleBirth: DataGenerator.boolean(),
|
|
cardColor: cardColor,
|
|
);
|
|
_registrationData.addChild(newChild);
|
|
_usedColors.add(cardColor);
|
|
});
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_scrollListener();
|
|
if (_scrollController.hasClients && _scrollController.position.maxScrollExtent > 0.0) {
|
|
_scrollController.animateTo(_scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _removeChild(int index) {
|
|
if (_registrationData.children.length > 1 && index >= 0 && index < _registrationData.children.length) {
|
|
setState(() {
|
|
// Ne pas retirer la couleur de _usedColors pour éviter sa réutilisation
|
|
_registrationData.children.removeAt(index);
|
|
});
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
|
|
}
|
|
}
|
|
|
|
Future<void> _pickImage(int childIndex) async {
|
|
final ImagePicker picker = ImagePicker();
|
|
try {
|
|
final XFile? pickedFile = await picker.pickImage(
|
|
source: ImageSource.gallery, imageQuality: 70, maxWidth: 1024, maxHeight: 1024);
|
|
if (pickedFile != null) {
|
|
setState(() {
|
|
if (childIndex < _registrationData.children.length) {
|
|
_registrationData.children[childIndex].imageFile = File(pickedFile.path);
|
|
}
|
|
});
|
|
}
|
|
} catch (e) { print("Erreur image: $e"); }
|
|
}
|
|
|
|
Future<void> _selectDate(BuildContext context, int childIndex) async {
|
|
final ChildData currentChild = _registrationData.children[childIndex];
|
|
final DateTime now = DateTime.now();
|
|
DateTime initialDatePickerDate = now;
|
|
DateTime firstDatePickerDate = DateTime(1980); DateTime lastDatePickerDate = now;
|
|
|
|
if (currentChild.isUnbornChild) {
|
|
firstDatePickerDate = now; lastDatePickerDate = now.add(const Duration(days: 300));
|
|
if (currentChild.dob.isNotEmpty) {
|
|
try {
|
|
List<String> parts = currentChild.dob.split('/');
|
|
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
|
|
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
|
|
initialDatePickerDate = parsedDate;
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
} else {
|
|
if (currentChild.dob.isNotEmpty) {
|
|
try {
|
|
List<String> parts = currentChild.dob.split('/');
|
|
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
|
|
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
|
|
initialDatePickerDate = parsedDate;
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
final DateTime? picked = await showDatePicker(
|
|
context: context, initialDate: initialDatePickerDate, firstDate: firstDatePickerDate,
|
|
lastDate: lastDatePickerDate, locale: const Locale('fr', 'FR'),
|
|
);
|
|
if (picked != null) {
|
|
setState(() {
|
|
currentChild.dob = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final screenSize = MediaQuery.of(context).size;
|
|
return Scaffold(
|
|
body: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
|
|
),
|
|
Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text('Étape 3/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
'Informations Enfants',
|
|
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 30),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 150.0),
|
|
child: SizedBox(
|
|
height: 684.0,
|
|
child: ShaderMask(
|
|
shaderCallback: (Rect bounds) {
|
|
final Color leftFade = (_isScrollable && _showLeftFade) ? Colors.transparent : Colors.black;
|
|
final Color rightFade = (_isScrollable && _showRightFade) ? Colors.transparent : Colors.black;
|
|
if (!_isScrollable) { return LinearGradient(colors: const <Color>[Colors.black, Colors.black, Colors.black, Colors.black], stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0],).createShader(bounds); }
|
|
return LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: <Color>[ leftFade, Colors.black, Colors.black, rightFade ], stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0], ).createShader(bounds);
|
|
},
|
|
blendMode: BlendMode.dstIn,
|
|
child: Scrollbar(
|
|
controller: _scrollController,
|
|
thumbVisibility: true,
|
|
child: ListView.builder(
|
|
controller: _scrollController,
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
|
itemCount: _registrationData.children.length + 1,
|
|
itemBuilder: (context, index) {
|
|
if (index < _registrationData.children.length) {
|
|
// Carte Enfant
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 20.0),
|
|
child: _ChildCardWidget(
|
|
key: ValueKey(_registrationData.children[index].hashCode), // Utiliser une clé basée sur les données
|
|
childData: _registrationData.children[index],
|
|
childIndex: index,
|
|
onPickImage: () => _pickImage(index),
|
|
onDateSelect: () => _selectDate(context, index),
|
|
onFirstNameChanged: (value) => setState(() => _registrationData.children[index].firstName = value),
|
|
onLastNameChanged: (value) => setState(() => _registrationData.children[index].lastName = value),
|
|
onTogglePhotoConsent: (newValue) => setState(() => _registrationData.children[index].photoConsent = newValue),
|
|
onToggleMultipleBirth: (newValue) => setState(() => _registrationData.children[index].multipleBirth = newValue),
|
|
onToggleIsUnborn: (newValue) => setState(() {
|
|
_registrationData.children[index].isUnbornChild = newValue;
|
|
// Générer une nouvelle date si on change le statut
|
|
_registrationData.children[index].dob = DataGenerator.dob(isUnborn: newValue);
|
|
}),
|
|
onRemove: () => _removeChild(index),
|
|
canBeRemoved: _registrationData.children.length > 1,
|
|
),
|
|
);
|
|
} else {
|
|
// Bouton Ajouter
|
|
return Center(
|
|
child: HoverReliefWidget(
|
|
onPressed: _addChild,
|
|
borderRadius: BorderRadius.circular(15),
|
|
child: Image.asset('assets/images/plus.png', height: 80, width: 80),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
// Chevrons de navigation
|
|
Positioned(
|
|
top: screenSize.height / 2 - 20,
|
|
left: 40,
|
|
child: IconButton(
|
|
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
|
|
onPressed: () => Navigator.pop(context),
|
|
tooltip: 'Retour',
|
|
),
|
|
),
|
|
Positioned(
|
|
top: screenSize.height / 2 - 20,
|
|
right: 40,
|
|
child: IconButton(
|
|
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
|
onPressed: () {
|
|
// TODO: Validation (si nécessaire)
|
|
Navigator.pushNamed(context, '/parent-register/step4', arguments: _registrationData);
|
|
},
|
|
tooltip: 'Suivant',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Widget pour la carte enfant (adapté pour prendre ChildData et des callbacks)
|
|
class _ChildCardWidget extends StatefulWidget { // Transformé en StatefulWidget pour gérer les contrôleurs internes
|
|
final ChildData childData;
|
|
final int childIndex;
|
|
final VoidCallback onPickImage;
|
|
final VoidCallback onDateSelect;
|
|
final ValueChanged<String> onFirstNameChanged;
|
|
final ValueChanged<String> onLastNameChanged;
|
|
final ValueChanged<bool> onTogglePhotoConsent;
|
|
final ValueChanged<bool> onToggleMultipleBirth;
|
|
final ValueChanged<bool> onToggleIsUnborn;
|
|
final VoidCallback onRemove;
|
|
final bool canBeRemoved;
|
|
|
|
const _ChildCardWidget({
|
|
required Key key,
|
|
required this.childData,
|
|
required this.childIndex,
|
|
required this.onPickImage,
|
|
required this.onDateSelect,
|
|
required this.onFirstNameChanged,
|
|
required this.onLastNameChanged,
|
|
required this.onTogglePhotoConsent,
|
|
required this.onToggleMultipleBirth,
|
|
required this.onToggleIsUnborn,
|
|
required this.onRemove,
|
|
required this.canBeRemoved,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<_ChildCardWidget> createState() => _ChildCardWidgetState();
|
|
}
|
|
|
|
class _ChildCardWidgetState extends State<_ChildCardWidget> {
|
|
late TextEditingController _firstNameController;
|
|
late TextEditingController _lastNameController;
|
|
late TextEditingController _dobController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Initialiser les contrôleurs avec les données du widget
|
|
_firstNameController = TextEditingController(text: widget.childData.firstName);
|
|
_lastNameController = TextEditingController(text: widget.childData.lastName);
|
|
_dobController = TextEditingController(text: widget.childData.dob);
|
|
|
|
// Ajouter des listeners pour mettre à jour les données sources via les callbacks
|
|
_firstNameController.addListener(() => widget.onFirstNameChanged(_firstNameController.text));
|
|
_lastNameController.addListener(() => widget.onLastNameChanged(_lastNameController.text));
|
|
// Pour dob, la mise à jour se fait via _selectDate, pas besoin de listener ici
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant _ChildCardWidget oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
// Mettre à jour les contrôleurs si les données externes changent
|
|
// (peut arriver si on recharge l'état global)
|
|
if (widget.childData.firstName != _firstNameController.text) {
|
|
_firstNameController.text = widget.childData.firstName;
|
|
}
|
|
if (widget.childData.lastName != _lastNameController.text) {
|
|
_lastNameController.text = widget.childData.lastName;
|
|
}
|
|
if (widget.childData.dob != _dobController.text) {
|
|
_dobController.text = widget.childData.dob;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_firstNameController.dispose();
|
|
_lastNameController.dispose();
|
|
_dobController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final File? currentChildImage = widget.childData.imageFile;
|
|
// Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond
|
|
final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
|
|
? Colors.purple.shade200
|
|
: (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200); // Placeholder pour autres couleurs
|
|
final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90);
|
|
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
|
|
|
|
return Container(
|
|
width: 345.0 * 1.1, // 379.5
|
|
height: 570.0 * 1.2, // 684.0
|
|
padding: const EdgeInsets.all(22.0 * 1.1), // 24.2
|
|
decoration: BoxDecoration(
|
|
image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.cover),
|
|
borderRadius: BorderRadius.circular(20 * 1.1), // 22
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
HoverReliefWidget(
|
|
onPressed: widget.onPickImage,
|
|
borderRadius: BorderRadius.circular(10),
|
|
initialShadowColor: initialPhotoShadow,
|
|
hoverShadowColor: hoverPhotoShadow,
|
|
child: SizedBox(
|
|
height: 200.0,
|
|
width: 200.0,
|
|
child: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(5.0 * 1.1), // 5.5
|
|
child: currentChildImage != null
|
|
? ClipRRect(borderRadius: BorderRadius.circular(10 * 1.1), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover))
|
|
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12.0 * 1.1), // Augmenté pour plus d'espace après la photo
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)),
|
|
Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor),
|
|
],
|
|
),
|
|
const SizedBox(height: 9.0 * 1.1), // 9.9
|
|
CustomAppTextField(
|
|
controller: _firstNameController,
|
|
labelText: 'Prénom',
|
|
hintText: 'Facultatif si à naître',
|
|
isRequired: !widget.childData.isUnbornChild,
|
|
fieldHeight: 55.0 * 1.1, // 60.5
|
|
),
|
|
const SizedBox(height: 6.0 * 1.1), // 6.6
|
|
CustomAppTextField(
|
|
controller: _lastNameController,
|
|
labelText: 'Nom',
|
|
hintText: 'Nom de l\'enfant',
|
|
enabled: true,
|
|
fieldHeight: 55.0 * 1.1, // 60.5
|
|
),
|
|
const SizedBox(height: 9.0 * 1.1), // 9.9
|
|
CustomAppTextField(
|
|
controller: _dobController,
|
|
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
|
|
hintText: 'JJ/MM/AAAA',
|
|
readOnly: true,
|
|
onTap: widget.onDateSelect,
|
|
suffixIcon: Icons.calendar_today,
|
|
fieldHeight: 55.0 * 1.1, // 60.5
|
|
),
|
|
const SizedBox(height: 11.0 * 1.1), // 12.1
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
AppCustomCheckbox(
|
|
label: 'Consentement photo',
|
|
value: widget.childData.photoConsent,
|
|
onChanged: widget.onTogglePhotoConsent,
|
|
checkboxSize: 22.0 * 1.1, // 24.2
|
|
),
|
|
const SizedBox(height: 6.0 * 1.1), // 6.6
|
|
AppCustomCheckbox(
|
|
label: 'Naissance multiple',
|
|
value: widget.childData.multipleBirth,
|
|
onChanged: widget.onToggleMultipleBirth,
|
|
checkboxSize: 22.0 * 1.1, // 24.2
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
if (widget.canBeRemoved)
|
|
Positioned(
|
|
top: -5, right: -5,
|
|
child: InkWell(
|
|
onTap: widget.onRemove,
|
|
customBorder: const CircleBorder(),
|
|
child: Image.asset(
|
|
'images/red_cross2.png',
|
|
width: 36,
|
|
height: 36,
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |