petitspas/frontend/lib/screens/auth/am_register_step2_screen.dart
Julien Martin df87abbb85 feat(auth): Renommer "Nanny" en "Assistante Maternelle" (AM)
- Création du modèle am_registration_data.dart
- Création des 4 écrans d'inscription AM (steps 1-4)
- Mise à jour du bouton "Assistante Maternelle" dans register_choice
- Conformité CDC : pas de champs mot de passe dans les formulaires
- Préremplissage des données de test pour faciliter le développement

Ref: Ticket #XX - Renommage workflow inscription AM
2026-01-28 16:43:16 +01:00

355 lines
17 KiB
Dart

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/am_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';
class AmRegisterStep2Screen extends StatefulWidget {
const AmRegisterStep2Screen({super.key});
@override
State<AmRegisterStep2Screen> createState() => _AmRegisterStep2ScreenState();
}
class _AmRegisterStep2ScreenState extends State<AmRegisterStep2Screen> {
final _formKey = GlobalKey<FormState>();
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<AmRegistrationData>(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énérer des données de test si les champs sont vides
if (data.dateOfBirth == null && data.nir.isEmpty) {
_selectedDate = DateTime(1985, 3, 15);
_dateOfBirthController.text = DateFormat('dd/MM/yyyy').format(_selectedDate!);
_birthCityController.text = DataGenerator.city();
_birthCountryController.text = 'France';
_nirController.text = '${DataGenerator.randomIntInRange(1, 3)}${DataGenerator.randomIntInRange(80, 96)}${DataGenerator.randomIntInRange(1, 13).toString().padLeft(2, '0')}${DataGenerator.randomIntInRange(1, 100).toString().padLeft(2, '0')}${DataGenerator.randomIntInRange(100, 1000).toString().padLeft(3, '0')}${DataGenerator.randomIntInRange(100, 1000).toString().padLeft(3, '0')}${DataGenerator.randomIntInRange(10, 100).toString().padLeft(2, '0')}';
_agrementController.text = 'AM${DataGenerator.randomIntInRange(10000, 100000)}';
_capacityController.text = DataGenerator.randomIntInRange(1, 5).toString();
_photoPathFramework = 'assets/images/icon_assmat.png';
_photoConsent = true;
}
// 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<void> _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<void> _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<AmRegistrationData>(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('/am-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),
Text(
'Vos informations professionnelles',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Container(
width: screenSize.width * 0.6,
padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50),
constraints: const BoxConstraints(minHeight: 650),
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(cardColor.path), fit: BoxFit.fill),
),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
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\nde 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: 32),
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: 32),
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: 32),
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: 32),
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('/am-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',
),
),
],
),
);
}
}