refactor(widgets): Extraire ProfessionalInfoFormScreen en widget réutilisable
Nouveau widget professional_info_form_screen.dart : - Formulaire complet d'infos professionnelles pour AM - Gestion de la photo avec sélection et consentement - Champs : ville/pays/date de naissance, NIR, agrément, capacité - Validations intégrées (NIR 13 chiffres, capacité > 0, etc.) AM Step 2 refactorisé : - Utilise le nouveau ProfessionalInfoFormScreen - Code réduit de ~280 lignes à ~75 lignes - Logique de génération de données de test préservée - Préparé pour réutilisation dans les récapitulatifs Impact : -205 lignes de code
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:io';
|
||||
import '../models/card_assets.dart';
|
||||
import 'custom_app_text_field.dart';
|
||||
import 'app_custom_checkbox.dart';
|
||||
import 'hover_relief_widget.dart';
|
||||
|
||||
/// Données pour le formulaire d'informations professionnelles
|
||||
class ProfessionalInfoData {
|
||||
final String? photoPath;
|
||||
final File? photoFile;
|
||||
final bool photoConsent;
|
||||
final DateTime? dateOfBirth;
|
||||
final String birthCity;
|
||||
final String birthCountry;
|
||||
final String nir;
|
||||
final String agrementNumber;
|
||||
final int? capacity;
|
||||
|
||||
ProfessionalInfoData({
|
||||
this.photoPath,
|
||||
this.photoFile,
|
||||
this.photoConsent = false,
|
||||
this.dateOfBirth,
|
||||
this.birthCity = '',
|
||||
this.birthCountry = '',
|
||||
this.nir = '',
|
||||
this.agrementNumber = '',
|
||||
this.capacity,
|
||||
});
|
||||
}
|
||||
|
||||
/// Widget générique pour le formulaire d'informations professionnelles
|
||||
/// Utilisé pour l'inscription des Assistantes Maternelles
|
||||
class ProfessionalInfoFormScreen extends StatefulWidget {
|
||||
final String stepText;
|
||||
final String title;
|
||||
final CardColorHorizontal cardColor;
|
||||
final ProfessionalInfoData? initialData;
|
||||
final String previousRoute;
|
||||
final Function(ProfessionalInfoData) onSubmit;
|
||||
final Future<void> Function()? onPickPhoto;
|
||||
|
||||
const ProfessionalInfoFormScreen({
|
||||
super.key,
|
||||
required this.stepText,
|
||||
required this.title,
|
||||
required this.cardColor,
|
||||
this.initialData,
|
||||
required this.previousRoute,
|
||||
required this.onSubmit,
|
||||
this.onPickPhoto,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ProfessionalInfoFormScreen> createState() => _ProfessionalInfoFormScreenState();
|
||||
}
|
||||
|
||||
class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
final _dateOfBirthController = TextEditingController();
|
||||
final _birthCityController = TextEditingController();
|
||||
final _birthCountryController = TextEditingController();
|
||||
final _nirController = TextEditingController();
|
||||
final _agrementController = TextEditingController();
|
||||
final _capacityController = TextEditingController();
|
||||
|
||||
DateTime? _selectedDate;
|
||||
String? _photoPathFramework;
|
||||
File? _photoFile;
|
||||
bool _photoConsent = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final data = widget.initialData;
|
||||
if (data != null) {
|
||||
_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() ?? '';
|
||||
_photoPathFramework = data.photoPath;
|
||||
_photoFile = data.photoFile;
|
||||
_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)),
|
||||
firstDate: DateTime(1920, 1),
|
||||
lastDate: DateTime.now().subtract(const Duration(days: 365 * 18)),
|
||||
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 {
|
||||
if (widget.onPickPhoto != null) {
|
||||
await widget.onPickPhoto!();
|
||||
} else {
|
||||
// Comportement par défaut : utiliser un asset de test
|
||||
setState(() {
|
||||
_photoPathFramework = 'assets/images/icon_assmat.png';
|
||||
_photoFile = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
final data = ProfessionalInfoData(
|
||||
photoPath: _photoPathFramework,
|
||||
photoFile: _photoFile,
|
||||
photoConsent: _photoConsent,
|
||||
dateOfBirth: _selectedDate,
|
||||
birthCity: _birthCityController.text,
|
||||
birthCountry: _birthCountryController.text,
|
||||
nir: _nirController.text,
|
||||
agrementNumber: _agrementController.text,
|
||||
capacity: int.tryParse(_capacityController.text),
|
||||
);
|
||||
|
||||
widget.onSubmit(data);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
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(widget.stepText, style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
widget.title,
|
||||
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(widget.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,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
HoverReliefWidget(
|
||||
onPressed: _pickPhoto,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
initialShadowColor: initialPhotoShadow,
|
||||
hoverShadowColor: hoverPhotoShadow,
|
||||
child: SizedBox(
|
||||
height: 270,
|
||||
width: 270,
|
||||
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),
|
||||
AppCustomCheckbox(
|
||||
label: 'J\'accepte l\'utilisation\nde ma photo.',
|
||||
value: _photoConsent,
|
||||
onChanged: (val) => setState(() => _photoConsent = val ?? false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 30),
|
||||
// Colonne Droite: Champs de naissance
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
CustomAppTextField(
|
||||
controller: _birthCityController,
|
||||
labelText: 'Ville de naissance',
|
||||
hintText: 'Votre ville de naissance',
|
||||
fieldWidth: double.infinity,
|
||||
labelFontSize: 22.0,
|
||||
inputFontSize: 20.0,
|
||||
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,
|
||||
labelFontSize: 22.0,
|
||||
inputFontSize: 20.0,
|
||||
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,
|
||||
fieldWidth: double.infinity,
|
||||
labelFontSize: 22.0,
|
||||
inputFontSize: 20.0,
|
||||
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,
|
||||
labelFontSize: 22.0,
|
||||
inputFontSize: 20.0,
|
||||
validator: (v) {
|
||||
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';
|
||||
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,
|
||||
labelFontSize: 22.0,
|
||||
inputFontSize: 20.0,
|
||||
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,
|
||||
labelFontSize: 22.0,
|
||||
inputFontSize: 20.0,
|
||||
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(widget.previousRoute);
|
||||
}
|
||||
},
|
||||
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',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user