- 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
114 lines
4.4 KiB
Dart
114 lines
4.4 KiB
Dart
import 'package:flutter/services.dart';
|
||
|
||
/// Utilitaires NIR (Numéro d'Inscription au Répertoire) – INSEE, 15 caractères.
|
||
/// Corse : 2A (2A) et 2B (2B) au lieu de 19/20. Clé de contrôle : 97 - (NIR13 mod 97).
|
||
|
||
/// Normalise le NIR : 15 caractères, sans espaces ni séparateurs. Corse conservée (2A/2B).
|
||
String normalizeNir(String input) {
|
||
if (input.isEmpty) return '';
|
||
final cleaned = input.replaceAll(' ', '').replaceAll('-', '').replaceAll('/', '').toUpperCase();
|
||
final buf = StringBuffer();
|
||
int i = 0;
|
||
while (i < cleaned.length && buf.length < 15) {
|
||
final c = cleaned[i];
|
||
if (buf.length < 5) {
|
||
if (c.compareTo('0') >= 0 && c.compareTo('9') <= 0) buf.write(c);
|
||
i++;
|
||
} else if (buf.length == 5) {
|
||
if (c == '2' && i + 1 < cleaned.length && (cleaned[i + 1] == 'A' || cleaned[i + 1] == 'B')) {
|
||
buf.write('2');
|
||
buf.write(cleaned[i + 1]);
|
||
i += 2;
|
||
} else if ((c == 'A' || c == 'B')) {
|
||
buf.write('2');
|
||
buf.write(c);
|
||
i++;
|
||
} else if (c.compareTo('0') >= 0 && c.compareTo('9') <= 0) {
|
||
buf.write(c);
|
||
if (i + 1 < cleaned.length && cleaned[i + 1].compareTo('0') >= 0 && cleaned[i + 1].compareTo('9') <= 0) {
|
||
buf.write(cleaned[i + 1]);
|
||
i += 2;
|
||
} else {
|
||
i++;
|
||
}
|
||
} else {
|
||
i++;
|
||
}
|
||
} else {
|
||
if (c.compareTo('0') >= 0 && c.compareTo('9') <= 0) buf.write(c);
|
||
i++;
|
||
}
|
||
}
|
||
return buf.toString().length > 15 ? buf.toString().substring(0, 15) : buf.toString();
|
||
}
|
||
|
||
/// Retourne la chaîne brute à 15 caractères (chiffres + 2A ou 2B).
|
||
String nirToRaw(String normalized) {
|
||
String s = normalized.replaceAll(' ', '').replaceAll('-', '').replaceAll('/', '');
|
||
if (s.length > 15) s = s.substring(0, 15);
|
||
return s;
|
||
}
|
||
|
||
/// Formate pour affichage : 1 12 34 56 789 012-34 ou 1 12 34 2A 789 012-34
|
||
String formatNir(String raw) {
|
||
final r = nirToRaw(raw);
|
||
if (r.length < 15) return r;
|
||
final dept = r.substring(5, 7);
|
||
final isCorsica = dept == '2A' || dept == '2B';
|
||
if (isCorsica) {
|
||
return '${r.substring(0, 5)} ${r.substring(5, 7)} ${r.substring(7, 10)} ${r.substring(10, 13)}-${r.substring(13, 15)}';
|
||
}
|
||
return '${r.substring(0, 1)} ${r.substring(1, 3)} ${r.substring(3, 5)} ${r.substring(5, 7)} ${r.substring(7, 10)} ${r.substring(10, 13)}-${r.substring(13, 15)}';
|
||
}
|
||
|
||
/// Vérifie le format : 15 caractères, structure 1+2+2+2+3+3+2, département 2A/2B autorisé.
|
||
bool _isFormatValid(String raw) {
|
||
if (raw.length != 15) return false;
|
||
final dept = raw.substring(5, 7);
|
||
final restDigits = raw.substring(0, 5) + (dept == '2A' ? '19' : dept == '2B' ? '18' : dept) + raw.substring(7, 15);
|
||
if (!RegExp(r'^[12]\d{12}\d{2}$').hasMatch(restDigits)) return false;
|
||
return RegExp(r'^[12]\d{4}(?:\d{2}|2A|2B)\d{6}$').hasMatch(raw);
|
||
}
|
||
|
||
/// Calcule la clé de contrôle (97 - (NIR13 mod 97)). Pour 2A→19, 2B→18.
|
||
int _controlKey(String raw13) {
|
||
String n = raw13;
|
||
if (raw13.length >= 7 && (raw13.substring(5, 7) == '2A' || raw13.substring(5, 7) == '2B')) {
|
||
n = raw13.substring(0, 5) + (raw13.substring(5, 7) == '2A' ? '19' : '18') + raw13.substring(7);
|
||
}
|
||
final big = int.tryParse(n);
|
||
if (big == null) return -1;
|
||
return 97 - (big % 97);
|
||
}
|
||
|
||
/// Valide le NIR (format + clé). Retourne null si valide, message d'erreur sinon.
|
||
String? validateNir(String? value) {
|
||
if (value == null || value.isEmpty) return 'NIR requis';
|
||
final raw = nirToRaw(value).toUpperCase();
|
||
if (raw.length != 15) return 'Le NIR doit contenir 15 caractères (chiffres, ou 2A/2B pour la Corse)';
|
||
if (!_isFormatValid(raw)) return 'Format NIR invalide (ex. 1 12 34 56 789 012-34 ou 2A pour la Corse)';
|
||
final key = _controlKey(raw.substring(0, 13));
|
||
final keyStr = key >= 0 && key <= 99 ? key.toString().padLeft(2, '0') : '';
|
||
final expectedKey = raw.substring(13, 15);
|
||
if (key < 0 || keyStr != expectedKey) return 'Clé de contrôle NIR invalide';
|
||
return null;
|
||
}
|
||
|
||
/// Formateur de saisie : affiche le NIR formaté (1 12 34 56 789 012-34) et limite à 15 caractères utiles.
|
||
class NirInputFormatter extends TextInputFormatter {
|
||
@override
|
||
TextEditingValue formatEditUpdate(
|
||
TextEditingValue oldValue,
|
||
TextEditingValue newValue,
|
||
) {
|
||
final raw = normalizeNir(newValue.text);
|
||
if (raw.isEmpty) return newValue;
|
||
final formatted = formatNir(raw);
|
||
final offset = formatted.length;
|
||
return TextEditingValue(
|
||
text: formatted,
|
||
selection: TextSelection.collapsed(offset: offset),
|
||
);
|
||
}
|
||
}
|