feat: ajout du parcours complet d’inscription parent avec UI harmonisée et gestion centralisée des données

This commit is contained in:
Julien Martin 2025-05-12 16:42:24 +02:00
parent d3663a28ad
commit 5156f4fefb
61 changed files with 2807 additions and 278 deletions

2
.gitignore vendored
View File

@ -46,6 +46,8 @@ coverage/
*.tmp
*.temp
.cache/
Archives/**
Xcf/**
# Release notes
CHANGELOG.md

View File

@ -236,4 +236,22 @@ Pour chaque évolution identifiée, ce document suivra la structure suivante :
- Messages d'erreur clairs en cas de :
- Email non trouvé
- Lien expiré
- Mot de passe non conforme
- Mot de passe non conforme
## X. Amélioration de la Gestion des Photos Utilisateurs (Proposition)
### X.1 Recadrage et Redimensionnement des Photos
#### X.1.1 Fonctionnalités
- **Contexte :** Lors du téléchargement de photos par les utilisateurs (photos de profil, photos d'enfants).
- **Besoin :** Permettre à l'utilisateur de recadrer l'image (notamment en format carré pour les avatars) et potentiellement de la faire pivoter ou de zoomer avant son enregistrement final.
- **Objectif :** Améliorer l'expérience utilisateur, assurer une meilleure qualité et cohérence visuelle des images stockées et affichées dans l'application.
#### X.1.2 Solution Technique Envisagée (pour discussion)
- L'intégration d'une librairie Flutter tierce dédiée au recadrage d'image (par exemple, `image_cropper` ou `crop_image`) sera nécessaire après la sélection initiale de l'image via `image_picker`.
- La tentative initiale avec `image_cropper` (version 5.0.1) a rencontré des difficultés techniques d'intégration (erreur "Too many positional arguments" persistante avec `AndroidUiSettings`) et a été mise en attente. Une investigation plus approfondie ou l'évaluation d'alternatives sera requise.
#### X.1.3 Impact sur l'application
- Modification du flux de sélection d'image dans les écrans concernés (ex: `parent_register_step3_screen.dart`).
- Ajout potentiel de nouvelles dépendances et configurations spécifiques aux plateformes.
- Mise à jour de la documentation utilisateur si cette fonctionnalité est implémentée.

View File

@ -0,0 +1,32 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Suppression de l'activité pour image_cropper -->
<!--
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
-->
<!-- Don't delete the meta-data below. -->
<meta-data

View File

@ -0,0 +1,44 @@
package io.flutter.plugins;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
/**
* Generated file. Do not edit.
* This file is generated by the Flutter tool based on the
* plugins that support the Android platform.
*/
@Keep
public final class GeneratedPluginRegistrant {
private static final String TAG = "GeneratedPluginRegistrant";
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin image_picker_android, io.flutter.plugins.imagepicker.ImagePickerPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
}
}
}

View File

@ -0,0 +1 @@
flutter.sdk=C:\\Users\\marti\\dev\\flutter

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

View File

@ -1,44 +1,43 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'screens/auth/login_screen.dart';
import 'screens/legal/legal_page.dart';
import 'screens/legal/privacy_page.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation
// import 'package:provider/provider.dart'; // Supprimer Provider
import 'navigation/app_router.dart';
// import 'theme/app_theme.dart'; // Supprimer AppTheme
// import 'theme/theme_provider.dart'; // Supprimer ThemeProvider
void main() => runApp(const PtiPasApp());
void main() {
runApp(const MyApp()); // Exécution simple
}
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (_, __) => const LoginPage(),
),
GoRoute(
path: '/legal',
builder: (_, __) => const LegalPage(),
),
GoRoute(
path: '/privacy',
builder: (_, __) => const PrivacyPage(),
),
],
);
class PtiPasApp extends StatelessWidget {
const PtiPasApp({super.key});
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
// Pas besoin de Provider.of ici
return MaterialApp(
title: 'P\'titsPas',
routerConfig: _router,
debugShowCheckedModeBanner: false,
theme: ThemeData(
fontFamily: 'Merienda',
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF8AD0C8),
brightness: Brightness.light,
theme: ThemeData.light().copyWith( // Utiliser un thème simple par défaut
textTheme: GoogleFonts.meriendaTextTheme(
ThemeData.light().textTheme,
),
// TODO: Définir les couleurs principales si besoin
),
localizationsDelegates: const [ // Configuration pour la localisation
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [ // Langues supportées
Locale('fr', 'FR'), // Français
// Locale('en', 'US'), // Anglais, si besoin
],
locale: const Locale('fr', 'FR'), // Forcer la locale française par défaut
initialRoute: AppRouter.login,
onGenerateRoute: AppRouter.generateRoute,
debugShowCheckedModeBanner: false,
);
}
}

View File

@ -0,0 +1,25 @@
enum CardColorVertical {
red('assets/cards/card_red.png'),
pink('assets/cards/card_pink.png'),
peach('assets/cards/card_peach.png'),
lime('assets/cards/card_lime.png'),
lavender('assets/cards/card_lavender.png'),
green('assets/cards/card_green.png'),
blue('assets/cards/card_blue.png');
final String path;
const CardColorVertical(this.path);
}
enum CardColorHorizontal {
red('assets/cards/card_red_h.png'),
pink('assets/cards/card_pink_h.png'),
peach('assets/cards/card_peach_h.png'),
lime('assets/cards/card_lime_h.png'),
lavender('assets/cards/card_lavender_h.png'),
green('assets/cards/card_green_h.png'),
blue('assets/cards/card_blue_h.png');
final String path;
const CardColorHorizontal(this.path);
}

View File

@ -0,0 +1,97 @@
import 'dart:io'; // Pour File
import '../models/card_assets.dart'; // Import de l'enum CardColorVertical
class ParentData {
String firstName;
String lastName;
String address; // Rue et numéro
String postalCode; // Ajout
String city; // Ajout
String phone;
String email;
String password; // Peut-être pas nécessaire pour le récap, mais pour la création initiale si
File? profilePicture; // Chemin ou objet File
ParentData({
this.firstName = '',
this.lastName = '',
this.address = '', // Rue
this.postalCode = '', // Ajout
this.city = '', // Ajout
this.phone = '',
this.email = '',
this.password = '',
this.profilePicture,
});
}
class ChildData {
String firstName;
String lastName;
String dob; // Date de naissance ou prévisionnelle
bool photoConsent;
bool multipleBirth;
bool isUnbornChild;
File? imageFile;
CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte
ChildData({
this.firstName = '',
this.lastName = '',
this.dob = '',
this.photoConsent = false,
this.multipleBirth = false,
this.isUnbornChild = false,
this.imageFile,
required this.cardColor, // Rendre requis dans le constructeur
});
}
class UserRegistrationData {
ParentData parent1;
ParentData? parent2; // Optionnel
List<ChildData> children;
String motivationText;
bool cguAccepted;
UserRegistrationData({
ParentData? parent1Data,
this.parent2,
List<ChildData>? childrenData,
this.motivationText = '',
this.cguAccepted = false,
}) : parent1 = parent1Data ?? ParentData(),
children = childrenData ?? [];
// Méthode pour ajouter/mettre à jour le parent 1
void updateParent1(ParentData data) {
parent1 = data;
}
// Méthode pour ajouter/mettre à jour le parent 2
void updateParent2(ParentData? data) {
parent2 = data;
}
// Méthode pour ajouter un enfant
void addChild(ChildData child) {
children.add(child);
}
// Méthode pour mettre à jour un enfant (si nécessaire plus tard)
void updateChild(int index, ChildData child) {
if (index >= 0 && index < children.length) {
children[index] = child;
}
}
// Mettre à jour la motivation
void updateMotivation(String text) {
motivationText = text;
}
// Accepter les CGU
void acceptCGU() {
cguAccepted = true;
}
}

View File

