petitspas/frontend/lib/screens/auth/parent_register_step3_screen.dart
Julien Martin b99745e0fe feat(#112): reprise après refus — dossier complet, email resoumission
- GET/PATCH reprise-dossier enrichis (parents, enfants, motivation, fiche AM)
- Front: lien mail, modale identify login, wizards préremplis, PATCH complet
- Email accusé resoumission aux parents avec n° de dossier
- Fixes préremplissage AM (dates, places, ValueKey étape 2)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 19:19:55 +02:00

522 lines
22 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
),
),
),
],
);
}
}