feat(inscription-am): places disponibles, UI étape 2, plafond capacité 4 côté app

- POST /auth/register/am : places_disponibles, enregistrement place_disponible, contrôle ≤ capacité

- Formulaire pro AM : places sur la ligne capacité, mobile un champ par ligne, carte desktop plus compacte (espacements, polices, labelFieldSpacing)

- Capacité et places validées jusqu’à 4 dans l’app (kAmCapaciteAccueilMax) ; hint NIR « 13 chiffres + clé »

- Scripts d’inscription AM (Node + smoke shell) : places_disponibles

Made-with: Cursor
This commit is contained in:
MARTIN Julien 2026-04-13 13:13:27 +02:00
parent 58607cdbc9
commit b4b44cb1b9
12 changed files with 163 additions and 68 deletions

View File

@ -26,6 +26,7 @@ curl -s -w "\n\nHTTP %{http_code}\n" -X POST "$BASE_URL/auth/register/am" \
"nir": "186127500100279", "nir": "186127500100279",
"numero_agrement": "AGR-SMOKE-CURL-001", "numero_agrement": "AGR-SMOKE-CURL-001",
"capacite_accueil": 3, "capacite_accueil": 3,
"places_disponibles": 2,
"acceptation_cgu": true, "acceptation_cgu": true,
"acceptation_privacy": true "acceptation_privacy": true
}' }'

View File