@ -1,30 +1,105 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../screens/auth/login_screen.dart';
import '../screens/auth/parent_register_screen.dart';
import '../screens/auth/register_choice_screen.dart';
import '../screens/auth/parent_register_step1_screen.dart';
import '../screens/auth/parent_register_step2_screen.dart';
import '../screens/auth/parent_register_step3_screen.dart';
import '../screens/auth/parent_register_step4_screen.dart';
import '../screens/auth/parent_register_step5_screen.dart';
import '../screens/home/home_screen.dart';
import '../models/user_registration_data.dart';
class AppRouter {
static const String login = '/login';
static const String parentRegister = '/parent-register';
static const String registerChoice = '/register-choice';
static const String parentRegisterStep1 = '/parent-register/step1';
static const String parentRegisterStep2 = '/parent-register/step2';
static const String parentRegisterStep3 = '/parent-register/step3';
static const String parentRegisterStep4 = '/parent-register/step4';
static const String parentRegisterStep5 = '/parent-register/step5';
static const String home = '/home';
static Route<dynamic> generateRoute(RouteSettings settings) {
Widget screen;
bool slideTransition = false;
Object? args = settings.arguments;
Widget buildErrorScreen(String step) {
print("Erreur: Données UserRegistrationData manquantes ou de mauvais type pour l'étape $step");
return const ParentRegisterStep1Screen();
}
switch (settings.name) {
case login:
return MaterialPageRoute(builder: (_) => const LoginScreen());
case parentRegister:
return MaterialPageRoute(builder: (_) => const ParentRegisterScreen());
screen = const LoginPage();
break;
case registerChoice:
screen = const RegisterChoiceScreen();
slideTransition = true;
break;
case parentRegisterStep1:
screen = const ParentRegisterStep1Screen();
slideTransition = true;
break;
case parentRegisterStep2:
if (args is UserRegistrationData) {
screen = ParentRegisterStep2Screen(registrationData: args);
} else {
screen = buildErrorScreen('2');
}
slideTransition = true;
break;
case parentRegisterStep3:
if (args is UserRegistrationData) {
screen = ParentRegisterStep3Screen(registrationData: args);
} else {
screen = buildErrorScreen('3');
}
slideTransition = true;
break;
case parentRegisterStep4:
if (args is UserRegistrationData) {
screen = ParentRegisterStep4Screen(registrationData: args);
} else {
screen = buildErrorScreen('4');
}
slideTransition = true;
break;
case parentRegisterStep5:
if (args is UserRegistrationData) {
screen = ParentRegisterStep5Screen(registrationData: args);
} else {
screen = buildErrorScreen('5');
}
slideTransition = true;
break;
case home:
return MaterialPageRoute(builder: (_) => const HomeScreen());
screen = const HomeScreen();
break;
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('Route non définie: ${settings.name}'),
),
screen = Scaffold(
body: Center(
child: Text('Route non définie : ${settings.name}'),
),
);
}
if (slideTransition) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => screen,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween);
return SlideTransition(position: offsetAnimation, child: child);
},
transitionDuration: const Duration(milliseconds: 400),
);
} else {
return MaterialPageRoute(builder: (_) => screen);
}
}
}

View File

@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:url_launcher/url_launcher.dart';
import 'package:p_tits_pas/services/bug_report_service.dart';
import 'package:go_router/go_router.dart';
import '../../widgets/image_button.dart';
import '../../widgets/custom_app_text_field.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@ -103,68 +105,40 @@ class _LoginPageState extends State<LoginPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Labels au-dessus des champs
// Champs côte à côte
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
'Email',
style: GoogleFonts.merienda(
fontSize: 20,
color: Colors.black87,
fontWeight: FontWeight.w600,
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 20),
child: Text(
'Mot de passe',
style: GoogleFonts.merienda(
fontSize: 20,
color: Colors.black87,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
const SizedBox(height: 10),
// Champs côte à côte
Row(
children: [
Expanded(
child: _ImageTextField(
bg: 'assets/images/field_email.png',
width: 400,
height: 53,
hint: 'Email',
child: CustomAppTextField(
controller: _emailController,
labelText: 'Email',
hintText: 'Votre adresse email',
validator: _validateEmail,
style: CustomAppTextFieldStyle.lavande,
fieldHeight: 53,
fieldWidth: double.infinity,
),
),
const SizedBox(width: 20),
Expanded(
child: _ImageTextField(
bg: 'assets/images/field_password.png',
width: 400,
height: 53,
hint: 'Mot de passe',
obscure: true,
child: CustomAppTextField(
controller: _passwordController,
labelText: 'Mot de passe',
hintText: 'Votre mot de passe',
obscureText: true,
validator: _validatePassword,
style: CustomAppTextFieldStyle.jaune,
fieldHeight: 53,
fieldWidth: double.infinity,
),
),
],
),
const SizedBox(height: 20), // Réduit l'espacement
const SizedBox(height: 20),
// Bouton centré
Center(
child: _ImageButton(
child: ImageButton(
bg: 'assets/images/btn_green.png',
width: 300,
height: 40,
@ -199,7 +173,7 @@ class _LoginPageState extends State<LoginPage> {
Center(
child: TextButton(
onPressed: () {
Navigator.pushNamed(context, '/parent-register');
Navigator.pushNamed(context, '/register-choice');
},
child: Text(
'Créer un compte',
@ -393,120 +367,6 @@ class ImageDimensions {
ImageDimensions({required this.width, required this.height});
}
//
// Champ texte avec fond image
//
class _ImageTextField extends StatelessWidget {
final String bg;
final double width;
final double height;
final String hint;
final bool obscure;
final TextEditingController? controller;
final String? Function(String?)? validator;
const _ImageTextField({
required this.bg,
required this.width,
required this.height,
required this.hint,
this.obscure = false,
this.controller,
this.validator,
});
@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(bg),
fit: BoxFit.fill,
),
),
child: TextFormField(
controller: controller,
obscureText: obscure,
textAlign: TextAlign.left,
style: GoogleFonts.merienda(
fontSize: height * 0.25,
color: Colors.black87,
),
validator: validator,
decoration: InputDecoration(
border: InputBorder.none,
hintText: hint,
hintStyle: GoogleFonts.merienda(
fontSize: height * 0.25,
color: Colors.black38,
),
contentPadding: EdgeInsets.symmetric(
horizontal: width * 0.1,
vertical: height * 0.3,
),
errorStyle: GoogleFonts.merienda(
fontSize: height * 0.2,
color: Colors.red,
),
),
),
);
}
}
//
// Bouton avec fond image
//
class _ImageButton extends StatelessWidget {
final String bg;
final double width;
final double height;
final String text;
final Color textColor;
final VoidCallback onPressed;
const _ImageButton({
required this.bg,
required this.width,
required this.height,
required this.text,
required this.textColor,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(bg),
fit: BoxFit.fill,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
child: Center(
child: Text(
text,
style: GoogleFonts.merienda(
fontSize: height * 0.4,
color: textColor,
fontWeight: FontWeight.w600,
),
),
),
),
),
);
}
}
//
// Lien du pied de page
//

View File

@ -0,0 +1,226 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../models/user_registration_data.dart'; // Import du modèle de données
import '../../utils/data_generator.dart'; // Import du générateur de données
import '../../widgets/custom_app_text_field.dart'; // Import du widget CustomAppTextField
import '../../models/card_assets.dart'; // Import des enums de cartes
class ParentRegisterStep1Screen extends StatefulWidget {
const ParentRegisterStep1Screen({super.key});
@override
State<ParentRegisterStep1Screen> createState() => _ParentRegisterStep1ScreenState();
}
class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
final _formKey = GlobalKey<FormState>();
late UserRegistrationData _registrationData;
// Contrôleurs pour les champs (restauration CP et Ville)
final _lastNameController = TextEditingController();
final _firstNameController = TextEditingController();
final _phoneController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _addressController = TextEditingController(); // Rue seule
final _postalCodeController = TextEditingController(); // Restauré
final _cityController = TextEditingController(); // Restauré
@override
void initState() {
super.initState();
_registrationData = UserRegistrationData();
_generateAndFillData();
}
void _generateAndFillData() {
final String genFirstName = DataGenerator.firstName();
final String genLastName = DataGenerator.lastName();
// Utilisation des méthodes publiques de DataGenerator
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
_firstNameController.text = genFirstName;
_lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName);
_passwordController.text = DataGenerator.password();
_confirmPasswordController.text = _passwordController.text;
}
@override
void dispose() {
_lastNameController.dispose();
_firstNameController.dispose();
_phoneController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_addressController.dispose();
_postalCodeController.dispose();
_cityController.dispose();
super.dispose();
}
@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é
Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Indicateur d'étape (à rendre dynamique)
Text(
'Étape 1/5',
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 10),
// Texte d'instruction
Text(
'Informations du Parent Principal',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
// Carte jaune contenant le formulaire
Container(
width: screenSize.width * 0.6,
padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50),
constraints: const BoxConstraints(minHeight: 570),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(CardColorHorizontal.peach.path),
fit: BoxFit.fill,
),
),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Créez votre mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) {
if (value == null || value.isEmpty) return 'Mot de passe requis';
if (value.length < 6) return '6 caractères minimum';
return null;
})),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmez le mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) {
if (value == null || value.isEmpty) return 'Confirmation requise';
if (value != _passwordController.text) return 'Ne correspond pas';
return null;
})),
],
),
const SizedBox(height: 20),
CustomAppTextField(
controller: _addressController,
labelText: 'Adresse (N° et Rue)',
hintText: 'Numéro et nom de votre rue',
style: CustomAppTextFieldStyle.beige,
fieldWidth: double.infinity,
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
const SizedBox(width: 20),
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
],
),
),
),
],
),
),
),
// Chevron de navigation gauche (Retour)
Positioned(
top: screenSize.height / 2 - 20, // Centré verticalement
left: 40,
child: IconButton(
icon: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(math.pi), // Inverse horizontalement
child: Image.asset('assets/images/chevron_right.png', height: 40),
),
onPressed: () => Navigator.pop(context), // Retour à l'écran de choix
tooltip: 'Retour',
),
),
// Chevron de navigation droit (Suivant)
Positioned(
top: screenSize.height / 2 - 20, // Centré verticalement
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
_registrationData.updateParent1(
ParentData(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
address: _addressController.text, // Rue
postalCode: _postalCodeController.text, // Ajout
city: _cityController.text, // Ajout
phone: _phoneController.text,
email: _emailController.text,
password: _passwordController.text,
)
);
Navigator.pushNamed(context, '/parent-register/step2', arguments: _registrationData);
}
},
tooltip: 'Suivant',
),
),
],
),
);
}
}

