Charge existingPhotoUrl dans les cartes enfant (étapes 3 et 5) et pré-coche le consentement photo lorsqu'une photo est déjà en base. Co-authored-by: Cursor <cursoragent@cursor.com>
522 lines
22 KiB
Dart
522 lines
22 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 'package:flutter/foundation.dart' show kIsWeb;
|
||
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 '../../models/card_assets.dart';
|
||
import '../../config/display_config.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import 'package:p_tits_pas/utils/name_format_utils.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();
|
||
}
|
||
|
||
/// Même logique que nom / prénom parent : normalisation avant passage à l’étape suivante.
|
||
void _normalizeChildrenNamesAndGoToStep4(
|
||
BuildContext context,
|
||
UserRegistrationData registrationData,
|
||
) {
|
||
for (var i = 0; i < registrationData.children.length; i++) {
|
||
final c = registrationData.children[i];
|
||
final fn = formatPersonNameCase(c.firstName);
|
||
final ln = formatPersonNameCase(c.lastName);
|
||
if (fn != c.firstName || ln != c.lastName) {
|
||
registrationData.updateChild(i, c.copyWith(firstName: fn, lastName: ln));
|
||
}
|
||
}
|
||
context.go('/parent-register-step4');
|
||
}
|
||
|
||
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(() {
|
||
// 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: '',
|
||
dob: '',
|
||
isUnbornChild: false,
|
||
photoConsent: false,
|
||
multipleBirth: false,
|
||
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: 60,
|
||
maxWidth: 900,
|
||
maxHeight: 900,
|
||
);
|
||
if (pickedFile != null) {
|
||
if (childIndex < registrationData.children.length) {
|
||
final oldChild = registrationData.children[childIndex];
|
||
final bytes = await pickedFile.readAsBytes();
|
||
if (bytes.isEmpty) return;
|
||
File? file;
|
||
if (!kIsWeb) {
|
||
try {
|
||
final f = File(pickedFile.path);
|
||
if (await f.exists()) file = f;
|
||
} catch (_) {}
|
||
}
|
||
final updatedChild = oldChild.copyWith(
|
||
imageBytes: bytes,
|
||
imageFile: file,
|
||
existingPhotoUrl: null,
|
||
);
|
||
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 = oldChild.copyWith(
|
||
dob: "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}",
|
||
);
|
||
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('parent_register_child_$index'),
|
||
childData: registrationData.children[index],
|
||
childIndex: index,
|
||
onPickImage: () => _pickImage(index, registrationData),
|
||
onClearImage: () => setState(() {
|
||
final c = registrationData.children[index];
|
||
registrationData.updateChild(
|
||
index, c.copyWith(imageFile: null, imageBytes: null, existingPhotoUrl: null));
|
||
}),
|
||
onDateSelect: () => _selectDate(context, index, registrationData),
|
||
onFirstNameChanged: (value) => setState(() {
|
||
final c = registrationData.children[index];
|
||
registrationData.updateChild(index, c.copyWith(firstName: value));
|
||
}),
|
||
onLastNameChanged: (value) => setState(() {
|
||
final c = registrationData.children[index];
|
||
registrationData.updateChild(index, c.copyWith(lastName: value));
|
||
}),
|
||
onGenreChanged: (value) => setState(() {
|
||
final c = registrationData.children[index];
|
||
registrationData.updateChild(index, c.copyWith(genre: value));
|
||
}),
|
||
onTogglePhotoConsent: (newValue) {
|
||
final oldChild = registrationData.children[index];
|
||
registrationData.updateChild(index, oldChild.copyWith(photoConsent: newValue));
|
||
},
|
||
onToggleIsUnborn: (newValue) {
|
||
final oldChild = registrationData.children[index];
|
||
var g = oldChild.genre;
|
||
if (!newValue && g == 'Autre') {
|
||
g = '';
|
||
}
|
||
registrationData.updateChild(
|
||
index,
|
||
oldChild.copyWith(isUnbornChild: newValue, genre: g),
|
||
);
|
||
},
|
||
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, registrationData),
|
||
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('parent_register_child_$index'),
|
||
childData: registrationData.children[index],
|
||
childIndex: index,
|
||
onPickImage: () => _pickImage(index, registrationData),
|
||
onClearImage: () => setState(() {
|
||
final c = registrationData.children[index];
|
||
registrationData.updateChild(
|
||
index, c.copyWith(imageFile: null, imageBytes: null, existingPhotoUrl: null));
|
||
}),
|
||
onDateSelect: () => _selectDate(context, index, registrationData),
|
||
onFirstNameChanged: (value) => setState(() {
|
||
final c = registrationData.children[index];
|
||
registrationData.updateChild(index, c.copyWith(firstName: value));
|
||
}),
|
||
onLastNameChanged: (value) => setState(() {
|
||
final c = registrationData.children[index];
|
||
registrationData.updateChild(index, c.copyWith(lastName: value));
|
||
}),
|
||
onGenreChanged: (value) => setState(() {
|
||
final c = registrationData.children[index];
|
||
registrationData.updateChild(index, c.copyWith(genre: value));
|
||
}),
|
||
onTogglePhotoConsent: (newValue) {
|
||
final oldChild = registrationData.children[index];
|
||
registrationData.updateChild(index, oldChild.copyWith(photoConsent: newValue));
|
||
},
|
||
onToggleIsUnborn: (newValue) {
|
||
final oldChild = registrationData.children[index];
|
||
var g = oldChild.genre;
|
||
if (!newValue && g == 'Autre') {
|
||
g = '';
|
||
}
|
||
registrationData.updateChild(
|
||
index,
|
||
oldChild.copyWith(isUnbornChild: newValue, genre: g),
|
||
);
|
||
},
|
||
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,
|
||
UserRegistrationData registrationData,
|
||
) {
|
||
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: () {
|
||
_normalizeChildrenNamesAndGoToStep4(context, registrationData);
|
||
},
|
||
width: double.infinity,
|
||
height: 50,
|
||
fontSize: 16,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
} |