fix(#132): UX création enfant — champs, genre et validation.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
6b89d7b405
commit
7a3d997ff9
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
||||||
|
|
||||||
@ -11,10 +12,50 @@ String formatIsoDateFr(String? s, {String ifEmpty = '–'}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convertit `dd/MM/yyyy` (ou ISO) en `yyyy-MM-dd` pour l'API.
|
/// Affiche une date ISO en saisie guidée `jj / mm / aaaa`.
|
||||||
|
String formatIsoDateFrInput(String? s, {String ifEmpty = ''}) {
|
||||||
|
if (s == null || s.trim().isEmpty) return ifEmpty;
|
||||||
|
try {
|
||||||
|
final dt = DateTime.parse(s.trim());
|
||||||
|
return formatFrenchDateDigits(
|
||||||
|
DateFormat('ddMMyyyy').format(dt),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return formatFrenchDateDigits(s.replaceAll(RegExp(r'\D'), ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formate jusqu’à 8 chiffres en `jj / mm / aaaa`.
|
||||||
|
String formatFrenchDateDigits(String digits) {
|
||||||
|
final d = digits.replaceAll(RegExp(r'\D'), '');
|
||||||
|
final limited = d.length > 8 ? d.substring(0, 8) : d;
|
||||||
|
final buf = StringBuffer();
|
||||||
|
for (var i = 0; i < limited.length; i++) {
|
||||||
|
if (i == 2 || i == 4) buf.write(' / ');
|
||||||
|
buf.write(limited[i]);
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convertit `dd/MM/yyyy`, `dd / MM / yyyy`, 8 chiffres ou ISO en `yyyy-MM-dd`.
|
||||||
String? parseFrDateToIso(String text) {
|
String? parseFrDateToIso(String text) {
|
||||||
final t = text.trim();
|
final t = text.trim();
|
||||||
if (t.isEmpty) return null;
|
if (t.isEmpty) return null;
|
||||||
|
|
||||||
|
final digits = t.replaceAll(RegExp(r'\D'), '');
|
||||||
|
if (digits.length == 8) {
|
||||||
|
final dd = digits.substring(0, 2);
|
||||||
|
final mm = digits.substring(2, 4);
|
||||||
|
final yyyy = digits.substring(4, 8);
|
||||||
|
try {
|
||||||
|
return DateFormat('dd/MM/yyyy')
|
||||||
|
.parseStrict('$dd/$mm/$yyyy')
|
||||||
|
.toIso8601String()
|
||||||
|
.split('T')
|
||||||
|
.first;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return DateFormat('dd/MM/yyyy')
|
return DateFormat('dd/MM/yyyy')
|
||||||
.parseStrict(t)
|
.parseStrict(t)
|
||||||
@ -96,3 +137,43 @@ String formatChildAgeLabel({
|
|||||||
return 'Né le ${formatIsoDateFr(birthDate)}';
|
return 'Né le ${formatIsoDateFr(birthDate)}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Saisie date FR : 8 chiffres → `jj / mm / aaaa`.
|
||||||
|
class FrenchDateInputFormatter extends TextInputFormatter {
|
||||||
|
const FrenchDateInputFormatter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextEditingValue formatEditUpdate(
|
||||||
|
TextEditingValue oldValue,
|
||||||
|
TextEditingValue newValue,
|
||||||
|
) {
|
||||||
|
final digits = newValue.text.replaceAll(RegExp(r'\D'), '');
|
||||||
|
final limited = digits.length > 8 ? digits.substring(0, 8) : digits;
|
||||||
|
final formatted = formatFrenchDateDigits(limited);
|
||||||
|
|
||||||
|
final digitsBeforeCursor = newValue.text
|
||||||
|
.substring(0, newValue.selection.start.clamp(0, newValue.text.length))
|
||||||
|
.replaceAll(RegExp(r'\D'), '')
|
||||||
|
.length
|
||||||
|
.clamp(0, limited.length);
|
||||||
|
|
||||||
|
return TextEditingValue(
|
||||||
|
text: formatted,
|
||||||
|
selection: TextSelection.collapsed(
|
||||||
|
offset: _cursorOffset(formatted, digitsBeforeCursor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _cursorOffset(String formatted, int digitsBeforeCursor) {
|
||||||
|
if (digitsBeforeCursor <= 0) return 0;
|
||||||
|
var seen = 0;
|
||||||
|
for (var i = 0; i < formatted.length; i++) {
|
||||||
|
if (RegExp(r'\d').hasMatch(formatted[i])) {
|
||||||
|
seen++;
|
||||||
|
if (seen >= digitsBeforeCursor) return i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return formatted.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
/// Formatage affichage prénom / nom (capitalisation par mot et segments après `-` ou `'`).
|
/// Formatage affichage prénom / nom (capitalisation par mot et segments après `-` ou `'`).
|
||||||
|
|
||||||
String formatPersonNameCase(String raw) {
|
String formatPersonNameCase(String raw) {
|
||||||
@ -9,6 +11,22 @@ String formatPersonNameCase(String raw) {
|
|||||||
return words.map(_capitalizeComposedWord).join(' ');
|
return words.map(_capitalizeComposedWord).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Variante saisie live : pas de majuscule tant que le mot n’a qu’une lettre ;
|
||||||
|
/// dès la 2ᵉ lettre, capitalisation comme [formatPersonNameCase].
|
||||||
|
String formatPersonNameCaseTyping(String raw) {
|
||||||
|
if (raw.isEmpty) return raw;
|
||||||
|
final trailingMatch = RegExp(r'(\s*)$').firstMatch(raw);
|
||||||
|
final trailing = trailingMatch?.group(1) ?? '';
|
||||||
|
final core = raw.substring(0, raw.length - trailing.length);
|
||||||
|
if (core.isEmpty) return raw;
|
||||||
|
final words = core.split(RegExp(r'\s+'));
|
||||||
|
final formatted = words.map((w) {
|
||||||
|
if (w.length < 2) return w;
|
||||||
|
return _capitalizeComposedWord(w);
|
||||||
|
}).join(' ');
|
||||||
|
return formatted + trailing;
|
||||||
|
}
|
||||||
|
|
||||||
String _capitalizeComposedWord(String word) {
|
String _capitalizeComposedWord(String word) {
|
||||||
if (word.isEmpty) {
|
if (word.isEmpty) {
|
||||||
return word;
|
return word;
|
||||||
@ -30,3 +48,26 @@ String _capitalizeComposedWord(String word) {
|
|||||||
}
|
}
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formateur de saisie prénom / nom (capitalisation progressive).
|
||||||
|
class PersonNameInputFormatter extends TextInputFormatter {
|
||||||
|
const PersonNameInputFormatter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextEditingValue formatEditUpdate(
|
||||||
|
TextEditingValue oldValue,
|
||||||
|
TextEditingValue newValue,
|
||||||
|
) {
|
||||||
|
final formatted = formatPersonNameCaseTyping(newValue.text);
|
||||||
|
if (formatted == newValue.text) return newValue;
|
||||||
|
|
||||||
|
final sel = newValue.selection;
|
||||||
|
final offset = sel.isValid
|
||||||
|
? sel.baseOffset.clamp(0, formatted.length)
|
||||||
|
: formatted.length;
|
||||||
|
return TextEditingValue(
|
||||||
|
text: formatted,
|
||||||
|
selection: TextSelection.collapsed(offset: offset),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
||||||
import 'package:p_tits_pas/models/enfant_admin_model.dart';
|
import 'package:p_tits_pas/models/enfant_admin_model.dart';
|
||||||
@ -9,6 +8,7 @@ import 'package:p_tits_pas/services/auth_service.dart';
|
|||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/utils/date_display_utils.dart';
|
import 'package:p_tits_pas/utils/date_display_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
||||||
|
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_am_edit_modal.dart';
|
import 'package:p_tits_pas/widgets/admin/common/admin_am_edit_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart';
|
import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_parent_edit_modal.dart';
|
import 'package:p_tits_pas/widgets/admin/common/admin_parent_edit_modal.dart';
|
||||||
@ -50,7 +50,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
late final TextEditingController _birthCtrl;
|
late final TextEditingController _birthCtrl;
|
||||||
late final TextEditingController _dueCtrl;
|
late final TextEditingController _dueCtrl;
|
||||||
late String _status;
|
late String _status;
|
||||||
late String _gender;
|
late String? _gender;
|
||||||
late bool _consentPhoto;
|
late bool _consentPhoto;
|
||||||
late bool _isMultiple;
|
late bool _isMultiple;
|
||||||
bool _dirty = false;
|
bool _dirty = false;
|
||||||
@ -99,6 +99,22 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
|
|
||||||
bool get _busy => _saving || _deleting;
|
bool get _busy => _saving || _deleting;
|
||||||
|
|
||||||
|
/// Création : tous les champs sauf photo / consentement ; édition : au moins une modif.
|
||||||
|
bool get _canSubmit {
|
||||||
|
if (_busy) return false;
|
||||||
|
if (widget.isCreating) return _isCreateFormComplete;
|
||||||
|
return _dirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _isCreateFormComplete {
|
||||||
|
if (_selectedFamily == null) return false;
|
||||||
|
if (formatPersonNameCase(_prenomCtrl.text).isEmpty) return false;
|
||||||
|
if (formatPersonNameCase(_nomCtrl.text).isEmpty) return false;
|
||||||
|
if (_dateToIso(_activeDateCtrl.text) == null) return false;
|
||||||
|
if (_gender == null || !_availableGenders.contains(_gender)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -106,34 +122,61 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
_prenomCtrl = TextEditingController(text: e?.firstName ?? '');
|
_prenomCtrl = TextEditingController(text: e?.firstName ?? '');
|
||||||
_nomCtrl = TextEditingController(text: e?.lastName ?? '');
|
_nomCtrl = TextEditingController(text: e?.lastName ?? '');
|
||||||
_birthCtrl = TextEditingController(
|
_birthCtrl = TextEditingController(
|
||||||
text: formatIsoDateFr(e?.birthDate, ifEmpty: ''),
|
text: formatIsoDateFrInput(e?.birthDate, ifEmpty: ''),
|
||||||
);
|
);
|
||||||
_dueCtrl = TextEditingController(
|
_dueCtrl = TextEditingController(
|
||||||
text: formatIsoDateFr(e?.dueDate, ifEmpty: ''),
|
text: formatIsoDateFrInput(e?.dueDate, ifEmpty: ''),
|
||||||
);
|
);
|
||||||
_status = normalizeEnfantStatus(e?.status);
|
_status = normalizeEnfantStatus(e?.status);
|
||||||
if (!enfantStatusValues.contains(_status)) {
|
if (!enfantStatusValues.contains(_status)) {
|
||||||
_status = 'sans_garde';
|
_status = 'sans_garde';
|
||||||
}
|
}
|
||||||
_gender = _normalizeGender(e?.gender, allowUnknown: _isUnborn);
|
if (widget.isCreating) {
|
||||||
|
_gender = null;
|
||||||
|
} else {
|
||||||
|
_gender = _normalizeGender(e?.gender, allowUnknown: _isUnborn);
|
||||||
|
}
|
||||||
_consentPhoto = e?.consentPhoto ?? false;
|
_consentPhoto = e?.consentPhoto ?? false;
|
||||||
_isMultiple = e?.isMultiple ?? false;
|
_isMultiple = e?.isMultiple ?? false;
|
||||||
for (final c in [_prenomCtrl, _nomCtrl, _birthCtrl, _dueCtrl]) {
|
for (final c in [_prenomCtrl, _nomCtrl]) {
|
||||||
c.addListener(_markDirty);
|
c.addListener(_onNameChanged);
|
||||||
|
}
|
||||||
|
for (final c in [_birthCtrl, _dueCtrl]) {
|
||||||
|
c.addListener(_onDateChanged);
|
||||||
}
|
}
|
||||||
_prenomCtrl.addListener(_onNameChanged);
|
|
||||||
_nomCtrl.addListener(_onNameChanged);
|
|
||||||
_loadCurrentUserRole();
|
_loadCurrentUserRole();
|
||||||
if (widget.isCreating) {
|
if (widget.isCreating) {
|
||||||
_loadingAm = false;
|
_loadingAm = false;
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
} else {
|
} else {
|
||||||
|
_prenomCtrl.addListener(_markDirty);
|
||||||
|
_nomCtrl.addListener(_markDirty);
|
||||||
_loadLinkedAm();
|
_loadLinkedAm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onNameChanged() => setState(() {});
|
void _onNameChanged() => setState(() {});
|
||||||
|
|
||||||
|
void _onDateChanged() {
|
||||||
|
setState(() => _dirty = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Date prévisionnelle strictement avant aujourd’hui (statut à naître).
|
||||||
|
bool get _dueDateInPast {
|
||||||
|
if (!_isUnborn) return false;
|
||||||
|
final iso = _dateToIso(_dueCtrl.text);
|
||||||
|
if (iso == null) return false;
|
||||||
|
try {
|
||||||
|
final due = DateTime.parse(iso);
|
||||||
|
final now = DateTime.now();
|
||||||
|
final dueDay = DateTime(due.year, due.month, due.day);
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
return dueDay.isBefore(today);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadLinkedAm() async {
|
Future<void> _loadLinkedAm() async {
|
||||||
if (widget.isCreating) {
|
if (widget.isCreating) {
|
||||||
setState(() => _loadingAm = false);
|
setState(() => _loadingAm = false);
|
||||||
@ -191,8 +234,8 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _coerceGenderForStatus() {
|
void _coerceGenderForStatus() {
|
||||||
if (!_availableGenders.contains(_gender)) {
|
if (_gender != null && !_availableGenders.contains(_gender)) {
|
||||||
_gender = 'H';
|
_gender = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,8 +364,8 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
await UserService.updateEnfant(
|
await UserService.updateEnfant(
|
||||||
enfantId: enfantId,
|
enfantId: enfantId,
|
||||||
body: {
|
body: {
|
||||||
'first_name': _prenomCtrl.text.trim(),
|
'first_name': formatPersonNameCase(_prenomCtrl.text),
|
||||||
'last_name': _nomCtrl.text.trim(),
|
'last_name': formatPersonNameCase(_nomCtrl.text),
|
||||||
'status': _status,
|
'status': _status,
|
||||||
'gender': _gender,
|
'gender': _gender,
|
||||||
if (_isUnborn)
|
if (_isUnborn)
|
||||||
@ -363,8 +406,8 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final prenom = _prenomCtrl.text.trim();
|
final prenom = formatPersonNameCase(_prenomCtrl.text);
|
||||||
final nom = _nomCtrl.text.trim();
|
final nom = formatPersonNameCase(_nomCtrl.text);
|
||||||
if (prenom.isEmpty || nom.isEmpty) {
|
if (prenom.isEmpty || nom.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Prénom et nom sont obligatoires')),
|
const SnackBar(content: Text('Prénom et nom sont obligatoires')),
|
||||||
@ -622,70 +665,145 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
InputDecoration _inputDecoration({String? hint}) {
|
/// Conserve la date saisie en basculant naissance ↔ prévisionnelle.
|
||||||
return ValidationFieldDecoration.input(hint: hint).copyWith(
|
void _preserveDateOnStatusChange(String from, String to) {
|
||||||
|
final becomingUnborn = to == 'a_naitre' && from != 'a_naitre';
|
||||||
|
final leavingUnborn = from == 'a_naitre' && to != 'a_naitre';
|
||||||
|
if (becomingUnborn) {
|
||||||
|
if (_dueCtrl.text.trim().isEmpty &&
|
||||||
|
_birthCtrl.text.trim().isNotEmpty) {
|
||||||
|
_dueCtrl.text = _birthCtrl.text;
|
||||||
|
}
|
||||||
|
} else if (leavingUnborn) {
|
||||||
|
if (_birthCtrl.text.trim().isEmpty &&
|
||||||
|
_dueCtrl.text.trim().isNotEmpty) {
|
||||||
|
_birthCtrl.text = _dueCtrl.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InputDecoration _inputDecoration({String? hint, bool error = false}) {
|
||||||
|
final base = ValidationFieldDecoration.input(hint: hint).copyWith(
|
||||||
isDense: false,
|
isDense: false,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||||
);
|
);
|
||||||
|
if (!error) return base;
|
||||||
|
final border = OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: BorderSide(color: Colors.red.shade400),
|
||||||
|
);
|
||||||
|
return base.copyWith(
|
||||||
|
fillColor: Colors.red.shade50,
|
||||||
|
enabledBorder: border,
|
||||||
|
focusedBorder: border,
|
||||||
|
border: border,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _dropdownField({
|
Widget _dropdownField({
|
||||||
Key? key,
|
Key? key,
|
||||||
required String value,
|
required String? value,
|
||||||
required List<MapEntry<String, String>> items,
|
required List<MapEntry<String, String>> items,
|
||||||
required ValueChanged<String> onChanged,
|
required ValueChanged<String> onChanged,
|
||||||
|
String? hint,
|
||||||
}) {
|
}) {
|
||||||
final safeValue =
|
final safeValue =
|
||||||
items.any((e) => e.key == value) ? value : items.first.key;
|
value != null && items.any((e) => e.key == value) ? value : null;
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: _fieldHeight,
|
height: _fieldHeight,
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
key: key,
|
key: key,
|
||||||
value: safeValue,
|
value: safeValue,
|
||||||
|
hint: hint != null
|
||||||
|
? Text(hint, style: TextStyle(color: Colors.grey.shade600))
|
||||||
|
: null,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
decoration: _inputDecoration(),
|
decoration: _inputDecoration(),
|
||||||
items: items
|
items: items
|
||||||
.map((e) => DropdownMenuItem(value: e.key, child: Text(e.value)))
|
.map((e) => DropdownMenuItem(value: e.key, child: Text(e.value)))
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: _busy ? null : (v) {
|
onChanged: _busy
|
||||||
if (v != null) onChanged(v);
|
? null
|
||||||
},
|
: (v) {
|
||||||
|
if (v != null) onChanged(v);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _textField(TextEditingController controller, {String? hint, Key? key}) {
|
Widget _textField(
|
||||||
|
TextEditingController controller, {
|
||||||
|
String? hint,
|
||||||
|
Key? key,
|
||||||
|
TextInputType? keyboardType,
|
||||||
|
List<TextInputFormatter>? inputFormatters,
|
||||||
|
bool error = false,
|
||||||
|
Widget? suffixIcon,
|
||||||
|
}) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: _fieldHeight,
|
height: _fieldHeight,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
key: key,
|
key: key,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
inputFormatters: inputFormatters,
|
||||||
textAlignVertical: TextAlignVertical.center,
|
textAlignVertical: TextAlignVertical.center,
|
||||||
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
style: TextStyle(
|
||||||
decoration: _inputDecoration(hint: hint),
|
color: error ? Colors.red.shade800 : Colors.black87,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
decoration: _inputDecoration(
|
||||||
|
hint: hint,
|
||||||
|
error: error,
|
||||||
|
).copyWith(suffixIcon: suffixIcon),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _dateField() {
|
||||||
|
final past = _dueDateInPast;
|
||||||
|
return _textField(
|
||||||
|
_activeDateCtrl,
|
||||||
|
hint: 'jj / mm / aaaa',
|
||||||
|
key: ValueKey(_status),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: const [FrenchDateInputFormatter()],
|
||||||
|
error: past,
|
||||||
|
suffixIcon: past
|
||||||
|
? Tooltip(
|
||||||
|
message:
|
||||||
|
'Date prévisionnelle antérieure à aujourd’hui',
|
||||||
|
child: Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.red.shade700,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _fieldsGrid() {
|
Widget _fieldsGrid() {
|
||||||
return ValidationEditableSection(
|
return ValidationEditableSection(
|
||||||
rowLayout: _fieldsRowLayout,
|
rowLayout: _fieldsRowLayout,
|
||||||
fields: [
|
fields: [
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Prénom',
|
label: 'Prénom',
|
||||||
field: _textField(_prenomCtrl),
|
field: _textField(
|
||||||
|
_prenomCtrl,
|
||||||
|
inputFormatters: const [PersonNameInputFormatter()],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Nom',
|
label: 'Nom',
|
||||||
field: _textField(_nomCtrl),
|
field: _textField(
|
||||||
|
_nomCtrl,
|
||||||
|
inputFormatters: const [PersonNameInputFormatter()],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: _dateFieldLabel,
|
label: _dateFieldLabel,
|
||||||
field: _textField(
|
field: _dateField(),
|
||||||
_activeDateCtrl,
|
|
||||||
hint: 'jj/mm/aaaa',
|
|
||||||
key: ValueKey(_status),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Statut',
|
label: 'Statut',
|
||||||
@ -701,6 +819,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_preserveDateOnStatusChange(_status, v);
|
||||||
_status = v;
|
_status = v;
|
||||||
_coerceGenderForStatus();
|
_coerceGenderForStatus();
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
@ -716,6 +835,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
field: _dropdownField(
|
field: _dropdownField(
|
||||||
key: ValueKey('genre-$_status'),
|
key: ValueKey('genre-$_status'),
|
||||||
value: _gender,
|
value: _gender,
|
||||||
|
hint: 'Veuillez sélectionner le genre',
|
||||||
items: _availableGenders
|
items: _availableGenders
|
||||||
.map((g) => MapEntry(g, _genderLabel(g)))
|
.map((g) => MapEntry(g, _genderLabel(g)))
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -1090,7 +1210,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String get _placementTitle {
|
String get _placementTitle {
|
||||||
if (widget.isCreating) return 'Famille / dossier';
|
if (widget.isCreating) return 'Sélection du dossier de la famille';
|
||||||
return _isScolarise ? 'Scolarisation' : 'Assistante maternelle';
|
return _isScolarise ? 'Scolarisation' : 'Assistante maternelle';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1314,7 +1434,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ValidationModalTheme.primaryElevatedStyle,
|
style: ValidationModalTheme.primaryElevatedStyle,
|
||||||
onPressed: !_dirty || _busy ? null : _save,
|
onPressed: !_canSubmit || _busy ? null : _save,
|
||||||
child: _saving
|
child: _saving
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 18,
|
width: 18,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user