feat(frontend): inscription — UI cartes enfant, formatage noms, focus Tab
- Formulaire infos perso : ordre de tabulation explicite, Checkbox Material pour le consentement photo, formatage nom/prénom/ville à la perte de focus et à la soumission (name_format_utils). - Carte enfant : cadre photo, croix, ombres, consentement ; formatage prénom/nom enfant ; normalisation avant passage étape 4. - Asset photo_frame.png ; HoverReliefWidget clipBehavior. - Ajustements payload / modèle / étapes inscription liés au parcours. Made-with: Cursor
This commit is contained in:
parent
a723412043
commit
110240b682
BIN
frontend/assets/images/photo_frame.png
Normal file
BIN
frontend/assets/images/photo_frame.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
@ -28,6 +28,8 @@ class ParentData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ChildData {
|
class ChildData {
|
||||||
|
static const Object _unsetImage = Object();
|
||||||
|
|
||||||
String firstName;
|
String firstName;
|
||||||
String lastName;
|
String lastName;
|
||||||
String dob; // Date de naissance ou prévisionnelle
|
String dob; // Date de naissance ou prévisionnelle
|
||||||
@ -59,7 +61,7 @@ class ChildData {
|
|||||||
bool? photoConsent,
|
bool? photoConsent,
|
||||||
bool? multipleBirth,
|
bool? multipleBirth,
|
||||||
bool? isUnbornChild,
|
bool? isUnbornChild,
|
||||||
File? imageFile,
|
Object? imageFile = _unsetImage,
|
||||||
CardColorVertical? cardColor,
|
CardColorVertical? cardColor,
|
||||||
}) {
|
}) {
|
||||||
return ChildData(
|
return ChildData(
|
||||||
@ -70,7 +72,7 @@ class ChildData {
|
|||||||
photoConsent: photoConsent ?? this.photoConsent,
|
photoConsent: photoConsent ?? this.photoConsent,
|
||||||
multipleBirth: multipleBirth ?? this.multipleBirth,
|
multipleBirth: multipleBirth ?? this.multipleBirth,
|
||||||
isUnbornChild: isUnbornChild ?? this.isUnbornChild,
|
isUnbornChild: isUnbornChild ?? this.isUnbornChild,
|
||||||
imageFile: imageFile ?? this.imageFile,
|
imageFile: identical(imageFile, _unsetImage) ? this.imageFile : imageFile as File?,
|
||||||
cardColor: cardColor ?? this.cardColor,
|
cardColor: cardColor ?? this.cardColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import '../../models/card_assets.dart';
|
|||||||
import '../../config/display_config.dart';
|
import '../../config/display_config.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||||||
|
|
||||||
class ParentRegisterStep3Screen extends StatefulWidget {
|
class ParentRegisterStep3Screen extends StatefulWidget {
|
||||||
// final UserRegistrationData registrationData; // Supprimé
|
// final UserRegistrationData registrationData; // Supprimé
|
||||||
@ -74,6 +75,22 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Même logique que nom / prénom parent : normalisation avant passage à l’étape suivante.
|
||||||
|
void _normalizeChildrenNamesAndGoToStep4(
|
||||||
|
BuildContext context,
|
||||||
|
UserRegistrationData registrationData,
|
||||||
|
) {
|
||||||
|
for (var i = 0; i < registrationData.children.length; i++) {
|
||||||
|
final c = registrationData.children[i];
|
||||||
|
final fn = formatPersonNameCase(c.firstName);
|
||||||
|
final ln = formatPersonNameCase(c.lastName);
|
||||||
|
if (fn != c.firstName || ln != c.lastName) {
|
||||||
|
registrationData.updateChild(i, c.copyWith(firstName: fn, lastName: ln));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.go('/parent-register-step4');
|
||||||
|
}
|
||||||
|
|
||||||
void _scrollListener() {
|
void _scrollListener() {
|
||||||
if (!_scrollController.hasClients) return;
|
if (!_scrollController.hasClients) return;
|
||||||
final position = _scrollController.position;
|
final position = _scrollController.position;
|
||||||
@ -272,6 +289,10 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
childData: registrationData.children[index],
|
childData: registrationData.children[index],
|
||||||
childIndex: index,
|
childIndex: index,
|
||||||
onPickImage: () => _pickImage(index, registrationData),
|
onPickImage: () => _pickImage(index, registrationData),
|
||||||
|
onClearImage: () => setState(() {
|
||||||
|
final c = registrationData.children[index];
|
||||||
|
registrationData.updateChild(index, c.copyWith(imageFile: null));
|
||||||
|
}),
|
||||||
onDateSelect: () => _selectDate(context, index, registrationData),
|
onDateSelect: () => _selectDate(context, index, registrationData),
|
||||||
onFirstNameChanged: (value) => setState(() {
|
onFirstNameChanged: (value) => setState(() {
|
||||||
final c = registrationData.children[index];
|
final c = registrationData.children[index];
|
||||||
@ -281,17 +302,24 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
final c = registrationData.children[index];
|
final c = registrationData.children[index];
|
||||||
registrationData.updateChild(index, c.copyWith(lastName: value));
|
registrationData.updateChild(index, c.copyWith(lastName: value));
|
||||||
}),
|
}),
|
||||||
|
onGenreChanged: (value) => setState(() {
|
||||||
|
final c = registrationData.children[index];
|
||||||
|
registrationData.updateChild(index, c.copyWith(genre: value));
|
||||||
|
}),
|
||||||
onTogglePhotoConsent: (newValue) {
|
onTogglePhotoConsent: (newValue) {
|
||||||
final oldChild = registrationData.children[index];
|
final oldChild = registrationData.children[index];
|
||||||
registrationData.updateChild(index, oldChild.copyWith(photoConsent: newValue));
|
registrationData.updateChild(index, oldChild.copyWith(photoConsent: newValue));
|
||||||
},
|
},
|
||||||
onToggleMultipleBirth: (newValue) {
|
|
||||||
final oldChild = registrationData.children[index];
|
|
||||||
registrationData.updateChild(index, oldChild.copyWith(multipleBirth: newValue));
|
|
||||||
},
|
|
||||||
onToggleIsUnborn: (newValue) {
|
onToggleIsUnborn: (newValue) {
|
||||||
final oldChild = registrationData.children[index];
|
final oldChild = registrationData.children[index];
|
||||||
registrationData.updateChild(index, oldChild.copyWith(isUnbornChild: newValue));
|
var g = oldChild.genre;
|
||||||
|
if (!newValue && g == 'Autre') {
|
||||||
|
g = '';
|
||||||
|
}
|
||||||
|
registrationData.updateChild(
|
||||||
|
index,
|
||||||
|
oldChild.copyWith(isUnbornChild: newValue, genre: g),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onRemove: () => _removeChild(index, registrationData),
|
onRemove: () => _removeChild(index, registrationData),
|
||||||
canBeRemoved: registrationData.children.length > 1,
|
canBeRemoved: registrationData.children.length > 1,
|
||||||
@ -314,7 +342,7 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
|
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
// Boutons navigation en bas du scroll
|
// Boutons navigation en bas du scroll
|
||||||
_buildMobileButtons(context, config, screenSize),
|
_buildMobileButtons(context, config, screenSize, registrationData),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -368,6 +396,10 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
childData: registrationData.children[index],
|
childData: registrationData.children[index],
|
||||||
childIndex: index,
|
childIndex: index,
|
||||||
onPickImage: () => _pickImage(index, registrationData),
|
onPickImage: () => _pickImage(index, registrationData),
|
||||||
|
onClearImage: () => setState(() {
|
||||||
|
final c = registrationData.children[index];
|
||||||
|
registrationData.updateChild(index, c.copyWith(imageFile: null));
|
||||||
|
}),
|
||||||
onDateSelect: () => _selectDate(context, index, registrationData),
|
onDateSelect: () => _selectDate(context, index, registrationData),
|
||||||
onFirstNameChanged: (value) => setState(() {
|
onFirstNameChanged: (value) => setState(() {
|
||||||
final c = registrationData.children[index];
|
final c = registrationData.children[index];
|
||||||
@ -377,17 +409,24 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
final c = registrationData.children[index];
|
final c = registrationData.children[index];
|
||||||
registrationData.updateChild(index, c.copyWith(lastName: value));
|
registrationData.updateChild(index, c.copyWith(lastName: value));
|
||||||
}),
|
}),
|
||||||
|
onGenreChanged: (value) => setState(() {
|
||||||
|
final c = registrationData.children[index];
|
||||||
|
registrationData.updateChild(index, c.copyWith(genre: value));
|
||||||
|
}),
|
||||||
onTogglePhotoConsent: (newValue) {
|
onTogglePhotoConsent: (newValue) {
|
||||||
final oldChild = registrationData.children[index];
|
final oldChild = registrationData.children[index];
|
||||||
registrationData.updateChild(index, oldChild.copyWith(photoConsent: newValue));
|
registrationData.updateChild(index, oldChild.copyWith(photoConsent: newValue));
|
||||||
},
|
},
|
||||||
onToggleMultipleBirth: (newValue) {
|
|
||||||
final oldChild = registrationData.children[index];
|
|
||||||
registrationData.updateChild(index, oldChild.copyWith(multipleBirth: newValue));
|
|
||||||
},
|
|
||||||
onToggleIsUnborn: (newValue) {
|
onToggleIsUnborn: (newValue) {
|
||||||
final oldChild = registrationData.children[index];
|
final oldChild = registrationData.children[index];
|
||||||
registrationData.updateChild(index, oldChild.copyWith(isUnbornChild: newValue));
|
var g = oldChild.genre;
|
||||||
|
if (!newValue && g == 'Autre') {
|
||||||
|
g = '';
|
||||||
|
}
|
||||||
|
registrationData.updateChild(
|
||||||
|
index,
|
||||||
|
oldChild.copyWith(isUnbornChild: newValue, genre: g),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onRemove: () => _removeChild(index, registrationData),
|
onRemove: () => _removeChild(index, registrationData),
|
||||||
canBeRemoved: registrationData.children.length > 1,
|
canBeRemoved: registrationData.children.length > 1,
|
||||||
@ -416,7 +455,12 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Boutons navigation mobile
|
/// Boutons navigation mobile
|
||||||
Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) {
|
Widget _buildMobileButtons(
|
||||||
|
BuildContext context,
|
||||||
|
DisplayConfig config,
|
||||||
|
Size screenSize,
|
||||||
|
UserRegistrationData registrationData,
|
||||||
|
) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -444,7 +488,7 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
text: 'Suivant',
|
text: 'Suivant',
|
||||||
style: NavigationButtonStyle.green,
|
style: NavigationButtonStyle.green,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.go('/parent-register-step4');
|
_normalizeChildrenNamesAndGoToStep4(context, registrationData);
|
||||||
},
|
},
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 50,
|
height: 50,
|
||||||
|
|||||||
@ -248,11 +248,12 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
|
|||||||
childIndex: index,
|
childIndex: index,
|
||||||
mode: DisplayMode.readonly,
|
mode: DisplayMode.readonly,
|
||||||
onPickImage: () {},
|
onPickImage: () {},
|
||||||
|
onClearImage: () {},
|
||||||
onDateSelect: () {},
|
onDateSelect: () {},
|
||||||
onFirstNameChanged: (v) {},
|
onFirstNameChanged: (v) {},
|
||||||
onLastNameChanged: (v) {},
|
onLastNameChanged: (v) {},
|
||||||
|
onGenreChanged: (v) {},
|
||||||
onTogglePhotoConsent: (v) {},
|
onTogglePhotoConsent: (v) {},
|
||||||
onToggleMultipleBirth: (v) {},
|
|
||||||
onToggleIsUnborn: (v) {},
|
onToggleIsUnborn: (v) {},
|
||||||
onRemove: () {},
|
onRemove: () {},
|
||||||
canBeRemoved: false,
|
canBeRemoved: false,
|
||||||
|
|||||||
32
frontend/lib/utils/name_format_utils.dart
Normal file
32
frontend/lib/utils/name_format_utils.dart
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/// Formatage affichage prénom / nom (capitalisation par mot et segments après `-` ou `'`).
|
||||||
|
|
||||||
|
String formatPersonNameCase(String raw) {
|
||||||
|
final trimmed = raw.trim();
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
final words = trimmed.split(RegExp(r'\s+'));
|
||||||
|
return words.map(_capitalizeComposedWord).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
String _capitalizeComposedWord(String word) {
|
||||||
|
if (word.isEmpty) {
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
final lower = word.toLowerCase();
|
||||||
|
const separators = <String>{'-', "'", '’'};
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
var capitalizeNext = true;
|
||||||
|
|
||||||
|
for (var i = 0; i < lower.length; i++) {
|
||||||
|
final char = lower[i];
|
||||||
|
if (capitalizeNext && RegExp(r'[a-zà-öø-ÿ]').hasMatch(char)) {
|
||||||
|
buffer.write(char.toUpperCase());
|
||||||
|
capitalizeNext = false;
|
||||||
|
} else {
|
||||||
|
buffer.write(char);
|
||||||
|
capitalizeNext = separators.contains(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
@ -59,12 +59,17 @@ class ParentRegistrationPayload {
|
|||||||
for (var i = 0; i < d.children.length; i++) {
|
for (var i = 0; i < d.children.length; i++) {
|
||||||
final c = d.children[i];
|
final c = d.children[i];
|
||||||
final label = 'Enfant ${i + 1}';
|
final label = 'Enfant ${i + 1}';
|
||||||
// Pas de saisie genre sur la carte (widget revenu à b18d5c8) : valeur par défaut côté JSON.
|
if (!c.photoConsent) {
|
||||||
|
return '$label : le consentement photo est obligatoire.';
|
||||||
|
}
|
||||||
if (c.isUnbornChild) {
|
if (c.isUnbornChild) {
|
||||||
final due = _ddMmYyyyToIso(c.dob);
|
final due = _ddMmYyyyToIso(c.dob);
|
||||||
if (due == null) {
|
if (due == null) {
|
||||||
return '$label : indiquez une date de naissance prévisionnelle.';
|
return '$label : indiquez une date de naissance prévisionnelle.';
|
||||||
}
|
}
|
||||||
|
if (!apiGenres.contains(c.genre)) {
|
||||||
|
return '$label : indiquez Fille, Garçon ou Inconnu.';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (c.firstName.trim().length < 2) {
|
if (c.firstName.trim().length < 2) {
|
||||||
return '$label : le prénom doit contenir au moins 2 caractères.';
|
return '$label : le prénom doit contenir au moins 2 caractères.';
|
||||||
@ -73,6 +78,9 @@ class ParentRegistrationPayload {
|
|||||||
if (birth == null) {
|
if (birth == null) {
|
||||||
return '$label : indiquez une date de naissance valide.';
|
return '$label : indiquez une date de naissance valide.';
|
||||||
}
|
}
|
||||||
|
if (c.genre != 'H' && c.genre != 'F') {
|
||||||
|
return '$label : indiquez Fille ou Garçon.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +142,7 @@ class ParentRegistrationPayload {
|
|||||||
static Map<String, dynamic> _childToJson(ChildData c, int index, String parentNom) {
|
static Map<String, dynamic> _childToJson(ChildData c, int index, String parentNom) {
|
||||||
final map = <String, dynamic>{
|
final map = <String, dynamic>{
|
||||||
'genre': apiGenres.contains(c.genre) ? c.genre : 'Autre',
|
'genre': apiGenres.contains(c.genre) ? c.genre : 'Autre',
|
||||||
'grossesse_multiple': c.multipleBirth,
|
'grossesse_multiple': false,
|
||||||
};
|
};
|
||||||
|
|
||||||
final prenom = c.firstName.trim();
|
final prenom = c.firstName.trim();
|
||||||
|
|||||||
@ -8,6 +8,8 @@ class AppCustomCheckbox extends StatelessWidget {
|
|||||||
final double checkboxSize;
|
final double checkboxSize;
|
||||||
final double checkmarkSizeFactor;
|
final double checkmarkSizeFactor;
|
||||||
final double fontSize;
|
final double fontSize;
|
||||||
|
/// Survol (desktop) ou appui long (mobile) pour afficher un texte d’aide.
|
||||||
|
final String? tooltip;
|
||||||
|
|
||||||
const AppCustomCheckbox({
|
const AppCustomCheckbox({
|
||||||
super.key,
|
super.key,
|
||||||
@ -17,13 +19,17 @@ class AppCustomCheckbox extends StatelessWidget {
|
|||||||
this.checkboxSize = 20.0,
|
this.checkboxSize = 20.0,
|
||||||
this.checkmarkSizeFactor = 1.4,
|
this.checkmarkSizeFactor = 1.4,
|
||||||
this.fontSize = 16.0,
|
this.fontSize = 16.0,
|
||||||
|
this.tooltip,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return Row(
|
||||||
onTap: () => onChanged(!value), // Inverse la valeur au clic
|
mainAxisSize: MainAxisSize.min,
|
||||||
behavior: HitTestBehavior.opaque, // Pour s'assurer que toute la zone du Row est cliquable
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => onChanged(!value),
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -49,16 +55,31 @@ class AppCustomCheckbox extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
// Utiliser Flexible pour que le texte ne cause pas d'overflow si trop long
|
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style: GoogleFonts.merienda(fontSize: fontSize),
|
style: GoogleFonts.merienda(fontSize: fontSize),
|
||||||
overflow: TextOverflow.ellipsis, // Gérer le texte long
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
if (tooltip != null && tooltip!.trim().isNotEmpty) ...[
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Tooltip(
|
||||||
|
message: tooltip!.trim(),
|
||||||
|
waitDuration: const Duration(milliseconds: 300),
|
||||||
|
triggerMode: TooltipTriggerMode.tap,
|
||||||
|
showDuration: const Duration(seconds: 6),
|
||||||
|
child: Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
size: fontSize * 1.2,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'dart:io' show File;
|
import 'dart:io' show File;
|
||||||
@ -6,9 +8,17 @@ import '../models/user_registration_data.dart';
|
|||||||
import '../models/card_assets.dart';
|
import '../models/card_assets.dart';
|
||||||
import 'custom_app_text_field.dart';
|
import 'custom_app_text_field.dart';
|
||||||
import 'form_field_wrapper.dart';
|
import 'form_field_wrapper.dart';
|
||||||
import 'app_custom_checkbox.dart';
|
|
||||||
import 'hover_relief_widget.dart';
|
import 'hover_relief_widget.dart';
|
||||||
import '../config/display_config.dart';
|
import '../config/display_config.dart';
|
||||||
|
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||||||
|
|
||||||
|
const String _photoConsentTooltip =
|
||||||
|
'Obligatoire : cochez cette case pour autoriser l’utilisation de la photo de l’enfant.\n'
|
||||||
|
'Suivi du dossier et organisation de l’accueil (affichage interne, outils pédagogiques).\n'
|
||||||
|
'Dans le respect de la politique de confidentialité.';
|
||||||
|
|
||||||
|
/// Cadre photo (centre transparent), même dossier que `photo.png`.
|
||||||
|
const String _photoSketchFrameAsset = 'assets/images/photo_frame.png';
|
||||||
|
|
||||||
/// Widget pour afficher et éditer une carte enfant
|
/// Widget pour afficher et éditer une carte enfant
|
||||||
/// Utilisé dans le workflow d'inscription des parents
|
/// Utilisé dans le workflow d'inscription des parents
|
||||||
@ -16,11 +26,14 @@ class ChildCardWidget extends StatefulWidget {
|
|||||||
final ChildData childData;
|
final ChildData childData;
|
||||||
final int childIndex;
|
final int childIndex;
|
||||||
final VoidCallback onPickImage;
|
final VoidCallback onPickImage;
|
||||||
|
/// Retire la photo sélectionnée (placeholder à la place).
|
||||||
|
final VoidCallback onClearImage;
|
||||||
final VoidCallback onDateSelect;
|
final VoidCallback onDateSelect;
|
||||||
final ValueChanged<String> onFirstNameChanged;
|
final ValueChanged<String> onFirstNameChanged;
|
||||||
final ValueChanged<String> onLastNameChanged;
|
final ValueChanged<String> onLastNameChanged;
|
||||||
|
/// `H`, `F` ou `Autre` (API). « Inconnu » à l’UI = `Autre`.
|
||||||
|
final ValueChanged<String> onGenreChanged;
|
||||||
final ValueChanged<bool> onTogglePhotoConsent;
|
final ValueChanged<bool> onTogglePhotoConsent;
|
||||||
final ValueChanged<bool> onToggleMultipleBirth;
|
|
||||||
final ValueChanged<bool> onToggleIsUnborn;
|
final ValueChanged<bool> onToggleIsUnborn;
|
||||||
final VoidCallback onRemove;
|
final VoidCallback onRemove;
|
||||||
final bool canBeRemoved;
|
final bool canBeRemoved;
|
||||||
@ -32,11 +45,12 @@ class ChildCardWidget extends StatefulWidget {
|
|||||||
required this.childData,
|
required this.childData,
|
||||||
required this.childIndex,
|
required this.childIndex,
|
||||||
required this.onPickImage,
|
required this.onPickImage,
|
||||||
|
required this.onClearImage,
|
||||||
required this.onDateSelect,
|
required this.onDateSelect,
|
||||||
required this.onFirstNameChanged,
|
required this.onFirstNameChanged,
|
||||||
required this.onLastNameChanged,
|
required this.onLastNameChanged,
|
||||||
|
required this.onGenreChanged,
|
||||||
required this.onTogglePhotoConsent,
|
required this.onTogglePhotoConsent,
|
||||||
required this.onToggleMultipleBirth,
|
|
||||||
required this.onToggleIsUnborn,
|
required this.onToggleIsUnborn,
|
||||||
required this.onRemove,
|
required this.onRemove,
|
||||||
required this.canBeRemoved,
|
required this.canBeRemoved,
|
||||||
@ -49,10 +63,24 @@ class ChildCardWidget extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ChildCardWidgetState extends State<ChildCardWidget> {
|
class _ChildCardWidgetState extends State<ChildCardWidget> {
|
||||||
|
/// Largeur desktop : proportionnelle au côté du carré photo (512×1024 sur les PNG → portrait ~1:2 ; même idée qu’avant #78 : 345×1,1 pour 200 px de côté).
|
||||||
|
static double _desktopEditableCardWidth({
|
||||||
|
required double photoSide,
|
||||||
|
required double maxWidth,
|
||||||
|
required double scaleFactor,
|
||||||
|
}) {
|
||||||
|
final fromPhoto = photoSide * (345.0 * 1.1 / 200.0);
|
||||||
|
final minForFields = 300.0 + 44.0 * scaleFactor;
|
||||||
|
return math.min(maxWidth, math.max(minForFields, fromPhoto));
|
||||||
|
}
|
||||||
|
|
||||||
late TextEditingController _firstNameController;
|
late TextEditingController _firstNameController;
|
||||||
late TextEditingController _lastNameController;
|
late TextEditingController _lastNameController;
|
||||||
late TextEditingController _dobController;
|
late TextEditingController _dobController;
|
||||||
|
|
||||||
|
FocusNode? _firstNameFocus;
|
||||||
|
FocusNode? _lastNameFocus;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -65,6 +93,38 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
_firstNameController.addListener(() => widget.onFirstNameChanged(_firstNameController.text));
|
_firstNameController.addListener(() => widget.onFirstNameChanged(_firstNameController.text));
|
||||||
_lastNameController.addListener(() => widget.onLastNameChanged(_lastNameController.text));
|
_lastNameController.addListener(() => widget.onLastNameChanged(_lastNameController.text));
|
||||||
// Pour dob, la mise à jour se fait via _selectDate, pas besoin de listener ici
|
// Pour dob, la mise à jour se fait via _selectDate, pas besoin de listener ici
|
||||||
|
|
||||||
|
if (widget.mode == DisplayMode.editable) {
|
||||||
|
_firstNameFocus = FocusNode();
|
||||||
|
_lastNameFocus = FocusNode();
|
||||||
|
_firstNameFocus!.addListener(_onFirstNameFocusChange);
|
||||||
|
_lastNameFocus!.addListener(_onLastNameFocusChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFirstNameFocusChange() {
|
||||||
|
if (_firstNameFocus == null || _firstNameFocus!.hasFocus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_applyPersonNameFormat(_firstNameController);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLastNameFocusChange() {
|
||||||
|
if (_lastNameFocus == null || _lastNameFocus!.hasFocus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_applyPersonNameFormat(_lastNameController);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyPersonNameFormat(TextEditingController controller) {
|
||||||
|
final formatted = formatPersonNameCase(controller.text);
|
||||||
|
if (formatted == controller.text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.value = TextEditingValue(
|
||||||
|
text: formatted,
|
||||||
|
selection: TextSelection.collapsed(offset: formatted.length),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -85,6 +145,10 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_firstNameFocus?.removeListener(_onFirstNameFocusChange);
|
||||||
|
_lastNameFocus?.removeListener(_onLastNameFocusChange);
|
||||||
|
_firstNameFocus?.dispose();
|
||||||
|
_lastNameFocus?.dispose();
|
||||||
_firstNameController.dispose();
|
_firstNameController.dispose();
|
||||||
_lastNameController.dispose();
|
_lastNameController.dispose();
|
||||||
_dobController.dispose();
|
_dobController.dispose();
|
||||||
@ -115,11 +179,22 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
|
final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
|
||||||
? Colors.purple.shade200
|
? Colors.purple.shade200
|
||||||
: (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200);
|
: (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200);
|
||||||
final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90);
|
final Color initialPhotoShadow =
|
||||||
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
|
Color.alphaBlend(Colors.black.withValues(alpha: 0.22), baseCardColorForShadow);
|
||||||
|
final Color hoverPhotoShadow =
|
||||||
|
Color.alphaBlend(Colors.black.withValues(alpha: 0.32), baseCardColorForShadow);
|
||||||
|
|
||||||
|
final double photoSide = 200.0 * (config.isMobile ? 0.8 : 1.0);
|
||||||
|
final double editableCardWidth = config.isMobile
|
||||||
|
? double.infinity
|
||||||
|
: _desktopEditableCardWidth(
|
||||||
|
photoSide: photoSide,
|
||||||
|
maxWidth: screenSize.width * 0.92,
|
||||||
|
scaleFactor: scaleFactor,
|
||||||
|
);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: config.isMobile ? double.infinity : screenSize.width * 0.6,
|
width: editableCardWidth,
|
||||||
// On retire la hauteur fixe pour laisser le contenu définir la taille, comme les autres cartes
|
// On retire la hauteur fixe pour laisser le contenu définir la taille, comme les autres cartes
|
||||||
// height: config.isMobile ? null : 600.0 * scaleFactor,
|
// height: config.isMobile ? null : 600.0 * scaleFactor,
|
||||||
padding: EdgeInsets.all(22.0 * scaleFactor),
|
padding: EdgeInsets.all(22.0 * scaleFactor),
|
||||||
@ -132,24 +207,20 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// ... (contenu existant)
|
_buildEditablePhotoArea(
|
||||||
HoverReliefWidget(
|
context: context,
|
||||||
onPressed: config.isReadonly ? null : widget.onPickImage,
|
config: config,
|
||||||
borderRadius: BorderRadius.circular(10),
|
scaleFactor: scaleFactor,
|
||||||
initialShadowColor: initialPhotoShadow,
|
photoSide: photoSide,
|
||||||
hoverShadowColor: hoverPhotoShadow,
|
currentChildImage: currentChildImage,
|
||||||
child: SizedBox(
|
initialPhotoShadow: initialPhotoShadow,
|
||||||
height: 200.0 * (config.isMobile ? 0.8 : 1.0),
|
hoverPhotoShadow: hoverPhotoShadow,
|
||||||
width: 200.0 * (config.isMobile ? 0.8 : 1.0),
|
|
||||||
child: Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(5.0 * scaleFactor),
|
|
||||||
child: currentChildImage != null
|
|
||||||
? ClipRRect(borderRadius: BorderRadius.circular(10 * scaleFactor), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover))
|
|
||||||
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 8.0 * scaleFactor),
|
||||||
|
_buildPhotoConsentRow(
|
||||||
|
context: context,
|
||||||
|
config: config,
|
||||||
|
labelFontSize: config.isMobile ? 13.0 : 16.0,
|
||||||
),
|
),
|
||||||
SizedBox(height: 10.0 * scaleFactor),
|
SizedBox(height: 10.0 * scaleFactor),
|
||||||
Row(
|
Row(
|
||||||
@ -173,13 +244,16 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 8.0 * scaleFactor),
|
SizedBox(height: 8.0 * scaleFactor),
|
||||||
|
_buildGenreSegment(context, config, scaleFactor),
|
||||||
|
SizedBox(height: 8.0 * scaleFactor),
|
||||||
_buildField(
|
_buildField(
|
||||||
config: config,
|
config: config,
|
||||||
scaleFactor: scaleFactor,
|
scaleFactor: scaleFactor,
|
||||||
label: 'Prénom',
|
label: 'Prénom',
|
||||||
controller: _firstNameController,
|
controller: _firstNameController,
|
||||||
hint: 'Facultatif si à naître',
|
hint: widget.childData.isUnbornChild ? 'Facultatif' : 'Prénom',
|
||||||
isRequired: !widget.childData.isUnbornChild,
|
isRequired: !widget.childData.isUnbornChild,
|
||||||
|
focusNode: _firstNameFocus,
|
||||||
),
|
),
|
||||||
SizedBox(height: 5.0 * scaleFactor),
|
SizedBox(height: 5.0 * scaleFactor),
|
||||||
_buildField(
|
_buildField(
|
||||||
@ -188,6 +262,7 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
label: 'Nom',
|
label: 'Nom',
|
||||||
controller: _lastNameController,
|
controller: _lastNameController,
|
||||||
hint: 'Nom de l\'enfant',
|
hint: 'Nom de l\'enfant',
|
||||||
|
focusNode: _lastNameFocus,
|
||||||
),
|
),
|
||||||
SizedBox(height: 8.0 * scaleFactor),
|
SizedBox(height: 8.0 * scaleFactor),
|
||||||
_buildField(
|
_buildField(
|
||||||
@ -200,27 +275,6 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
onTap: config.isReadonly ? null : widget.onDateSelect,
|
onTap: config.isReadonly ? null : widget.onDateSelect,
|
||||||
suffixIcon: Icons.calendar_today,
|
suffixIcon: Icons.calendar_today,
|
||||||
),
|
),
|
||||||
SizedBox(height: 10.0 * scaleFactor),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
AppCustomCheckbox(
|
|
||||||
label: 'Consentement photo',
|
|
||||||
value: widget.childData.photoConsent,
|
|
||||||
onChanged: config.isReadonly ? (v) {} : widget.onTogglePhotoConsent,
|
|
||||||
checkboxSize: config.isMobile ? 20.0 : 22.0 * scaleFactor,
|
|
||||||
fontSize: config.isMobile ? 13.0 : 16.0,
|
|
||||||
),
|
|
||||||
SizedBox(height: 5.0 * scaleFactor),
|
|
||||||
AppCustomCheckbox(
|
|
||||||
label: 'Naissance multiple',
|
|
||||||
value: widget.childData.multipleBirth,
|
|
||||||
onChanged: config.isReadonly ? (v) {} : widget.onToggleMultipleBirth,
|
|
||||||
checkboxSize: config.isMobile ? 20.0 : 22.0 * scaleFactor,
|
|
||||||
fontSize: config.isMobile ? 13.0 : 16.0,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (widget.canBeRemoved && !config.isReadonly)
|
if (widget.canBeRemoved && !config.isReadonly)
|
||||||
@ -230,7 +284,7 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
onTap: widget.onRemove,
|
onTap: widget.onRemove,
|
||||||
customBorder: const CircleBorder(),
|
customBorder: const CircleBorder(),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/images/red_cross2.png',
|
'assets/images/cross.png',
|
||||||
width: config.isMobile ? 30 : 36,
|
width: config.isMobile ? 30 : 36,
|
||||||
height: config.isMobile ? 30 : 36,
|
height: config.isMobile ? 30 : 36,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
@ -252,6 +306,104 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildEditablePhotoArea({
|
||||||
|
required BuildContext context,
|
||||||
|
required DisplayConfig config,
|
||||||
|
required double scaleFactor,
|
||||||
|
required double photoSide,
|
||||||
|
required File? currentChildImage,
|
||||||
|
required Color initialPhotoShadow,
|
||||||
|
required Color hoverPhotoShadow,
|
||||||
|
}) {
|
||||||
|
final canInteract = !config.isReadonly;
|
||||||
|
final outerRadius = BorderRadius.circular(10 * scaleFactor);
|
||||||
|
// Arrondi photo (sous le cadre) : proportionnel au carré, plus fort qu’avant (~10×scaleFactor).
|
||||||
|
final photoClipRadius = BorderRadius.circular(photoSide * 0.14);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
HoverReliefWidget(
|
||||||
|
onPressed: canInteract ? widget.onPickImage : null,
|
||||||
|
borderRadius: outerRadius,
|
||||||
|
initialElevation: 10,
|
||||||
|
hoverElevation: 16,
|
||||||
|
initialShadowColor: initialPhotoShadow,
|
||||||
|
hoverShadowColor: hoverPhotoShadow,
|
||||||
|
// Pas de clip Material sur la pile : l’arrondi de la photo est géré par ClipRRect (plus marqué).
|
||||||
|
clipBehavior:
|
||||||
|
currentChildImage != null ? Clip.none : Clip.antiAlias,
|
||||||
|
child: SizedBox(
|
||||||
|
width: photoSide,
|
||||||
|
height: photoSide,
|
||||||
|
child: currentChildImage == null
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: outerRadius,
|
||||||
|
child: Center(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/photo.png',
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
width: photoSide,
|
||||||
|
height: photoSide,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
// Pas de fond opaque : les coins hors ClipRRect laissent voir la carte (aquarelle).
|
||||||
|
Positioned.fill(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: photoClipRadius,
|
||||||
|
child: kIsWeb
|
||||||
|
? Image.network(
|
||||||
|
currentChildImage.path,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: Image.file(
|
||||||
|
currentChildImage,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: Image.asset(
|
||||||
|
_photoSketchFrameAsset,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
errorBuilder: (context, error, stackTrace) =>
|
||||||
|
const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (currentChildImage != null && canInteract)
|
||||||
|
Positioned(
|
||||||
|
top: 8 * scaleFactor,
|
||||||
|
right: 8 * scaleFactor,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: widget.onClearImage,
|
||||||
|
customBorder: const CircleBorder(),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/cross.png',
|
||||||
|
width: config.isMobile ? 26 : 30,
|
||||||
|
height: config.isMobile ? 26 : 30,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Layout SPÉCIAL Readonly Desktop (Ancien Design Horizontal)
|
/// Layout SPÉCIAL Readonly Desktop (Ancien Design Horizontal)
|
||||||
Widget _buildReadonlyDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
Widget _buildReadonlyDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
// Convertir la couleur verticale (pour mobile) en couleur horizontale (pour desktop/récap)
|
// Convertir la couleur verticale (pour mobile) en couleur horizontale (pour desktop/récap)
|
||||||
@ -310,11 +462,14 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// PHOTO (1/3)
|
// PHOTO (1/3) + consentement sous la photo
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 1,
|
flex: 1,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: AspectRatio(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -337,6 +492,14 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildPhotoConsentRow(
|
||||||
|
context: context,
|
||||||
|
config: config,
|
||||||
|
labelFontSize: 16.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 32),
|
const SizedBox(width: 32),
|
||||||
@ -356,35 +519,14 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :',
|
widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :',
|
||||||
_dobController.text
|
_dobController.text
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildReadonlyField('Genre :', _genreDisplayLabel(widget.childData.genre)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
|
||||||
|
|
||||||
// Consentements
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
AppCustomCheckbox(
|
|
||||||
label: 'Consentement photo',
|
|
||||||
value: widget.childData.photoConsent,
|
|
||||||
onChanged: (v) {}, // Readonly
|
|
||||||
checkboxSize: 22.0,
|
|
||||||
fontSize: 16.0,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 32),
|
|
||||||
AppCustomCheckbox(
|
|
||||||
label: 'Naissance multiple',
|
|
||||||
value: widget.childData.multipleBirth,
|
|
||||||
onChanged: (v) {}, // Readonly
|
|
||||||
checkboxSize: 22.0,
|
|
||||||
fontSize: 16.0,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -460,6 +602,12 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildPhotoConsentRow(
|
||||||
|
context: context,
|
||||||
|
config: config,
|
||||||
|
labelFontSize: 14.0,
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Champs
|
// Champs
|
||||||
@ -471,37 +619,8 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :',
|
widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :',
|
||||||
_dobController.text
|
_dobController.text
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Consentements
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
AppCustomCheckbox(
|
|
||||||
label: 'Consentement photo',
|
|
||||||
value: widget.childData.photoConsent,
|
|
||||||
onChanged: (v) {},
|
|
||||||
checkboxSize: 20.0,
|
|
||||||
fontSize: 14.0,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
_buildReadonlyField('Genre :', _genreDisplayLabel(widget.childData.genre)),
|
||||||
children: [
|
|
||||||
AppCustomCheckbox(
|
|
||||||
label: 'Naissance multiple',
|
|
||||||
value: widget.childData.multipleBirth,
|
|
||||||
onChanged: (v) {},
|
|
||||||
checkboxSize: 20.0,
|
|
||||||
fontSize: 14.0,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -525,6 +644,111 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPhotoConsentRow({
|
||||||
|
required BuildContext context,
|
||||||
|
required DisplayConfig config,
|
||||||
|
required double labelFontSize,
|
||||||
|
}) {
|
||||||
|
final readonly = config.isReadonly;
|
||||||
|
final primary = Theme.of(context).colorScheme.primary;
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: widget.childData.photoConsent,
|
||||||
|
onChanged: readonly
|
||||||
|
? null
|
||||||
|
: (v) {
|
||||||
|
if (v != null) widget.onTogglePhotoConsent(v);
|
||||||
|
},
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
activeColor: primary,
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
'Consentement photo',
|
||||||
|
style: GoogleFonts.merienda(fontSize: labelFontSize),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Tooltip(
|
||||||
|
message: _photoConsentTooltip,
|
||||||
|
waitDuration: const Duration(milliseconds: 300),
|
||||||
|
triggerMode: TooltipTriggerMode.tap,
|
||||||
|
showDuration: const Duration(seconds: 6),
|
||||||
|
child: Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
size: labelFontSize * 1.2,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _genreDisplayLabel(String genre) {
|
||||||
|
switch (genre) {
|
||||||
|
case 'F':
|
||||||
|
return 'Fille';
|
||||||
|
case 'H':
|
||||||
|
return 'Garçon';
|
||||||
|
case 'Autre':
|
||||||
|
return 'Inconnu';
|
||||||
|
default:
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fille / Garçon ; « Inconnu » (`Autre`) uniquement si enfant à naître.
|
||||||
|
Widget _buildGenreSegment(BuildContext context, DisplayConfig config, double scaleFactor) {
|
||||||
|
if (config.isReadonly) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
final selected = widget.childData.genre;
|
||||||
|
final isUnborn = widget.childData.isUnbornChild;
|
||||||
|
final fontSize = config.isMobile ? 13.0 : 14.0 * scaleFactor;
|
||||||
|
final padV = config.isMobile ? 6.0 : 8.0 * scaleFactor;
|
||||||
|
|
||||||
|
Widget seg(String label, String api) {
|
||||||
|
final on = selected == api;
|
||||||
|
return Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () => widget.onGenreChanged(api),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: padV),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
backgroundColor: on ? Colors.black.withValues(alpha: 0.08) : Colors.transparent,
|
||||||
|
foregroundColor: Colors.black87,
|
||||||
|
side: BorderSide(
|
||||||
|
width: on ? 2 : 1,
|
||||||
|
color: on ? Theme.of(context).primaryColor : Colors.black38,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: GoogleFonts.merienda(fontSize: fontSize, fontWeight: on ? FontWeight.w700 : FontWeight.w500),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
seg('Fille', 'F'),
|
||||||
|
seg('Garçon', 'H'),
|
||||||
|
if (isUnborn) seg('Inconnu', 'Autre'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper pour champ Readonly style "Beige"
|
/// Helper pour champ Readonly style "Beige"
|
||||||
Widget _buildReadonlyField(String label, String value) {
|
Widget _buildReadonlyField(String label, String value) {
|
||||||
return Column(
|
return Column(
|
||||||
@ -566,6 +790,7 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
bool readOnly = false,
|
bool readOnly = false,
|
||||||
VoidCallback? onTap,
|
VoidCallback? onTap,
|
||||||
IconData? suffixIcon,
|
IconData? suffixIcon,
|
||||||
|
FocusNode? focusNode,
|
||||||
}) {
|
}) {
|
||||||
if (config.isReadonly) {
|
if (config.isReadonly) {
|
||||||
return FormFieldWrapper(
|
return FormFieldWrapper(
|
||||||
@ -576,6 +801,7 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
} else {
|
} else {
|
||||||
return CustomAppTextField(
|
return CustomAppTextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
labelText: label,
|
labelText: label,
|
||||||
hintText: hint ?? label,
|
hintText: hint ?? label,
|
||||||
isRequired: isRequired,
|
isRequired: isRequired,
|
||||||
|
|||||||
@ -10,6 +10,8 @@ class HoverReliefWidget extends StatefulWidget {
|
|||||||
final bool enableHoverEffect; // Pour activer/désactiver l'effet de survol
|
final bool enableHoverEffect; // Pour activer/désactiver l'effet de survol
|
||||||
final Color initialShadowColor; // Nouveau paramètre
|
final Color initialShadowColor; // Nouveau paramètre
|
||||||
final Color hoverShadowColor; // Nouveau paramètre
|
final Color hoverShadowColor; // Nouveau paramètre
|
||||||
|
/// `Clip.none` : ne pas découper le child (ex. trou du cadre PNG par-dessus la photo).
|
||||||
|
final Clip clipBehavior;
|
||||||
|
|
||||||
const HoverReliefWidget({
|
const HoverReliefWidget({
|
||||||
required this.child,
|
required this.child,
|
||||||
@ -21,6 +23,7 @@ class HoverReliefWidget extends StatefulWidget {
|
|||||||
this.enableHoverEffect = true, // Par défaut, l'effet est activé
|
this.enableHoverEffect = true, // Par défaut, l'effet est activé
|
||||||
this.initialShadowColor = const Color(0x26000000), // Default: Colors.black.withOpacity(0.15)
|
this.initialShadowColor = const Color(0x26000000), // Default: Colors.black.withOpacity(0.15)
|
||||||
this.hoverShadowColor = const Color(0x4D000000), // Default: Colors.black.withOpacity(0.3)
|
this.hoverShadowColor = const Color(0x4D000000), // Default: Colors.black.withOpacity(0.3)
|
||||||
|
this.clipBehavior = Clip.antiAlias,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -49,7 +52,7 @@ class _HoverReliefWidgetState extends State<HoverReliefWidget> {
|
|||||||
elevation: elevation,
|
elevation: elevation,
|
||||||
shadowColor: shadowColor,
|
shadowColor: shadowColor,
|
||||||
borderRadius: widget.borderRadius,
|
borderRadius: widget.borderRadius,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: widget.clipBehavior,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -62,7 +65,7 @@ class _HoverReliefWidgetState extends State<HoverReliefWidget> {
|
|||||||
elevation: widget.initialElevation, // Utilise l'élévation initiale
|
elevation: widget.initialElevation, // Utilise l'élévation initiale
|
||||||
shadowColor: widget.initialShadowColor, // Appliqué ici pour l'état non cliquable
|
shadowColor: widget.initialShadowColor, // Appliqué ici pour l'état non cliquable
|
||||||
borderRadius: widget.borderRadius,
|
borderRadius: widget.borderRadius,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: widget.clipBehavior,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||||
|
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
@ -76,6 +77,19 @@ class PersonalInfoFormScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||||
|
/// Ordre de tabulation explicite (étape 1 / parent 2, etc.)
|
||||||
|
static const double _focusSecondPersonToggle = 1;
|
||||||
|
static const double _focusSameAddressToggle = 2;
|
||||||
|
static const double _focusLastName = 10;
|
||||||
|
static const double _focusFirstName = 11;
|
||||||
|
static const double _focusPhone = 12;
|
||||||
|
static const double _focusEmail = 13;
|
||||||
|
static const double _focusAddress = 14;
|
||||||
|
static const double _focusPostal = 15;
|
||||||
|
static const double _focusCity = 16;
|
||||||
|
static const double _focusNavPrevious = 100;
|
||||||
|
static const double _focusNavNext = 101;
|
||||||
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
late TextEditingController _lastNameController;
|
late TextEditingController _lastNameController;
|
||||||
late TextEditingController _firstNameController;
|
late TextEditingController _firstNameController;
|
||||||
@ -89,6 +103,10 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
bool _sameAddress = false;
|
bool _sameAddress = false;
|
||||||
bool _fieldsEnabled = true;
|
bool _fieldsEnabled = true;
|
||||||
|
|
||||||
|
FocusNode? _lastNameFocus;
|
||||||
|
FocusNode? _firstNameFocus;
|
||||||
|
FocusNode? _cityFocus;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -109,10 +127,57 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
_sameAddress = widget.initialSameAddress ?? false;
|
_sameAddress = widget.initialSameAddress ?? false;
|
||||||
_updateAddressFields();
|
_updateAddressFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (widget.mode == DisplayMode.editable) {
|
||||||
|
_lastNameFocus = FocusNode();
|
||||||
|
_firstNameFocus = FocusNode();
|
||||||
|
_cityFocus = FocusNode();
|
||||||
|
_lastNameFocus!.addListener(_onLastNameFocusChange);
|
||||||
|
_firstNameFocus!.addListener(_onFirstNameFocusChange);
|
||||||
|
_cityFocus!.addListener(_onCityFocusChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLastNameFocusChange() {
|
||||||
|
if (_lastNameFocus == null || _lastNameFocus!.hasFocus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_applyPersonNameFormat(_lastNameController);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFirstNameFocusChange() {
|
||||||
|
if (_firstNameFocus == null || _firstNameFocus!.hasFocus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_applyPersonNameFormat(_firstNameController);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCityFocusChange() {
|
||||||
|
if (_cityFocus == null || _cityFocus!.hasFocus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_applyPersonNameFormat(_cityController);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyPersonNameFormat(TextEditingController controller) {
|
||||||
|
final formatted = formatPersonNameCase(controller.text);
|
||||||
|
if (formatted == controller.text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.value = TextEditingValue(
|
||||||
|
text: formatted,
|
||||||
|
selection: TextSelection.collapsed(offset: formatted.length),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_lastNameFocus?.removeListener(_onLastNameFocusChange);
|
||||||
|
_firstNameFocus?.removeListener(_onFirstNameFocusChange);
|
||||||
|
_cityFocus?.removeListener(_onCityFocusChange);
|
||||||
|
_lastNameFocus?.dispose();
|
||||||
|
_firstNameFocus?.dispose();
|
||||||
|
_cityFocus?.dispose();
|
||||||
_lastNameController.dispose();
|
_lastNameController.dispose();
|
||||||
_firstNameController.dispose();
|
_firstNameController.dispose();
|
||||||
_phoneController.dispose();
|
_phoneController.dispose();
|
||||||
@ -131,7 +196,35 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _wrapScrollBodyIfEditable({
|
||||||
|
required DisplayConfig config,
|
||||||
|
required Widget child,
|
||||||
|
}) {
|
||||||
|
if (!config.isEditable) return child;
|
||||||
|
return FocusTraversalGroup(
|
||||||
|
policy: OrderedTraversalPolicy(),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _orderedFocus({
|
||||||
|
required bool enabled,
|
||||||
|
required double order,
|
||||||
|
required Widget child,
|
||||||
|
}) {
|
||||||
|
if (!enabled) return child;
|
||||||
|
return FocusTraversalOrder(
|
||||||
|
order: NumericFocusOrder(order),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _handleSubmit() {
|
void _handleSubmit() {
|
||||||
|
if (widget.mode == DisplayMode.editable) {
|
||||||
|
_applyPersonNameFormat(_lastNameController);
|
||||||
|
_applyPersonNameFormat(_firstNameController);
|
||||||
|
_applyPersonNameFormat(_cityController);
|
||||||
|
}
|
||||||
if (widget.mode == DisplayMode.readonly || _formKey.currentState!.validate()) {
|
if (widget.mode == DisplayMode.readonly || _formKey.currentState!.validate()) {
|
||||||
final data = PersonalInfoData(
|
final data = PersonalInfoData(
|
||||||
firstName: _firstNameController.text,
|
firstName: _firstNameController.text,
|
||||||
@ -169,6 +262,8 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
Center(
|
Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 40.0),
|
padding: const EdgeInsets.symmetric(vertical: 40.0),
|
||||||
|
child: _wrapScrollBodyIfEditable(
|
||||||
|
config: config,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@ -191,17 +286,18 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
),
|
),
|
||||||
SizedBox(height: config.isMobile ? 16 : 30),
|
SizedBox(height: config.isMobile ? 16 : 30),
|
||||||
_buildCard(context, config, screenSize),
|
_buildCard(context, config, screenSize),
|
||||||
|
|
||||||
// Boutons mobile sous la carte (dans le scroll)
|
|
||||||
if (config.isMobile) ...[
|
if (config.isMobile) ...[
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: screenSize.width * 0.05, // Même marge que la carte (0.9 = 0.05 de chaque côté)
|
horizontal: screenSize.width * 0.05,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
child: _orderedFocus(
|
||||||
|
enabled: config.isEditable,
|
||||||
|
order: _focusNavPrevious,
|
||||||
child: HoverReliefWidget(
|
child: HoverReliefWidget(
|
||||||
child: CustomNavigationButton(
|
child: CustomNavigationButton(
|
||||||
text: 'Précédent',
|
text: 'Précédent',
|
||||||
@ -219,8 +315,12 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16), // Écart entre les boutons
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
child: _orderedFocus(
|
||||||
|
enabled: config.isEditable,
|
||||||
|
order: _focusNavNext,
|
||||||
child: HoverReliefWidget(
|
child: HoverReliefWidget(
|
||||||
child: CustomNavigationButton(
|
child: CustomNavigationButton(
|
||||||
text: 'Suivant',
|
text: 'Suivant',
|
||||||
@ -232,6 +332,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -241,6 +342,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
// Chevrons de navigation (desktop uniquement)
|
// Chevrons de navigation (desktop uniquement)
|
||||||
if (!config.isMobile) ...[
|
if (!config.isMobile) ...[
|
||||||
Positioned(
|
Positioned(
|
||||||
@ -522,7 +624,10 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Transform.scale(
|
_orderedFocus(
|
||||||
|
enabled: config.isEditable,
|
||||||
|
order: _focusSecondPersonToggle,
|
||||||
|
child: Transform.scale(
|
||||||
scale: config.isMobile ? 0.85 : 1.0,
|
scale: config.isMobile ? 0.85 : 1.0,
|
||||||
child: Switch(
|
child: Switch(
|
||||||
value: _hasSecondPerson,
|
value: _hasSecondPerson,
|
||||||
@ -535,6 +640,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
activeColor: Theme.of(context).primaryColor,
|
activeColor: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -558,7 +664,10 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Transform.scale(
|
_orderedFocus(
|
||||||
|
enabled: config.isEditable,
|
||||||
|
order: _focusSameAddressToggle,
|
||||||
|
child: Transform.scale(
|
||||||
scale: config.isMobile ? 0.85 : 1.0,
|
scale: config.isMobile ? 0.85 : 1.0,
|
||||||
child: Switch(
|
child: Switch(
|
||||||
value: _sameAddress,
|
value: _sameAddress,
|
||||||
@ -571,6 +680,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
activeColor: Theme.of(context).primaryColor,
|
activeColor: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -606,6 +716,8 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
controller: _lastNameController,
|
controller: _lastNameController,
|
||||||
hint: 'Votre nom de famille',
|
hint: 'Votre nom de famille',
|
||||||
enabled: _fieldsEnabled,
|
enabled: _fieldsEnabled,
|
||||||
|
focusOrder: _focusLastName,
|
||||||
|
focusNode: _lastNameFocus,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
@ -616,6 +728,8 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
controller: _firstNameController,
|
controller: _firstNameController,
|
||||||
hint: 'Votre prénom',
|
hint: 'Votre prénom',
|
||||||
enabled: _fieldsEnabled,
|
enabled: _fieldsEnabled,
|
||||||
|
focusOrder: _focusFirstName,
|
||||||
|
focusNode: _firstNameFocus,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -634,6 +748,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.phone,
|
||||||
enabled: _fieldsEnabled,
|
enabled: _fieldsEnabled,
|
||||||
inputFormatters: _phoneInputFormatters,
|
inputFormatters: _phoneInputFormatters,
|
||||||
|
focusOrder: _focusPhone,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
@ -645,6 +760,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
hint: 'Votre adresse e-mail',
|
hint: 'Votre adresse e-mail',
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
enabled: _fieldsEnabled,
|
enabled: _fieldsEnabled,
|
||||||
|
focusOrder: _focusEmail,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -658,6 +774,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
controller: _addressController,
|
controller: _addressController,
|
||||||
hint: 'Numéro et nom de votre rue',
|
hint: 'Numéro et nom de votre rue',
|
||||||
enabled: _fieldsEnabled && !_sameAddress,
|
enabled: _fieldsEnabled && !_sameAddress,
|
||||||
|
focusOrder: _focusAddress,
|
||||||
),
|
),
|
||||||
SizedBox(height: verticalSpacing),
|
SizedBox(height: verticalSpacing),
|
||||||
|
|
||||||
@ -673,6 +790,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
hint: 'Code postal',
|
hint: 'Code postal',
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
enabled: _fieldsEnabled && !_sameAddress,
|
enabled: _fieldsEnabled && !_sameAddress,
|
||||||
|
focusOrder: _focusPostal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
@ -684,6 +802,8 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
controller: _cityController,
|
controller: _cityController,
|
||||||
hint: 'Votre ville',
|
hint: 'Votre ville',
|
||||||
enabled: _fieldsEnabled && !_sameAddress,
|
enabled: _fieldsEnabled && !_sameAddress,
|
||||||
|
focusOrder: _focusCity,
|
||||||
|
focusNode: _cityFocus,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -852,6 +972,8 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
controller: _lastNameController,
|
controller: _lastNameController,
|
||||||
hint: 'Votre nom de famille',
|
hint: 'Votre nom de famille',
|
||||||
enabled: _fieldsEnabled,
|
enabled: _fieldsEnabled,
|
||||||
|
focusOrder: _focusLastName,
|
||||||
|
focusNode: _lastNameFocus,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
@ -862,6 +984,8 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
controller: _firstNameController,
|
controller: _firstNameController,
|
||||||
hint: 'Votre prénom',
|
hint: 'Votre prénom',
|
||||||
enabled: _fieldsEnabled,
|
enabled: _fieldsEnabled,
|
||||||
|
focusOrder: _focusFirstName,
|
||||||
|
focusNode: _firstNameFocus,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
@ -874,6 +998,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.phone,
|
||||||
enabled: _fieldsEnabled,
|
enabled: _fieldsEnabled,
|
||||||
inputFormatters: _phoneInputFormatters,
|
inputFormatters: _phoneInputFormatters,
|
||||||
|
focusOrder: _focusPhone,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
@ -885,6 +1010,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
hint: 'Votre adresse e-mail',
|
hint: 'Votre adresse e-mail',
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
enabled: _fieldsEnabled,
|
enabled: _fieldsEnabled,
|
||||||
|
focusOrder: _focusEmail,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
@ -895,6 +1021,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
controller: _addressController,
|
controller: _addressController,
|
||||||
hint: 'Numéro et nom de votre rue',
|
hint: 'Numéro et nom de votre rue',
|
||||||
enabled: _fieldsEnabled && !_sameAddress,
|
enabled: _fieldsEnabled && !_sameAddress,
|
||||||
|
focusOrder: _focusAddress,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
@ -906,6 +1033,7 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
hint: 'Code postal',
|
hint: 'Code postal',
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
enabled: _fieldsEnabled && !_sameAddress,
|
enabled: _fieldsEnabled && !_sameAddress,
|
||||||
|
focusOrder: _focusPostal,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
@ -916,6 +1044,8 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
controller: _cityController,
|
controller: _cityController,
|
||||||
hint: 'Votre ville',
|
hint: 'Votre ville',
|
||||||
enabled: _fieldsEnabled && !_sameAddress,
|
enabled: _fieldsEnabled && !_sameAddress,
|
||||||
|
focusOrder: _focusCity,
|
||||||
|
focusNode: _cityFocus,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -930,6 +1060,8 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
TextInputType? keyboardType,
|
TextInputType? keyboardType,
|
||||||
bool enabled = true,
|
bool enabled = true,
|
||||||
List<TextInputFormatter>? inputFormatters,
|
List<TextInputFormatter>? inputFormatters,
|
||||||
|
double? focusOrder,
|
||||||
|
FocusNode? focusNode,
|
||||||
}) {
|
}) {
|
||||||
if (config.isReadonly) {
|
if (config.isReadonly) {
|
||||||
// Mode readonly : utiliser FormFieldWrapper (téléphone formaté pour affichage)
|
// Mode readonly : utiliser FormFieldWrapper (téléphone formaté pour affichage)
|
||||||
@ -942,9 +1074,9 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
value: displayValue,
|
value: displayValue,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Mode éditable : style adapté mobile/desktop
|
Widget field = CustomAppTextField(
|
||||||
return CustomAppTextField(
|
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
labelText: label,
|
labelText: label,
|
||||||
hintText: hint ?? label,
|
hintText: hint ?? label,
|
||||||
style: CustomAppTextFieldStyle.beige,
|
style: CustomAppTextFieldStyle.beige,
|
||||||
@ -956,6 +1088,13 @@ class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
|||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
inputFormatters: inputFormatters,
|
inputFormatters: inputFormatters,
|
||||||
);
|
);
|
||||||
|
if (focusOrder != null) {
|
||||||
|
field = FocusTraversalOrder(
|
||||||
|
order: NumericFocusOrder(focusOrder),
|
||||||
|
child: field,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return field;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user