Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86ceb558a4 | ||
|
|
cc3b52db16 | ||
|
|
3c7f4f6e16 | ||
|
|
dcd407a3da | ||
|
|
fde63f8e72 | ||
|
|
1f8f1b9507 |
@@ -1,4 +1,3 @@
|
|||||||
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';
|
||||||
|
|
||||||
@@ -12,50 +11,10 @@ String formatIsoDateFr(String? s, {String ifEmpty = '–'}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Affiche une date ISO en saisie guidée `jj / mm / aaaa`.
|
/// Convertit `dd/MM/yyyy` (ou ISO) en `yyyy-MM-dd` pour l'API.
|
||||||
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)
|
||||||
@@ -137,43 +96,3 @@ 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,5 +1,3 @@
|
|||||||
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) {
|
||||||
@@ -11,22 +9,6 @@ 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;
|
||||||
@@ -48,26 +30,3 @@ 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,5 +1,6 @@
|
|||||||
|
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';
|
||||||
@@ -8,7 +9,6 @@ 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,22 +99,6 @@ 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();
|
||||||
@@ -122,61 +106,34 @@ 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: formatIsoDateFrInput(e?.birthDate, ifEmpty: ''),
|
text: formatIsoDateFr(e?.birthDate, ifEmpty: ''),
|
||||||
);
|
);
|
||||||
_dueCtrl = TextEditingController(
|
_dueCtrl = TextEditingController(
|
||||||
text: formatIsoDateFrInput(e?.dueDate, ifEmpty: ''),
|
text: formatIsoDateFr(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';
|
||||||
}
|
}
|
||||||
if (widget.isCreating) {
|
_gender = _normalizeGender(e?.gender, allowUnknown: _isUnborn);
|
||||||
_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]) {
|
for (final c in [_prenomCtrl, _nomCtrl, _birthCtrl, _dueCtrl]) {
|
||||||
c.addListener(_onNameChanged);
|
c.addListener(_markDirty);
|
||||||
}
|
|
||||||
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);
|
||||||
@@ -234,8 +191,8 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _coerceGenderForStatus() {
|
void _coerceGenderForStatus() {
|
||||||
if (_gender != null && !_availableGenders.contains(_gender)) {
|
if (!_availableGenders.contains(_gender)) {
|
||||||
_gender = null;
|
_gender = 'H';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,8 +321,8 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
await UserService.updateEnfant(
|
await UserService.updateEnfant(
|
||||||
enfantId: enfantId,
|
enfantId: enfantId,
|
||||||
body: {
|
body: {
|
||||||
'first_name': formatPersonNameCase(_prenomCtrl.text),
|
'first_name': _prenomCtrl.text.trim(),
|
||||||
'last_name': formatPersonNameCase(_nomCtrl.text),
|
'last_name': _nomCtrl.text.trim(),
|
||||||
'status': _status,
|
'status': _status,
|
||||||
'gender': _gender,
|
'gender': _gender,
|
||||||
if (_isUnborn)
|
if (_isUnborn)
|
||||||
@@ -406,8 +363,8 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final prenom = formatPersonNameCase(_prenomCtrl.text);
|
final prenom = _prenomCtrl.text.trim();
|
||||||
final nom = formatPersonNameCase(_nomCtrl.text);
|
final nom = _nomCtrl.text.trim();
|
||||||
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')),
|
||||||
@@ -443,13 +400,11 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
'last_name': nom,
|
'last_name': nom,
|
||||||
'status': _status,
|
'status': _status,
|
||||||
'gender': _gender,
|
'gender': _gender,
|
||||||
if (_isUnborn) ...{
|
if (_isUnborn)
|
||||||
if (_dateToIso(_dueCtrl.text) != null)
|
if (_dateToIso(_dueCtrl.text) != null)
|
||||||
'due_date': _dateToIso(_dueCtrl.text),
|
'due_date': _dateToIso(_dueCtrl.text)
|
||||||
} else ...{
|
else if (_dateToIso(_birthCtrl.text) != null)
|
||||||
if (_dateToIso(_birthCtrl.text) != null)
|
|
||||||
'birth_date': _dateToIso(_birthCtrl.text),
|
'birth_date': _dateToIso(_birthCtrl.text),
|
||||||
},
|
|
||||||
'consent_photo': _consentPhoto,
|
'consent_photo': _consentPhoto,
|
||||||
'is_multiple': _isMultiple,
|
'is_multiple': _isMultiple,
|
||||||
},
|
},
|
||||||
@@ -665,145 +620,70 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Conserve la date saisie en basculant naissance ↔ prévisionnelle.
|
InputDecoration _inputDecoration({String? hint}) {
|
||||||
void _preserveDateOnStatusChange(String from, String to) {
|
return ValidationFieldDecoration.input(hint: hint).copyWith(
|
||||||
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 =
|
||||||
value != null && items.any((e) => e.key == value) ? value : null;
|
items.any((e) => e.key == value) ? value : items.first.key;
|
||||||
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
|
onChanged: _busy ? null : (v) {
|
||||||
? null
|
if (v != null) onChanged(v);
|
||||||
: (v) {
|
},
|
||||||
if (v != null) onChanged(v);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _textField(
|
Widget _textField(TextEditingController controller, {String? hint, Key? key}) {
|
||||||
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: TextStyle(
|
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
||||||
color: error ? Colors.red.shade800 : Colors.black87,
|
decoration: _inputDecoration(hint: hint),
|
||||||
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(
|
field: _textField(_prenomCtrl),
|
||||||
_prenomCtrl,
|
|
||||||
inputFormatters: const [PersonNameInputFormatter()],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Nom',
|
label: 'Nom',
|
||||||
field: _textField(
|
field: _textField(_nomCtrl),
|
||||||
_nomCtrl,
|
|
||||||
inputFormatters: const [PersonNameInputFormatter()],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: _dateFieldLabel,
|
label: _dateFieldLabel,
|
||||||
field: _dateField(),
|
field: _textField(
|
||||||
|
_activeDateCtrl,
|
||||||
|
hint: 'jj/mm/aaaa',
|
||||||
|
key: ValueKey(_status),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Statut',
|
label: 'Statut',
|
||||||
@@ -819,7 +699,6 @@ 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;
|
||||||
@@ -835,7 +714,6 @@ 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(),
|
||||||
@@ -1210,7 +1088,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String get _placementTitle {
|
String get _placementTitle {
|
||||||
if (widget.isCreating) return 'Sélection du dossier de la famille';
|
if (widget.isCreating) return 'Famille / dossier';
|
||||||
return _isScolarise ? 'Scolarisation' : 'Assistante maternelle';
|
return _isScolarise ? 'Scolarisation' : 'Assistante maternelle';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1434,7 +1312,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ValidationModalTheme.primaryElevatedStyle,
|
style: ValidationModalTheme.primaryElevatedStyle,
|
||||||
onPressed: !_canSubmit || _busy ? null : _save,
|
onPressed: !_dirty || _busy ? null : _save,
|
||||||
child: _saving
|
child: _saving
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 18,
|
width: 18,
|
||||||
|
|||||||
Reference in New Issue
Block a user