feat(frontend): NIR 15 car., formatage, validation, widget dédié (#102)

- 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
This commit is contained in:
2026-02-26 13:49:57 +01:00
parent a9c6b9e15b
commit 721f40599b
4 changed files with 196 additions and 25 deletions
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
@@ -6,7 +7,9 @@ import 'dart:math' as math;
import 'dart:io';
import '../models/card_assets.dart';
import '../config/display_config.dart';
import '../utils/nir_utils.dart';
import 'custom_app_text_field.dart';
import 'nir_text_field.dart';
import 'form_field_wrapper.dart';
import 'app_custom_checkbox.dart';
import 'hover_relief_widget.dart';
@@ -97,7 +100,8 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
: '';
_birthCityController.text = data.birthCity;
_birthCountryController.text = data.birthCountry;
_nirController.text = data.nir;
final nirRaw = nirToRaw(data.nir);
_nirController.text = nirRaw.length == 15 ? formatNir(nirRaw) : data.nir;
_agrementController.text = data.agrementNumber;
_capacityController.text = data.capacity?.toString() ?? '';
_photoPathFramework = data.photoPath;
@@ -161,7 +165,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
dateOfBirth: _selectedDate,
birthCity: _birthCityController.text,
birthCountry: _birthCountryController.text,
nir: _nirController.text,
nir: normalizeNir(_nirController.text),
agrementNumber: _agrementController.text,
capacity: int.tryParse(_capacityController.text),
);
@@ -499,7 +503,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
children: [
Expanded(flex: 2, child: _buildReadonlyField('Date de naissance', _dateOfBirthController.text)),
const SizedBox(width: 16),
Expanded(flex: 3, child: _buildReadonlyField('NIR', _nirController.text)),
Expanded(flex: 3, child: _buildReadonlyField('NIR', _formatNirForDisplay(_nirController.text))),
],
),
const SizedBox(height: 12),
@@ -525,6 +529,12 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
);
}
/// NIR formaté pour affichage (1 12 34 56 789 012-34 ou 2A pour la Corse).
String _formatNirForDisplay(String value) {
final raw = nirToRaw(value);
return raw.length == 15 ? formatNir(raw) : value;
}
/// Helper pour champ Readonly style "Beige"
Widget _buildReadonlyField(String label, String value) {
return Column(
@@ -609,18 +619,12 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
],
),
SizedBox(height: verticalSpacing),
_buildField(
config: config,
label: 'N° Sécurité Sociale (NIR)',
NirTextField(
controller: _nirController,
hint: 'Votre NIR à 13 chiffres',
keyboardType: TextInputType.number,
validator: (v) {
if (v == null || v.isEmpty) return 'NIR requis';
if (v.length != 13) return 'Le NIR doit contenir 13 chiffres';
if (!RegExp(r'^[1-3]').hasMatch(v[0])) return 'Le NIR doit commencer par 1, 2 ou 3';
return null;
},
fieldWidth: double.infinity,
fieldHeight: config.isMobile ? 45.0 : 53.0,
labelFontSize: config.isMobile ? 15.0 : 22.0,
inputFontSize: config.isMobile ? 14.0 : 20.0,
),
SizedBox(height: verticalSpacing),
Row(
@@ -695,18 +699,12 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
),
const SizedBox(height: 12),
_buildField(
config: config,
label: 'N° Sécurité Sociale (NIR)',
NirTextField(
controller: _nirController,
hint: 'Votre NIR à 13 chiffres',
keyboardType: TextInputType.number,
validator: (v) {
if (v == null || v.isEmpty) return 'NIR requis';
if (v.length != 13) return 'Le NIR doit contenir 13 chiffres';
if (!RegExp(r'^[1-3]').hasMatch(v[0])) return 'Le NIR doit commencer par 1, 2 ou 3';
return null;
},
fieldWidth: double.infinity,
fieldHeight: 45.0,
labelFontSize: 15.0,
inputFontSize: 14.0,
),
const SizedBox(height: 12),
@@ -796,6 +794,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
VoidCallback? onTap,
IconData? suffixIcon,
String? Function(String?)? validator,
List<TextInputFormatter>? inputFormatters,
}) {
if (config.isReadonly) {
return FormFieldWrapper(
@@ -817,6 +816,7 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
onTap: onTap,
suffixIcon: suffixIcon,
validator: validator,
inputFormatters: inputFormatters,
);
}
}