petitspas/frontend/lib/utils/nir_utils.dart
Julien Martin ae610733cc feat(#156): création dossier AM staff — API + wizard dashboard.
Squash depuis develop : POST /assistantes-maternelles/dossier (actif +
mail MDP), AmDossierWizard create/review, validations inscription.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 16:53:18 +02:00

140 lines
4.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (complet ou en cours de saisie) :
/// `1 12 34 56 789 012 - 34` ou `1 12 34 2A 789 012 - 34` (Corse).
String formatNir(String raw) {
final r = nirToRaw(raw).toUpperCase();
if (r.isEmpty) return '';
final buf = StringBuffer();
for (var i = 0; i < r.length && i < 15; i++) {
if (i == 1 || i == 3 || i == 5 || i == 7 || i == 10) {
buf.write(' ');
} else if (i == 13) {
buf.write(' - ');
}
buf.write(r[i]);
}
return buf.toString();
}
/// Aligné sur le backend (NIR côté API / nir.util.ts) : sexe 13, département 2A ou 2B pour la Corse.
bool _isFormatValid(String raw) {
final r = raw.toUpperCase();
if (r.length != 15) return false;
return RegExp(r'^[1-3]\d{4}(?:2A|2B|\d{2})\d{6}\d{2}$').hasMatch(r);
}
/// Clé INSEE : 97 - (NIR13 mod 97). Corse : 2A→19, 2B→20 (aligné backend).
int _controlKey(String raw13) {
String n;
if (raw13.length >= 7 && raw13.substring(5, 7) == '2A') {
n = '${raw13.substring(0, 5)}19${raw13.substring(7)}';
} else if (raw13.length >= 7 && raw13.substring(5, 7) == '2B') {
n = '${raw13.substring(0, 5)}20${raw13.substring(7)}';
} else {
n = '${raw13.substring(0, 5)}${raw13.substring(5, 7)}${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;
}
/// Validation pendant la saisie : pas derreur si incomplet encore plausible ;
/// dès 15 caractères, même contrôles que [validateNir].
String? validateNirTyping(String? value) {
if (value == null || value.trim().isEmpty) return null;
final raw = nirToRaw(value).toUpperCase();
if (raw.isEmpty) return null;
if (raw[0] != '1' && raw[0] != '2' && raw[0] != '3') {
return 'Format NIR invalide (ex. 1 12 34 56 789 012 - 34 ou 2A pour la Corse)';
}
if (raw.length < 15) return null;
return validateNir(value);
}
/// Formateur de saisie : affiche le NIR formaté au fil de la frappe et limite à 15 caractères utiles.
class NirInputFormatter extends TextInputFormatter {
const NirInputFormatter();
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final raw = normalizeNir(newValue.text);
if (raw.isEmpty) {
return const TextEditingValue(
text: '',
selection: TextSelection.collapsed(offset: 0),
);
}
final formatted = formatNir(raw);
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}
}