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:
parent
58607cdbc9
commit
b4b44cb1b9
@ -26,6 +26,7 @@ curl -s -w "\n\nHTTP %{http_code}\n" -X POST "$BASE_URL/auth/register/am" \
|
||||
"nir": "186127500100279",
|
||||
"numero_agrement": "AGR-SMOKE-CURL-001",
|
||||
"capacite_accueil": 3,
|
||||
"places_disponibles": 2,
|
||||
"acceptation_cgu": true,
|
||||
"acceptation_privacy": true
|
||||
}'
|
||||
|
||||
@ -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 nirValidation = validateNir(nirNormalized, {
|
||||
dateNaissance: dto.date_naissance,
|
||||
|
||||
@ -138,6 +138,17 @@ export class RegisterAMCompletDto {
|
||||
@Max(10, { message: 'La capacité ne peut pas dépasser 10' })
|
||||
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)
|
||||
// ============================================
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
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 {
|
||||
// Step 1: Identity Info
|
||||
String firstName = '';
|
||||
@ -27,6 +30,8 @@ class AmRegistrationData extends ChangeNotifier {
|
||||
/// Date d'obtention de l'agrément — obligatoire (API `date_agrement`).
|
||||
DateTime? agreementDate;
|
||||
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
|
||||
String presentationText = '';
|
||||
@ -72,6 +77,7 @@ class AmRegistrationData extends ChangeNotifier {
|
||||
String? agrementNumber,
|
||||
DateTime? agreementDate,
|
||||
int? capacity,
|
||||
int? placesAvailable,
|
||||
}) {
|
||||
this.photoPath = photoPath;
|
||||
this.photoBytes = photoBytes;
|
||||
@ -85,6 +91,7 @@ class AmRegistrationData extends ChangeNotifier {
|
||||
this.agrementNumber = agrementNumber ?? this.agrementNumber;
|
||||
this.agreementDate = agreementDate ?? this.agreementDate;
|
||||
this.capacity = capacity ?? this.capacity;
|
||||
this.placesAvailable = placesAvailable ?? this.placesAvailable;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -125,7 +132,10 @@ class AmRegistrationData extends ChangeNotifier {
|
||||
agreementDate != null &&
|
||||
capacity != null &&
|
||||
capacity! >= 1 &&
|
||||
capacity! <= 10;
|
||||
capacity! <= kAmCapaciteAccueilMax &&
|
||||
placesAvailable != null &&
|
||||
placesAvailable! >= 0 &&
|
||||
placesAvailable! <= capacity!;
|
||||
|
||||
bool get isStep3Complete =>
|
||||
// presentationText is optional as per CDC (message au gestionnaire)
|
||||
@ -142,7 +152,7 @@ class AmRegistrationData extends ChangeNotifier {
|
||||
'phone: $phone, email: $email, '
|
||||
// 'photoPath: $photoPath, photoConsent: $photoConsent, ' // Commenté car déplacé/modifié
|
||||
'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, '
|
||||
'presentationText: $presentationText, cguAccepted: $cguAccepted)';
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ class AmRegisterStep2Screen extends StatelessWidget {
|
||||
agrementNumber: registrationData.agrementNumber,
|
||||
agreementDate: registrationData.agreementDate,
|
||||
capacity: registrationData.capacity,
|
||||
placesAvailable: registrationData.placesAvailable,
|
||||
);
|
||||
|
||||
return ProfessionalInfoFormScreen(
|
||||
@ -46,6 +47,7 @@ class AmRegisterStep2Screen extends StatelessWidget {
|
||||
agrementNumber: data.agrementNumber,
|
||||
agreementDate: data.agreementDate,
|
||||
capacity: data.capacity,
|
||||
placesAvailable: data.placesAvailable,
|
||||
);
|
||||
context.go('/am-register-step3');
|
||||
},
|
||||
|
||||
@ -206,6 +206,7 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
|
||||
agrementNumber: data.agrementNumber,
|
||||
agreementDate: data.agreementDate,
|
||||
capacity: data.capacity,
|
||||
placesAvailable: data.placesAvailable,
|
||||
photoConsent: data.photoConsent,
|
||||
),
|
||||
onSubmit: (d) {},
|
||||
|
||||
@ -165,6 +165,9 @@ class AuthService {
|
||||
if (data.agreementDate == null) {
|
||||
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? photoFilename;
|
||||
|
||||
@ -211,6 +214,7 @@ class AuthService {
|
||||
'date_agrement':
|
||||
'${data.agreementDate!.year}-${data.agreementDate!.month.toString().padLeft(2, '0')}-${data.agreementDate!.day.toString().padLeft(2, '0')}',
|
||||
'capacite_accueil': data.capacity ?? 1,
|
||||
'places_disponibles': data.placesAvailable!,
|
||||
'biographie': data.presentationText.isNotEmpty ? data.presentationText : null,
|
||||
'acceptation_cgu': data.cguAccepted,
|
||||
'acceptation_privacy': data.cguAccepted,
|
||||
|
||||
@ -27,6 +27,8 @@ class CustomAppTextField extends StatefulWidget {
|
||||
final IconData? suffixIcon;
|
||||
final double labelFontSize;
|
||||
final double inputFontSize;
|
||||
/// Espace vertical entre le libellé et la zone de saisie.
|
||||
final double labelFieldSpacing;
|
||||
final bool showLabel;
|
||||
final Iterable<String>? autofillHints;
|
||||
final TextInputAction? textInputAction;
|
||||
@ -56,6 +58,7 @@ class CustomAppTextField extends StatefulWidget {
|
||||
this.suffixIcon,
|
||||
this.labelFontSize = 18.0,
|
||||
this.inputFontSize = 18.0,
|
||||
this.labelFieldSpacing = 6.0,
|
||||
this.autofillHints,
|
||||
this.textInputAction,
|
||||
this.onFieldSubmitted,
|
||||
@ -105,7 +108,7 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SizedBox(height: widget.labelFieldSpacing),
|
||||
],
|
||||
// Pas de hauteur fixe sur le TextFormField : le message d’erreur du
|
||||
// validateur s’affiche en dessous ; un SizedBox fixe le masquait.
|
||||
|
||||
@ -18,12 +18,13 @@ class NirTextField extends StatelessWidget {
|
||||
final bool enabled;
|
||||
final bool readOnly;
|
||||
final CustomAppTextFieldStyle style;
|
||||
final double labelFieldSpacing;
|
||||
|
||||
const NirTextField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
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.fieldWidth = double.infinity,
|
||||
this.fieldHeight = 53.0,
|
||||
@ -32,6 +33,7 @@ class NirTextField extends StatelessWidget {
|
||||
this.enabled = true,
|
||||
this.readOnly = false,
|
||||
this.style = CustomAppTextFieldStyle.beige,
|
||||
this.labelFieldSpacing = 6.0,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -50,6 +52,7 @@ class NirTextField extends StatelessWidget {
|
||||
enabled: enabled,
|
||||
readOnly: readOnly,
|
||||
style: style,
|
||||
labelFieldSpacing: labelFieldSpacing,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:io';
|
||||
import '../models/am_registration_data.dart' show kAmCapaciteAccueilMax;
|
||||
import '../models/card_assets.dart';
|
||||
import '../config/display_config.dart';
|
||||
import '../utils/nir_utils.dart';
|
||||
@ -34,6 +35,7 @@ class ProfessionalInfoData {
|
||||
/// Date d'obtention de l'agrément (obligatoire, API `date_agrement`).
|
||||
final DateTime? agreementDate;
|
||||
final int? capacity;
|
||||
final int? placesAvailable;
|
||||
|
||||
ProfessionalInfoData({
|
||||
this.photoPath,
|
||||
@ -48,6 +50,7 @@ class ProfessionalInfoData {
|
||||
this.agrementNumber = '',
|
||||
this.agreementDate,
|
||||
this.capacity,
|
||||
this.placesAvailable,
|
||||
});
|
||||
}
|
||||
|
||||
@ -94,6 +97,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
final _agrementController = TextEditingController();
|
||||
final _agreementDateController = TextEditingController();
|
||||
final _capacityController = TextEditingController();
|
||||
final _placesAvailableController = TextEditingController();
|
||||
|
||||
FocusNode? _birthCityFocus;
|
||||
FocusNode? _birthCountryFocus;
|
||||
@ -133,6 +137,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
? DateFormat('dd/MM/yyyy').format(data.agreementDate!)
|
||||
: '';
|
||||
_capacityController.text = data.capacity?.toString() ?? '';
|
||||
_placesAvailableController.text = data.placesAvailable?.toString() ?? '';
|
||||
_photoPathFramework = data.photoPath;
|
||||
_photoFile = data.photoFile;
|
||||
_photoBytes = data.photoBytes;
|
||||
@ -180,9 +185,20 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
_agrementController.dispose();
|
||||
_agreementDateController.dispose();
|
||||
_capacityController.dispose();
|
||||
_placesAvailableController.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 {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
@ -285,6 +301,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
agrementNumber: _agrementController.text,
|
||||
agreementDate: _selectedAgreementDate,
|
||||
capacity: int.tryParse(_capacityController.text),
|
||||
placesAvailable: int.tryParse(_placesAvailableController.text),
|
||||
);
|
||||
|
||||
widget.onSubmit(data);
|
||||
@ -308,7 +325,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
),
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 40.0),
|
||||
padding: EdgeInsets.symmetric(vertical: config.isMobile ? 40.0 : 28.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@ -329,7 +346,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: config.isMobile ? 16 : 30),
|
||||
SizedBox(height: config.isMobile ? 16 : 20),
|
||||
_buildCard(context, config, screenSize),
|
||||
|
||||
// Boutons mobile sous la carte
|
||||
@ -400,7 +417,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
Container(
|
||||
width: config.isMobile ? screenSize.width * 0.9 : screenSize.width * 0.6,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: config.isMobile ? 20 : (config.isReadonly ? 30 : 50),
|
||||
vertical: config.isMobile ? 20 : (config.isReadonly ? 24 : 28),
|
||||
horizontal: config.isMobile ? 24 : 50,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
@ -428,7 +445,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(height: config.isMobile ? 20.0 : 14.0),
|
||||
],
|
||||
config.isMobile
|
||||
? _buildMobileFields(context, config)
|
||||
@ -550,7 +567,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Contenu
|
||||
Expanded(
|
||||
@ -578,7 +595,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(height: 5),
|
||||
AppCustomCheckbox(
|
||||
label: 'J\'accepte l\'utilisation\nde ma photo.',
|
||||
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
|
||||
Expanded(
|
||||
@ -605,7 +622,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
Expanded(flex: 3, child: _buildReadonlyField('Ville de naissance', _birthCityController.text)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 5),
|
||||
|
||||
// Ligne 2 : Pays + NIR
|
||||
Row(
|
||||
@ -615,7 +632,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
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
|
||||
Row(
|
||||
@ -634,12 +651,19 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
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: [
|
||||
Text(
|
||||
label,
|
||||
style: GoogleFonts.merienda(fontSize: 18.0, fontWeight: FontWeight.w600),
|
||||
style: GoogleFonts.merienda(fontSize: 16.0, fontWeight: FontWeight.w600),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 3),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 45.0, // Hauteur réduite pour compacter
|
||||
@ -685,7 +709,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
),
|
||||
child: Text(
|
||||
value.isNotEmpty ? value : '-',
|
||||
style: GoogleFonts.merienda(fontSize: 16.0),
|
||||
style: GoogleFonts.merienda(fontSize: 14.0),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
@ -695,7 +719,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
|
||||
/// Layout DESKTOP : Photo à gauche, champs à droite
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -708,7 +732,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
width: 300,
|
||||
child: _buildPhotoSection(context, config),
|
||||
),
|
||||
const SizedBox(width: 30),
|
||||
const SizedBox(width: 22),
|
||||
// Champs à droite
|
||||
Expanded(
|
||||
child: Column(
|
||||
@ -751,8 +775,9 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
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,
|
||||
labelFontSize: config.isMobile ? 15.0 : 20.0,
|
||||
inputFontSize: config.isMobile ? 14.0 : 18.0,
|
||||
labelFieldSpacing: config.isMobile ? 6.0 : 3.0,
|
||||
),
|
||||
SizedBox(height: verticalSpacing),
|
||||
Row(
|
||||
@ -767,7 +792,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
validator: (v) => v!.isEmpty ? 'Agrément requis' : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
config: config,
|
||||
@ -784,20 +809,42 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
],
|
||||
),
|
||||
SizedBox(height: verticalSpacing),
|
||||
_buildField(
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
config: config,
|
||||
label: 'Capacité d\'accueil',
|
||||
controller: _capacityController,
|
||||
hint: 'De 1 à 10 enfants',
|
||||
hint: 'De 1 à $kAmCapaciteAccueilMax enfants',
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (v) {
|
||||
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 10';
|
||||
if (n > 10) return 'Maximum 10 (plafond agrément)';
|
||||
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,21 +899,15 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
_buildField(
|
||||
config: config,
|
||||
label: 'N° d\'agrément',
|
||||
controller: _agrementController,
|
||||
hint: 'N° agrément',
|
||||
hint: 'Votre numéro d\'agrément',
|
||||
validator: (v) => v!.isEmpty ? 'Agrément requis' : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
const SizedBox(height: 12),
|
||||
_buildField(
|
||||
config: config,
|
||||
label: 'Date d\'obtention de l\'agrément',
|
||||
controller: _agreementDateController,
|
||||
@ -877,24 +918,34 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
validator: (_) =>
|
||||
_selectedAgreementDate == null ? 'Date d\'obtention requise' : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildField(
|
||||
config: config,
|
||||
label: 'Capacité d\'accueil',
|
||||
controller: _capacityController,
|
||||
hint: 'De 1 à 10 enfants',
|
||||
hint: 'De 1 à $kAmCapaciteAccueilMax enfants',
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (v) {
|
||||
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 10';
|
||||
if (n > 10) return 'Maximum 10 (plafond agrément)';
|
||||
if (n == null || n < 1) {
|
||||
return 'Entrez un nombre entre 1 et $kAmCapaciteAccueilMax';
|
||||
}
|
||||
if (n > kAmCapaciteAccueilMax) {
|
||||
return 'Maximum $kAmCapaciteAccueilMax';
|
||||
}
|
||||
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,
|
||||
isMobile: config.isMobile,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(height: config.isMobile ? 10.0 : 6.0),
|
||||
AppCustomCheckbox(
|
||||
label: 'J\'accepte l\'utilisation\nde ma photo.',
|
||||
value: _photoConsent,
|
||||
@ -959,8 +1010,9 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
||||
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,
|
||||
labelFontSize: config.isMobile ? 15.0 : 20.0,
|
||||
inputFontSize: config.isMobile ? 14.0 : 18.0,
|
||||
labelFieldSpacing: config.isMobile ? 6.0 : 3.0,
|
||||
keyboardType: keyboardType ?? TextInputType.text,
|
||||
readOnly: readOnly,
|
||||
onTap: onTap,
|
||||
|
||||
@ -61,6 +61,7 @@ try {
|
||||
numero_agrement: 'AGR-2019-095001',
|
||||
date_agrement: '2019-09-01',
|
||||
capacite_accueil: 4,
|
||||
places_disponibles: 2,
|
||||
biographie:
|
||||
"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.',
|
||||
|
||||
@ -60,6 +60,7 @@ try {
|
||||
numero_agrement: 'AGR-2017-095002',
|
||||
date_agrement: '2017-06-15',
|
||||
capacite_accueil: 3,
|
||||
places_disponibles: 1,
|
||||
biographie:
|
||||
"Assistante maternelle expérimentée. Née à l'étranger. Spécialité 1-3 ans. " +
|
||||
'Accueil à la journée. 1 place disponible.',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user