- 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.
472 lines
18 KiB
Dart
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;
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
} |