feat(inscription): email accusé réception, photos enfants, formulaires identité
- Backend: mail après inscription parent avec n° dossier, UPLOAD_PHOTOS_DIR, réponse API - Frontend: imageBytes + payload photo, utils email/code postal/téléphone, champs dédiés - Formulaires admin, login (focus), personal_info, child_card, custom_app_text_field Made-with: Cursor
This commit is contained in:
@@ -8,46 +8,127 @@ String normalizePhone(String raw) {
|
||||
return digits.length > 10 ? digits.substring(0, 10) : digits;
|
||||
}
|
||||
|
||||
/// Indique si [digitsOnly] (déjà normalisé par [normalizePhone]) commence par `0`.
|
||||
/// Chaîne vide : `true` (pas encore de saisie).
|
||||
bool frenchNationalPhoneStartsWithZero(String digitsOnly) {
|
||||
if (digitsOnly.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
return digitsOnly.startsWith('0');
|
||||
}
|
||||
|
||||
/// Validation téléphone France : 10 chiffres, commence par `0`, 2ᵉ chiffre 1–9 (format national).
|
||||
///
|
||||
/// Utiliser sur tout champ « numéro français » (inscription, admin, relais, etc.).
|
||||
/// Si [allowEmpty] est `true`, une valeur vide ou blanche est acceptée.
|
||||
String? validateFrenchNationalPhone(String? raw, {bool allowEmpty = false}) {
|
||||
final trimmed = raw?.trim() ?? '';
|
||||
if (trimmed.isEmpty) {
|
||||
return allowEmpty ? null : 'Le numéro de téléphone est obligatoire.';
|
||||
}
|
||||
final digits = normalizePhone(trimmed);
|
||||
if (digits.isEmpty) {
|
||||
return allowEmpty ? null : 'Le numéro de téléphone est obligatoire.';
|
||||
}
|
||||
if (!frenchNationalPhoneStartsWithZero(digits)) {
|
||||
return 'En France, le numéro doit commencer par 0 (ex. 06 12 34 56 78).';
|
||||
}
|
||||
if (digits.length < 10) {
|
||||
return 'Le numéro doit contenir 10 chiffres.';
|
||||
}
|
||||
if (digits.length > 10) {
|
||||
return 'Le numéro ne peut pas dépasser 10 chiffres.';
|
||||
}
|
||||
if (!RegExp(r'^0[1-9]\d{8}$').hasMatch(digits)) {
|
||||
return 'Numéro de téléphone invalide.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Retourne le numéro formaté pour l'affichage (ex. "06 12 34 56 78").
|
||||
/// Si [raw] est vide après normalisation, retourne [raw] tel quel (pour afficher "–" etc.).
|
||||
String formatPhoneForDisplay(String raw) {
|
||||
if (raw.trim().isEmpty) return raw;
|
||||
if (raw.trim().isEmpty) {
|
||||
return raw;
|
||||
}
|
||||
final normalized = normalizePhone(raw);
|
||||
if (normalized.isEmpty) return raw;
|
||||
if (normalized.isEmpty) {
|
||||
return raw;
|
||||
}
|
||||
return formatFrenchPhoneDigits(normalized);
|
||||
}
|
||||
|
||||
/// Affiche les chiffres par paires (ex. `0612345678` → `06 12 34 56 78`).
|
||||
String formatFrenchPhoneDigits(String normalizedDigits) {
|
||||
if (normalizedDigits.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
final buffer = StringBuffer();
|
||||
for (var i = 0; i < normalized.length; i++) {
|
||||
if (i > 0 && i.isEven) buffer.write(' ');
|
||||
buffer.write(normalized[i]);
|
||||
for (var i = 0; i < normalizedDigits.length; i++) {
|
||||
if (i > 0 && i.isEven) {
|
||||
buffer.write(' ');
|
||||
}
|
||||
buffer.write(normalizedDigits[i]);
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Formatter de saisie : uniquement chiffres, espaces automatiques toutes les 2 chiffres, max 10 chiffres.
|
||||
int _cursorOffsetInFormattedPhone(String normalized, int digitCountBeforeCursor) {
|
||||
final k = digitCountBeforeCursor.clamp(0, normalized.length);
|
||||
if (k == 0) {
|
||||
return 0;
|
||||
}
|
||||
return formatFrenchPhoneDigits(normalized.substring(0, k)).length;
|
||||
}
|
||||
|
||||
/// Formatter de saisie : chiffres uniquement, paires espacées, max 10 chiffres.
|
||||
///
|
||||
/// - Si le premier chiffre est **1 à 7**, un **0** est ajouté automatiquement devant (ex. `6` → `06`).
|
||||
/// - Si l’utilisateur tape **8** ou **9** en premier, la saisie est ignorée (numéros spéciaux `08` / `09`).
|
||||
/// - S’il commence déjà par **0**, aucun préfixe n’est ajouté.
|
||||
class FrenchPhoneNumberFormatter extends TextInputFormatter {
|
||||
const FrenchPhoneNumberFormatter();
|
||||
|
||||
static const String _autoPrefixFirstDigits = '1234567';
|
||||
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
final digits = newValue.text.replaceAll(RegExp(r'\D'), '');
|
||||
var digits = newValue.text.replaceAll(RegExp(r'\D'), '');
|
||||
var didPrependZero = false;
|
||||
|
||||
if (digits.isNotEmpty) {
|
||||
final first = digits[0];
|
||||
if (first == '8' || first == '9') {
|
||||
return oldValue;
|
||||
}
|
||||
if (first != '0' && _autoPrefixFirstDigits.contains(first)) {
|
||||
digits = '0$digits';
|
||||
didPrependZero = true;
|
||||
if (digits.length > 10) {
|
||||
digits = digits.substring(0, 10);
|
||||
}
|
||||
} else if (first != '0') {
|
||||
return oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
final normalized = digits.length > 10 ? digits.substring(0, 10) : digits;
|
||||
final buffer = StringBuffer();
|
||||
for (var i = 0; i < normalized.length; i++) {
|
||||
if (i > 0 && i.isEven) buffer.write(' ');
|
||||
buffer.write(normalized[i]);
|
||||
}
|
||||
final formatted = buffer.toString();
|
||||
final formatted = formatFrenchPhoneDigits(normalized);
|
||||
|
||||
// Conserver la position du curseur : compter les chiffres avant la sélection
|
||||
final sel = newValue.selection;
|
||||
final digitsBeforeCursor = newValue.text
|
||||
var digitsBeforeCursor = newValue.text
|
||||
.substring(0, sel.start.clamp(0, newValue.text.length))
|
||||
.replaceAll(RegExp(r'\D'), '')
|
||||
.length;
|
||||
final newOffset = digitsBeforeCursor + (digitsBeforeCursor > 0 ? digitsBeforeCursor ~/ 2 : 0);
|
||||
final clampedOffset = newOffset.clamp(0, formatted.length);
|
||||
if (didPrependZero) {
|
||||
digitsBeforeCursor = (digitsBeforeCursor + 1).clamp(0, normalized.length);
|
||||
}
|
||||
|
||||
final clampedOffset =
|
||||
_cursorOffsetInFormattedPhone(normalized, digitsBeforeCursor).clamp(0, formatted.length);
|
||||
|
||||
return TextEditingValue(
|
||||
text: formatted,
|
||||
@@ -55,3 +136,10 @@ class FrenchPhoneNumberFormatter extends TextInputFormatter {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Formatters à réutiliser sur tout champ téléphone France ([TextFormField], [CustomAppTextField], etc.).
|
||||
final List<TextInputFormatter> frenchPhoneInputFormatters = [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
const FrenchPhoneNumberFormatter(),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user