View File

@ -0,0 +1,255 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../models/user_registration_data.dart'; // Import du modèle
import '../../utils/data_generator.dart'; // Import du générateur
import '../../widgets/custom_app_text_field.dart'; // Import du widget
import '../../models/card_assets.dart'; // Import des enums de cartes
class ParentRegisterStep2Screen extends StatefulWidget {
final UserRegistrationData registrationData; // Accepte les données de l'étape 1
const ParentRegisterStep2Screen({super.key, required this.registrationData});
@override
State<ParentRegisterStep2Screen> createState() => _ParentRegisterStep2ScreenState();
}
class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
final _formKey = GlobalKey<FormState>();
late UserRegistrationData _registrationData; // Copie locale pour modification
bool _addParent2 = true; // Pour le test, on ajoute toujours le parent 2
bool _sameAddressAsParent1 = false; // Peut être généré aléatoirement aussi
// Contrôleurs pour les champs du parent 2 (restauration CP et Ville)
final _lastNameController = TextEditingController();
final _firstNameController = TextEditingController();
final _phoneController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _addressController = TextEditingController(); // Rue seule
final _postalCodeController = TextEditingController(); // Restauré
final _cityController = TextEditingController(); // Restauré
@override
void initState() {
super.initState();
_registrationData = widget.registrationData; // Récupère les données de l'étape 1
if (_addParent2) {
_generateAndFillParent2Data();
}
}
void _generateAndFillParent2Data() {
final String genFirstName = DataGenerator.firstName();
final String genLastName = DataGenerator.lastName();
_firstNameController.text = genFirstName;
_lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName);
_passwordController.text = DataGenerator.password();
_confirmPasswordController.text = _passwordController.text;
_sameAddressAsParent1 = DataGenerator.boolean();
if (!_sameAddressAsParent1) {
// Générer adresse, CP, Ville séparément
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
} else {
// Vider les champs si même adresse (seront désactivés)
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
}
}
@override
void dispose() {
_lastNameController.dispose();
_firstNameController.dispose();
_phoneController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_addressController.dispose();
_postalCodeController.dispose();
_cityController.dispose();
super.dispose();
}
bool get _parent2FieldsEnabled => _addParent2;
bool get _addressFieldsEnabled => _addParent2 && !_sameAddressAsParent1;
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Étape 2/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 10),
Text(
'Informations du Deuxième Parent (Optionnel)',
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Container(
width: screenSize.width * 0.6,
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(CardColorHorizontal.blue.path), fit: BoxFit.fill),
),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 12,
child: Row(children: [
const Icon(Icons.person_add_alt_1, size: 20), const SizedBox(width: 8),
Flexible(child: Text('Ajouter Parent 2 ?', style: GoogleFonts.merienda(fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis)),
const Spacer(),
Switch(value: _addParent2, onChanged: (val) => setState(() {
_addParent2 = val ?? false;
if (_addParent2) _generateAndFillParent2Data(); else _clearParent2Fields();
}), activeColor: Theme.of(context).primaryColor),
]),
),
Expanded(flex: 1, child: const SizedBox()),
Expanded(
flex: 12,
child: Row(children: [
Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey),
const SizedBox(width: 8),
Flexible(child: Text('Même Adresse ?', style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), overflow: TextOverflow.ellipsis)),
const Spacer(),
Switch(value: _sameAddressAsParent1, onChanged: _addParent2 ? (val) => setState(() {
_sameAddressAsParent1 = val ?? false;
if (_sameAddressAsParent1) {
_addressController.text = _registrationData.parent1.address;
_postalCodeController.text = _registrationData.parent1.postalCode;
_cityController.text = _registrationData.parent1.city;
} else {
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
}
}) : null, activeColor: Theme.of(context).primaryColor),
]),
),
]),
const SizedBox(height: 25),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Nom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Prénom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son téléphone', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son email', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Son mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v.length < 6 ? '6 car. min' : null)) : null)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmer mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v != _passwordController.text ? 'Différent' : null)) : null)),
],
),
const SizedBox(height: 20),
CustomAppTextField(controller: _addressController, labelText: 'Adresse (N° et Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
const SizedBox(width: 20),
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
],
),
),
),
),
],
),
),
),
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),
tooltip: 'Retour',
),
),
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
if (!_addParent2 || (_formKey.currentState?.validate() ?? false)) {
if (_addParent2) {
_registrationData.updateParent2(
ParentData(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
address: _sameAddressAsParent1 ? _registrationData.parent1.address : _addressController.text,
postalCode: _sameAddressAsParent1 ? _registrationData.parent1.postalCode : _postalCodeController.text,
city: _sameAddressAsParent1 ? _registrationData.parent1.city : _cityController.text,
phone: _phoneController.text,
email: _emailController.text,
password: _passwordController.text,
)
);
} else {
_registrationData.updateParent2(null);
}
Navigator.pushNamed(context, '/parent-register/step3', arguments: _registrationData);
}
},
tooltip: 'Suivant',
),
),
],
),
);
}
void _clearParent2Fields() {
_formKey.currentState?.reset();
_lastNameController.clear(); _firstNameController.clear(); _phoneController.clear();
_emailController.clear(); _passwordController.clear(); _confirmPasswordController.clear();
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
_sameAddressAsParent1 = false;
setState(() {});
}
}

View File

