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",
|
"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
|
||||||
}'
|
}'
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@ -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)';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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');
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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) {},
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 d’erreur du
|
// Pas de hauteur fixe sur le TextFormField : le message d’erreur du
|
||||||
// validateur s’affiche en dessous ; un SizedBox fixe le masquait.
|
// validateur s’affiche en dessous ; un SizedBox fixe le masquait.
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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.',
|
||||||
|
|||||||
@ -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.',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user