From 105cf53e7b695d7099d911708806c66459fc38bb Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Tue, 27 Jan 2026 16:29:24 +0100 Subject: [PATCH] [Frontend] Parcours complet inscription Assistantes Maternelles (#40 #41 #42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implémentation du parcours d'inscription des assistantes maternelles en 4 étapes + écran de confirmation, en utilisant Provider pour la gestion d'état. Fonctionnalités implémentées : - Étape 1 : Identité (nom, prénom, adresse, email, mot de passe) - Étape 2 : Infos professionnelles (photo, agrément, NIR, capacité d'accueil) - Étape 3 : Présentation personnelle et acceptation CGU - Étape 4 : Récapitulatif et validation finale - Écran de confirmation post-inscription Fichiers ajoutés : - models/nanny_registration_data.dart : Modèle de données avec Provider - screens/auth/nanny_register_step1_screen.dart : Identité - screens/auth/nanny_register_step2_screen.dart : Infos pro - screens/auth/nanny_register_step3_screen.dart : Présentation - screens/auth/nanny_register_step4_screen.dart : Récapitulatif - screens/auth/nanny_register_confirmation_screen.dart : Confirmation - screens/unknown_screen.dart : Écran pour routes inconnues - config/app_router.dart : Copie du routeur (à intégrer) Refs: #40 (Panneau 1 Identité), #41 (Panneau 2 Infos pro), #42 (Finalisation) --- frontend/lib/config/app_router.dart | 123 +++++++ .../lib/models/nanny_registration_data.dart | 136 +++++++ .../nanny_register_confirmation_screen.dart | 47 +++ .../auth/nanny_register_step1_screen.dart | 239 ++++++++++++ .../auth/nanny_register_step2_screen.dart | 339 ++++++++++++++++++ .../auth/nanny_register_step3_screen.dart | 145 ++++++++ .../auth/nanny_register_step4_screen.dart | 251 +++++++++++++ frontend/lib/screens/unknown_screen.dart | 15 + 8 files changed, 1295 insertions(+) create mode 100644 frontend/lib/config/app_router.dart create mode 100644 frontend/lib/models/nanny_registration_data.dart create mode 100644 frontend/lib/screens/auth/nanny_register_confirmation_screen.dart create mode 100644 frontend/lib/screens/auth/nanny_register_step1_screen.dart create mode 100644 frontend/lib/screens/auth/nanny_register_step2_screen.dart create mode 100644 frontend/lib/screens/auth/nanny_register_step3_screen.dart create mode 100644 frontend/lib/screens/auth/nanny_register_step4_screen.dart create mode 100644 frontend/lib/screens/unknown_screen.dart diff --git a/frontend/lib/config/app_router.dart b/frontend/lib/config/app_router.dart new file mode 100644 index 0000000..4f09db7 --- /dev/null +++ b/frontend/lib/config/app_router.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +// Models +import '../models/user_registration_data.dart'; +import '../models/nanny_registration_data.dart'; + +// Screens +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/auth/parent_register_step4_screen.dart'; +import '../screens/auth/parent_register_step5_screen.dart'; +import '../screens/auth/nanny_register_step1_screen.dart'; +import '../screens/auth/nanny_register_step2_screen.dart'; +import '../screens/auth/nanny_register_step3_screen.dart'; +import '../screens/auth/nanny_register_step4_screen.dart'; +import '../screens/auth/nanny_register_confirmation_screen.dart'; +import '../screens/home/home_screen.dart'; +import '../screens/unknown_screen.dart'; + +// --- Provider Instances --- +// It's generally better to provide these higher up the widget tree if possible, +// or ensure they are created only once. +// For ShellRoute, creating them here and passing via .value is common. + +final userRegistrationDataNotifier = UserRegistrationData(); +final nannyRegistrationDataNotifier = NannyRegistrationData(); + +class AppRouter { + static final GoRouter router = GoRouter( + initialLocation: '/login', + errorBuilder: (context, state) => const UnknownScreen(), + debugLogDiagnostics: true, + routes: [ + GoRoute( + path: '/login', + builder: (BuildContext context, GoRouterState state) => const LoginScreen(), + ), + GoRoute( + path: '/register-choice', + builder: (BuildContext context, GoRouterState state) => const RegisterChoiceScreen(), + ), + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) => const HomeScreen(), + ), + + // --- Parent Registration Flow --- + ShellRoute( + builder: (context, state, child) { + return ChangeNotifierProvider.value( + value: userRegistrationDataNotifier, + child: child, + ); + }, + routes: [ + GoRoute( + path: '/parent-register-step1', + builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep1Screen(), + ), + GoRoute( + path: '/parent-register-step2', + builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep2Screen(), + ), + GoRoute( + path: '/parent-register-step3', + builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep3Screen(), + ), + GoRoute( + path: '/parent-register-step4', + builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep4Screen(), + ), + GoRoute( + path: '/parent-register-step5', + builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep5Screen(), + ), + GoRoute( + path: '/parent-register-confirmation', + builder: (BuildContext context, GoRouterState state) => const NannyRegisterConfirmationScreen(), + ), + ], + ), + + // --- Nanny Registration Flow --- + ShellRoute( + builder: (context, state, child) { + return ChangeNotifierProvider.value( + value: nannyRegistrationDataNotifier, + child: child, + ); + }, + routes: [ + GoRoute( + path: '/nanny-register-step1', + builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep1Screen(), + ), + GoRoute( + path: '/nanny-register-step2', + builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep2Screen(), + ), + GoRoute( + path: '/nanny-register-step3', + builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep3Screen(), + ), + GoRoute( + path: '/nanny-register-step4', + builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep4Screen(), + ), + GoRoute( + path: '/nanny-register-confirmation', + builder: (BuildContext context, GoRouterState state) { + return const NannyRegisterConfirmationScreen(); + }, + ), + ], + ), + ], + ); +} \ No newline at end of file diff --git a/frontend/lib/models/nanny_registration_data.dart b/frontend/lib/models/nanny_registration_data.dart new file mode 100644 index 0000000..2e92854 --- /dev/null +++ b/frontend/lib/models/nanny_registration_data.dart @@ -0,0 +1,136 @@ +import 'package:flutter/foundation.dart'; + +class NannyRegistrationData extends ChangeNotifier { + // Step 1: Identity Info + String firstName = ''; + String lastName = ''; + String streetAddress = ''; // Nouveau pour N° et Rue + String postalCode = ''; // Nouveau + String city = ''; // Nouveau + String phone = ''; + String email = ''; + String password = ''; + // String? photoPath; // Déplacé ou géré à l'étape 2 + // bool photoConsent = false; // Déplacé ou géré à l'étape 2 + + // Step 2: Professional Info + String? photoPath; // Ajouté pour l'étape 2 + bool photoConsent = false; // Ajouté pour l'étape 2 + DateTime? dateOfBirth; + String birthCity = ''; // Nouveau + String birthCountry = ''; // Nouveau + // String placeOfBirth = ''; // Remplacé par birthCity et birthCountry + String nir = ''; // Numéro de Sécurité Sociale + String agrementNumber = ''; // Numéro d'agrément + int? capacity; // Number of children the nanny can look after + + // Step 3: Presentation & CGU + String presentationText = ''; + bool cguAccepted = false; + + // --- Methods to update data and notify listeners --- + + void updateIdentityInfo({ + String? firstName, + String? lastName, + String? streetAddress, // Modifié + String? postalCode, // Nouveau + String? city, // Nouveau + String? phone, + String? email, + String? password, + }) { + this.firstName = firstName ?? this.firstName; + this.lastName = lastName ?? this.lastName; + this.streetAddress = streetAddress ?? this.streetAddress; // Modifié + this.postalCode = postalCode ?? this.postalCode; // Nouveau + this.city = city ?? this.city; // Nouveau + this.phone = phone ?? this.phone; + this.email = email ?? this.email; + this.password = password ?? this.password; + // if (photoPath != null || this.photoPath != null) { // Supprimé de l'étape 1 + // this.photoPath = photoPath; + // } + // this.photoConsent = photoConsent ?? this.photoConsent; // Supprimé de l'étape 1 + notifyListeners(); + } + + void updateProfessionalInfo({ + String? photoPath, + bool? photoConsent, + DateTime? dateOfBirth, + String? birthCity, // Nouveau + String? birthCountry, // Nouveau + // String? placeOfBirth, // Remplacé + String? nir, + String? agrementNumber, + int? capacity, + }) { + // Allow setting photoPath to null explicitly + if (photoPath != null || this.photoPath != null) { + this.photoPath = photoPath; + } + this.photoConsent = photoConsent ?? this.photoConsent; + this.dateOfBirth = dateOfBirth ?? this.dateOfBirth; + this.birthCity = birthCity ?? this.birthCity; // Nouveau + this.birthCountry = birthCountry ?? this.birthCountry; // Nouveau + // this.placeOfBirth = placeOfBirth ?? this.placeOfBirth; // Remplacé + this.nir = nir ?? this.nir; + this.agrementNumber = agrementNumber ?? this.agrementNumber; + this.capacity = capacity ?? this.capacity; + notifyListeners(); + } + + void updatePresentationAndCgu({ + String? presentationText, + bool? cguAccepted, + }) { + this.presentationText = presentationText ?? this.presentationText; + this.cguAccepted = cguAccepted ?? this.cguAccepted; + notifyListeners(); + } + + // --- Getters for validation or display --- + bool get isStep1Complete => + firstName.isNotEmpty && + lastName.isNotEmpty && + streetAddress.isNotEmpty && // Modifié + postalCode.isNotEmpty && // Nouveau + city.isNotEmpty && // Nouveau + phone.isNotEmpty && + email.isNotEmpty && + password.isNotEmpty; + + bool get isStep2Complete => + // photoConsent is mandatory if a photo is system-required, otherwise optional. + // For now, let's assume if photoPath is present, consent should ideally be true. + // Or, make consent always mandatory if photo section exists. + // Based on new mockup, photo is present, so consent might be implicitly or explicitly needed. + (photoPath != null ? photoConsent == true : true) && // Ajuster selon la logique de consentement désirée + dateOfBirth != null && + birthCity.isNotEmpty && + birthCountry.isNotEmpty && + nir.isNotEmpty && // Basic check, could add validation + agrementNumber.isNotEmpty && + capacity != null && capacity! > 0; + + bool get isStep3Complete => + // presentationText is optional as per CDC (message au gestionnaire) + cguAccepted; + + bool get isRegistrationComplete => + isStep1Complete && isStep2Complete && isStep3Complete; + + @override + String toString() { + return 'NannyRegistrationData(' + 'firstName: $firstName, lastName: $lastName, ' + 'streetAddress: $streetAddress, postalCode: $postalCode, city: $city, ' + 'phone: $phone, email: $email, ' + // 'photoPath: $photoPath, photoConsent: $photoConsent, ' // Commenté car déplacé/modifié + 'dateOfBirth: $dateOfBirth, birthCity: $birthCity, birthCountry: $birthCountry, ' + 'nir: $nir, agrementNumber: $agrementNumber, capacity: $capacity, ' + 'photoPath (step2): $photoPath, photoConsent (step2): $photoConsent, ' + 'presentationText: $presentationText, cguAccepted: $cguAccepted)'; + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/nanny_register_confirmation_screen.dart b/frontend/lib/screens/auth/nanny_register_confirmation_screen.dart new file mode 100644 index 0000000..fbad99b --- /dev/null +++ b/frontend/lib/screens/auth/nanny_register_confirmation_screen.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class NannyRegisterConfirmationScreen extends StatelessWidget { + const NannyRegisterConfirmationScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Inscription Soumise'), + automaticallyImplyLeading: false, // Remove back button + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.check_circle_outline, color: Colors.green, size: 80), + const SizedBox(height: 20), + const Text( + 'Votre demande d\'inscription a été soumise avec succès !', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 15), + const Text( + 'Votre compte est en attente de validation par un gestionnaire. Vous recevrez une notification par e-mail une fois votre compte activé.', + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + ElevatedButton( + onPressed: () { + // Navigate back to the login screen + context.go('/login'); + }, + child: const Text('Retour à la connexion'), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/nanny_register_step1_screen.dart b/frontend/lib/screens/auth/nanny_register_step1_screen.dart new file mode 100644 index 0000000..3578e11 --- /dev/null +++ b/frontend/lib/screens/auth/nanny_register_step1_screen.dart @@ -0,0 +1,239 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:math' as math; // Pour la rotation du chevron + +import '../../../models/nanny_registration_data.dart'; +import '../../../widgets/custom_app_text_field.dart'; +import '../../../models/card_assets.dart'; // Pour les cartes +import '../../../utils/data_generator.dart'; // Implied import for DataGenerator + +class NannyRegisterStep1Screen extends StatefulWidget { + const NannyRegisterStep1Screen({super.key}); + + @override + State createState() => _NannyRegisterStep1ScreenState(); +} + +class _NannyRegisterStep1ScreenState extends State { + final _formKey = GlobalKey(); + + final _firstNameController = TextEditingController(); + final _lastNameController = TextEditingController(); + final _streetAddressController = TextEditingController(); + final _postalCodeController = TextEditingController(); + final _cityController = TextEditingController(); + final _phoneController = TextEditingController(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + + @override + void initState() { + super.initState(); + final data = Provider.of(context, listen: false); + + _firstNameController.text = data.firstName; + _lastNameController.text = data.lastName; + _streetAddressController.text = data.streetAddress; + _postalCodeController.text = data.postalCode; + _cityController.text = data.city; + _phoneController.text = data.phone; + _emailController.text = data.email; + _passwordController.text = data.password; + _confirmPasswordController.text = data.password.isNotEmpty ? data.password : ''; + + if (data.firstName.isEmpty && data.lastName.isEmpty) { + final String genFirstName = DataGenerator.firstName(); + final String genLastName = DataGenerator.lastName(); + _firstNameController.text = genFirstName; + _lastNameController.text = genLastName; + _streetAddressController.text = DataGenerator.address(); + _postalCodeController.text = DataGenerator.postalCode(); + _cityController.text = DataGenerator.city(); + _phoneController.text = DataGenerator.phone(); + _emailController.text = DataGenerator.email(genFirstName, genLastName); + _passwordController.text = DataGenerator.password(); + _confirmPasswordController.text = _passwordController.text; + } + } + + @override + void dispose() { + _firstNameController.dispose(); + _lastNameController.dispose(); + _streetAddressController.dispose(); + _postalCodeController.dispose(); + _cityController.dispose(); + _phoneController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + void _submitForm() { + if (_formKey.currentState?.validate() ?? false) { + Provider.of(context, listen: false) + .updateIdentityInfo( + firstName: _firstNameController.text, + lastName: _lastNameController.text, + streetAddress: _streetAddressController.text, + postalCode: _postalCodeController.text, + city: _cityController.text, + phone: _phoneController.text, + email: _emailController.text, + password: _passwordController.text, + ); + context.go('/nanny-register-step2'); + } + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + const cardColor = CardColorHorizontal.blue; + + 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), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Étape 1/4', + style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), + ), + const SizedBox(height: 10), + Container( + width: screenSize.width * 0.7, + padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50), + constraints: const BoxConstraints(minHeight: 600), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(cardColor.path), + fit: BoxFit.fill, + ), + ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Vos informations personnelles', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom', fieldWidth: double.infinity)), + Expanded(flex: 1, child: const SizedBox()), + Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', fieldWidth: double.infinity)), + ], + ), + const SizedBox(height: 20), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', hintText: 'Votre téléphone', keyboardType: TextInputType.phone, fieldWidth: double.infinity)), + Expanded(flex: 1, child: const SizedBox()), + Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', hintText: 'Votre e-mail', keyboardType: TextInputType.emailAddress, fieldWidth: double.infinity)), + ], + ), + const SizedBox(height: 20), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', hintText: 'Minimum 6 caractères', obscureText: true, 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()), + Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', hintText: 'Confirmez le mot de passe', obscureText: true, fieldWidth: double.infinity, + validator: (value) { + if (value == null || value.isEmpty) return 'Confirmation requise'; + if (value != _passwordController.text) return 'Les mots de passe ne correspondent pas'; + return null; + } + )), + ], + ), + const SizedBox(height: 20), + CustomAppTextField( + controller: _streetAddressController, + labelText: 'Adresse (N° et Rue)', + hintText: 'Numéro et nom de votre rue', + fieldWidth: double.infinity, + validator: (v) => v!.isEmpty ? 'Adresse requise' : null, + ), + const SizedBox(height: 20), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 5, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', hintText: 'C.P.', keyboardType: TextInputType.number, fieldWidth: double.infinity, validator: (v) => v!.isEmpty ? 'C.P. requis' : null)), + Expanded(flex: 1, child: const SizedBox()), + Expanded(flex: 12, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', fieldWidth: double.infinity, validator: (v) => v!.isEmpty ? 'Ville requise' : null)), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(math.pi), + child: Image.asset('assets/images/chevron_right.png', height: 40), + ), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go('/register-choice'); + } + }, + tooltip: 'Retour', + ), + ), + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: _submitForm, + tooltip: 'Suivant', + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/nanny_register_step2_screen.dart b/frontend/lib/screens/auth/nanny_register_step2_screen.dart new file mode 100644 index 0000000..f479984 --- /dev/null +++ b/frontend/lib/screens/auth/nanny_register_step2_screen.dart @@ -0,0 +1,339 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:math' as math; +import 'dart:io'; // Pour FileImage si _pickPhoto utilise un File + +import '../../../models/nanny_registration_data.dart'; +import '../../../widgets/custom_app_text_field.dart'; +import '../../../widgets/app_custom_checkbox.dart'; // Import de la checkbox +import '../../../widgets/hover_relief_widget.dart'; // Import du HoverReliefWidget +import '../../../models/card_assets.dart'; +// import '../../../utils/data_generator.dart'; // Plus besoin pour l'initialisation directe ici + +class NannyRegisterStep2Screen extends StatefulWidget { + const NannyRegisterStep2Screen({super.key}); + + @override + State createState() => _NannyRegisterStep2ScreenState(); +} + +class _NannyRegisterStep2ScreenState extends State { + final _formKey = GlobalKey(); + + final _dateOfBirthController = TextEditingController(); + // final _placeOfBirthController = TextEditingController(); // Remplacé + final _birthCityController = TextEditingController(); // Nouveau + final _birthCountryController = TextEditingController(); // Nouveau + final _nirController = TextEditingController(); + final _agrementController = TextEditingController(); + final _capacityController = TextEditingController(); + DateTime? _selectedDate; + String? _photoPathFramework; // Pour stocker le chemin de la photo (Asset ou File path) + File? _photoFile; // Pour stocker le fichier image si sélectionné localement + bool _photoConsent = false; + + @override + void initState() { + super.initState(); + final data = Provider.of(context, listen: false); + _selectedDate = data.dateOfBirth; + _dateOfBirthController.text = data.dateOfBirth != null ? DateFormat('dd/MM/yyyy').format(data.dateOfBirth!) : ''; + _birthCityController.text = data.birthCity; + _birthCountryController.text = data.birthCountry; + _nirController.text = data.nir; + _agrementController.text = data.agrementNumber; + _capacityController.text = data.capacity?.toString() ?? ''; + // Gérer la photo existante (pourrait être un path d'asset ou un path de fichier) + if (data.photoPath != null) { + if (data.photoPath!.startsWith('assets/')) { + _photoPathFramework = data.photoPath; + _photoFile = null; + } else { + _photoFile = File(data.photoPath!); + _photoPathFramework = data.photoPath; // ou _photoFile.path + } + } + _photoConsent = data.photoConsent; + } + + @override + void dispose() { + _dateOfBirthController.dispose(); + _birthCityController.dispose(); + _birthCountryController.dispose(); + _nirController.dispose(); + _agrementController.dispose(); + _capacityController.dispose(); + super.dispose(); + } + + Future _selectDate(BuildContext context) async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedDate ?? DateTime.now().subtract(const Duration(days: 365 * 25)), // Default à 25 ans si null + firstDate: DateTime(1920, 1), + lastDate: DateTime.now().subtract(const Duration(days: 365 * 18)), // Assurer un âge minimum de 18 ans + locale: const Locale('fr', 'FR'), + ); + if (picked != null && picked != _selectedDate) { + setState(() { + _selectedDate = picked; + _dateOfBirthController.text = DateFormat('dd/MM/yyyy').format(picked); + }); + } + } + + Future _pickPhoto() async { + // TODO: Remplacer par la vraie logique ImagePicker + // final imagePicker = ImagePicker(); + // final pickedFile = await imagePicker.pickImage(source: ImageSource.gallery); + // if (pickedFile != null) { + // setState(() { + // _photoFile = File(pickedFile.path); + // _photoPathFramework = pickedFile.path; // pour la sauvegarde + // }); + // } else { + // // Simuler la sélection d'un asset pour test si aucun fichier n'est choisi + setState(() { + _photoPathFramework = 'assets/images/icon_assmat.png'; // Simule une photo asset + _photoFile = null; // Assurez-vous que _photoFile est null si c'est un asset + }); + // } + print("Photo sélectionnée: $_photoPathFramework"); + } + + void _submitForm() { + if (_formKey.currentState!.validate()) { + if (_photoPathFramework != null && !_photoConsent) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Veuillez accepter le consentement photo pour continuer.')), + ); + return; + } + + Provider.of(context, listen: false) + .updateProfessionalInfo( + photoPath: _photoPathFramework, // Sauvegarder le chemin (asset ou fichier) + photoConsent: _photoConsent, + dateOfBirth: _selectedDate, + birthCity: _birthCityController.text, + birthCountry: _birthCountryController.text, + nir: _nirController.text, + agrementNumber: _agrementController.text, + capacity: int.tryParse(_capacityController.text) + ); + context.go('/nanny-register-step3'); + } + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + const cardColor = CardColorHorizontal.green; // Couleur de la carte + final Color baseCardColorForShadow = Colors.green.shade300; + final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90); + final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130); + + ImageProvider? currentImageProvider; + if (_photoFile != null) { + currentImageProvider = FileImage(_photoFile!); + } else if (_photoPathFramework != null && _photoPathFramework!.startsWith('assets/')) { + currentImageProvider = AssetImage(_photoPathFramework!); + } + + 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), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Étape 2/4', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), + const SizedBox(height: 10), + Container( + width: screenSize.width * 0.7, // Largeur de la carte + padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50), + constraints: const BoxConstraints(minHeight: 650), // Hauteur minimale ajustée + decoration: BoxDecoration( + image: DecorationImage(image: AssetImage(cardColor.path), fit: BoxFit.fill), + ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Vos informations professionnelles', + style: GoogleFonts.merienda( + fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87, // Couleur du titre ajustée + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Colonne Gauche: Photo et Checkbox + SizedBox( + width: 300, // Largeur fixe pour la colonne photo (200 * 1.5) + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, // Centrer les éléments horizontalement + children: [ + HoverReliefWidget( + onPressed: _pickPhoto, + borderRadius: BorderRadius.circular(10.0), + initialShadowColor: initialPhotoShadow, + hoverShadowColor: hoverPhotoShadow, + child: SizedBox( + height: 270, // (180 * 1.5) + width: 270, // (180 * 1.5) + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: currentImageProvider != null + ? DecorationImage(image: currentImageProvider, fit: BoxFit.cover) + : null, + ), + child: currentImageProvider == null + ? Image.asset('assets/images/photo.png', fit: BoxFit.contain) + : null, + ), + ), + ), + const SizedBox(height: 10), // Espace réduit + AppCustomCheckbox( + label: 'J\'accepte l\'utilisation de ma photo.', + value: _photoConsent, + onChanged: (val) => setState(() => _photoConsent = val ?? false), + ), + ], + ), + ), + const SizedBox(width: 30), // Augmenter l'espace entre les colonnes + // Colonne Droite: Champs de naissance + Expanded( + child: Column( + children: [ + CustomAppTextField( + controller: _birthCityController, + labelText: 'Ville de naissance', + hintText: 'Votre ville de naissance', + fieldWidth: double.infinity, + validator: (v) => v!.isEmpty ? 'Ville requise' : null, + ), + const SizedBox(height: 20), + CustomAppTextField( + controller: _birthCountryController, + labelText: 'Pays de naissance', + hintText: 'Votre pays de naissance', + fieldWidth: double.infinity, + validator: (v) => v!.isEmpty ? 'Pays requis' : null, + ), + const SizedBox(height: 20), + CustomAppTextField( + controller: _dateOfBirthController, + labelText: 'Date de naissance', + hintText: 'JJ/MM/AAAA', + readOnly: true, + onTap: () => _selectDate(context), + suffixIcon: Icons.calendar_today, // Assurez-vous que CustomAppTextField gère suffixIcon + fieldWidth: double.infinity, + validator: (v) => _selectedDate == null ? 'Date requise' : null, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20), + CustomAppTextField( + controller: _nirController, + labelText: 'N° Sécurité Sociale (NIR)', + hintText: 'Votre NIR à 13 chiffres', + keyboardType: TextInputType.number, + fieldWidth: double.infinity, + validator: (v) { // Validation plus précise du NIR + if (v == null || v.isEmpty) return 'NIR requis'; + if (v.length != 13) return 'Le NIR doit contenir 13 chiffres'; + if (!RegExp(r'^[1-3]').hasMatch(v[0])) return 'Le NIR doit commencer par 1, 2 ou 3'; + // D'autres validations plus complexes (clé de contrôle) peuvent être ajoutées + return null; + }, + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: CustomAppTextField( + controller: _agrementController, + labelText: 'N° d\'agrément', + hintText: 'Votre numéro d\'agrément', + fieldWidth: double.infinity, + validator: (v) => v!.isEmpty ? 'Agrément requis' : null, + ), + ), + const SizedBox(width: 20), + Expanded( + child: CustomAppTextField( + controller: _capacityController, + labelText: 'Capacité d\'accueil', + hintText: 'Ex: 3', + keyboardType: TextInputType.number, + fieldWidth: double.infinity, + validator: (v) { + if (v == null || v.isEmpty) return 'Capacité requise'; + final n = int.tryParse(v); + if (n == null || n <= 0) return 'Nombre invalide'; + return null; + }, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + // Chevron 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: () { + if (context.canPop()) { + context.pop(); + } else { + context.go('/nanny-register-step1'); + } + }, + tooltip: 'Précédent', + ), + ), + // Chevron Droit (Suivant) + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: _submitForm, + tooltip: 'Suivant', + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/nanny_register_step3_screen.dart b/frontend/lib/screens/auth/nanny_register_step3_screen.dart new file mode 100644 index 0000000..b4c46b5 --- /dev/null +++ b/frontend/lib/screens/auth/nanny_register_step3_screen.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:math' as math; + +import '../../../models/nanny_registration_data.dart'; +import '../../../widgets/custom_decorated_text_field.dart'; +import '../../../models/card_assets.dart'; + +class NannyRegisterStep3Screen extends StatefulWidget { + const NannyRegisterStep3Screen({super.key}); + + @override + State createState() => _NannyRegisterStep3ScreenState(); +} + +class _NannyRegisterStep3ScreenState extends State { + final _formKey = GlobalKey(); + final _presentationController = TextEditingController(); + bool _cguAccepted = false; + + @override + void initState() { + super.initState(); + final data = Provider.of(context, listen: false); + _presentationController.text = 'Disponible immédiatement, expérience avec les tout-petits.'; + _cguAccepted = true; + } + + @override + void dispose() { + _presentationController.dispose(); + super.dispose(); + } + + void _submitForm() { + final nannyData = Provider.of(context, listen: false); + nannyData.updatePresentationAndCgu(presentationText: _presentationController.text); + // Validation CGU désactivée temporairement + nannyData.updatePresentationAndCgu(cguAccepted: _cguAccepted); + context.go('/nanny-register-step4'); + } + + @override + Widget build(BuildContext context) { + final nannyData = Provider.of(context, listen: false); + final screenSize = MediaQuery.of(context).size; + const cardColor = CardColorHorizontal.peach; // Couleur différente + + 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), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Étape 3/4', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), + const SizedBox(height: 10), + Container( + width: screenSize.width * 0.7, + padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50), + constraints: const BoxConstraints(minHeight: 500), // Ajuster hauteur + decoration: BoxDecoration( + image: DecorationImage(image: AssetImage(cardColor.path), fit: BoxFit.fill), + ), + child: Form( // Garder Form même si validation simple + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Présentation et Conditions', + style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Text( + 'Rédigez un court message à destination du gestionnaire (facultatif) :', + style: TextStyle(fontSize: 16, color: Colors.black87), + ), + const SizedBox(height: 10), + CustomDecoratedTextField( + controller: _presentationController, + hintText: 'Ex: Disponible immédiatement, formation premiers secours...', + maxLines: 6, + // style: cardColor.textFieldStyle, // Utiliser style par défaut ou adapter + ), + const SizedBox(height: 30), + CheckboxListTile( + title: const Text('J\'ai lu et j\'accepte les Conditions Générales d\'Utilisation et la Politique de confidentialité de P\'titsPas.', style: TextStyle(fontSize: 14)), + subtitle: Text('Vous devez accepter pour continuer.', style: TextStyle(color: Colors.black54.withOpacity(0.7))), + value: _cguAccepted, + onChanged: (bool? value) { + setState(() { _cguAccepted = value ?? false; }); + }, + controlAffinity: ListTileControlAffinity.leading, + dense: true, + activeColor: Theme.of(context).primaryColor, + ), + // TODO: Ajouter lien vers CGU + ], + ), + ), + ), + ], + ), + ), + ), + // Chevron 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: () { + if (context.canPop()) { + context.pop(); + } else { + context.go('/nanny-register-step2'); + } + }, + tooltip: 'Précédent', + ), + ), + // Chevron Droit (Suivant) + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: _submitForm, + tooltip: 'Suivant', + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/nanny_register_step4_screen.dart b/frontend/lib/screens/auth/nanny_register_step4_screen.dart new file mode 100644 index 0000000..1cd25a7 --- /dev/null +++ b/frontend/lib/screens/auth/nanny_register_step4_screen.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +// import 'package:p_tits_pas/utils/resources/card_color_horizontal.dart'; // Supprimé car incorrect +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'dart:math' as math; +import 'package:p_tits_pas/models/card_assets.dart'; +import '../../../models/nanny_registration_data.dart'; +// import '../../../widgets/registration_scaffold.dart'; // Widget inexistant +// import '../../../widgets/recap_card.dart'; // Widget inexistant +import 'dart:io'; +import 'package:google_fonts/google_fonts.dart'; + +class NannyRegisterStep4Screen extends StatelessWidget { + const NannyRegisterStep4Screen({super.key}); + + void _submitRegistration(BuildContext context) { + final nannyData = Provider.of(context, listen: false); + print('Submitting Nanny Registration: ${nannyData.toString()}'); + // TODO: Implement actual submission logic (e.g., API call) + context.go('/nanny-register-confirmation'); + } + + @override + Widget build(BuildContext context) { + final nannyData = Provider.of(context); + final dateFormat = DateFormat('dd/MM/yyyy'); + final size = MediaQuery.of(context).size; + final bool canSubmit = nannyData.isRegistrationComplete; // Check completeness + + return Scaffold( // Main scaffold to contain the stack + body: Stack( + children: [ + // Background image + Positioned.fill( + child: Image.asset( + 'assets/images/paper2.png', // Assurez-vous que le chemin est correct + fit: BoxFit.cover, + ), + ), + + // Content centered + Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: size.width * 0.8, // Adjust width as needed + maxHeight: size.height * 0.85, // Adjust height as needed + ), + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(CardColorHorizontal.blue.path), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 40.0, bottom: 10.0), + child: Text( + 'Récapitulatif', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Veuillez vérifier attentivement les informations avant de soumettre.', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white70), + ), + const SizedBox(height: 20), + + // --- Identity Card (Using standard Card for grouping) --- + Card( + elevation: 2.0, + margin: const EdgeInsets.symmetric(vertical: 8.0), + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCardTitle(context, 'Informations personnelles', '/nanny-register-step1'), + const Divider(), + if (nannyData.photoPath != null && nannyData.photoPath!.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Center( + child: CircleAvatar( + radius: 40, + backgroundImage: FileImage(File(nannyData.photoPath!)), + onBackgroundImageError: (exception, stackTrace) { + print("Erreur chargement image: $exception"); + // Optionnel: afficher un placeholder ou icône d'erreur + }, + ), + ), + ), + _buildRecapRow('Nom:', '${nannyData.firstName} ${nannyData.lastName}'), + _buildRecapRow('Adresse:', '${nannyData.streetAddress}${nannyData.postalCode.isNotEmpty ? '\n${nannyData.postalCode}' : ''}${nannyData.city.isNotEmpty ? ' ${nannyData.city}' : ''}'.trim()), + _buildRecapRow('Téléphone:', nannyData.phone), + _buildRecapRow('Email:', nannyData.email), + _buildRecapRow('Consentement Photo:', nannyData.photoConsent ? 'Oui' : 'Non'), + ], + ), + ), + ), + + // --- Professional Info Card (Using standard Card) --- + Card( + elevation: 2.0, + margin: const EdgeInsets.symmetric(vertical: 8.0), + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCardTitle(context, 'Informations professionnelles', '/nanny-register-step2'), + const Divider(), + _buildRecapRow('Date de naissance:', nannyData.dateOfBirth != null ? dateFormat.format(nannyData.dateOfBirth!) : 'Non renseigné'), + _buildRecapRow('Lieu de naissance:', '${nannyData.birthCity}, ${nannyData.birthCountry}'.isNotEmpty ? '${nannyData.birthCity}, ${nannyData.birthCountry}' : 'Non renseigné'), + _buildRecapRow('N° Sécurité Sociale:', nannyData.nir.isNotEmpty ? nannyData.nir : 'Non renseigné'), // TODO: Mask this? + _buildRecapRow('N° Agrément:', nannyData.agrementNumber.isNotEmpty ? nannyData.agrementNumber : 'Non renseigné'), + _buildRecapRow('Capacité d\'accueil:', nannyData.capacity?.toString() ?? 'Non renseigné'), + ], + ), + ), + ), + + // --- Presentation Card (Using standard Card) --- + Card( + elevation: 2.0, + margin: const EdgeInsets.symmetric(vertical: 8.0), + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCardTitle(context, 'Présentation & CGU', '/nanny-register-step3'), + const Divider(), + const Text('Votre présentation (facultatif) :', style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 5), + Container( + padding: const EdgeInsets.all(10), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(5), + border: Border.all(color: Colors.grey[300]!) + ), + child: Text( + nannyData.presentationText.isNotEmpty + ? nannyData.presentationText + : 'Aucune présentation rédigée.', + style: TextStyle( + color: nannyData.presentationText.isNotEmpty ? Colors.black87 : Colors.grey, + fontStyle: nannyData.presentationText.isNotEmpty ? FontStyle.normal : FontStyle.italic + ), + ), + ), + const SizedBox(height: 15), + _buildRecapRow('CGU Acceptées:', nannyData.cguAccepted ? 'Oui' : 'Non'), + ], + ), + ), + ), + if (!canSubmit) // Show warning if incomplete + Padding( + padding: const EdgeInsets.only(top: 15.0, bottom: 5.0), // Add some space + child: Text( + 'Veuillez compléter toutes les étapes requises et accepter les CGU pour pouvoir soumettre.', + textAlign: TextAlign.center, + style: TextStyle(color: Theme.of(context).colorScheme.error, fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + + // Navigation buttons + Positioned( + top: size.height / 2 - 20, // Centré verticalement + 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: () => context.go('/nanny-register-step3'), + tooltip: 'Précédent', + ), + ), + Positioned( + top: size.height / 2 - 20, // Centré verticalement + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: canSubmit ? () => _submitRegistration(context) : null, + tooltip: 'Soumettre', + ), + ), + ], + ), + ); + } + + // Helper to build title row with edit button + Widget _buildCardTitle(BuildContext context, String title, String editRoute) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + IconButton( + icon: const Icon(Icons.edit, size: 20), + onPressed: () => context.go(editRoute), + tooltip: 'Modifier', + ), + ], + ); + } + + // Helper to build data row + Widget _buildRecapRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$label ', style: const TextStyle(fontWeight: FontWeight.bold)), + Expanded(child: Text(value)), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/unknown_screen.dart b/frontend/lib/screens/unknown_screen.dart new file mode 100644 index 0000000..509f849 --- /dev/null +++ b/frontend/lib/screens/unknown_screen.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class UnknownScreen extends StatelessWidget { + const UnknownScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Page Introuvable')), + body: const Center( + child: Text('Désolé, cette page n\'existe pas.'), + ), + ); + } +} \ No newline at end of file