@ -0,0 +1,487 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import 'package:flutter/gestures.dart'; // Pour PointerDeviceKind
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
import '../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField
import '../../widgets/app_custom_checkbox.dart'; // Import du nouveau widget Checkbox
import '../../models/user_registration_data.dart'; // Import du modèle de données
import '../../utils/data_generator.dart'; // Import du générateur
import '../../models/card_assets.dart'; // Import des enums de cartes
// La classe _ChildFormData est supprimée car on utilise ChildData du modèle
class ParentRegisterStep3Screen extends StatefulWidget {
final UserRegistrationData registrationData; // Accepte les données
const ParentRegisterStep3Screen({super.key, required this.registrationData});
@override
State<ParentRegisterStep3Screen> createState() => _ParentRegisterStep3ScreenState();
}
class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
late UserRegistrationData _registrationData; // Stocke l'état complet
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();
_registrationData = widget.registrationData;
// 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 avec des données générées
if (_registrationData.children.isEmpty) {
_addChild();
}
_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() {
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) {
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) async {
final ImagePicker picker = ImagePicker();
try {
final XFile? pickedFile = await picker.pickImage(
source: ImageSource.gallery, imageQuality: 70, maxWidth: 1024, maxHeight: 1024);
if (pickedFile != null) {
setState(() {
if (childIndex < _registrationData.children.length) {
_registrationData.children[childIndex].imageFile = File(pickedFile.path);
}
});
}
} catch (e) { print("Erreur image: $e"); }
}
Future<void> _selectDate(BuildContext context, int childIndex) 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) {
setState(() {
currentChild.dob = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
});
}
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
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),
onDateSelect: () => _selectDate(context, index),
onFirstNameChanged: (value) => setState(() => _registrationData.children[index].firstName = value),
onLastNameChanged: (value) => setState(() => _registrationData.children[index].lastName = value),
onTogglePhotoConsent: (newValue) => setState(() => _registrationData.children[index].photoConsent = newValue),
onToggleMultipleBirth: (newValue) => setState(() => _registrationData.children[index].multipleBirth = newValue),
onToggleIsUnborn: (newValue) => setState(() {
_registrationData.children[index].isUnbornChild = newValue;
// Générer une nouvelle date si on change le statut
_registrationData.children[index].dob = DataGenerator.dob(isUnborn: newValue);
}),
onRemove: () => _removeChild(index),
canBeRemoved: _registrationData.children.length > 1,
),
);
} else {
// Bouton Ajouter
return Center(
child: HoverReliefWidget(
onPressed: _addChild,
borderRadius: BorderRadius.circular(15),
child: Image.asset('assets/images/plus.png', height: 80, width: 80),
),
);
}
},
),
),
),
),
),
const SizedBox(height: 20),
],
),
),
// Chevrons de navigation
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),
tooltip: 'Retour',
),
),
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
// TODO: Validation (si nécessaire)
Navigator.pushNamed(context, '/parent-register/step4', arguments: _registrationData);
},
tooltip: 'Suivant',
),
),
],
),
);
}
}
// Widget pour la carte enfant (adapté pour prendre ChildData et des callbacks)
class _ChildCardWidget extends StatefulWidget { // Transformé en StatefulWidget pour gérer les contrôleurs internes
final ChildData childData;
final int childIndex;
final VoidCallback onPickImage;
final VoidCallback onDateSelect;
final ValueChanged<String> onFirstNameChanged;
final ValueChanged<String> onLastNameChanged;
final ValueChanged<bool> onTogglePhotoConsent;
final ValueChanged<bool> onToggleMultipleBirth;
final ValueChanged<bool> onToggleIsUnborn;
final VoidCallback onRemove;
final bool canBeRemoved;
const _ChildCardWidget({
required Key key,
required this.childData,
required this.childIndex,
required this.onPickImage,
required this.onDateSelect,
required this.onFirstNameChanged,
required this.onLastNameChanged,
required this.onTogglePhotoConsent,
required this.onToggleMultipleBirth,
required this.onToggleIsUnborn,
required this.onRemove,
required this.canBeRemoved,
}) : super(key: key);
@override
State<_ChildCardWidget> createState() => _ChildCardWidgetState();
}
class _ChildCardWidgetState extends State<_ChildCardWidget> {
late TextEditingController _firstNameController;
late TextEditingController _lastNameController;
late TextEditingController _dobController;
@override
void initState() {
super.initState();
// Initialiser les contrôleurs avec les données du widget
_firstNameController = TextEditingController(text: widget.childData.firstName);
_lastNameController = TextEditingController(text: widget.childData.lastName);
_dobController = TextEditingController(text: widget.childData.dob);
// Ajouter des listeners pour mettre à jour les données sources via les callbacks
_firstNameController.addListener(() => widget.onFirstNameChanged(_firstNameController.text));
_lastNameController.addListener(() => widget.onLastNameChanged(_lastNameController.text));
// Pour dob, la mise à jour se fait via _selectDate, pas besoin de listener ici
}
@override
void didUpdateWidget(covariant _ChildCardWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// Mettre à jour les contrôleurs si les données externes changent
// (peut arriver si on recharge l'état global)
if (widget.childData.firstName != _firstNameController.text) {
_firstNameController.text = widget.childData.firstName;
}
if (widget.childData.lastName != _lastNameController.text) {
_lastNameController.text = widget.childData.lastName;
}
if (widget.childData.dob != _dobController.text) {
_dobController.text = widget.childData.dob;
}
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_dobController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final File? currentChildImage = widget.childData.imageFile;
// Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond
final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
? Colors.purple.shade200
: (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200); // Placeholder pour autres couleurs
final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90);
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
return Container(
width: 345.0 * 1.1, // 379.5
height: 570.0 * 1.2, // 684.0
padding: const EdgeInsets.all(22.0 * 1.1), // 24.2
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.cover),
borderRadius: BorderRadius.circular(20 * 1.1), // 22
),
child: Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
HoverReliefWidget(
onPressed: widget.onPickImage,
borderRadius: BorderRadius.circular(10),
initialShadowColor: initialPhotoShadow,
hoverShadowColor: hoverPhotoShadow,
child: SizedBox(
height: 200.0,
width: 200.0,
child: Center(
child: Padding(
padding: const EdgeInsets.all(5.0 * 1.1), // 5.5
child: currentChildImage != null
? ClipRRect(borderRadius: BorderRadius.circular(10 * 1.1), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover))
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
),
),
),
),
const SizedBox(height: 12.0 * 1.1), // Augmenté pour plus d'espace après la photo
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)),
Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor),
],
),
const SizedBox(height: 9.0 * 1.1), // 9.9
CustomAppTextField(
controller: _firstNameController,
labelText: 'Prénom',
hintText: 'Facultatif si à naître',
isRequired: !widget.childData.isUnbornChild,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 6.0 * 1.1), // 6.6
CustomAppTextField(
controller: _lastNameController,
labelText: 'Nom',
hintText: 'Nom de l\'enfant',
enabled: true,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 9.0 * 1.1), // 9.9
CustomAppTextField(
controller: _dobController,
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
hintText: 'JJ/MM/AAAA',
readOnly: true,
onTap: widget.onDateSelect,
suffixIcon: Icons.calendar_today,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 11.0 * 1.1), // 12.1
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppCustomCheckbox(
label: 'Consentement photo',
value: widget.childData.photoConsent,
onChanged: widget.onTogglePhotoConsent,
checkboxSize: 22.0 * 1.1, // 24.2
),
const SizedBox(height: 6.0 * 1.1), // 6.6
AppCustomCheckbox(
label: 'Naissance multiple',
value: widget.childData.multipleBirth,
onChanged: widget.onToggleMultipleBirth,
checkboxSize: 22.0 * 1.1, // 24.2
),
],
),
],
),
if (widget.canBeRemoved)
Positioned(
top: -5, right: -5,
child: InkWell(
onTap: widget.onRemove,
customBorder: const CircleBorder(),
child: Image.asset(
'images/red_cross2.png',
width: 36,
height: 36,
fit: BoxFit.contain,
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,217 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:p_tits_pas/widgets/custom_decorated_text_field.dart'; // Import du nouveau widget
import 'dart:math' as math; // Pour la rotation du chevron
import 'package:p_tits_pas/widgets/app_custom_checkbox.dart'; // Import de la checkbox personnalisée
// import 'package:p_tits_pas/models/placeholder_registration_data.dart'; // Remplacé
import '../../models/user_registration_data.dart'; // Import du vrai modèle
import '../../utils/data_generator.dart'; // Import du générateur
import '../../models/card_assets.dart'; // Import des enums de cartes
class ParentRegisterStep4Screen extends StatefulWidget {
final UserRegistrationData registrationData; // Accepte les données
const ParentRegisterStep4Screen({super.key, required this.registrationData});
@override
State<ParentRegisterStep4Screen> createState() => _ParentRegisterStep4ScreenState();
}
class _ParentRegisterStep4ScreenState extends State<ParentRegisterStep4Screen> {
late UserRegistrationData _registrationData; // État local
final _motivationController = TextEditingController();
bool _cguAccepted = true; // Pour le test, CGU acceptées par défaut
@override
void initState() {
super.initState();
_registrationData = widget.registrationData;
_motivationController.text = DataGenerator.motivation(); // Générer la motivation
}
@override
void dispose() {
_motivationController.dispose();
super.dispose();
}
void _showCGUModal() {
// Un long texte Lorem Ipsum pour simuler les CGU
const String loremIpsumText = '''
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna.
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna. Etiam et felis dolor.
Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
''';
showDialog<void>(
context: context,
barrierDismissible: false, // L'utilisateur doit utiliser le bouton
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
'Conditions Générales d\'Utilisation',
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
),
content: SizedBox(
width: MediaQuery.of(dialogContext).size.width * 0.7, // 70% de la largeur de l'écran
height: MediaQuery.of(dialogContext).size.height * 0.6, // 60% de la hauteur de l'écran
child: SingleChildScrollView(
child: Text(
loremIpsumText,
style: GoogleFonts.merienda(fontSize: 13),
textAlign: TextAlign.justify,
),
),
),
actionsPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
actionsAlignment: MainAxisAlignment.center,
actions: <Widget>[
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(dialogContext).primaryColor,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
),
child: Text(
'Valider et Accepter',
style: GoogleFonts.merienda(fontSize: 15, color: Colors.white, fontWeight: FontWeight.bold),
),
onPressed: () {
Navigator.of(dialogContext).pop(); // Ferme la modale
setState(() {
_cguAccepted = true; // Met à jour l'état
});
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final cardWidth = screenSize.width * 0.6; // Largeur de la carte (60% de l'écran)
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
final cardHeight = cardWidth / imageAspectRatio;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Étape 4/5',
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 20),
Text(
'Motivation de votre demande',
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Container(
width: cardWidth,
height: cardHeight,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(CardColorHorizontal.green.path),
fit: BoxFit.fill,
),
),
child: Padding(
padding: const EdgeInsets.all(40.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: CustomDecoratedTextField(
controller: _motivationController,
hintText: 'Écrivez ici pour motiver votre demande...',
fieldHeight: cardHeight * 0.6,
maxLines: 10,
expandDynamically: true,
fontSize: 18.0,
),
),
const SizedBox(height: 20),
GestureDetector(
onTap: () {
if (!_cguAccepted) {
_showCGUModal();
}
},
child: AppCustomCheckbox(
label: 'J\'accepte les conditions générales d\'utilisation',
value: _cguAccepted,
onChanged: (newValue) {
if (!_cguAccepted) {
_showCGUModal();
} else {
setState(() => _cguAccepted = false);
}
},
),
),
],
),
),
),
],
),
),
),
// Chevrons de navigation
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),
tooltip: 'Retour',
),
),
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: _cguAccepted
? () {
_registrationData.updateMotivation(_motivationController.text);
_registrationData.acceptCGU();
Navigator.pushNamed(
context,
'/parent-register/step5',
arguments: _registrationData
);
}
: null,
tooltip: 'Suivant',
),
),
],
),
);
}
}