@ -418,6 +418,12 @@ export class AuthService {
); );
} }
if (dto.places_disponibles > dto.capacite_accueil) {
throw new BadRequestException(
'Le nombre de places disponibles ne peut pas dépasser la capacité d\'accueil.',
);
}
const nirNormalized = (dto.nir || '').replace(/\s/g, '').toUpperCase(); const nirNormalized = (dto.nir || '').replace(/\s/g, '').toUpperCase();
const nirValidation = validateNir(nirNormalized, { const nirValidation = validateNir(nirNormalized, {
dateNaissance: dto.date_naissance, dateNaissance: dto.date_naissance,

View File

@ -138,6 +138,17 @@ export class RegisterAMCompletDto {
@Max(10, { message: 'La capacité ne peut pas dépasser 10' }) @Max(10, { message: 'La capacité ne peut pas dépasser 10' })
capacite_accueil: number; capacite_accueil: number;
@ApiProperty({
example: 2,
description: 'Nombre de places libres actuellement (≤ capacité d\'accueil)',
minimum: 0,
maximum: 10,
})
@IsInt()
@Min(0, { message: 'Les places disponibles ne peuvent pas être négatives' })
@Max(10, { message: 'Les places disponibles ne peuvent pas dépasser 10' })
places_disponibles: number;
// ============================================ // ============================================
// ÉTAPE 3 : PRÉSENTATION (Optionnel) // ÉTAPE 3 : PRÉSENTATION (Optionnel)
// ============================================ // ============================================

View File

@ -1,5 +1,8 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
/// Accueil simultané : plafond légal courant en France pour une AM agréée (enfants de moins de 3 ans).
const int kAmCapaciteAccueilMax = 4;
class AmRegistrationData extends ChangeNotifier { class AmRegistrationData extends ChangeNotifier {
// Step 1: Identity Info // Step 1: Identity Info
String firstName = ''; String firstName = '';
@ -27,6 +30,8 @@ class AmRegistrationData extends ChangeNotifier {
/// Date d'obtention de l'agrément obligatoire (API `date_agrement`). /// Date d'obtention de l'agrément obligatoire (API `date_agrement`).
DateTime? agreementDate; DateTime? agreementDate;
int? capacity; // Number of children the AM can look after int? capacity; // Number of children the AM can look after
/// Places libres actuellement (0 valeur capacité) API `places_disponibles`.
int? placesAvailable;
// Step 3: Presentation & CGU // Step 3: Presentation & CGU
String presentationText = ''; String presentationText = '';
@ -72,6 +77,7 @@ class AmRegistrationData extends ChangeNotifier {
String? agrementNumber, String? agrementNumber,
DateTime? agreementDate, DateTime? agreementDate,
int? capacity, int? capacity,
int? placesAvailable,
}) { }) {
this.photoPath = photoPath; this.photoPath = photoPath;
this.photoBytes = photoBytes; this.photoBytes = photoBytes;
@ -85,6 +91,7 @@ class AmRegistrationData extends ChangeNotifier {
this.agrementNumber = agrementNumber ?? this.agrementNumber; this.agrementNumber = agrementNumber ?? this.agrementNumber;
this.agreementDate = agreementDate ?? this.agreementDate; this.agreementDate = agreementDate ?? this.agreementDate;
this.capacity = capacity ?? this.capacity; this.capacity = capacity ?? this.capacity;
this.placesAvailable = placesAvailable ?? this.placesAvailable;
notifyListeners(); notifyListeners();
} }
@ -125,7 +132,10 @@ class AmRegistrationData extends ChangeNotifier {
agreementDate != null && agreementDate != null &&
capacity != null && capacity != null &&
capacity! >= 1 && capacity! >= 1 &&
capacity! <= 10; capacity! <= kAmCapaciteAccueilMax &&
placesAvailable != null &&
placesAvailable! >= 0 &&
placesAvailable! <= capacity!;
bool get isStep3Complete => bool get isStep3Complete =>
// presentationText is optional as per CDC (message au gestionnaire) // presentationText is optional as per CDC (message au gestionnaire)
@ -142,7 +152,7 @@ class AmRegistrationData extends ChangeNotifier {
'phone: $phone, email: $email, ' 'phone: $phone, email: $email, '
// 'photoPath: $photoPath, photoConsent: $photoConsent, ' // Commenté car déplacé/modifié // 'photoPath: $photoPath, photoConsent: $photoConsent, ' // Commenté car déplacé/modifié
'dateOfBirth: $dateOfBirth, birthCity: $birthCity, birthCountry: $birthCountry, ' 'dateOfBirth: $dateOfBirth, birthCity: $birthCity, birthCountry: $birthCountry, '
'nir: $nir, agrementNumber: $agrementNumber, agreementDate: $agreementDate, capacity: $capacity, ' 'nir: $nir, agrementNumber: $agrementNumber, agreementDate: $agreementDate, capacity: $capacity, placesAvailable: $placesAvailable, '
'photoPath (step2): $photoPath, photoConsent (step2): $photoConsent, ' 'photoPath (step2): $photoPath, photoConsent (step2): $photoConsent, '
'presentationText: $presentationText, cguAccepted: $cguAccepted)'; 'presentationText: $presentationText, cguAccepted: $cguAccepted)';
} }

View File

@ -25,6 +25,7 @@ class AmRegisterStep2Screen extends StatelessWidget {
agrementNumber: registrationData.agrementNumber, agrementNumber: registrationData.agrementNumber,
agreementDate: registrationData.agreementDate, agreementDate: registrationData.agreementDate,
capacity: registrationData.capacity, capacity: registrationData.capacity,
placesAvailable: registrationData.placesAvailable,
); );
return ProfessionalInfoFormScreen( return ProfessionalInfoFormScreen(
@ -46,6 +47,7 @@ class AmRegisterStep2Screen extends StatelessWidget {
agrementNumber: data.agrementNumber, agrementNumber: data.agrementNumber,
agreementDate: data.agreementDate, agreementDate: data.agreementDate,
capacity: data.capacity, capacity: data.capacity,
placesAvailable: data.placesAvailable,
); );
context.go('/am-register-step3'); context.go('/am-register-step3');
}, },

View File

@ -206,6 +206,7 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
agrementNumber: data.agrementNumber, agrementNumber: data.agrementNumber,
agreementDate: data.agreementDate, agreementDate: data.agreementDate,
capacity: data.capacity, capacity: data.capacity,
placesAvailable: data.placesAvailable,
photoConsent: data.photoConsent, photoConsent: data.photoConsent,
), ),
onSubmit: (d) {}, onSubmit: (d) {},

View File

@ -165,6 +165,9 @@ class AuthService {
if (data.agreementDate == null) { if (data.agreementDate == null) {
throw Exception('La date d\'obtention de l\'agrément est requise.'); throw Exception('La date d\'obtention de l\'agrément est requise.');
} }
if (data.placesAvailable == null) {
throw Exception('Le nombre de places disponibles est requis.');
}
String? photoBase64; String? photoBase64;
String? photoFilename; String? photoFilename;
@ -211,6 +214,7 @@ class AuthService {
'date_agrement': 'date_agrement':
'${data.agreementDate!.year}-${data.agreementDate!.month.toString().padLeft(2, '0')}-${data.agreementDate!.day.toString().padLeft(2, '0')}', '${data.agreementDate!.year}-${data.agreementDate!.month.toString().padLeft(2, '0')}-${data.agreementDate!.day.toString().padLeft(2, '0')}',
'capacite_accueil': data.capacity ?? 1, 'capacite_accueil': data.capacity ?? 1,
'places_disponibles': data.placesAvailable!,
'biographie': data.presentationText.isNotEmpty ? data.presentationText : null, 'biographie': data.presentationText.isNotEmpty ? data.presentationText : null,
'acceptation_cgu': data.cguAccepted, 'acceptation_cgu': data.cguAccepted,
'acceptation_privacy': data.cguAccepted, 'acceptation_privacy': data.cguAccepted,

View File

@ -27,6 +27,8 @@ class CustomAppTextField extends StatefulWidget {
final IconData? suffixIcon; final IconData? suffixIcon;
final double labelFontSize; final double labelFontSize;
final double inputFontSize; final double inputFontSize;
/// Espace vertical entre le libellé et la zone de saisie.
final double labelFieldSpacing;
final bool showLabel; final bool showLabel;
final Iterable<String>? autofillHints; final Iterable<String>? autofillHints;
final TextInputAction? textInputAction; final TextInputAction? textInputAction;
@ -56,6 +58,7 @@ class CustomAppTextField extends StatefulWidget {
this.suffixIcon, this.suffixIcon,
this.labelFontSize = 18.0, this.labelFontSize = 18.0,
this.inputFontSize = 18.0, this.inputFontSize = 18.0,
this.labelFieldSpacing = 6.0,
this.autofillHints, this.autofillHints,
this.textInputAction, this.textInputAction,
this.onFieldSubmitted, this.onFieldSubmitted,
@ -105,7 +108,7 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
const SizedBox(height: 6), SizedBox(height: widget.labelFieldSpacing),
], ],
// Pas de hauteur fixe sur le TextFormField : le message derreur du // Pas de hauteur fixe sur le TextFormField : le message derreur du
// validateur saffiche en dessous ; un SizedBox fixe le masquait. // validateur saffiche en dessous ; un SizedBox fixe le masquait.

View File

@ -18,12 +18,13 @@ class NirTextField extends StatelessWidget {
final bool enabled; final bool enabled;
final bool readOnly; final bool readOnly;
final CustomAppTextFieldStyle style; final CustomAppTextFieldStyle style;
final double labelFieldSpacing;
const NirTextField({ const NirTextField({
super.key, super.key,
required this.controller, required this.controller,
this.labelText = 'N° Sécurité Sociale (NIR)', this.labelText = 'N° Sécurité Sociale (NIR)',
this.hintText = '15 caractères dont clé valide (dépt 2A ou 2B si Corse)', this.hintText = '13 chiffres + clé',
this.validator, this.validator,
this.fieldWidth = double.infinity, this.fieldWidth = double.infinity,
this.fieldHeight = 53.0, this.fieldHeight = 53.0,
@ -32,6 +33,7 @@ class NirTextField extends StatelessWidget {
this.enabled = true, this.enabled = true,
this.readOnly = false, this.readOnly = false,
this.style = CustomAppTextFieldStyle.beige, this.style = CustomAppTextFieldStyle.beige,
this.labelFieldSpacing = 6.0,
}); });
@override @override
@ -50,6 +52,7 @@ class NirTextField extends StatelessWidget {
enabled: enabled, enabled: enabled,
readOnly: readOnly, readOnly: readOnly,
style: style, style: style,
labelFieldSpacing: labelFieldSpacing,
); );
} }
} }

View File

@ -7,6 +7,7 @@ import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:io'; import 'dart:io';
import '../models/am_registration_data.dart' show kAmCapaciteAccueilMax;
import '../models/card_assets.dart'; import '../models/card_assets.dart';
import '../config/display_config.dart'; import '../config/display_config.dart';
import '../utils/nir_utils.dart'; import '../utils/nir_utils.dart';
@ -34,6 +35,7 @@ class ProfessionalInfoData {
/// Date d'obtention de l'agrément (obligatoire, API `date_agrement`). /// Date d'obtention de l'agrément (obligatoire, API `date_agrement`).
final DateTime? agreementDate; final DateTime? agreementDate;
final int? capacity; final int? capacity;
final int? placesAvailable;
ProfessionalInfoData({ ProfessionalInfoData({
this.photoPath, this.photoPath,
@ -48,6 +50,7 @@ class ProfessionalInfoData {
this.agrementNumber = '', this.agrementNumber = '',
this.agreementDate, this.agreementDate,
this.capacity, this.capacity,
this.placesAvailable,
}); });
} }
@ -94,6 +97,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
final _agrementController = TextEditingController(); final _agrementController = TextEditingController();
final _agreementDateController = TextEditingController(); final _agreementDateController = TextEditingController();
final _capacityController = TextEditingController(); final _capacityController = TextEditingController();
final _placesAvailableController = TextEditingController();
FocusNode? _birthCityFocus; FocusNode? _birthCityFocus;
FocusNode? _birthCountryFocus; FocusNode? _birthCountryFocus;
@ -133,6 +137,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
? DateFormat('dd/MM/yyyy').format(data.agreementDate!) ? DateFormat('dd/MM/yyyy').format(data.agreementDate!)
: ''; : '';
_capacityController.text = data.capacity?.toString() ?? ''; _capacityController.text = data.capacity?.toString() ?? '';
_placesAvailableController.text = data.placesAvailable?.toString() ?? '';
_photoPathFramework = data.photoPath; _photoPathFramework = data.photoPath;
_photoFile = data.photoFile; _photoFile = data.photoFile;
_photoBytes = data.photoBytes; _photoBytes = data.photoBytes;
@ -180,9 +185,20 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
_agrementController.dispose(); _agrementController.dispose();
_agreementDateController.dispose(); _agreementDateController.dispose();
_capacityController.dispose(); _capacityController.dispose();
_placesAvailableController.dispose();
super.dispose(); super.dispose();
} }
String? _validatePlacesAvailable(String? v) {
if (v == null || v.isEmpty) return 'Places disponibles requises';
final n = int.tryParse(v);
if (n == null || n < 0) return 'Nombre entre 0 et la capacité';
if (n > kAmCapaciteAccueilMax) return 'Maximum $kAmCapaciteAccueilMax';
final cap = int.tryParse(_capacityController.text);
if (cap != null && n > cap) return 'Ne peut pas dépasser la capacité';
return null;
}
Future<void> _selectDate(BuildContext context) async { Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker( final DateTime? picked = await showDatePicker(
context: context, context: context,
@ -285,6 +301,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
agrementNumber: _agrementController.text, agrementNumber: _agrementController.text,
agreementDate: _selectedAgreementDate, agreementDate: _selectedAgreementDate,
capacity: int.tryParse(_capacityController.text), capacity: int.tryParse(_capacityController.text),
placesAvailable: int.tryParse(_placesAvailableController.text),
); );
widget.onSubmit(data); widget.onSubmit(data);
@ -308,7 +325,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
), ),
Center( Center(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0), padding: EdgeInsets.symmetric(vertical: config.isMobile ? 40.0 : 28.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@ -329,7 +346,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: config.isMobile ? 16 : 30), SizedBox(height: config.isMobile ? 16 : 20),
_buildCard(context, config, screenSize), _buildCard(context, config, screenSize),
// Boutons mobile sous la carte // Boutons mobile sous la carte
@ -400,7 +417,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
Container( Container(
width: config.isMobile ? screenSize.width * 0.9 : screenSize.width * 0.6, width: config.isMobile ? screenSize.width * 0.9 : screenSize.width * 0.6,
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: config.isMobile ? 20 : (config.isReadonly ? 30 : 50), vertical: config.isMobile ? 20 : (config.isReadonly ? 24 : 28),
horizontal: config.isMobile ? 24 : 50, horizontal: config.isMobile ? 24 : 50,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -428,7 +445,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 20), SizedBox(height: config.isMobile ? 20.0 : 14.0),
], ],
config.isMobile config.isMobile
? _buildMobileFields(context, config) ? _buildMobileFields(context, config)
@ -550,7 +567,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
), ),
], ],
), ),
const SizedBox(height: 18), const SizedBox(height: 10),
// Contenu // Contenu
Expanded( Expanded(
@ -578,7 +595,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
}, },
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 5),
AppCustomCheckbox( AppCustomCheckbox(
label: 'J\'accepte l\'utilisation\nde ma photo.', label: 'J\'accepte l\'utilisation\nde ma photo.',
value: _photoConsent, value: _photoConsent,
@ -589,7 +606,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
], ],
), ),
), ),
const SizedBox(width: 32), const SizedBox(width: 24),
// CHAMPS (2/3) - Layout optimisé compact // CHAMPS (2/3) - Layout optimisé compact
Expanded( Expanded(
@ -605,7 +622,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
Expanded(flex: 3, child: _buildReadonlyField('Ville de naissance', _birthCityController.text)), Expanded(flex: 3, child: _buildReadonlyField('Ville de naissance', _birthCityController.text)),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 5),
// Ligne 2 : Pays + NIR // Ligne 2 : Pays + NIR
Row( Row(
@ -615,7 +632,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
Expanded(flex: 3, child: _buildReadonlyField('NIR', _formatNirForDisplay(_nirController.text))), Expanded(flex: 3, child: _buildReadonlyField('NIR', _formatNirForDisplay(_nirController.text))),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 5),
// Ligne 3 : N° agrément + Date d'obtention // Ligne 3 : N° agrément + Date d'obtention
Row( Row(
@ -634,12 +651,19 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
), ),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 5),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: _buildReadonlyField('Capacité d\'accueil', _capacityController.text), child: _buildReadonlyField('Capacité d\'accueil', _capacityController.text),
), ),
const SizedBox(width: 16),
Expanded(
child: _buildReadonlyField(
'Places disponibles',
_placesAvailableController.text,
),
),
], ],
), ),
], ],
@ -668,10 +692,10 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
children: [ children: [
Text( Text(
label, label,
style: GoogleFonts.merienda(fontSize: 18.0, fontWeight: FontWeight.w600), style: GoogleFonts.merienda(fontSize: 16.0, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 4), const SizedBox(height: 3),
Container( Container(
width: double.infinity, width: double.infinity,
height: 45.0, // Hauteur réduite pour compacter height: 45.0, // Hauteur réduite pour compacter
@ -685,7 +709,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
), ),
child: Text( child: Text(
value.isNotEmpty ? value : '-', value.isNotEmpty ? value : '-',
style: GoogleFonts.merienda(fontSize: 16.0), style: GoogleFonts.merienda(fontSize: 14.0),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@ -695,7 +719,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
/// Layout DESKTOP : Photo à gauche, champs à droite /// Layout DESKTOP : Photo à gauche, champs à droite
Widget _buildDesktopFields(BuildContext context, DisplayConfig config) { Widget _buildDesktopFields(BuildContext context, DisplayConfig config) {
final double verticalSpacing = config.isReadonly ? 16.0 : 32.0; final double verticalSpacing = config.isReadonly ? 8.0 : 14.0;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -708,7 +732,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
width: 300, width: 300,
child: _buildPhotoSection(context, config), child: _buildPhotoSection(context, config),
), ),
const SizedBox(width: 30), const SizedBox(width: 22),
// Champs à droite // Champs à droite
Expanded( Expanded(
child: Column( child: Column(
@ -751,8 +775,9 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
controller: _nirController, controller: _nirController,
fieldWidth: double.infinity, fieldWidth: double.infinity,
fieldHeight: config.isMobile ? 45.0 : 53.0, fieldHeight: config.isMobile ? 45.0 : 53.0,
labelFontSize: config.isMobile ? 15.0 : 22.0, labelFontSize: config.isMobile ? 15.0 : 20.0,
inputFontSize: config.isMobile ? 14.0 : 20.0, inputFontSize: config.isMobile ? 14.0 : 18.0,
labelFieldSpacing: config.isMobile ? 6.0 : 3.0,
), ),
SizedBox(height: verticalSpacing), SizedBox(height: verticalSpacing),
Row( Row(
@ -767,7 +792,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
validator: (v) => v!.isEmpty ? 'Agrément requis' : null, validator: (v) => v!.isEmpty ? 'Agrément requis' : null,
), ),
), ),
const SizedBox(width: 20), const SizedBox(width: 14),
Expanded( Expanded(
child: _buildField( child: _buildField(
config: config, config: config,
@ -784,19 +809,41 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
], ],
), ),
SizedBox(height: verticalSpacing), SizedBox(height: verticalSpacing),
_buildField( Row(
config: config, crossAxisAlignment: CrossAxisAlignment.start,
label: 'Capacité d\'accueil', children: [
controller: _capacityController, Expanded(
hint: 'De 1 à 10 enfants', child: _buildField(
keyboardType: TextInputType.number, config: config,
validator: (v) { label: 'Capacité d\'accueil',
if (v == null || v.isEmpty) return 'Capacité requise'; controller: _capacityController,
final n = int.tryParse(v); hint: 'De 1 à $kAmCapaciteAccueilMax enfants',
if (n == null || n < 1) return 'Entrez un nombre entre 1 et 10'; keyboardType: TextInputType.number,
if (n > 10) return 'Maximum 10 (plafond agrément)'; validator: (v) {
return null; if (v == null || v.isEmpty) return 'Capacité requise';
}, final n = int.tryParse(v);
if (n == null || n < 1) {
return 'Entrez un nombre entre 1 et $kAmCapaciteAccueilMax';
}
if (n > kAmCapaciteAccueilMax) {
return 'Maximum $kAmCapaciteAccueilMax';
}
return null;
},
),
),
const SizedBox(width: 14),
Expanded(
child: _buildField(
config: config,
label: 'Places disponibles',
controller: _placesAvailableController,
hint: 'Entre 0 et la capacité',
keyboardType: TextInputType.number,
validator: _validatePlacesAvailable,
),
),
],
), ),
], ],
); );
@ -852,49 +899,53 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( _buildField(
crossAxisAlignment: CrossAxisAlignment.start, config: config,
children: [ label: 'N° d\'agrément',
Expanded( controller: _agrementController,
child: _buildField( hint: 'Votre numéro d\'agrément',
config: config, validator: (v) => v!.isEmpty ? 'Agrément requis' : null,
label: 'N° d\'agrément', ),
controller: _agrementController, const SizedBox(height: 12),
hint: 'N° agrément', _buildField(
validator: (v) => v!.isEmpty ? 'Agrément requis' : null, config: config,
), label: 'Date d\'obtention de l\'agrément',
), controller: _agreementDateController,
const SizedBox(width: 12), hint: 'JJ/MM/AAAA',
Expanded( readOnly: true,
child: _buildField( onTap: () => _selectAgreementDate(context),
config: config, suffixIcon: Icons.calendar_today,
label: 'Date d\'obtention de l\'agrément', validator: (_) =>
controller: _agreementDateController, _selectedAgreementDate == null ? 'Date d\'obtention requise' : null,
hint: 'JJ/MM/AAAA',
readOnly: true,
onTap: () => _selectAgreementDate(context),
suffixIcon: Icons.calendar_today,
validator: (_) =>
_selectedAgreementDate == null ? 'Date d\'obtention requise' : null,
),
),
],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildField( _buildField(
config: config, config: config,
label: 'Capacité d\'accueil', label: 'Capacité d\'accueil',
controller: _capacityController, controller: _capacityController,
hint: 'De 1 à 10 enfants', hint: 'De 1 à $kAmCapaciteAccueilMax enfants',
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
validator: (v) { validator: (v) {
if (v == null || v.isEmpty) return 'Capacité requise'; if (v == null || v.isEmpty) return 'Capacité requise';
final n = int.tryParse(v); final n = int.tryParse(v);
if (n == null || n < 1) return 'Entrez un nombre entre 1 et 10'; if (n == null || n < 1) {
if (n > 10) return 'Maximum 10 (plafond agrément)'; return 'Entrez un nombre entre 1 et $kAmCapaciteAccueilMax';
}
if (n > kAmCapaciteAccueilMax) {
return 'Maximum $kAmCapaciteAccueilMax';
}
return null; return null;
}, },
), ),
const SizedBox(height: 12),
_buildField(
config: config,
label: 'Places disponibles',
controller: _placesAvailableController,
hint: 'Entre 0 et la capacité',
keyboardType: TextInputType.number,
validator: _validatePlacesAvailable,
),
], ],
); );
} }
@ -918,7 +969,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
baseShadowColor: Colors.green.shade300, baseShadowColor: Colors.green.shade300,
isMobile: config.isMobile, isMobile: config.isMobile,
), ),
const SizedBox(height: 10), SizedBox(height: config.isMobile ? 10.0 : 6.0),
AppCustomCheckbox( AppCustomCheckbox(
label: 'J\'accepte l\'utilisation\nde ma photo.', label: 'J\'accepte l\'utilisation\nde ma photo.',
value: _photoConsent, value: _photoConsent,
@ -959,8 +1010,9 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
hintText: hint ?? label, hintText: hint ?? label,
fieldWidth: double.infinity, fieldWidth: double.infinity,
fieldHeight: config.isMobile ? 45.0 : 53.0, fieldHeight: config.isMobile ? 45.0 : 53.0,
labelFontSize: config.isMobile ? 15.0 : 22.0, labelFontSize: config.isMobile ? 15.0 : 20.0,
inputFontSize: config.isMobile ? 14.0 : 20.0, inputFontSize: config.isMobile ? 14.0 : 18.0,
labelFieldSpacing: config.isMobile ? 6.0 : 3.0,
keyboardType: keyboardType ?? TextInputType.text, keyboardType: keyboardType ?? TextInputType.text,
readOnly: readOnly, readOnly: readOnly,
onTap: onTap, onTap: onTap,

View File

@ -61,6 +61,7 @@ try {
numero_agrement: 'AGR-2019-095001', numero_agrement: 'AGR-2019-095001',
date_agrement: '2019-09-01', date_agrement: '2019-09-01',
capacite_accueil: 4, capacite_accueil: 4,
places_disponibles: 2,
biographie: biographie:
"Assistante maternelle agréée depuis 2019. Née en Corse à Ajaccio. Spécialité bébés 0-18 mois. " + "Assistante maternelle agréée depuis 2019. Née en Corse à Ajaccio. Spécialité bébés 0-18 mois. " +
'Accueil bienveillant et cadre sécurisant. 2 places disponibles.', 'Accueil bienveillant et cadre sécurisant. 2 places disponibles.',

View File

@ -60,6 +60,7 @@ try {
numero_agrement: 'AGR-2017-095002', numero_agrement: 'AGR-2017-095002',
date_agrement: '2017-06-15', date_agrement: '2017-06-15',
capacite_accueil: 3, capacite_accueil: 3,
places_disponibles: 1,
biographie: biographie:
"Assistante maternelle expérimentée. Née à l'étranger. Spécialité 1-3 ans. " + "Assistante maternelle expérimentée. Née à l'étranger. Spécialité 1-3 ans. " +
'Accueil à la journée. 1 place disponible.', 'Accueil à la journée. 1 place disponible.',