petitspas/frontend/lib/screens/auth/parent_register_step3_screen.dart
Julien Martin df56ba11df feat(auth): Amélioration UI et logique inscription parent étape 3
- Ajout du switch "Enfant à naître" et ajustement du champ prénom.

- Amélioration de la gestion de l'affichage des photos (placeholder, kIsWeb).

- Refactorisation des boutons avec HoverReliefWidget.

- Localisation du DatePicker en français.

- Nettoyage de l'intégration (annulée) de image_cropper.

- Mise à jour de EVOLUTIONS_CDC.md.
2025-05-06 23:44:10 +02:00

472 lines
18 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 '../../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
// TODO: Créer un modèle de données pour l'enfant
// class ChildData { ... }
class ParentRegisterStep3Screen extends StatefulWidget {
const ParentRegisterStep3Screen({super.key});
@override
State<ParentRegisterStep3Screen> createState() => _ParentRegisterStep3ScreenState();
}
class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
// TODO: Gérer une liste d'enfants et leurs contrôleurs respectifs
// List<ChildData> _children = [ChildData()]; // Commencer avec un enfant
final _formKey = GlobalKey<FormState>(); // Une clé par enfant sera nécessaire si validation complexe
// Contrôleurs pour le premier enfant (pour l'instant)
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _dobController = TextEditingController();
bool _photoConsent = false;
bool _multipleBirth = false;
bool _isUnbornChild = false; // Nouvelle variable d'état
// TODO: Ajouter variable pour stocker l'image sélectionnée (par enfant)
// File? _childImage;
// File? _childImage; // Déjà présent et commenté
// Liste pour stocker les images des enfants (si gestion multi-enfants)
List<File?> _childImages = [null]; // Initialiser avec null pour le premier enfant
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_dobController.dispose();
// TODO: Disposer les contrôleurs de tous les enfants
super.dispose();
}
// TODO: Pré-remplir le nom de famille avec celui du parent 1
@override
void initState() {
super.initState();
}
Future<void> _selectDate(BuildContext context) async {
final DateTime now = DateTime.now();
DateTime initialDatePickerDate = now;
DateTime firstDatePickerDate = DateTime(1980);
DateTime lastDatePickerDate = now;
if (_isUnbornChild) {
firstDatePickerDate = now; // Ne peut pas être avant aujourd'hui si à naître
lastDatePickerDate = now.add(const Duration(days: 300)); // Environ 10 mois dans le futur
// Si une date de naissance avait été entrée, on la garde pour initialDate si elle est dans la nouvelle plage
if (_dobController.text.isNotEmpty) {
try {
// Tenter de parser la date existante
List<String> parts = _dobController.text.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) { /* Ignorer si le format est incorrect */ }
}
} else {
// Si une date prévisionnelle avait été entrée, on la garde pour initialDate si elle est dans la nouvelle plage
if (_dobController.text.isNotEmpty) {
try {
List<String> parts = _dobController.text.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) { /* Ignorer */ }
}
}
final DateTime? picked = await showDatePicker(
context: context,
initialDate: initialDatePickerDate,
firstDate: firstDatePickerDate,
lastDate: lastDatePickerDate,
locale: const Locale('fr', 'FR'),
);
if (picked != null) {
setState(() {
_dobController.text = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
});
}
}
// Méthode pour sélectionner une image
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) {
// On utilise directement le fichier sélectionné, sans recadrage
setState(() {
if (childIndex < _childImages.length) {
_childImages[childIndex] = File(pickedFile.path);
} else {
print("Erreur: Index d'enfant hors limites pour l'image.");
}
});
} // Fin de if (pickedFile != null)
} catch (e) {
print("Erreur lors de la sélection de l'image: $e");
}
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: [
// Fond papier
Positioned.fill(
child: Image.asset(
'assets/images/paper2.png',
fit: BoxFit.cover,
repeat: ImageRepeat.repeat,
),
),
// Contenu centré et scrollable
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0), // Ajout de padding vertical
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Indicateur d'étape
Text(
'Étape 3/X', // Mettre à jour le numéro d'étape total
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 10),
// Texte d'instruction
Text(
'Merci de renseigner les informations de/vos enfant(s) :',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
// Zone principale : Cartes enfants + Bouton Ajouter
// Utilisation d'une Row pour placer côte à côte comme sur la maquette
// Il faudra peut-être ajuster pour les petits écrans (Wrap?)
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, // CHANGED: pour centrer verticalement
children: [
// TODO: Remplacer par une ListView ou Column dynamique basée sur _children
// Pour l'instant, une seule carte
_buildChildCard(context, 0), // Index 0 pour le premier enfant
const SizedBox(width: 30),
HoverReliefWidget(
onPressed: () {
print("Ajouter un enfant via HoverReliefWidget");
// setState(() { _children.add(ChildData()); });
},
borderRadius: BorderRadius.circular(15),
child: Image.asset(
'assets/images/plus.png',
height: 80,
width: 80,
),
),
],
),
],
),
),
),
// Chevrons de navigation (identiques aux étapes précédentes)
// Chevron Gauche (Retour Step 2)
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), // Retour étape 2
tooltip: 'Retour',
),
),
// Chevron Droit (Suivant Step 4)
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
// TODO: Valider les infos enfants et Naviguer vers l'étape 4
print('Passer à l\'étape 4 (Situation familiale)');
// Navigator.pushNamed(context, '/parent-register/step4');
},
tooltip: 'Suivant',
),
),
],
),
);
}
// Widget pour construire UNE carte enfant
// L'index permettra de lier aux bons contrôleurs et données
Widget _buildChildCard(BuildContext context, int index) {
final File? currentChildImage = (index < _childImages.length) ? _childImages[index] : null;
// TODO: Déterminer la couleur de base de card_lavander.png et ajuster ces couleurs d'ombre
final Color baseLavandeColor = Colors.purple.shade200; // Placeholder pour la couleur de la carte lavande
final Color initialPhotoShadow = baseLavandeColor.withAlpha(90);
final Color hoverPhotoShadow = baseLavandeColor.withAlpha(130);
return Container(
width: 300,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
image: const DecorationImage(
image: AssetImage('assets/images/card_lavander.png'),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
HoverReliefWidget(
onPressed: () {
_pickImage(index);
},
borderRadius: BorderRadius.circular(10),
initialShadowColor: initialPhotoShadow, // Ombre lavande
hoverShadowColor: hoverPhotoShadow, // Ombre lavande au survol
child: SizedBox(
height: 100,
width: 100,
child: Center(
child: Padding(
padding: const EdgeInsets.all(5.0),
child: currentChildImage != null
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: kIsWeb // Condition pour le Web
? Image.network( // Utiliser Image.network pour le Web
currentChildImage.path,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
// Optionnel: Afficher un placeholder ou un message en cas d'erreur de chargement
print("Erreur de chargement de l'image réseau: $error");
return const Icon(Icons.broken_image, size: 40);
},
)
: Image.file( // Utiliser Image.file pour les autres plateformes
currentChildImage,
fit: BoxFit.cover,
),
)
: Image.asset(
'assets/images/photo.png',
fit: BoxFit.contain,
),
),
),
),
),
const SizedBox(height: 10), // Espace après la photo
// Nouveau Switch pour "Enfant à naître ?"
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // Aligner le label à gauche, switch à droite
children: [
Text(
'Enfant à naître ?',
style: GoogleFonts.merienda(fontSize: 14, fontWeight: FontWeight.w600),
),
Switch(
value: _isUnbornChild,
onChanged: (bool newValue) {
setState(() {
_isUnbornChild = newValue;
// Optionnel: Réinitialiser la date si le type change
// _dobController.clear();
});
},
activeColor: Theme.of(context).primaryColor, // Utiliser une couleur de thème
),
],
),
const SizedBox(height: 15), // Espace après le switch
_buildTextField(
_firstNameController,
'Prénom',
hintText: _isUnbornChild ? 'Prénom (optionnel)' : 'Prénom de l\'enfant', // HintText ajusté
isRequired: !_isUnbornChild,
),
const SizedBox(height: 10),
_buildTextField(_lastNameController, 'Nom', hintText: 'Nom de l\'enfant', enabled: true),
const SizedBox(height: 10),
_buildTextField(
_dobController,
_isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
hintText: 'JJ/MM/AAAA',
readOnly: true,
onTap: () => _selectDate(context),
suffixIcon: Icons.calendar_today,
),
const SizedBox(height: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCustomCheckbox(
label: 'Consentement photo',
value: _photoConsent,
onChanged: (newValue) {
setState(() => _photoConsent = newValue);
}
),
const SizedBox(height: 10),
_buildCustomCheckbox(
label: 'Naissance multiple',
value: _multipleBirth,
onChanged: (newValue) {
setState(() => _multipleBirth = newValue);
}
),
],
),
],
),
);
}
// Widget pour construire une checkbox personnalisée
Widget _buildCustomCheckbox({required String label, required bool value, required ValueChanged<bool> onChanged}) {
const double checkboxSize = 20.0;
const double checkmarkSizeFactor = 1.4; // Augmenté pour une coche plus grande
return GestureDetector(
onTap: () => onChanged(!value),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox( // Envelopper le Stack dans un SizedBox pour fixer sa taille
width: checkboxSize,
height: checkboxSize,
child: Stack(
alignment: Alignment.center,
clipBehavior: Clip.none,
children: [
Image.asset(
'assets/images/square.png',
height: checkboxSize, // Taille fixe
width: checkboxSize, // Taille fixe
),
if (value)
Image.asset(
'assets/images/coche.png',
height: checkboxSize * checkmarkSizeFactor,
width: checkboxSize * checkmarkSizeFactor,
),
],
),
),
const SizedBox(width: 10),
Text(label, style: GoogleFonts.merienda(fontSize: 14)),
],
),
);
}
// Widget pour construire les champs de texte (peut être externalisé)
// Ajout de onTap et suffixIcon pour le DatePicker
Widget _buildTextField(
TextEditingController controller,
String label, {
TextInputType? keyboardType,
bool obscureText = false,
String? hintText,
bool enabled = true,
bool readOnly = false,
VoidCallback? onTap,
IconData? suffixIcon,
bool isRequired = true, // Nouveau paramètre, par défaut à true
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: GoogleFonts.merienda(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87),
),
const SizedBox(height: 4),
Container(
height: 45, // Hauteur fixe pour correspondre à l'image de fond
decoration: const BoxDecoration(
image: DecorationImage( // Rétablir input_field_bg.png
image: AssetImage('assets/images/input_field_bg.png'),
fit: BoxFit.fill,
),
// Pas de borderRadius ici si l'image de fond les a déjà
),
child: TextFormField(
controller: controller,
keyboardType: keyboardType,
obscureText: obscureText,
enabled: enabled,
readOnly: readOnly,
onTap: onTap,
style: GoogleFonts.merienda(fontSize: 15, color: enabled ? Colors.black87 : Colors.grey),
textAlignVertical: TextAlignVertical.center,
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12), // Augmentation du padding vertical
hintText: hintText,
hintStyle: GoogleFonts.merienda(fontSize: 15, color: Colors.black38),
suffixIcon: suffixIcon != null ? Padding(
padding: const EdgeInsets.only(right: 8.0), // Espace pour l'icône
child: Icon(suffixIcon, color: Colors.black54, size: 20),
) : null,
isDense: true, // Aide à réduire la hauteur par défaut
),
validator: (value) {
if (!enabled) return null;
if (readOnly) return null;
if (isRequired && (value == null || value.isEmpty)) { // Validation conditionnée par isRequired
return 'Ce champ est obligatoire';
}
// TODO: Validations spécifiques (à garder si pertinent pour d'autres champs)
return null;
},
),
),
],
);
}
}