View File

@ -0,0 +1,464 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../models/user_registration_data.dart'; // Utilisation du vrai modèle
import '../../widgets/image_button.dart'; // Import du ImageButton
import '../../models/card_assets.dart'; // Import des enums de cartes
import 'package:flutter/foundation.dart' show kIsWeb;
import '../../widgets/custom_decorated_text_field.dart'; // Import du CustomDecoratedTextField
// Nouvelle méthode helper pour afficher un champ de type "lecture seule" stylisé
Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) {
const FontWeight labelFontWeight = FontWeight.w600;
// Ne pas afficher le label si labelFontSize est 0 ou si label est vide
bool showLabel = label.isNotEmpty && labelFontSize > 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showLabel)
Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)),
if (showLabel)
const SizedBox(height: 4),
// Utiliser Expanded si multiLine et pas de hauteur fixe, sinon Container
multiLine && fieldHeight == null
? Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
decoration: BoxDecoration(
image: const DecorationImage(
image: AssetImage('assets/images/input_field_bg.png'),
fit: BoxFit.fill,
),
),
child: SingleChildScrollView( // Pour le défilement si le texte dépasse
child: Text(
value.isNotEmpty ? value : '-',
style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0), // Garder une taille de texte par défaut si label caché
maxLines: null, // Permettre un nombre illimité de lignes
),
),
),
)
: Container(
width: double.infinity,
height: multiLine ? null : fieldHeight,
constraints: multiLine ? BoxConstraints(minHeight: fieldHeight) : null,
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
decoration: BoxDecoration(
image: const DecorationImage(
image: AssetImage('assets/images/input_field_bg.png'),
fit: BoxFit.fill,
),
),
child: Text(
value.isNotEmpty ? value : '-',
style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0),
maxLines: multiLine ? null : 1,
overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis,
),
),
],
);
}
class ParentRegisterStep5Screen extends StatelessWidget {
final UserRegistrationData registrationData;
const ParentRegisterStep5Screen({super.key, required this.registrationData});
// Méthode pour construire la carte Parent 1
Widget _buildParent1Card(BuildContext context, ParentData data) {
const double verticalSpacing = 28.0; // Espacement vertical augmenté
const double labelFontSize = 22.0; // Taille de label augmentée
List<Widget> details = [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)),
],
),
const SizedBox(height: verticalSpacing),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)),
],
),
const SizedBox(height: verticalSpacing),
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize),
];
return _SummaryCard(
backgroundImagePath: CardColorHorizontal.peach.path,
title: 'Parent Principal',
content: details,
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step1', arguments: registrationData),
);
}
// Méthode pour construire la carte Parent 2
Widget _buildParent2Card(BuildContext context, ParentData data) {
const double verticalSpacing = 28.0;
const double labelFontSize = 22.0;
List<Widget> details = [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)),
],
),
const SizedBox(height: verticalSpacing),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)),
],
),
const SizedBox(height: verticalSpacing),
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize),
];
return _SummaryCard(
backgroundImagePath: CardColorHorizontal.blue.path,
title: 'Deuxième Parent',
content: details,
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step2', arguments: registrationData),
);
}
// Méthode pour construire les cartes Enfants
List<Widget> _buildChildrenCards(BuildContext context, List<ChildData> children) {
return children.asMap().entries.map((entry) {
int index = entry.key;
ChildData child = entry.value;
CardColorHorizontal cardColorHorizontal = CardColorHorizontal.values.firstWhere(
(e) => e.name == child.cardColor.name,
orElse: () => CardColorHorizontal.lavender,
);
return Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Stack(
children: [
AspectRatio(
aspectRatio: 2.0,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(cardColorHorizontal.path),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(15),
),
child: Column(
children: [
// Titre centré dans la carte
Row(
children: [
Expanded(
child: Text(
'Enfant ${index + 1}' + (child.isUnbornChild ? ' (à naître)' : ''),
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
),
IconButton(
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
onPressed: () {
Navigator.of(context).pushNamed(
'/parent-register/step3',
arguments: registrationData,
);
},
tooltip: 'Modifier',
),
],
),
const SizedBox(height: 18),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// IMAGE SANS CADRE BLANC, PREND LA HAUTEUR
Expanded(
flex: 1,
child: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: AspectRatio(
aspectRatio: 1,
child: (child.imageFile != null)
? (kIsWeb
? Image.network(child.imageFile!.path, fit: BoxFit.cover)
: Image.file(child.imageFile!, fit: BoxFit.cover))
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
),
),
),
),
const SizedBox(width: 32),
// INFOS À DROITE (2/3)
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildDisplayFieldValue(context, 'Prénom :', child.firstName, labelFontSize: 22.0),
const SizedBox(height: 12),
_buildDisplayFieldValue(context, 'Nom :', child.lastName, labelFontSize: 22.0),
const SizedBox(height: 12),
_buildDisplayFieldValue(context, child.isUnbornChild ? 'Date de naissance :' : 'Date de naissance :', child.dob, labelFontSize: 22.0),
],
),
),
],
),
),
const SizedBox(height: 18),
// Ligne des consentements
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Checkbox(
value: child.photoConsent,
onChanged: null,
),
Text('Consentement photo', style: GoogleFonts.merienda(fontSize: 16)),
],
),
const SizedBox(width: 32),
Row(
children: [
Checkbox(
value: child.multipleBirth,
onChanged: null,
),
Text('Naissance multiple', style: GoogleFonts.merienda(fontSize: 16)),
],
),
],
),
],
),
),
),
],
),
);
}).toList();
}
// Méthode pour construire la carte Motivation
Widget _buildMotivationCard(BuildContext context, String motivation) {
return _SummaryCard(
backgroundImagePath: CardColorHorizontal.green.path,
title: 'Votre Motivation',
content: [
Expanded(
child: CustomDecoratedTextField(
controller: TextEditingController(text: motivation),
hintText: 'Aucune motivation renseignée.',
fieldHeight: 200,
maxLines: 10,
expandDynamically: true,
readOnly: true,
fontSize: 18.0,
),
),
],
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step4', arguments: registrationData),
);
}
// Helper pour afficher une ligne de détail (police et agencement amélioré)
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"$label: ",
style: GoogleFonts.merienda(fontSize: 18, fontWeight: FontWeight.w600),
),
Expanded(
child: Text(
value.isNotEmpty ? value : '-',
style: GoogleFonts.merienda(fontSize: 18),
softWrap: true,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran)
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
final cardHeight = cardWidth / imageAspectRatio;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0), // Padding horizontal supprimé ici
child: Padding( // Ajout du Padding horizontal externe
padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Étape 5/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 20),
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
const SizedBox(height: 30),
_buildParent1Card(context, registrationData.parent1),
const SizedBox(height: 20),
if (registrationData.parent2 != null) ...[
_buildParent2Card(context, registrationData.parent2!),
const SizedBox(height: 20),
],
..._buildChildrenCards(context, registrationData.children),
_buildMotivationCard(context, registrationData.motivationText),
const SizedBox(height: 40),
ImageButton(
bg: 'assets/images/btn_green.png',
text: 'Soumettre ma demande',
textColor: const Color(0xFF2D6A4F),
width: 350,
height: 50,
fontSize: 18,
onPressed: () {
print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}");
_showConfirmationModal(context);
},
),
],
),
),
),
),
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () => Navigator.pop(context), // Retour à l'étape 4
tooltip: 'Retour',
),
),
],
),
);
}
void _showConfirmationModal(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
'Demande enregistrée',
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
),
content: Text(
'Votre dossier a bien été pris en compte. Un gestionnaire le validera bientôt.',
style: GoogleFonts.merienda(fontSize: 14),
),
actions: <Widget>[
TextButton(
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
onPressed: () {
Navigator.of(dialogContext).pop(); // Ferme la modale
// TODO: Naviguer vers l'écran de connexion ou tableau de bord
Navigator.of(context).pushNamedAndRemoveUntil('/login', (Route<dynamic> route) => false);
},
),
],
);
},
);
}
}
// Widget générique _SummaryCard (ajusté)
class _SummaryCard extends StatelessWidget {
final String backgroundImagePath;
final String title;
final List<Widget> content;
final VoidCallback onEdit;
const _SummaryCard({
super.key,
required this.backgroundImagePath,
required this.title,
required this.content,
required this.onEdit,
});
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 2.0,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(backgroundImagePath),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(15),
),
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
title,
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
),
IconButton(
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
onPressed: onEdit,
tooltip: 'Modifier',
),
],
),
const SizedBox(height: 18),
Expanded(
child: Column(
children: content,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,162 @@
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 widget générique
import '../../models/card_assets.dart'; // Import des enums de cartes
class RegisterChoiceScreen extends StatelessWidget {
const RegisterChoiceScreen({super.key});
@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,
),
),
// Bouton Retour (chevron gauche)
Positioned(
top: 40,
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),
tooltip: 'Retour',
),
),
// Contenu principal en Row (Gauche / Droite)
Padding(
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
child: Row(
children: [
// Partie Gauche: Texte d'instruction centré
Expanded(
flex: 1,
child: Center(
child: Text(
'Veuillez choisir votre\ntype de compte :',
style: GoogleFonts.merienda(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.black87,
height: 1.5,
),
textAlign: TextAlign.center,
),
),
),
// Espace entre les deux parties
SizedBox(width: screenSize.width * 0.05),
// Partie Droite: Carte rose avec les boutons
Expanded(
flex: 1,
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: screenSize.height * 0.78, // Augmenté pour éviter l'overflow
),
child: AspectRatio(
aspectRatio: 2 / 3,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(CardColorVertical.pink.path),
fit: BoxFit.fill,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Bouton "Parents" avec HoverReliefWidget appliqué uniquement à l'image
_buildChoiceButton(
context: context,
iconPath: 'assets/images/icon_parents.png',
label: 'Parents',
onPressed: () {
Navigator.pushNamed(context, '/parent-register/step1');
},
),
// Bouton "Assistante Maternelle" avec HoverReliefWidget appliqué uniquement à l'image
_buildChoiceButton(
context: context,
iconPath: 'assets/images/icon_assmat.png',
label: 'Assistante Maternelle',
onPressed: () {
// TODO: Naviguer vers l'écran d'inscription assmat
print('Choix: Assistante Maternelle');
},
),
],
),
),
),
),
),
),
],
),
),
],
),
);
}
}
// Nouvelle méthode helper pour construire les boutons de choix
Widget _buildChoiceButton({
required BuildContext context,
required String iconPath,
required String label,
required VoidCallback onPressed,
}) {
// TODO: Déterminer la couleur de base de card_rose.png et ajuster ces couleurs d'ombre
final Color baseRoseColor = Colors.pink.shade300; // Placeholder
final Color initialShadow = baseRoseColor.withAlpha(90); // Rose plus foncé et transparent pour l'ombre initiale
final Color hoverShadow = baseRoseColor.withAlpha(130); // Rose encore plus foncé pour l'ombre au survol
return Column(
mainAxisSize: MainAxisSize.min,
children: [
HoverReliefWidget(
onPressed: onPressed,
borderRadius: BorderRadius.circular(15.0),
initialShadowColor: initialShadow, // Ombre rose initiale
hoverShadowColor: hoverShadow, // Ombre rose au survol
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Image.asset(iconPath, height: 140),
),
),
const SizedBox(height: 15),
Text(
label,
style: GoogleFonts.merienda(
fontSize: 26,
fontWeight: FontWeight.w600,
color: Colors.black.withOpacity(0.85),
),
textAlign: TextAlign.center,
),
],
);
}
// --- La classe HoverChoiceButton peut maintenant être supprimée si elle n'est plus utilisée ailleurs ---
// class HoverChoiceButton extends StatefulWidget { ... }
// class _HoverChoiceButtonState extends State<HoverChoiceButton> { ... }

