- nir_utils: normalizeNir, formatNir, validateNir (format + clé), Corse 2A/2B - NirInputFormatter: formatage auto à la saisie (espaces + tiret) - NirTextField: widget réutilisable pour champ NIR - professional_info_form_screen: NIR 15 car., affichage formaté à l'init - custom_app_text_field: paramètre inputFormatters Refs: #102 Made-with: Cursor
888 lines
30 KiB
Dart
888 lines
30 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.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 '../config/display_config.dart';
|
|
import '../utils/nir_utils.dart';
|
|
import 'custom_app_text_field.dart';
|
|
import 'nir_text_field.dart';
|
|
import 'form_field_wrapper.dart';
|
|
import 'app_custom_checkbox.dart';
|
|
import 'hover_relief_widget.dart';
|
|
import 'custom_navigation_button.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
|
|
/// Supporte mode éditable et readonly, responsive mobile/desktop
|
|
class ProfessionalInfoFormScreen extends StatefulWidget {
|
|
final DisplayMode mode;
|
|
final String stepText;
|
|
final String title;
|
|
final CardColorHorizontal cardColor;
|
|
final ProfessionalInfoData? initialData;
|
|
final String previousRoute;
|
|
final Function(ProfessionalInfoData) onSubmit;
|
|
final Future<void> Function()? onPickPhoto;
|
|
final bool embedContentOnly;
|
|
final VoidCallback? onEdit;
|
|
|
|
const ProfessionalInfoFormScreen({
|
|
super.key,
|
|
this.mode = DisplayMode.editable,
|
|
required this.stepText,
|
|
required this.title,
|
|
required this.cardColor,
|
|
this.initialData,
|
|
required this.previousRoute,
|
|
required this.onSubmit,
|
|
this.onPickPhoto,
|
|
this.embedContentOnly = false,
|
|
this.onEdit,
|
|
});
|
|
|
|
@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;
|
|
final nirRaw = nirToRaw(data.nir);
|
|
_nirController.text = nirRaw.length == 15 ? formatNir(nirRaw) : 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: normalizeNir(_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 config = DisplayConfig.fromContext(context, mode: widget.mode);
|
|
|
|
if (widget.embedContentOnly) {
|
|
return _buildCard(context, config, screenSize);
|
|
}
|
|
|
|
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: config.isMobile ? 13 : 16,
|
|
color: Colors.black54,
|
|
),
|
|
),
|
|
SizedBox(height: config.isMobile ? 6 : 10),
|
|
Text(
|
|
widget.title,
|
|
style: GoogleFonts.merienda(
|
|
fontSize: config.isMobile ? 18 : 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: config.isMobile ? 16 : 30),
|
|
_buildCard(context, config, screenSize),
|
|
|
|
// Boutons mobile sous la carte
|
|
if (config.isMobile) ...[
|
|
const SizedBox(height: 20),
|
|
_buildMobileButtons(context, config, screenSize),
|
|
const SizedBox(height: 10),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Chevrons desktop uniquement
|
|
if (!config.isMobile) ...[
|
|
// 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',
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
|
// Si mode Readonly Desktop : Layout spécial "Vintage" horizontal
|
|
if (config.isReadonly && !config.isMobile && widget.embedContentOnly) {
|
|
return _buildReadonlyDesktopCard(context, config, screenSize);
|
|
}
|
|
|
|
// Si mode Readonly Mobile : Layout spécial "Vintage" vertical
|
|
if (config.isReadonly && config.isMobile && widget.embedContentOnly) {
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
|
|
child: _buildMobileReadonlyCard(context, config, screenSize),
|
|
);
|
|
}
|
|
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
Container(
|
|
width: config.isMobile ? screenSize.width * 0.9 : screenSize.width * 0.6,
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: config.isMobile ? 20 : (config.isReadonly ? 30 : 50),
|
|
horizontal: config.isMobile ? 24 : 50,
|
|
),
|
|
decoration: BoxDecoration(
|
|
image: DecorationImage(
|
|
image: AssetImage(
|
|
config.isMobile
|
|
? _getVerticalCardAsset()
|
|
: widget.cardColor.path
|
|
),
|
|
fit: BoxFit.fill,
|
|
),
|
|
),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (widget.embedContentOnly) ...[
|
|
Text(
|
|
widget.title,
|
|
style: GoogleFonts.merienda(
|
|
fontSize: config.isMobile ? 18 : 22,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 20),
|
|
],
|
|
config.isMobile
|
|
? _buildMobileFields(context, config)
|
|
: _buildDesktopFields(context, config),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (config.isReadonly && widget.onEdit != null)
|
|
Positioned(
|
|
top: 10,
|
|
right: 10,
|
|
child: IconButton(
|
|
icon: const Icon(Icons.edit, color: Colors.black54),
|
|
onPressed: widget.onEdit,
|
|
tooltip: 'Modifier',
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Carte en mode readonly MOBILE avec hauteur adaptative
|
|
Widget _buildMobileReadonlyCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
|
return Container(
|
|
width: double.infinity,
|
|
// Pas de height fixe
|
|
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 24.0),
|
|
decoration: BoxDecoration(
|
|
image: DecorationImage(
|
|
image: AssetImage(_getVerticalCardAsset()),
|
|
fit: BoxFit.fill, // Fill pour s'adapter
|
|
),
|
|
borderRadius: BorderRadius.circular(15),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Titre + Edit Button
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
widget.title,
|
|
style: GoogleFonts.merienda(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
if (widget.onEdit != null)
|
|
const SizedBox(width: 28),
|
|
],
|
|
),
|
|
|
|
// Contenu aligné en haut
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 20.0),
|
|
child: _buildMobileFields(context, config),
|
|
),
|
|
],
|
|
),
|
|
|
|
if (widget.onEdit != null)
|
|
Positioned(
|
|
top: 0,
|
|
right: 0,
|
|
child: IconButton(
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(),
|
|
icon: const Icon(Icons.edit, color: Colors.black54, size: 24),
|
|
onPressed: widget.onEdit,
|
|
tooltip: 'Modifier',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Carte en mode readonly desktop avec AspectRatio 2:1
|
|
Widget _buildReadonlyDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
|
final cardWidth = screenSize.width / 2.0;
|
|
|
|
return SizedBox(
|
|
width: cardWidth,
|
|
child: AspectRatio(
|
|
aspectRatio: 2.0,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
|
|
decoration: BoxDecoration(
|
|
image: DecorationImage(
|
|
image: AssetImage(widget.cardColor.path),
|
|
fit: BoxFit.cover,
|
|
),
|
|
borderRadius: BorderRadius.circular(15),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Titre + Edit Button
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
widget.title,
|
|
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
if (widget.onEdit != null)
|
|
IconButton(
|
|
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
|
|
onPressed: widget.onEdit,
|
|
tooltip: 'Modifier',
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 18),
|
|
|
|
// Contenu
|
|
Expanded(
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
// PHOTO (1/3)
|
|
Expanded(
|
|
flex: 1,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
AspectRatio(
|
|
aspectRatio: 1,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(18),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(18),
|
|
child: _photoFile != null
|
|
? Image.file(_photoFile!, fit: BoxFit.cover)
|
|
: (_photoPathFramework != null && _photoPathFramework!.startsWith('assets/')
|
|
? Image.asset(_photoPathFramework!, fit: BoxFit.contain)
|
|
: Image.asset('assets/images/photo.png', fit: BoxFit.contain)),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
AppCustomCheckbox(
|
|
label: 'J\'accepte l\'utilisation\nde ma photo.',
|
|
value: _photoConsent,
|
|
onChanged: (v) {}, // Readonly
|
|
checkboxSize: 22.0,
|
|
fontSize: 14.0,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 32),
|
|
|
|
// CHAMPS (2/3) - Layout optimisé compact
|
|
Expanded(
|
|
flex: 2,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// Ligne 1 : Ville + Pays
|
|
Row(
|
|
children: [
|
|
Expanded(child: _buildReadonlyField('Ville de naissance', _birthCityController.text)),
|
|
const SizedBox(width: 16),
|
|
Expanded(child: _buildReadonlyField('Pays de naissance', _birthCountryController.text)),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Ligne 2 : Date + NIR (NIR prend plus de place si possible ou 50/50)
|
|
Row(
|
|
children: [
|
|
Expanded(flex: 2, child: _buildReadonlyField('Date de naissance', _dateOfBirthController.text)),
|
|
const SizedBox(width: 16),
|
|
Expanded(flex: 3, child: _buildReadonlyField('NIR', _formatNirForDisplay(_nirController.text))),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Ligne 3 : Agrément + Capacité
|
|
Row(
|
|
children: [
|
|
Expanded(flex: 3, child: _buildReadonlyField('N° Agrément', _agrementController.text)),
|
|
const SizedBox(width: 16),
|
|
Expanded(flex: 2, child: _buildReadonlyField('Capacité', _capacityController.text)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// NIR formaté pour affichage (1 12 34 56 789 012-34 ou 2A pour la Corse).
|
|
String _formatNirForDisplay(String value) {
|
|
final raw = nirToRaw(value);
|
|
return raw.length == 15 ? formatNir(raw) : value;
|
|
}
|
|
|
|
/// Helper pour champ Readonly style "Beige"
|
|
Widget _buildReadonlyField(String label, String value) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: GoogleFonts.merienda(fontSize: 18.0, fontWeight: FontWeight.w600),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Container(
|
|
width: double.infinity,
|
|
height: 45.0, // Hauteur réduite pour compacter
|
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0),
|
|
decoration: BoxDecoration(
|
|
image: const DecorationImage(
|
|
image: AssetImage('assets/images/bg_beige.png'),
|
|
fit: BoxFit.fill,
|
|
),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
value.isNotEmpty ? value : '-',
|
|
style: GoogleFonts.merienda(fontSize: 16.0),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Layout DESKTOP : Photo à gauche, champs à droite
|
|
Widget _buildDesktopFields(BuildContext context, DisplayConfig config) {
|
|
final double verticalSpacing = config.isReadonly ? 16.0 : 32.0;
|
|
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Photo + Checkbox à gauche
|
|
SizedBox(
|
|
width: 300,
|
|
child: _buildPhotoSection(context, config),
|
|
),
|
|
const SizedBox(width: 30),
|
|
// Champs à droite
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
_buildField(
|
|
config: config,
|
|
label: 'Ville de naissance',
|
|
controller: _birthCityController,
|
|
hint: 'Votre ville de naissance',
|
|
validator: (v) => v!.isEmpty ? 'Ville requise' : null,
|
|
),
|
|
SizedBox(height: verticalSpacing),
|
|
_buildField(
|
|
config: config,
|
|
label: 'Pays de naissance',
|
|
controller: _birthCountryController,
|
|
hint: 'Votre pays de naissance',
|
|
validator: (v) => v!.isEmpty ? 'Pays requis' : null,
|
|
),
|
|
SizedBox(height: verticalSpacing),
|
|
_buildField(
|
|
config: config,
|
|
label: 'Date de naissance',
|
|
controller: _dateOfBirthController,
|
|
hint: 'JJ/MM/AAAA',
|
|
readOnly: true,
|
|
onTap: () => _selectDate(context),
|
|
suffixIcon: Icons.calendar_today,
|
|
validator: (v) => _selectedDate == null ? 'Date requise' : null,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: verticalSpacing),
|
|
NirTextField(
|
|
controller: _nirController,
|
|
fieldWidth: double.infinity,
|
|
fieldHeight: config.isMobile ? 45.0 : 53.0,
|
|
labelFontSize: config.isMobile ? 15.0 : 22.0,
|
|
inputFontSize: config.isMobile ? 14.0 : 20.0,
|
|
),
|
|
SizedBox(height: verticalSpacing),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildField(
|
|
config: config,
|
|
label: 'N° d\'agrément',
|
|
controller: _agrementController,
|
|
hint: 'Votre numéro d\'agrément',
|
|
validator: (v) => v!.isEmpty ? 'Agrément requis' : null,
|
|
),
|
|
),
|
|
const SizedBox(width: 20),
|
|
Expanded(
|
|
child: _buildField(
|
|
config: config,
|
|
label: 'Capacité d\'accueil',
|
|
controller: _capacityController,
|
|
hint: 'Ex: 3',
|
|
keyboardType: TextInputType.number,
|
|
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;
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Layout MOBILE : Tout empilé verticalement
|
|
Widget _buildMobileFields(BuildContext context, DisplayConfig config) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Photo + Checkbox en premier
|
|
_buildPhotoSection(context, config),
|
|
const SizedBox(height: 20),
|
|
|
|
_buildField(
|
|
config: config,
|
|
label: 'Ville de naissance',
|
|
controller: _birthCityController,
|
|
hint: 'Votre ville de naissance',
|
|
validator: (v) => v!.isEmpty ? 'Ville requise' : null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
_buildField(
|
|
config: config,
|
|
label: 'Pays de naissance',
|
|
controller: _birthCountryController,
|
|
hint: 'Votre pays de naissance',
|
|
validator: (v) => v!.isEmpty ? 'Pays requis' : null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
_buildField(
|
|
config: config,
|
|
label: 'Date de naissance',
|
|
controller: _dateOfBirthController,
|
|
hint: 'JJ/MM/AAAA',
|
|
readOnly: true,
|
|
onTap: () => _selectDate(context),
|
|
suffixIcon: Icons.calendar_today,
|
|
validator: (v) => _selectedDate == null ? 'Date requise' : null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
NirTextField(
|
|
controller: _nirController,
|
|
fieldWidth: double.infinity,
|
|
fieldHeight: 45.0,
|
|
labelFontSize: 15.0,
|
|
inputFontSize: 14.0,
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
_buildField(
|
|
config: config,
|
|
label: 'N° d\'agrément',
|
|
controller: _agrementController,
|
|
hint: 'Votre numéro d\'agrément',
|
|
validator: (v) => v!.isEmpty ? 'Agrément requis' : null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
_buildField(
|
|
config: config,
|
|
label: 'Capacité d\'accueil',
|
|
controller: _capacityController,
|
|
hint: 'Ex: 3',
|
|
keyboardType: TextInputType.number,
|
|
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;
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Section photo + checkbox
|
|
Widget _buildPhotoSection(BuildContext context, DisplayConfig config) {
|
|
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!);
|
|
}
|
|
|
|
final photoSize = config.isMobile ? 200.0 : 270.0;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
HoverReliefWidget(
|
|
onPressed: _pickPhoto,
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
initialShadowColor: initialPhotoShadow,
|
|
hoverShadowColor: hoverPhotoShadow,
|
|
child: SizedBox(
|
|
height: photoSize,
|
|
width: photoSize,
|
|
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),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Construit un champ individuel
|
|
Widget _buildField({
|
|
required DisplayConfig config,
|
|
required String label,
|
|
required TextEditingController controller,
|
|
String? hint,
|
|
TextInputType? keyboardType,
|
|
bool readOnly = false,
|
|
VoidCallback? onTap,
|
|
IconData? suffixIcon,
|
|
String? Function(String?)? validator,
|
|
List<TextInputFormatter>? inputFormatters,
|
|
}) {
|
|
if (config.isReadonly) {
|
|
return FormFieldWrapper(
|
|
config: config,
|
|
label: label,
|
|
value: controller.text,
|
|
);
|
|
} else {
|
|
return CustomAppTextField(
|
|
controller: controller,
|
|
labelText: label,
|
|
hintText: hint ?? label,
|
|
fieldWidth: double.infinity,
|
|
fieldHeight: config.isMobile ? 45.0 : 53.0,
|
|
labelFontSize: config.isMobile ? 15.0 : 22.0,
|
|
inputFontSize: config.isMobile ? 14.0 : 20.0,
|
|
keyboardType: keyboardType ?? TextInputType.text,
|
|
readOnly: readOnly,
|
|
onTap: onTap,
|
|
suffixIcon: suffixIcon,
|
|
validator: validator,
|
|
inputFormatters: inputFormatters,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Boutons mobile
|
|
Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) {
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: screenSize.width * 0.05,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: HoverReliefWidget(
|
|
child: CustomNavigationButton(
|
|
text: 'Précédent',
|
|
style: NavigationButtonStyle.purple,
|
|
onPressed: () {
|
|
if (context.canPop()) {
|
|
context.pop();
|
|
} else {
|
|
context.go(widget.previousRoute);
|
|
}
|
|
},
|
|
width: double.infinity,
|
|
height: 50,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: HoverReliefWidget(
|
|
child: CustomNavigationButton(
|
|
text: 'Suivant',
|
|
style: NavigationButtonStyle.green,
|
|
onPressed: _submitForm,
|
|
width: double.infinity,
|
|
height: 50,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Retourne l'asset de carte vertical correspondant à la couleur
|
|
String _getVerticalCardAsset() {
|
|
switch (widget.cardColor) {
|
|
case CardColorHorizontal.blue:
|
|
return CardColorVertical.blue.path;
|
|
case CardColorHorizontal.green:
|
|
return CardColorVertical.green.path;
|
|
case CardColorHorizontal.lavender:
|
|
return CardColorVertical.lavender.path;
|
|
case CardColorHorizontal.lime:
|
|
return CardColorVertical.lime.path;
|
|
case CardColorHorizontal.peach:
|
|
return CardColorVertical.peach.path;
|
|
case CardColorHorizontal.pink:
|
|
return CardColorVertical.pink.path;
|
|
case CardColorHorizontal.red:
|
|
return CardColorVertical.red.path;
|
|
}
|
|
}
|
|
}
|