feat(auth): Amélioration UI et logique inscription parent étape 3
- Ajout du switch "Enfant à naître" et ajustement du champ prénom. - Amélioration de la gestion de l'affichage des photos (placeholder, kIsWeb). - Refactorisation des boutons avec HoverReliefWidget. - Localisation du DatePicker en français. - Nettoyage de l'intégration (annulée) de image_cropper. - Mise à jour de EVOLUTIONS_CDC.md.
@ -236,4 +236,22 @@ Pour chaque évolution identifiée, ce document suivra la structure suivante :
|
|||||||
- Messages d'erreur clairs en cas de :
|
- Messages d'erreur clairs en cas de :
|
||||||
- Email non trouvé
|
- Email non trouvé
|
||||||
- Lien expiré
|
- 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.
|
||||||
32
frontend/android/app/src/main/AndroidManifest.xml
Normal 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
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/android/local.properties
Normal file
@ -0,0 +1 @@
|
|||||||
|
flutter.sdk=C:\\Users\\marti\\dev\\flutter
|
||||||
BIN
frontend/assets/images/card_blue.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
frontend/assets/images/card_blue_h.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
frontend/assets/images/card_lavander.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/assets/images/card_lavander_h.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/assets/images/card_yellow_h.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/assets/images/coche.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
frontend/assets/images/photo.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/assets/images/plus.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
frontend/assets/images/square.png
Normal file
|
After Width: | Height: | Size: 890 KiB |
BIN
frontend/assets/images/switch_off.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
frontend/assets/images/switch_on.png
Normal file
|
After Width: | Height: | Size: 288 KiB |
@ -1,44 +1,43 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'screens/auth/login_screen.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation
|
||||||
import 'screens/legal/legal_page.dart';
|
// import 'package:provider/provider.dart'; // Supprimer Provider
|
||||||
import 'screens/legal/privacy_page.dart';
|
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(
|
class MyApp extends StatelessWidget {
|
||||||
routes: [
|
const MyApp({super.key});
|
||||||
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});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp.router(
|
// Pas besoin de Provider.of ici
|
||||||
|
|
||||||
|
return MaterialApp(
|
||||||
title: 'P\'titsPas',
|
title: 'P\'titsPas',
|
||||||
routerConfig: _router,
|
theme: ThemeData.light().copyWith( // Utiliser un thème simple par défaut
|
||||||
debugShowCheckedModeBanner: false,
|
textTheme: GoogleFonts.meriendaTextTheme(
|
||||||
theme: ThemeData(
|
ThemeData.light().textTheme,
|
||||||
fontFamily: 'Merienda',
|
|
||||||
colorScheme: ColorScheme.fromSeed(
|
|
||||||
seedColor: const Color(0xFF8AD0C8),
|
|
||||||
brightness: Brightness.light,
|
|
||||||
),
|
),
|
||||||
|
// 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,26 +1,76 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../screens/auth/login_screen.dart';
|
import '../screens/auth/login_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/home/home_screen.dart';
|
import '../screens/home/home_screen.dart';
|
||||||
|
|
||||||
class AppRouter {
|
class AppRouter {
|
||||||
static const String login = '/login';
|
static const String login = '/login';
|
||||||
|
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 home = '/home';
|
static const String home = '/home';
|
||||||
|
|
||||||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||||
|
Widget screen;
|
||||||
|
bool slideTransition = false;
|
||||||
|
|
||||||
switch (settings.name) {
|
switch (settings.name) {
|
||||||
case login:
|
case login:
|
||||||
return MaterialPageRoute(builder: (_) => const LoginScreen());
|
screen = const LoginPage();
|
||||||
|
break;
|
||||||
|
case registerChoice:
|
||||||
|
screen = const RegisterChoiceScreen();
|
||||||
|
slideTransition = true; // Activer la transition pour cet écran
|
||||||
|
break;
|
||||||
|
case parentRegisterStep1:
|
||||||
|
screen = const ParentRegisterStep1Screen();
|
||||||
|
slideTransition = true; // Activer la transition pour cet écran
|
||||||
|
break;
|
||||||
|
case parentRegisterStep2:
|
||||||
|
screen = const ParentRegisterStep2Screen();
|
||||||
|
slideTransition = true;
|
||||||
|
break;
|
||||||
|
case parentRegisterStep3:
|
||||||
|
screen = const ParentRegisterStep3Screen();
|
||||||
|
slideTransition = true;
|
||||||
|
break;
|
||||||
case home:
|
case home:
|
||||||
return MaterialPageRoute(builder: (_) => const HomeScreen());
|
screen = const HomeScreen();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return MaterialPageRoute(
|
screen = Scaffold(
|
||||||
builder: (_) => Scaffold(
|
body: Center(
|
||||||
body: Center(
|
child: Text('Route non définie : ${settings.name}'),
|
||||||
child: Text('Route non définie: [36m${settings.name}[0m'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (slideTransition) {
|
||||||
|
return PageRouteBuilder(
|
||||||
|
pageBuilder: (context, animation, secondaryAnimation) => screen,
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||||
|
const begin = Offset(1.0, 0.0); // Glisse depuis la droite
|
||||||
|
const end = Offset.zero;
|
||||||
|
const curve = Curves.easeInOut; // Animation douce
|
||||||
|
|
||||||
|
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), // Durée de la transition
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Transition par défaut pour les autres écrans
|
||||||
|
return MaterialPageRoute(builder: (_) => screen);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,7 +197,19 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
// Lien de création de compte
|
// Lien de création de compte
|
||||||
Center(
|
Center(
|
||||||
child: Container(), // Suppression du bouton 'Créer un compte'
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushNamed(context, '/register-choice');
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Créer un compte',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 16,
|
||||||
|
color: const Color(0xFF2D6A4F),
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20), // Réduit l'espacement en bas
|
const SizedBox(height: 20), // Réduit l'espacement en bas
|
||||||
],
|
],
|
||||||
|
|||||||
226
frontend/lib/screens/auth/parent_register_step1_screen.dart
Normal 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
|
||||||
|
|
||||||
|
class ParentRegisterStep1Screen extends StatefulWidget {
|
||||||
|
const ParentRegisterStep1Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ParentRegisterStep1Screen> createState() => _ParentRegisterStep1ScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
// Contrôleurs pour les champs
|
||||||
|
final _lastNameController = TextEditingController();
|
||||||
|
final _firstNameController = TextEditingController();
|
||||||
|
final _phoneController = TextEditingController();
|
||||||
|
final _emailController = TextEditingController();
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
final _confirmPasswordController = TextEditingController();
|
||||||
|
final _addressController = TextEditingController();
|
||||||
|
final _postalCodeController = TextEditingController();
|
||||||
|
final _cityController = TextEditingController();
|
||||||
|
|
||||||
|
@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/X',
|
||||||
|
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// Texte d'instruction
|
||||||
|
Text(
|
||||||
|
'Merci de renseigner les informations du premier parent :',
|
||||||
|
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: 40, horizontal: 50),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage('assets/images/card_yellow_h.png'),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildTextField(_lastNameController, 'Nom', hintText: 'Votre nom de famille')),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(child: _buildTextField(_firstNameController, 'Prénom', hintText: 'Votre prénom')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildTextField(_phoneController, 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone')),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(child: _buildTextField(_emailController, 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildTextField(_passwordController, 'Mot de passe', obscureText: true, hintText: 'Créez votre mot de passe')),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(child: _buildTextField(_confirmPasswordController, 'Confirmation', obscureText: true, hintText: 'Confirmez le mot de passe')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildTextField(_addressController, 'Adresse (Rue)', hintText: 'Numéro et nom de votre rue'),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(flex: 2, child: _buildTextField(_postalCodeController, 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal')),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(flex: 3, child: _buildTextField(_cityController, 'Ville', hintText: 'Votre ville')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// TODO: Sauvegarder les données du parent 1
|
||||||
|
Navigator.pushNamed(context, '/parent-register/step2'); // Naviguer vers l'étape 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: 'Suivant',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widget pour construire les champs de texte avec le fond personnalisé
|
||||||
|
Widget _buildTextField(
|
||||||
|
TextEditingController controller,
|
||||||
|
String label, {
|
||||||
|
TextInputType? keyboardType,
|
||||||
|
bool obscureText = false,
|
||||||
|
String? hintText,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$label :',
|
||||||
|
style: GoogleFonts.merienda(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Container(
|
||||||
|
height: 50,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage('assets/images/input_field_bg.png'),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
obscureText: obscureText,
|
||||||
|
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black87),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
|
||||||
|
hintText: hintText ?? label,
|
||||||
|
hintStyle: GoogleFonts.merienda(fontSize: 16, color: Colors.black38),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
// Validation désactivée
|
||||||
|
return null;
|
||||||
|
/*
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Ce champ est obligatoire';
|
||||||
|
}
|
||||||
|
// TODO: Ajouter des validations spécifiques (email, téléphone, mot de passe)
|
||||||
|
if (label == 'Confirmation' && value != _passwordController.text) {
|
||||||
|
return 'Les mots de passe ne correspondent pas';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
374
frontend/lib/screens/auth/parent_register_step2_screen.dart
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'dart:math' as math; // Pour la rotation du chevron
|
||||||
|
|
||||||
|
class ParentRegisterStep2Screen extends StatefulWidget {
|
||||||
|
const ParentRegisterStep2Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ParentRegisterStep2Screen> createState() => _ParentRegisterStep2ScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
// TODO: Recevoir les infos du parent 1 pour pré-remplir l'adresse
|
||||||
|
// String? _parent1Address;
|
||||||
|
// String? _parent1PostalCode;
|
||||||
|
// String? _parent1City;
|
||||||
|
|
||||||
|
bool _addParent2 = false; // Par défaut, on n'ajoute pas le parent 2
|
||||||
|
bool _sameAddressAsParent1 = false;
|
||||||
|
|
||||||
|
// Contrôleurs pour les champs du parent 2
|
||||||
|
final _lastNameController = TextEditingController();
|
||||||
|
final _firstNameController = TextEditingController();
|
||||||
|
final _phoneController = TextEditingController();
|
||||||
|
final _emailController = TextEditingController();
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
final _confirmPasswordController = TextEditingController();
|
||||||
|
final _addressController = TextEditingController();
|
||||||
|
final _postalCodeController = TextEditingController();
|
||||||
|
final _cityController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_lastNameController.dispose();
|
||||||
|
_firstNameController.dispose();
|
||||||
|
_phoneController.dispose();
|
||||||
|
_emailController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
_confirmPasswordController.dispose();
|
||||||
|
_addressController.dispose();
|
||||||
|
_postalCodeController.dispose();
|
||||||
|
_cityController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper pour activer/désactiver tous les champs sauf l'adresse
|
||||||
|
bool get _parent2FieldsEnabled => _addParent2;
|
||||||
|
// Helper pour activer/désactiver les champs d'adresse
|
||||||
|
bool get _addressFieldsEnabled => _addParent2 && !_sameAddressAsParent1;
|
||||||
|
|
||||||
|
@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
|
||||||
|
Text(
|
||||||
|
'Étape 2/X', // Mettre à jour le numéro d'étape total
|
||||||
|
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// Texte d'instruction
|
||||||
|
Text(
|
||||||
|
'Renseignez les informations du deuxième parent (optionnel) :',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Carte bleue contenant le formulaire
|
||||||
|
Container(
|
||||||
|
width: screenSize.width * 0.6,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
|
||||||
|
decoration: const BoxDecoration( // Retour à la décoration
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage('assets/images/card_blue_h.png'), // Utilisation de l'image horizontale
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Suppression du Stack et Transform.rotate
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: SingleChildScrollView( // Le SingleChildScrollView redevient l'enfant direct
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// --- Interrupteurs sur une ligne ---
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Option 1: Ajouter Parent 2
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildCustomSwitch(
|
||||||
|
value: _addParent2,
|
||||||
|
onChanged: (bool? newValue) {
|
||||||
|
final bool actualValue = newValue ?? false;
|
||||||
|
setState(() {
|
||||||
|
_addParent2 = actualValue;
|
||||||
|
if (!_addParent2) {
|
||||||
|
_formKey.currentState?.reset();
|
||||||
|
_lastNameController.clear();
|
||||||
|
_firstNameController.clear();
|
||||||
|
_phoneController.clear();
|
||||||
|
_emailController.clear();
|
||||||
|
_passwordController.clear();
|
||||||
|
_confirmPasswordController.clear();
|
||||||
|
_addressController.clear();
|
||||||
|
_postalCodeController.clear();
|
||||||
|
_cityController.clear();
|
||||||
|
_sameAddressAsParent1 = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
|
||||||
|
// Option 2: Même Adresse
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey), // Griser l'icône si désactivé
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
'Même Adresse ?',
|
||||||
|
style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), // Griser le texte si désactivé
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildCustomSwitch(
|
||||||
|
value: _sameAddressAsParent1,
|
||||||
|
onChanged: _addParent2 ? (bool? newValue) {
|
||||||
|
final bool actualValue = newValue ?? false;
|
||||||
|
setState(() {
|
||||||
|
_sameAddressAsParent1 = actualValue;
|
||||||
|
if (_sameAddressAsParent1) {
|
||||||
|
_addressController.clear();
|
||||||
|
_postalCodeController.clear();
|
||||||
|
_cityController.clear();
|
||||||
|
// TODO: Pré-remplir
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} : null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 25), // Espacement ajusté après les switchs
|
||||||
|
|
||||||
|
// --- Champs du Parent 2 (conditionnels) ---
|
||||||
|
// Nom & Prénom
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildTextField(_lastNameController, 'Nom', hintText: 'Nom du deuxième parent', enabled: _parent2FieldsEnabled)),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(child: _buildTextField(_firstNameController, 'Prénom', hintText: 'Prénom du deuxième parent', enabled: _parent2FieldsEnabled)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Téléphone & Email
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildTextField(_phoneController, 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son numéro de téléphone', enabled: _parent2FieldsEnabled)),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(child: _buildTextField(_emailController, 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son adresse e-mail', enabled: _parent2FieldsEnabled)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Mot de passe
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildTextField(_passwordController, 'Mot de passe', obscureText: true, hintText: 'Son mot de passe', enabled: _parent2FieldsEnabled)),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(child: _buildTextField(_confirmPasswordController, 'Confirmation', obscureText: true, hintText: 'Confirmer son mot de passe', enabled: _parent2FieldsEnabled)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// --- Champs Adresse (conditionnels) ---
|
||||||
|
_buildTextField(_addressController, 'Adresse (Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(flex: 2, child: _buildTextField(_postalCodeController, 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled)),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(flex: 3, child: _buildTextField(_cityController, 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Chevron de navigation gauche (Retour)
|
||||||
|
Positioned(
|
||||||
|
top: screenSize.height / 2 - 20,
|
||||||
|
left: 40,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Transform(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
transform: Matrix4.rotationY(math.pi),
|
||||||
|
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.pop(context), // Retour à l'étape 1
|
||||||
|
tooltip: 'Retour',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Chevron de navigation droit (Suivant)
|
||||||
|
Positioned(
|
||||||
|
top: screenSize.height / 2 - 20,
|
||||||
|
right: 40,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||||
|
onPressed: () {
|
||||||
|
// Si on n'ajoute pas de parent 2, on passe directement
|
||||||
|
if (!_addParent2) {
|
||||||
|
// Naviguer vers l'étape 3 (enfants)
|
||||||
|
print('Passer à l\'étape 3 (enfants) - Sans Parent 2');
|
||||||
|
Navigator.pushNamed(context, '/parent-register/step3');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Si on ajoute un parent 2
|
||||||
|
// Valider seulement si on n'utilise PAS la même adresse
|
||||||
|
bool isFormValid = true;
|
||||||
|
// TODO: Remettre la validation quand elle sera prête
|
||||||
|
/*
|
||||||
|
if (!_sameAddressAsParent1) {
|
||||||
|
isFormValid = _formKey.currentState?.validate() ?? false;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (isFormValid) {
|
||||||
|
// TODO: Sauvegarder les données du parent 2
|
||||||
|
print('Passer à l\'étape 3 (enfants) - Avec Parent 2');
|
||||||
|
Navigator.pushNamed(context, '/parent-register/step3');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: 'Suivant',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- NOUVEAU WIDGET ---
|
||||||
|
// Widget pour construire un switch personnalisé avec images
|
||||||
|
Widget _buildCustomSwitch({required bool value, required ValueChanged<bool?>? onChanged}) {
|
||||||
|
// --- DEBUG ---
|
||||||
|
print("Building Custom Switch with value: $value");
|
||||||
|
// -------------
|
||||||
|
const double switchHeight = 25.0;
|
||||||
|
const double switchWidth = 40.0;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: onChanged != null ? () => onChanged(!value) : null,
|
||||||
|
child: Opacity(
|
||||||
|
// Griser le switch si désactivé
|
||||||
|
opacity: onChanged != null ? 1.0 : 0.5,
|
||||||
|
child: Image.asset(
|
||||||
|
value ? 'assets/images/switch_on.png' : 'assets/images/switch_off.png',
|
||||||
|
height: switchHeight,
|
||||||
|
width: switchWidth,
|
||||||
|
fit: BoxFit.contain, // Ou BoxFit.fill selon le rendu souhaité
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widget pour construire les champs de texte (identique à l'étape 1)
|
||||||
|
Widget _buildTextField(
|
||||||
|
TextEditingController controller,
|
||||||
|
String label, {
|
||||||
|
TextInputType? keyboardType,
|
||||||
|
bool obscureText = false,
|
||||||
|
String? hintText,
|
||||||
|
bool enabled = true,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$label :',
|
||||||
|
style: GoogleFonts.merienda(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Container(
|
||||||
|
height: 50,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage('assets/images/input_field_bg.png'),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
obscureText: obscureText,
|
||||||
|
enabled: enabled,
|
||||||
|
style: GoogleFonts.merienda(fontSize: 16, color: enabled ? Colors.black87 : Colors.grey),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
|
||||||
|
hintText: hintText ?? label,
|
||||||
|
hintStyle: GoogleFonts.merienda(fontSize: 16, color: Colors.black38),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (!enabled) return null; // Ne pas valider si désactivé
|
||||||
|
// Le reste de la validation (commentée précédemment)
|
||||||
|
return null;
|
||||||
|
/*
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Ce champ est obligatoire';
|
||||||
|
}
|
||||||
|
// TODO: Validations spécifiques
|
||||||
|
if (label == 'Confirmation' && value != _passwordController.text) {
|
||||||
|
return 'Les mots de passe ne correspondent pas';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
472
frontend/lib/screens/auth/parent_register_step3_screen.dart
Normal file
@ -0,0 +1,472 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'dart:math' as math; // Pour la rotation du chevron
|
||||||
|
import '../../widgets/hover_relief_widget.dart'; // Import du nouveau widget
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
// import 'package:image_cropper/image_cropper.dart'; // Supprimé
|
||||||
|
import 'dart:io' show File, Platform; // Ajout de Platform
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb
|
||||||
|
|
||||||
|
// TODO: Créer un modèle de données pour l'enfant
|
||||||
|
// class ChildData { ... }
|
||||||
|
|
||||||
|
class ParentRegisterStep3Screen extends StatefulWidget {
|
||||||
|
const ParentRegisterStep3Screen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ParentRegisterStep3Screen> createState() => _ParentRegisterStep3ScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||||
|
// TODO: Gérer une liste d'enfants et leurs contrôleurs respectifs
|
||||||
|
// List<ChildData> _children = [ChildData()]; // Commencer avec un enfant
|
||||||
|
final _formKey = GlobalKey<FormState>(); // Une clé par enfant sera nécessaire si validation complexe
|
||||||
|
|
||||||
|
// Contrôleurs pour le premier enfant (pour l'instant)
|
||||||
|
final _firstNameController = TextEditingController();
|
||||||
|
final _lastNameController = TextEditingController();
|
||||||
|
final _dobController = TextEditingController();
|
||||||
|
bool _photoConsent = false;
|
||||||
|
bool _multipleBirth = false;
|
||||||
|
bool _isUnbornChild = false; // Nouvelle variable d'état
|
||||||
|
// TODO: Ajouter variable pour stocker l'image sélectionnée (par enfant)
|
||||||
|
// File? _childImage;
|
||||||
|
|
||||||
|
// File? _childImage; // Déjà présent et commenté
|
||||||
|
// Liste pour stocker les images des enfants (si gestion multi-enfants)
|
||||||
|
List<File?> _childImages = [null]; // Initialiser avec null pour le premier enfant
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_firstNameController.dispose();
|
||||||
|
_lastNameController.dispose();
|
||||||
|
_dobController.dispose();
|
||||||
|
// TODO: Disposer les contrôleurs de tous les enfants
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pré-remplir le nom de famille avec celui du parent 1
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _selectDate(BuildContext context) async {
|
||||||
|
final DateTime now = DateTime.now();
|
||||||
|
DateTime initialDatePickerDate = now;
|
||||||
|
DateTime firstDatePickerDate = DateTime(1980);
|
||||||
|
DateTime lastDatePickerDate = now;
|
||||||
|
|
||||||
|
if (_isUnbornChild) {
|
||||||
|
firstDatePickerDate = now; // Ne peut pas être avant aujourd'hui si à naître
|
||||||
|
lastDatePickerDate = now.add(const Duration(days: 300)); // Environ 10 mois dans le futur
|
||||||
|
// Si une date de naissance avait été entrée, on la garde pour initialDate si elle est dans la nouvelle plage
|
||||||
|
if (_dobController.text.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
// Tenter de parser la date existante
|
||||||
|
List<String> parts = _dobController.text.split('/');
|
||||||
|
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
|
||||||
|
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
|
||||||
|
initialDatePickerDate = parsedDate;
|
||||||
|
}
|
||||||
|
} catch (e) { /* Ignorer si le format est incorrect */ }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Si une date prévisionnelle avait été entrée, on la garde pour initialDate si elle est dans la nouvelle plage
|
||||||
|
if (_dobController.text.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
List<String> parts = _dobController.text.split('/');
|
||||||
|
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
|
||||||
|
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
|
||||||
|
initialDatePickerDate = parsedDate;
|
||||||
|
}
|
||||||
|
} catch (e) { /* Ignorer */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: initialDatePickerDate,
|
||||||
|
firstDate: firstDatePickerDate,
|
||||||
|
lastDate: lastDatePickerDate,
|
||||||
|
locale: const Locale('fr', 'FR'),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
_dobController.text = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour sélectionner une image
|
||||||
|
Future<void> _pickImage(int childIndex) async {
|
||||||
|
final ImagePicker picker = ImagePicker();
|
||||||
|
try {
|
||||||
|
final XFile? pickedFile = await picker.pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
imageQuality: 70,
|
||||||
|
maxWidth: 1024,
|
||||||
|
maxHeight: 1024,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pickedFile != null) {
|
||||||
|
// On utilise directement le fichier sélectionné, sans recadrage
|
||||||
|
setState(() {
|
||||||
|
if (childIndex < _childImages.length) {
|
||||||
|
_childImages[childIndex] = File(pickedFile.path);
|
||||||
|
} else {
|
||||||
|
print("Erreur: Index d'enfant hors limites pour l'image.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} // Fin de if (pickedFile != null)
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
print("Erreur lors de la sélection de l'image: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final screenSize = MediaQuery.of(context).size;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
// Fond papier
|
||||||
|
Positioned.fill(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/paper2.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
repeat: ImageRepeat.repeat,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Contenu centré et scrollable
|
||||||
|
Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 40.0), // Ajout de padding vertical
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Indicateur d'étape
|
||||||
|
Text(
|
||||||
|
'Étape 3/X', // Mettre à jour le numéro d'étape total
|
||||||
|
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// Texte d'instruction
|
||||||
|
Text(
|
||||||
|
'Merci de renseigner les informations de/vos enfant(s) :',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Zone principale : Cartes enfants + Bouton Ajouter
|
||||||
|
// Utilisation d'une Row pour placer côte à côte comme sur la maquette
|
||||||
|
// Il faudra peut-être ajuster pour les petits écrans (Wrap?)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center, // CHANGED: pour centrer verticalement
|
||||||
|
children: [
|
||||||
|
// TODO: Remplacer par une ListView ou Column dynamique basée sur _children
|
||||||
|
// Pour l'instant, une seule carte
|
||||||
|
_buildChildCard(context, 0), // Index 0 pour le premier enfant
|
||||||
|
|
||||||
|
const SizedBox(width: 30),
|
||||||
|
|
||||||
|
HoverReliefWidget(
|
||||||
|
onPressed: () {
|
||||||
|
print("Ajouter un enfant via HoverReliefWidget");
|
||||||
|
// setState(() { _children.add(ChildData()); });
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/plus.png',
|
||||||
|
height: 80,
|
||||||
|
width: 80,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Chevrons de navigation (identiques aux étapes précédentes)
|
||||||
|
// Chevron Gauche (Retour Step 2)
|
||||||
|
Positioned(
|
||||||
|
top: screenSize.height / 2 - 20,
|
||||||
|
left: 40,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Transform(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
transform: Matrix4.rotationY(math.pi),
|
||||||
|
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.pop(context), // Retour étape 2
|
||||||
|
tooltip: 'Retour',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Chevron Droit (Suivant Step 4)
|
||||||
|
Positioned(
|
||||||
|
top: screenSize.height / 2 - 20,
|
||||||
|
right: 40,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: Valider les infos enfants et Naviguer vers l'étape 4
|
||||||
|
print('Passer à l\'étape 4 (Situation familiale)');
|
||||||
|
// Navigator.pushNamed(context, '/parent-register/step4');
|
||||||
|
},
|
||||||
|
tooltip: 'Suivant',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widget pour construire UNE carte enfant
|
||||||
|
// L'index permettra de lier aux bons contrôleurs et données
|
||||||
|
Widget _buildChildCard(BuildContext context, int index) {
|
||||||
|
final File? currentChildImage = (index < _childImages.length) ? _childImages[index] : null;
|
||||||
|
|
||||||
|
// TODO: Déterminer la couleur de base de card_lavander.png et ajuster ces couleurs d'ombre
|
||||||
|
final Color baseLavandeColor = Colors.purple.shade200; // Placeholder pour la couleur de la carte lavande
|
||||||
|
final Color initialPhotoShadow = baseLavandeColor.withAlpha(90);
|
||||||
|
final Color hoverPhotoShadow = baseLavandeColor.withAlpha(130);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: 300,
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: const DecorationImage(
|
||||||
|
image: AssetImage('assets/images/card_lavander.png'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
HoverReliefWidget(
|
||||||
|
onPressed: () {
|
||||||
|
_pickImage(index);
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
initialShadowColor: initialPhotoShadow, // Ombre lavande
|
||||||
|
hoverShadowColor: hoverPhotoShadow, // Ombre lavande au survol
|
||||||
|
child: SizedBox(
|
||||||
|
height: 100,
|
||||||
|
width: 100,
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(5.0),
|
||||||
|
child: currentChildImage != null
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: kIsWeb // Condition pour le Web
|
||||||
|
? Image.network( // Utiliser Image.network pour le Web
|
||||||
|
currentChildImage.path,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
// Optionnel: Afficher un placeholder ou un message en cas d'erreur de chargement
|
||||||
|
print("Erreur de chargement de l'image réseau: $error");
|
||||||
|
return const Icon(Icons.broken_image, size: 40);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: Image.file( // Utiliser Image.file pour les autres plateformes
|
||||||
|
currentChildImage,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Image.asset(
|
||||||
|
'assets/images/photo.png',
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10), // Espace après la photo
|
||||||
|
|
||||||
|
// Nouveau Switch pour "Enfant à naître ?"
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, // Aligner le label à gauche, switch à droite
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Enfant à naître ?',
|
||||||
|
style: GoogleFonts.merienda(fontSize: 14, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: _isUnbornChild,
|
||||||
|
onChanged: (bool newValue) {
|
||||||
|
setState(() {
|
||||||
|
_isUnbornChild = newValue;
|
||||||
|
// Optionnel: Réinitialiser la date si le type change
|
||||||
|
// _dobController.clear();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
activeColor: Theme.of(context).primaryColor, // Utiliser une couleur de thème
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15), // Espace après le switch
|
||||||
|
|
||||||
|
_buildTextField(
|
||||||
|
_firstNameController,
|
||||||
|
'Prénom',
|
||||||
|
hintText: _isUnbornChild ? 'Prénom (optionnel)' : 'Prénom de l\'enfant', // HintText ajusté
|
||||||
|
isRequired: !_isUnbornChild,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
_buildTextField(_lastNameController, 'Nom', hintText: 'Nom de l\'enfant', enabled: true),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
_buildTextField(
|
||||||
|
_dobController,
|
||||||
|
_isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
|
||||||
|
hintText: 'JJ/MM/AAAA',
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () => _selectDate(context),
|
||||||
|
suffixIcon: Icons.calendar_today,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildCustomCheckbox(
|
||||||
|
label: 'Consentement photo',
|
||||||
|
value: _photoConsent,
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() => _photoConsent = newValue);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildCustomCheckbox(
|
||||||
|
label: 'Naissance multiple',
|
||||||
|
value: _multipleBirth,
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() => _multipleBirth = newValue);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widget pour construire une checkbox personnalisée
|
||||||
|
Widget _buildCustomCheckbox({required String label, required bool value, required ValueChanged<bool> onChanged}) {
|
||||||
|
const double checkboxSize = 20.0;
|
||||||
|
const double checkmarkSizeFactor = 1.4; // Augmenté pour une coche plus grande
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onChanged(!value),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox( // Envelopper le Stack dans un SizedBox pour fixer sa taille
|
||||||
|
width: checkboxSize,
|
||||||
|
height: checkboxSize,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/square.png',
|
||||||
|
height: checkboxSize, // Taille fixe
|
||||||
|
width: checkboxSize, // Taille fixe
|
||||||
|
),
|
||||||
|
if (value)
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/coche.png',
|
||||||
|
height: checkboxSize * checkmarkSizeFactor,
|
||||||
|
width: checkboxSize * checkmarkSizeFactor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(label, style: GoogleFonts.merienda(fontSize: 14)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widget pour construire les champs de texte (peut être externalisé)
|
||||||
|
// Ajout de onTap et suffixIcon pour le DatePicker
|
||||||
|
Widget _buildTextField(
|
||||||
|
TextEditingController controller,
|
||||||
|
String label, {
|
||||||
|
TextInputType? keyboardType,
|
||||||
|
bool obscureText = false,
|
||||||
|
String? hintText,
|
||||||
|
bool enabled = true,
|
||||||
|
bool readOnly = false,
|
||||||
|
VoidCallback? onTap,
|
||||||
|
IconData? suffixIcon,
|
||||||
|
bool isRequired = true, // Nouveau paramètre, par défaut à true
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: GoogleFonts.merienda(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
height: 45, // Hauteur fixe pour correspondre à l'image de fond
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
image: DecorationImage( // Rétablir input_field_bg.png
|
||||||
|
image: AssetImage('assets/images/input_field_bg.png'),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
// Pas de borderRadius ici si l'image de fond les a déjà
|
||||||
|
),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
obscureText: obscureText,
|
||||||
|
enabled: enabled,
|
||||||
|
readOnly: readOnly,
|
||||||
|
onTap: onTap,
|
||||||
|
style: GoogleFonts.merienda(fontSize: 15, color: enabled ? Colors.black87 : Colors.grey),
|
||||||
|
textAlignVertical: TextAlignVertical.center,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12), // Augmentation du padding vertical
|
||||||
|
hintText: hintText,
|
||||||
|
hintStyle: GoogleFonts.merienda(fontSize: 15, color: Colors.black38),
|
||||||
|
suffixIcon: suffixIcon != null ? Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0), // Espace pour l'icône
|
||||||
|
child: Icon(suffixIcon, color: Colors.black54, size: 20),
|
||||||
|
) : null,
|
||||||
|
isDense: true, // Aide à réduire la hauteur par défaut
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (!enabled) return null;
|
||||||
|
if (readOnly) return null;
|
||||||
|
if (isRequired && (value == null || value.isEmpty)) { // Validation conditionnée par isRequired
|
||||||
|
return 'Ce champ est obligatoire';
|
||||||
|
}
|
||||||
|
// TODO: Validations spécifiques (à garder si pertinent pour d'autres champs)
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
161
frontend/lib/screens/auth/register_choice_screen.dart
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
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: const BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage('assets/images/card_rose.png'),
|
||||||
|
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> { ... }
|
||||||
@ -1,50 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import '../../theme/theme_provider.dart';
|
|
||||||
import '../../theme/app_theme.dart';
|
|
||||||
|
|
||||||
class HomeScreen extends StatelessWidget {
|
class HomeScreen extends StatelessWidget {
|
||||||
const HomeScreen({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Accueil'),
|
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(
|
body: const Center(
|
||||||
child: Text('Bienvenue sur P\'titsPas !'),
|
child: Text('Bienvenue sur P\'titsPas !'),
|
||||||
|
|||||||
88
frontend/lib/widgets/hover_relief_widget.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -126,6 +126,11 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.3"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -240,6 +245,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1+1"
|
version: "0.2.1+1"
|
||||||
|
intl:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.19.0"
|
||||||
js:
|
js:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -9,6 +9,8 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
provider: ^6.1.1
|
provider: ^6.1.1
|
||||||
go_router: ^13.2.5
|
go_router: ^13.2.5
|
||||||
google_fonts: ^6.1.0
|
google_fonts: ^6.1.0
|
||||||
@ -27,13 +29,7 @@ flutter:
|
|||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
assets:
|
assets:
|
||||||
- assets/images/logo.png
|
- assets/images/ # Déclarer le dossier entier
|
||||||
- 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
|
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
- family: Merienda
|
- family: Merienda
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<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 -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
@ -27,11 +27,17 @@
|
|||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- 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>
|
<title>P'titsPas</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<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>
|
<script>
|
||||||
// The value below is injected by flutter build, do not touch.
|
// The value below is injected by flutter build, do not touch.
|
||||||
const serviceWorkerVersion = null;
|
const serviceWorkerVersion = null;
|
||||||
|
|||||||
55
lib/main.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||