- Support des modes Desktop/Mobile et Édition/Lecture seule - Refactoring des widgets de formulaire (PersonalInfo, ProfessionalInfo, Presentation, ChildCard) - Mise à jour des écrans de récapitulatif (ParentStep5, AmStep4) - Ajout de navigation (Précédent/Soumettre) sur mobile Closes #78 Co-authored-by: Cursor <cursoragent@cursor.com>
497 lines
24 KiB
Dart
497 lines
24 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:image_picker/image_picker.dart';
|
|
import 'dart:io' show File;
|
|
import '../../widgets/hover_relief_widget.dart';
|
|
import '../../widgets/child_card_widget.dart';
|
|
import '../../widgets/custom_navigation_button.dart';
|
|
import '../../models/user_registration_data.dart';
|
|
import '../../utils/data_generator.dart';
|
|
import '../../models/card_assets.dart';
|
|
import '../../config/display_config.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
class ParentRegisterStep3Screen extends StatefulWidget {
|
|
// final UserRegistrationData registrationData; // Supprimé
|
|
|
|
const ParentRegisterStep3Screen({super.key /*, required this.registrationData */}); // Modifié
|
|
|
|
@override
|
|
_ParentRegisterStep3ScreenState createState() =>
|
|
_ParentRegisterStep3ScreenState();
|
|
}
|
|
|
|
class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|
// late UserRegistrationData _registrationData; // Supprimé
|
|
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();
|
|
final registrationData = Provider.of<UserRegistrationData>(context, listen: false);
|
|
// _registrationData = registrationData; // Supprimé
|
|
|
|
// 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 APRÈS le premier build
|
|
if (registrationData.children.isEmpty) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_addChild(registrationData);
|
|
});
|
|
}
|
|
_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(UserRegistrationData registrationData) { // Prend registrationData
|
|
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, UserRegistrationData registrationData) {
|
|
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, UserRegistrationData registrationData) async {
|
|
final ImagePicker picker = ImagePicker();
|
|
try {
|
|
final XFile? pickedFile = await picker.pickImage(
|
|
source: ImageSource.gallery, imageQuality: 70, maxWidth: 1024, maxHeight: 1024);
|
|
if (pickedFile != null) {
|
|
if (childIndex < registrationData.children.length) {
|
|
final oldChild = registrationData.children[childIndex];
|
|
final updatedChild = ChildData(
|
|
firstName: oldChild.firstName,
|
|
lastName: oldChild.lastName,
|
|
dob: oldChild.dob,
|
|
photoConsent: oldChild.photoConsent,
|
|
multipleBirth: oldChild.multipleBirth,
|
|
isUnbornChild: oldChild.isUnbornChild,
|
|
imageFile: File(pickedFile.path),
|
|
cardColor: oldChild.cardColor,
|
|
);
|
|
registrationData.updateChild(childIndex, updatedChild);
|
|
}
|
|
}
|
|
} catch (e) { print("Erreur image: $e"); }
|
|
}
|
|
|
|
Future<void> _selectDate(BuildContext context, int childIndex, UserRegistrationData registrationData) 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) {
|
|
final oldChild = registrationData.children[childIndex];
|
|
final updatedChild = ChildData(
|
|
firstName: oldChild.firstName,
|
|
lastName: oldChild.lastName,
|
|
dob: "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}",
|
|
photoConsent: oldChild.photoConsent,
|
|
multipleBirth: oldChild.multipleBirth,
|
|
isUnbornChild: oldChild.isUnbornChild,
|
|
imageFile: oldChild.imageFile,
|
|
cardColor: oldChild.cardColor,
|
|
);
|
|
registrationData.updateChild(childIndex, updatedChild);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final registrationData = Provider.of<UserRegistrationData>(context /*, listen: true par défaut */);
|
|
final screenSize = MediaQuery.of(context).size;
|
|
final config = DisplayConfig.fromContext(context, mode: DisplayMode.editable);
|
|
|
|
return Scaffold(
|
|
body: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
|
|
),
|
|
config.isMobile
|
|
? _buildMobileLayout(context, config, screenSize, registrationData)
|
|
: _buildDesktopLayout(context, config, screenSize, registrationData),
|
|
// Chevrons desktop uniquement
|
|
if (!config.isMobile) ...[
|
|
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: () {
|
|
if (context.canPop()) {
|
|
context.pop();
|
|
} else {
|
|
context.go('/parent-register-step2');
|
|
}
|
|
},
|
|
tooltip: 'Retour',
|
|
),
|
|
),
|
|
Positioned(
|
|
top: screenSize.height / 2 - 20,
|
|
right: 40,
|
|
child: IconButton(
|
|
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
|
onPressed: () {
|
|
context.go('/parent-register-step4');
|
|
},
|
|
tooltip: 'Suivant',
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Layout MOBILE : Cartes empilées verticalement
|
|
Widget _buildMobileLayout(BuildContext context, DisplayConfig config, Size screenSize, UserRegistrationData registrationData) {
|
|
return Column(
|
|
children: [
|
|
// Header fixe
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 20.0),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
'Étape 3/5',
|
|
style: GoogleFonts.merienda(fontSize: 13, color: Colors.black54),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'Informations Enfants',
|
|
style: GoogleFonts.merienda(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
// Liste scrollable des cartes + boutons
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
|
|
child: Column(
|
|
children: [
|
|
// Générer les cartes enfants
|
|
for (int index = 0; index < registrationData.children.length; index++) ...[
|
|
ChildCardWidget(
|
|
key: ValueKey(registrationData.children[index].hashCode),
|
|
childData: registrationData.children[index],
|
|
childIndex: index,
|
|
onPickImage: () => _pickImage(index, registrationData),
|
|
onDateSelect: () => _selectDate(context, index, registrationData),
|
|
onFirstNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
|
|
firstName: value, lastName: registrationData.children[index].lastName, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
|
|
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
|
|
))),
|
|
onLastNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
|
|
firstName: registrationData.children[index].firstName, lastName: value, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
|
|
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
|
|
))),
|
|
onTogglePhotoConsent: (newValue) {
|
|
final oldChild = registrationData.children[index];
|
|
registrationData.updateChild(index, ChildData(
|
|
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: newValue,
|
|
multipleBirth: oldChild.multipleBirth, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
|
));
|
|
},
|
|
onToggleMultipleBirth: (newValue) {
|
|
final oldChild = registrationData.children[index];
|
|
registrationData.updateChild(index, ChildData(
|
|
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: oldChild.photoConsent,
|
|
multipleBirth: newValue, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
|
));
|
|
},
|
|
onToggleIsUnborn: (newValue) {
|
|
final oldChild = registrationData.children[index];
|
|
registrationData.updateChild(index, ChildData(
|
|
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: DataGenerator.dob(isUnborn: newValue),
|
|
photoConsent: oldChild.photoConsent, multipleBirth: oldChild.multipleBirth, isUnbornChild: newValue,
|
|
imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
|
));
|
|
},
|
|
onRemove: () => _removeChild(index, registrationData),
|
|
canBeRemoved: registrationData.children.length > 1,
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
|
|
// Bouton "+" carré à la fin de la liste
|
|
Center(
|
|
child: HoverReliefWidget(
|
|
onPressed: () => _addChild(registrationData),
|
|
borderRadius: BorderRadius.circular(15),
|
|
child: Container(
|
|
width: 50,
|
|
height: 50,
|
|
child: Image.asset('assets/images/plus.png', fit: BoxFit.contain),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 30),
|
|
// Boutons navigation en bas du scroll
|
|
_buildMobileButtons(context, config, screenSize),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Layout DESKTOP : Scroll horizontal avec fondu
|
|
Widget _buildDesktopLayout(BuildContext context, DisplayConfig config, Size screenSize, UserRegistrationData registrationData) {
|
|
return 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, registrationData),
|
|
onDateSelect: () => _selectDate(context, index, registrationData),
|
|
onFirstNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
|
|
firstName: value, lastName: registrationData.children[index].lastName, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
|
|
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
|
|
))),
|
|
onLastNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
|
|
firstName: registrationData.children[index].firstName, lastName: value, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
|
|
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
|
|
))),
|
|
onTogglePhotoConsent: (newValue) {
|
|
final oldChild = registrationData.children[index];
|
|
registrationData.updateChild(index, ChildData(
|
|
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: newValue,
|
|
multipleBirth: oldChild.multipleBirth, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
|
));
|
|
},
|
|
onToggleMultipleBirth: (newValue) {
|
|
final oldChild = registrationData.children[index];
|
|
registrationData.updateChild(index, ChildData(
|
|
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: oldChild.photoConsent,
|
|
multipleBirth: newValue, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
|
));
|
|
},
|
|
onToggleIsUnborn: (newValue) {
|
|
final oldChild = registrationData.children[index];
|
|
registrationData.updateChild(index, ChildData(
|
|
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: DataGenerator.dob(isUnborn: newValue),
|
|
photoConsent: oldChild.photoConsent, multipleBirth: oldChild.multipleBirth, isUnbornChild: newValue,
|
|
imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
|
));
|
|
},
|
|
onRemove: () => _removeChild(index, registrationData),
|
|
canBeRemoved: registrationData.children.length > 1,
|
|
),
|
|
);
|
|
} else {
|
|
// Bouton Ajouter Desktop (Gros bouton)
|
|
return Center(
|
|
child: HoverReliefWidget(
|
|
onPressed: () => _addChild(registrationData),
|
|
borderRadius: BorderRadius.circular(15),
|
|
child: Image.asset('assets/images/plus.png', height: 100, width: 100, fit: BoxFit.contain),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Boutons navigation mobile
|
|
Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: HoverReliefWidget(
|
|
child: CustomNavigationButton(
|
|
text: 'Précédent',
|
|
style: NavigationButtonStyle.purple,
|
|
onPressed: () {
|
|
if (context.canPop()) {
|
|
context.pop();
|
|
} else {
|
|
context.go('/parent-register-step2');
|
|
}
|
|
},
|
|
width: double.infinity,
|
|
height: 50,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: HoverReliefWidget(
|
|
child: CustomNavigationButton(
|
|
text: 'Suivant',
|
|
style: NavigationButtonStyle.green,
|
|
onPressed: () {
|
|
context.go('/parent-register-step4');
|
|
},
|
|
width: double.infinity,
|
|
height: 50,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
} |