View File

@ -1,50 +1,13 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../theme/theme_provider.dart';
import '../../theme/app_theme.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
String _getThemeName(ThemeType type) {
switch (type) {
case ThemeType.defaultTheme:
return "P'titsPas";
case ThemeType.pastelTheme:
return "Pastel";
case ThemeType.darkTheme:
return "Sombre";
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Accueil'),
actions: [
Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: DropdownButton<ThemeType>(
value: themeProvider.currentTheme,
items: ThemeType.values.map((ThemeType type) {
return DropdownMenuItem<ThemeType>(
value: type,
child: Text(_getThemeName(type)),
);
}).toList(),
onChanged: (ThemeType? newValue) {
if (newValue != null) {
themeProvider.setTheme(newValue);
}
},
),
);
},
),
],
),
body: const Center(
child: Text('Bienvenue sur P\'titsPas !'),

View File

@ -1,8 +1,6 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/user.dart';
import '../models/parent.dart';
import '../models/child.dart';
class AuthService {
static const String _usersKey = 'users';
@ -27,32 +25,6 @@ class AuthService {
throw Exception('Mode démonstration - Inscription désactivée');
}
// Méthode pour s'inscrire en tant que parent (mode démonstration)
Future<void> registerParent({
required String email,
required String password,
required String firstName,
required String lastName,
required String phoneNumber,
required String address,
required String city,
required String postalCode,
String? presentation,
required bool hasAcceptedCGU,
String? partnerFirstName,
String? partnerLastName,
String? partnerEmail,
String? partnerPhoneNumber,
String? partnerAddress,
String? partnerCity,
String? partnerPostalCode,
required List<Map<String, dynamic>> children,
required String motivation,
}) async {
// En mode démonstration, on ne fait rien
await Future.delayed(const Duration(seconds: 2)); // Simule un délai de traitement
}
// Méthode pour se déconnecter (mode démonstration)
static Future<void> logout() async {
// Ne fait rien en mode démonstration

View File

@ -0,0 +1,65 @@
import 'dart:math';
class DataGenerator {
static final Random _random = Random();
static final List<String> _firstNames = [
'Alice', 'Bob', 'Charlie', 'David', 'Eva', 'Félix', 'Gabrielle', 'Hugo', 'Inès', 'Jules',
'Léa', 'Manon', 'Nathan', 'Oscar', 'Pauline', 'Quentin', 'Raphaël', 'Sophie', 'Théo', 'Victoire'
];
static final List<String> _lastNames = [
'Martin', 'Bernard', 'Dubois', 'Thomas', 'Robert', 'Richard', 'Petit', 'Durand', 'Leroy', 'Moreau',
'Simon', 'Laurent', 'Lefebvre', 'Michel', 'Garcia', 'David', 'Bertrand', 'Roux', 'Vincent', 'Fournier'
];
static final List<String> _addressSuffixes = [
'Rue de la Paix', 'Boulevard des Rêves', 'Avenue du Soleil', 'Place des Étoiles', 'Chemin des Champs'
];
static final List<String> _motivationSnippets = [
'Nous cherchons une personne de confiance.',
'Nos horaires sont atypiques.',
'Notre enfant est plein de vie.',
'Nous souhaitons une garde à temps plein.',
'Une adaptation en douceur est primordiale pour nous.',
'Nous avons hâte de vous rencontrer.',
'La pédagogie Montessori nous intéresse.'
];
static String firstName() => _firstNames[_random.nextInt(_firstNames.length)];
static String lastName() => _lastNames[_random.nextInt(_lastNames.length)];
static String address() => "${_random.nextInt(100) + 1} ${_addressSuffixes[_random.nextInt(_addressSuffixes.length)]}";
static String postalCode() => "750${_random.nextInt(10)}${_random.nextInt(10)}";
static String city() => "Paris";
static String phone() => "06${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}";
static String email(String firstName, String lastName) => "${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com";
static String password() => "password123"; // Simple pour le test
static String dob({bool isUnborn = false}) {
final now = DateTime.now();
if (isUnborn) {
final provisionalDate = now.add(Duration(days: _random.nextInt(180) + 30)); // Entre 1 et 7 mois dans le futur
return "${provisionalDate.day.toString().padLeft(2, '0')}/${provisionalDate.month.toString().padLeft(2, '0')}/${provisionalDate.year}";
} else {
final birthYear = now.year - _random.nextInt(3); // Enfants de 0 à 2 ans
final birthMonth = _random.nextInt(12) + 1;
final birthDay = _random.nextInt(28) + 1; // Simple, évite les pbs de jours/mois
return "${birthDay.toString().padLeft(2, '0')}/${birthMonth.toString().padLeft(2, '0')}/${birthYear}";
}
}
static bool boolean() => _random.nextBool();
static String motivation() {
int count = _random.nextInt(3) + 2; // 2 à 4 phrases
List<String> chosenSnippets = [];
while(chosenSnippets.length < count) {
String snippet = _motivationSnippets[_random.nextInt(_motivationSnippets.length)];
if (!chosenSnippets.contains(snippet)) {
chosenSnippets.add(snippet);
}
}
return chosenSnippets.join(' ');
}
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppCustomCheckbox extends StatelessWidget {
final String label;
final bool value;
final ValueChanged<bool> onChanged;
final double checkboxSize;
final double checkmarkSizeFactor;
const AppCustomCheckbox({
super.key,
required this.label,
required this.value,
required this.onChanged,
this.checkboxSize = 20.0,
this.checkmarkSizeFactor = 1.4,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onChanged(!value), // Inverse la valeur au clic
behavior: HitTestBehavior.opaque, // Pour s'assurer que toute la zone du Row est cliquable
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: checkboxSize,
height: checkboxSize,
child: Stack(
alignment: Alignment.center,
clipBehavior: Clip.none,
children: [
Image.asset(
'assets/images/square.png',
height: checkboxSize,
width: checkboxSize,
),
if (value)
Image.asset(
'assets/images/coche.png',
height: checkboxSize * checkmarkSizeFactor,
width: checkboxSize * checkmarkSizeFactor,
),
],
),
),
const SizedBox(width: 10),
// Utiliser Flexible pour que le texte ne cause pas d'overflow si trop long
Flexible(
child: Text(
label,
style: GoogleFonts.merienda(fontSize: 16),
overflow: TextOverflow.ellipsis, // Gérer le texte long
),
),
],
),
);
}
}

View File

@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
// Définition de l'enum pour les styles de couleur/fond
enum CustomAppTextFieldStyle {
beige,
lavande,
jaune,
}
class CustomAppTextField extends StatefulWidget {
final TextEditingController controller;
final String labelText;
final String hintText;
final double fieldWidth;
final double fieldHeight;
final bool obscureText;
final TextInputType keyboardType;
final String? Function(String?)? validator;
final CustomAppTextFieldStyle style;
final bool isRequired;
final bool enabled;
final bool readOnly;
final VoidCallback? onTap;
final IconData? suffixIcon;
final double labelFontSize;
final double inputFontSize;
const CustomAppTextField({
super.key,
required this.controller,
required this.labelText,
this.hintText = '',
this.fieldWidth = 300.0,
this.fieldHeight = 53.0,
this.obscureText = false,
this.keyboardType = TextInputType.text,
this.validator,
this.style = CustomAppTextFieldStyle.beige,
this.isRequired = false,
this.enabled = true,
this.readOnly = false,
this.onTap,
this.suffixIcon,
this.labelFontSize = 18.0,
this.inputFontSize = 18.0,
});
@override
State<CustomAppTextField> createState() => _CustomAppTextFieldState();
}
class _CustomAppTextFieldState extends State<CustomAppTextField> {
String getBackgroundImagePath() {
switch (widget.style) {
case CustomAppTextFieldStyle.lavande:
return 'assets/images/input_field_lavande.png';
case CustomAppTextFieldStyle.jaune:
return 'assets/images/input_field_jaune.png';
case CustomAppTextFieldStyle.beige:
default:
return 'assets/images/input_field_bg.png';
}
}
@override
Widget build(BuildContext context) {
const double fontHeightMultiplier = 1.2;
const double internalVerticalPadding = 16.0;
final double dynamicFieldHeight = widget.fieldHeight;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.labelText,
style: GoogleFonts.merienda(
fontSize: widget.labelFontSize,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
SizedBox(
width: widget.fieldWidth,
height: dynamicFieldHeight,
child: Stack(
alignment: Alignment.centerLeft,
children: [
Positioned.fill(
child: Image.asset(
getBackgroundImagePath(),
fit: BoxFit.fill,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 8.0),
child: TextFormField(
controller: widget.controller,
obscureText: widget.obscureText,
keyboardType: widget.keyboardType,
enabled: widget.enabled,
readOnly: widget.readOnly,
onTap: widget.onTap,
style: GoogleFonts.merienda(
fontSize: widget.inputFontSize,
color: widget.enabled ? Colors.black87 : Colors.grey
),
validator: widget.validator ??
(value) {
if (!widget.enabled || widget.readOnly) return null;
if (widget.isRequired && (value == null || value.isEmpty)) {
return 'Ce champ est obligatoire';
}
return null;
},
decoration: InputDecoration(
hintText: widget.hintText,
hintStyle: GoogleFonts.merienda(fontSize: widget.inputFontSize, color: Colors.black54.withOpacity(0.7)),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
suffixIcon: widget.suffixIcon != null
? Padding(
padding: const EdgeInsets.only(right: 0.0),
child: Icon(widget.suffixIcon, color: Colors.black54, size: widget.inputFontSize * 1.1),
)
: null,
isDense: true,
),
textAlignVertical: TextAlignVertical.center,
),
),
],
),
),
],
);
}
}

