|
|
|
@@ -1,6 +1,8 @@
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
|
import 'package:p_tits_pas/utils/email_utils.dart';
|
|
|
|
|
import 'package:p_tits_pas/utils/nir_utils.dart';
|
|
|
|
|
import 'package:p_tits_pas/utils/phone_utils.dart';
|
|
|
|
|
import 'package:p_tits_pas/utils/postal_utils.dart';
|
|
|
|
|
import 'admin_detail_modal.dart';
|
|
|
|
|
|
|
|
|
@@ -449,6 +451,167 @@ class _ValidationPostalCodeFieldState extends State<ValidationPostalCodeField> {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Téléphone FR — formatters live + [validateFrenchNationalPhone] à la perte de focus.
|
|
|
|
|
class ValidationPhoneField extends StatefulWidget {
|
|
|
|
|
final TextEditingController controller;
|
|
|
|
|
final String? hintText;
|
|
|
|
|
final bool allowEmpty;
|
|
|
|
|
|
|
|
|
|
const ValidationPhoneField({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.controller,
|
|
|
|
|
this.hintText,
|
|
|
|
|
this.allowEmpty = false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<ValidationPhoneField> createState() => _ValidationPhoneFieldState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _ValidationPhoneFieldState extends State<ValidationPhoneField> {
|
|
|
|
|
final GlobalKey<FormFieldState<String>> _fieldKey =
|
|
|
|
|
GlobalKey<FormFieldState<String>>();
|
|
|
|
|
late final FocusNode _focusNode;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_focusNode = FocusNode();
|
|
|
|
|
_focusNode.addListener(_onFocusChange);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _onFocusChange() {
|
|
|
|
|
if (_focusNode.hasFocus) return;
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
if (!mounted || _focusNode.hasFocus) return;
|
|
|
|
|
final c = widget.controller;
|
|
|
|
|
final digits = normalizePhone(c.text);
|
|
|
|
|
final formatted = digits.isEmpty ? '' : formatPhoneForDisplay(digits);
|
|
|
|
|
if (formatted != c.text) {
|
|
|
|
|
c.value = TextEditingValue(
|
|
|
|
|
text: formatted,
|
|
|
|
|
selection: TextSelection.collapsed(offset: formatted.length),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
_fieldKey.currentState?.validate();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_focusNode.removeListener(_onFocusChange);
|
|
|
|
|
_focusNode.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return TextFormField(
|
|
|
|
|
key: _fieldKey,
|
|
|
|
|
controller: widget.controller,
|
|
|
|
|
focusNode: _focusNode,
|
|
|
|
|
keyboardType: TextInputType.phone,
|
|
|
|
|
textInputAction: TextInputAction.next,
|
|
|
|
|
inputFormatters: frenchPhoneInputFormatters,
|
|
|
|
|
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
|
|
|
|
decoration: ValidationFieldDecoration.input(hint: widget.hintText).copyWith(
|
|
|
|
|
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
|
|
|
|
errorMaxLines: 2,
|
|
|
|
|
),
|
|
|
|
|
validator: (value) =>
|
|
|
|
|
validateFrenchNationalPhone(value, allowEmpty: widget.allowEmpty),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// NIR — formatage live ([NirInputFormatter]) + validation au fil de la saisie / blur.
|
|
|
|
|
class ValidationNirField extends StatefulWidget {
|
|
|
|
|
final TextEditingController controller;
|
|
|
|
|
final String? hintText;
|
|
|
|
|
final bool allowEmpty;
|
|
|
|
|
|
|
|
|
|
const ValidationNirField({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.controller,
|
|
|
|
|
this.hintText,
|
|
|
|
|
this.allowEmpty = false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<ValidationNirField> createState() => _ValidationNirFieldState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _ValidationNirFieldState extends State<ValidationNirField> {
|
|
|
|
|
final GlobalKey<FormFieldState<String>> _fieldKey =
|
|
|
|
|
GlobalKey<FormFieldState<String>>();
|
|
|
|
|
late final FocusNode _focusNode;
|
|
|
|
|
bool _blurred = false;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_focusNode = FocusNode();
|
|
|
|
|
_focusNode.addListener(_onFocusChange);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _onFocusChange() {
|
|
|
|
|
if (_focusNode.hasFocus) return;
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
if (!mounted || _focusNode.hasFocus) return;
|
|
|
|
|
setState(() => _blurred = true);
|
|
|
|
|
final c = widget.controller;
|
|
|
|
|
final raw = nirToRaw(c.text).toUpperCase();
|
|
|
|
|
final formatted = raw.isEmpty ? '' : formatNir(raw);
|
|
|
|
|
if (formatted != c.text) {
|
|
|
|
|
c.value = TextEditingValue(
|
|
|
|
|
text: formatted,
|
|
|
|
|
selection: TextSelection.collapsed(offset: formatted.length),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
_fieldKey.currentState?.validate();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_focusNode.removeListener(_onFocusChange);
|
|
|
|
|
_focusNode.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String? _validator(String? value) {
|
|
|
|
|
if (_blurred) {
|
|
|
|
|
if (widget.allowEmpty && (value == null || value.trim().isEmpty)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return validateNir(value);
|
|
|
|
|
}
|
|
|
|
|
return validateNirTyping(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return TextFormField(
|
|
|
|
|
key: _fieldKey,
|
|
|
|
|
controller: widget.controller,
|
|
|
|
|
focusNode: _focusNode,
|
|
|
|
|
keyboardType: TextInputType.text,
|
|
|
|
|
textInputAction: TextInputAction.next,
|
|
|
|
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
|
|
|
|
inputFormatters: const [NirInputFormatter()],
|
|
|
|
|
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
|
|
|
|
decoration: ValidationFieldDecoration.input(
|
|
|
|
|
hint: widget.hintText ?? '1 12 34 56 789 012 - 34',
|
|
|
|
|
).copyWith(
|
|
|
|
|
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
|
|
|
|
errorMaxLines: 2,
|
|
|
|
|
),
|
|
|
|
|
onChanged: (_) => _fieldKey.currentState?.validate(),
|
|
|
|
|
validator: _validator,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Grille label/champ éditable (délègue à [ValidationFormGrid]).
|
|
|
|
|
class ValidationEditableSection extends StatelessWidget {
|
|
|
|
|
final List<ValidationLabeledField> fields;
|
|
|
|
|