View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class CustomDecoratedTextField extends StatelessWidget {
final TextEditingController controller;
final String hintText;
final int maxLines;
final double? fieldHeight; // Hauteur optionnelle pour le champ
final bool expandDynamically; // Nouvelle propriété
final bool readOnly;
final double fontSize;
const CustomDecoratedTextField({
super.key,
required this.controller,
this.hintText = 'Écrire votre texte ici...',
this.maxLines = 10, // Un nombre raisonnable de lignes par défaut si non dynamique
this.fieldHeight, // Si non fourni, la hauteur sera intrinsèque ou définie par l'image
this.expandDynamically = false, // Par défaut, non dynamique
this.readOnly = false,
this.fontSize = 15.0,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: fieldHeight, // Permet de forcer une hauteur si besoin
child: Stack(
alignment: Alignment.topLeft,
children: [
Image.asset(
'assets/images/square.png', // L'image de fond
fit: BoxFit.fill, // Pour remplir l'espace du Stack/SizedBox
width: double.infinity, // S'assurer qu'elle prend toute la largeur disponible
height: fieldHeight != null ? double.infinity : null, // Et toute la hauteur si fieldHeight est spécifié
),
Padding(
// Ajouter un padding interne pour que le texte ne colle pas aux bords de l'image
padding: const EdgeInsets.only(top: 25.0, bottom: 15.0, left: 20.0, right: 20.0), // Augmentation de la marge supérieure
child: TextFormField(
controller: controller,
keyboardType: TextInputType.multiline,
maxLines: expandDynamically ? null : maxLines, // S'étend dynamiquement si expandDynamically est true
style: GoogleFonts.merienda(fontSize: fontSize, color: Colors.black87),
textAlignVertical: TextAlignVertical.top,
readOnly: readOnly,
decoration: InputDecoration(
hintText: hintText,
hintStyle: GoogleFonts.merienda(fontSize: fontSize, color: Colors.black54.withOpacity(0.7)),
border: InputBorder.none, // Pas de bordure pour le TextFormField lui-même
contentPadding: EdgeInsets.zero, // Le padding est géré par le widget Padding externe
// Pour aligner le hintText en haut à gauche
alignLabelWithHint: true,
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
class HoverReliefWidget extends StatefulWidget {
final Widget child;
final VoidCallback? onPressed;
final BorderRadius borderRadius;
final double initialElevation;
final double hoverElevation;
final double scaleFactor;
final bool enableHoverEffect; // Pour activer/désactiver l'effet de survol
final Color initialShadowColor; // Nouveau paramètre
final Color hoverShadowColor; // Nouveau paramètre
const HoverReliefWidget({
required this.child,
this.onPressed,
this.borderRadius = const BorderRadius.all(Radius.circular(15.0)),
this.initialElevation = 4.0,
this.hoverElevation = 8.0,
this.scaleFactor = 1.03, // Légèrement réduit par rapport à l'exemple précédent
this.enableHoverEffect = true, // Par défaut, l'effet est activé
this.initialShadowColor = const Color(0x26000000), // Default: Colors.black.withOpacity(0.15)
this.hoverShadowColor = const Color(0x4D000000), // Default: Colors.black.withOpacity(0.3)
super.key,
});
@override
State<HoverReliefWidget> createState() => _HoverReliefWidgetState();
}
class _HoverReliefWidgetState extends State<HoverReliefWidget> {
bool _isHovering = false;
@override
Widget build(BuildContext context) {
final bool canHover = widget.enableHoverEffect && widget.onPressed != null;
final hoverTransform = Matrix4.identity()..scale(widget.scaleFactor);
final transform = _isHovering && canHover ? hoverTransform : Matrix4.identity();
final shadowColor = _isHovering && canHover ? widget.hoverShadowColor : widget.initialShadowColor;
final elevation = _isHovering && canHover ? widget.hoverElevation : widget.initialElevation;
Widget content = AnimatedContainer(
duration: const Duration(milliseconds: 200),
transform: transform,
transformAlignment: Alignment.center,
child: Material(
color: Colors.transparent,
elevation: elevation,
shadowColor: shadowColor,
borderRadius: widget.borderRadius,
clipBehavior: Clip.antiAlias,
child: widget.child,
),
);
if (widget.onPressed == null) {
// Si non cliquable, on retourne juste le contenu avec l'élévation initiale (pas de survol)
// Ajustement: pour toujours avoir un Material de base même si non cliquable et sans hover.
return Material(
color: Colors.transparent,
elevation: widget.initialElevation, // Utilise l'élévation initiale
shadowColor: widget.initialShadowColor, // Appliqué ici pour l'état non cliquable
borderRadius: widget.borderRadius,
clipBehavior: Clip.antiAlias,
child: widget.child,
);
}
return MouseRegion(
onEnter: (_) {
if (widget.enableHoverEffect) setState(() => _isHovering = true);
},
onExit: (_) {
if (widget.enableHoverEffect) setState(() => _isHovering = false);
},
cursor: SystemMouseCursors.click,
child: InkWell(
onTap: widget.onPressed,
borderRadius: widget.borderRadius,
hoverColor: Colors.transparent,
splashColor: Colors.grey.withOpacity(0.2),
highlightColor: Colors.grey.withOpacity(0.1),
child: content,
),
);
}
}

View File

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class ImageButton extends StatelessWidget {
final String bg;
final double width;
final double height;
final String text;
final Color textColor;
final VoidCallback onPressed;
final double fontSize; // Ajout pour la flexibilité
const ImageButton({
super.key,
required this.bg,
required this.width,
required this.height,
required this.text,
required this.textColor,
required this.onPressed,
this.fontSize = 16, // Valeur par défaut
});
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onPressed,
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(bg),
fit: BoxFit.fill,
),
),
child: Center(
child: Text(
text,
style: GoogleFonts.merienda(
color: textColor,
fontSize: fontSize, // Utilisation du paramètre
fontWeight: FontWeight.bold,
),
),
),
),
),
);
}
}

View File

@ -126,6 +126,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -240,6 +245,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
intl:
dependency: transitive
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
js:
dependency: "direct main"
description:

View File

@ -9,6 +9,8 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
provider: ^6.1.1
go_router: ^13.2.5
google_fonts: ^6.1.0
@ -27,13 +29,8 @@ flutter:
uses-material-design: true
assets:
- assets/images/logo.png
- assets/images/river_logo_desktop.png
- assets/images/paper2.png
- assets/images/field_email.png
- assets/images/field_password.png
- assets/images/btn_green.png
- assets/images/icon.png
- assets/images/ # Déclarer le dossier entier
- assets/cards/ # Nouveau dossier de cartes
fonts:
- family: Merienda

View File

@ -18,23 +18,30 @@
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="P'titsPas - Grandir pas à pas, sereinement">
<meta name="description" content="Application de gestion de la garde d'enfants pour les collectivités locales.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="P'titsPas">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="assets/images/icon.png"/>
<link rel="icon" type="image/png" href="favicon.png"/>
<title>P'titsPas</title>
<link rel="manifest" href="manifest.json">
<!-- Suppression des dépendances image_cropper web -->
<!--
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.css">
-->
<script>
// The value below is injected by flutter build, do not touch.
const serviceWorkerVersion = null;
const serviceWorkerVersion = "{{flutter_service_worker_version}}";
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>

55
lib/main.dart Normal file
View File

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:p_tits_pas/screens/auth/login_screen.dart';
import 'package:p_tits_pas/screens/auth/register_choice_screen.dart';
import 'package:p_tits_pas/screens/auth/parent_register_step1_screen.dart';
import 'package:p_tits_pas/screens/auth/parent_register_step2_screen.dart';
import 'package:p_tits_pas/screens/auth/parent_register_step3_screen.dart';
void main() {
// TODO: Initialiser SharedPreferences, Provider, etc.
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'P\'titsPas',
theme: ThemeData(
primarySwatch: Colors.blue, // TODO: Utiliser la palette de la charte graphique
textTheme: GoogleFonts.merriweatherTextTheme(
Theme.of(context).textTheme,
),
visualDensity: VisualDensity.adaptivePlatformDensity,
),
// Gestionnaire de routes initial (simple pour l'instant)
initialRoute: '/', // Ou '/login' selon le point d'entrée désiré
routes: {
'/': (context) => const LoginScreen(), // Exemple, pourrait être RegisterChoiceScreen aussi
'/login': (context) => const LoginScreen(),
'/register-choice': (context) => const RegisterChoiceScreen(),
'/parent-register/step1': (context) => const ParentRegisterStep1Screen(),
'/parent-register/step2': (context) => const ParentRegisterStep2Screen(),
'/parent-register/step3': (context) => const ParentRegisterStep3Screen(),
// TODO: Ajouter les autres routes (step 4, etc., dashboard...)
},
// Gestion des routes inconnues
onUnknownRoute: (settings) {
return MaterialPageRoute(
builder: (context) => Scaffold(
body: Center(
child: Text(
'Route inconnue :\n${settings.name}',
style: GoogleFonts.merriweather(fontSize: 20, color: Colors.red),
textAlign: TextAlign.center,
),
),
),
);
},
);
}
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,22 @@
CustomAppTextField(
controller: _firstNameController,
labelText: 'Prénom',
hintText: 'Facultatif si à naître',
isRequired: !widget.childData.isUnbornChild,
),
const SizedBox(height: 6.0),
CustomAppTextField(
controller: _lastNameController,
labelText: 'Nom',
hintText: 'Nom de l\'enfant',
enabled: true,
),
const SizedBox(height: 9.0),
CustomAppTextField(
controller: _dobController,
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
hintText: 'JJ/MM/AAAA',
readOnly: true,
onTap: widget.onDateSelect,
suffixIcon: Icons.calendar_today,
),

3
ressources/cartes.png Normal file
View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?><Error><Code>AuthenticationFailed</Code><Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
RequestId:32300bba-601e-0021-763f-bc1466000000
Time:2025-05-03T15:21:39.1449044Z</Message><AuthenticationErrorDetail>Signed expiry time [Sat, 03 May 2025 15:21:15 GMT] must be after signed start time [Sat, 03 May 2025 15:21:39 GMT]</AuthenticationErrorDetail></Error>

View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PtitsPas Propositions UI Wizard</title>
<link href="https://fonts.googleapis.com/css2?family=Merienda:wght@600&family=Inter:wght@400;500&display=swap" rel="stylesheet">
<style>
body{background:#fffef9;font-family:Inter,sans-serif;color:#2f2f2f;line-height:1.6;padding:2rem;}
h1,h2{font-family:Merienda,cursive;margin:.5rem 0;}
.grid{display:grid;gap:2rem;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));}
.card{background:#fff;border-radius:18px;box-shadow:0 4px 12px rgba(0,0,0,.05);padding:1.5rem;text-align:center;}
img{max-width:100%;height:auto;border-radius:12px;}
.palette{display:flex;justify-content:center;gap:.5rem;margin-top:.75rem;}
.swatch{width:28px;height:28px;border-radius:50%;}
.code{font-size:.75rem;margin-top:.25rem;}
</style>
</head>
<body>
<h1>Création de compte : idées denchaînement de cartes</h1>
<p>Chaque étape saffiche dans une «carte» pastel. En validant, la carte suivante glisse vers lavant (animation CSS: <code>transform: translateX(-100%)</code> + <code>opacity</code>). Trois styles proposés :</p>
<div class="grid">
<div class="card">
<h2>Style 1 : Watercolor</h2>
<img src="A_digital_graphic_design_image_displays_style_opti.png" alt="watercolor stack"/>
<div class="palette">
<div class="swatch" style="background:#FBC9C4"></div>
<div class="swatch" style="background:#FBD38B"></div>
<div class="swatch" style="background:#A9D8C6"></div>
</div>
<div class="code">#FBC9C4 · #FBD38B · #A9D8C6</div>
<p>Bords arrondis 22px, texture papier sur chaque carte.<br><b>Animation</b> : légère rotation (<em>tilt</em>) pour rappeler un paquet de cartes réaliste.</p>
</div>
<div class="card">
<h2>Style 2 : Minimal pastel</h2>
<img src="A_digital_graphic_design_image_displays_style_opti.png" alt="minimal stack"/>
<div class="palette">
<div class="swatch" style="background:#E3DFFD"></div>
<div class="swatch" style="background:#CFEAE3"></div>
<div class="swatch" style="background:#FFE88A"></div>
</div>
<div class="code">#E3DFFD · #CFEAE3 · #FFE88A</div>
<p>Cartes plates, ombre portée subtile (0 2 8 rgba0,05).<br><b>Animation</b> : slide horizontal + fondu rapide.</p>
</div>
<div class="card">
<h2>Style 3 : Modern vibrant</h2>
<img src="A_digital_graphic_design_image_displays_style_opti.png" alt="modern stack"/>
<div class="palette">
<div class="swatch" style="background:#FB86A2"></div>
<div class="swatch" style="background:#F3D468"></div>
<div class="swatch" style="background:#8AC1E3"></div>
</div>
<div class="code">#FB86A2 · #F3D468 · #8AC1E3</div>
<p>Coins arrondis 12px pour une touche «app mobile».<br><b>Animation</b> : carte sort par la gauche, nouvelle carte zoome légèrement.</p>
</div>
</div>
<footer style="font-size:.8rem;margin-top:2rem">© 2025 PtitsPas maquettes UI</footer>
</body>
</html>