- Panneau AM : validation NIR alignée API, date d’agrément requise côté app, capacité 1–10, n° agrément + date sur une ligne, ville/pays formatés au blur. - Widget RegistrationPhotoSlot (cadre, croix) partagé avec les cartes enfant. - AuthService : MIME PNG/JPEG pour la photo ; payload date_agrement. - Scripts register-am-dubois / mansouri ; chemins tests/ressources/photos ; doc test-data + seed ; smoke curl inscription AM. Made-with: Cursor
723 lines
27 KiB
Dart
723 lines
27 KiB
Dart
import 'dart:math' as math;
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:google_fonts/google_fonts.dart';
|
||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||
import '../models/user_registration_data.dart';
|
||
import '../models/card_assets.dart';
|
||
import 'custom_app_text_field.dart';
|
||
import 'form_field_wrapper.dart';
|
||
import 'registration_photo_slot.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é.';
|
||
|
||
bool _hasChildPhoto(ChildData c) {
|
||
return registrationPhotoSlotHasImage(
|
||
imageBytes: c.imageBytes,
|
||
imageFile: c.imageFile,
|
||
imagePathOrAsset: null,
|
||
);
|
||
}
|
||
|
||
Widget _buildChildPhotoImage(ChildData c, {required BoxFit fit}) {
|
||
final bytes = c.imageBytes;
|
||
if (bytes != null && bytes.isNotEmpty) {
|
||
return Image.memory(bytes, fit: fit);
|
||
}
|
||
final f = c.imageFile;
|
||
if (f != null) {
|
||
return kIsWeb ? Image.network(f.path, fit: fit) : Image.file(f, fit: fit);
|
||
}
|
||
return Image.asset('assets/images/photo.png', fit: BoxFit.contain);
|
||
}
|
||
|
||
/// Widget pour afficher et éditer une carte enfant
|
||
/// Utilisé dans le workflow d'inscription des parents
|
||
class ChildCardWidget extends StatefulWidget {
|
||
final ChildData childData;
|
||
final int childIndex;
|
||
final VoidCallback onPickImage;
|
||
/// Retire la photo sélectionnée (placeholder à la place).
|
||
final VoidCallback onClearImage;
|
||
final VoidCallback onDateSelect;
|
||
final ValueChanged<String> onFirstNameChanged;
|
||
final ValueChanged<String> onLastNameChanged;
|
||
/// `H`, `F` ou `Autre` (API). « Inconnu » à l’UI = `Autre`.
|
||
final ValueChanged<String> onGenreChanged;
|
||
final ValueChanged<bool> onTogglePhotoConsent;
|
||
final ValueChanged<bool> onToggleIsUnborn;
|
||
final VoidCallback onRemove;
|
||
final bool canBeRemoved;
|
||
final DisplayMode mode;
|
||
final VoidCallback? onEdit;
|
||
|
||
const ChildCardWidget({
|
||
required Key key,
|
||
required this.childData,
|
||
required this.childIndex,
|
||
required this.onPickImage,
|
||
required this.onClearImage,
|
||
required this.onDateSelect,
|
||
required this.onFirstNameChanged,
|
||
required this.onLastNameChanged,
|
||
required this.onGenreChanged,
|
||
required this.onTogglePhotoConsent,
|
||
required this.onToggleIsUnborn,
|
||
required this.onRemove,
|
||
required this.canBeRemoved,
|
||
this.mode = DisplayMode.editable,
|
||
this.onEdit,
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
State<ChildCardWidget> createState() => _ChildCardWidgetState();
|
||
}
|
||
|
||
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 _lastNameController;
|
||
late TextEditingController _dobController;
|
||
|
||
FocusNode? _firstNameFocus;
|
||
FocusNode? _lastNameFocus;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
// Initialiser les contrôleurs avec les données du widget
|
||
_firstNameController = TextEditingController(text: widget.childData.firstName);
|
||
_lastNameController = TextEditingController(text: widget.childData.lastName);
|
||
_dobController = TextEditingController(text: widget.childData.dob);
|
||
|
||
// Ajouter des listeners pour mettre à jour les données sources via les callbacks
|
||
_firstNameController.addListener(() => widget.onFirstNameChanged(_firstNameController.text));
|
||
_lastNameController.addListener(() => widget.onLastNameChanged(_lastNameController.text));
|
||
// 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
|
||
void didUpdateWidget(covariant ChildCardWidget oldWidget) {
|
||
super.didUpdateWidget(oldWidget);
|
||
// Mettre à jour les contrôleurs si les données externes changent
|
||
// (peut arriver si on recharge l'état global)
|
||
if (widget.childData.firstName != _firstNameController.text) {
|
||
_firstNameController.text = widget.childData.firstName;
|
||
}
|
||
if (widget.childData.lastName != _lastNameController.text) {
|
||
_lastNameController.text = widget.childData.lastName;
|
||
}
|
||
if (widget.childData.dob != _dobController.text) {
|
||
_dobController.text = widget.childData.dob;
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_firstNameFocus?.removeListener(_onFirstNameFocusChange);
|
||
_lastNameFocus?.removeListener(_onLastNameFocusChange);
|
||
_firstNameFocus?.dispose();
|
||
_lastNameFocus?.dispose();
|
||
_firstNameController.dispose();
|
||
_lastNameController.dispose();
|
||
_dobController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final config = DisplayConfig.fromContext(context, mode: widget.mode);
|
||
final screenSize = MediaQuery.of(context).size;
|
||
final scaleFactor = config.isMobile ? 0.9 : 1.1; // Réduire légèrement sur mobile
|
||
|
||
// Si mode Readonly Desktop : Layout spécial "Vintage" horizontal
|
||
if (config.isReadonly && !config.isMobile) {
|
||
return _buildReadonlyDesktopCard(context, config, screenSize);
|
||
}
|
||
|
||
// Si mode Readonly Mobile : Layout spécial "Vintage" vertical (1:2)
|
||
if (config.isReadonly && config.isMobile) {
|
||
return Padding(
|
||
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
|
||
child: _buildReadonlyMobileCard(context, config),
|
||
);
|
||
}
|
||
|
||
final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
|
||
? Colors.purple.shade200
|
||
: (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200);
|
||
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(
|
||
width: editableCardWidth,
|
||
// On retire la hauteur fixe pour laisser le contenu définir la taille, comme les autres cartes
|
||
// height: config.isMobile ? null : 600.0 * scaleFactor,
|
||
padding: EdgeInsets.all(22.0 * scaleFactor),
|
||
decoration: BoxDecoration(
|
||
image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.fill),
|
||
borderRadius: BorderRadius.circular(20 * scaleFactor),
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
RegistrationPhotoSlot(
|
||
side: photoSide,
|
||
scaleFactor: scaleFactor,
|
||
imageBytes: widget.childData.imageBytes,
|
||
imageFile: widget.childData.imageFile,
|
||
onTapPick: !config.isReadonly ? widget.onPickImage : null,
|
||
onClear: !config.isReadonly ? widget.onClearImage : null,
|
||
baseShadowColor: baseCardColorForShadow,
|
||
isMobile: config.isMobile,
|
||
),
|
||
SizedBox(height: 8.0 * scaleFactor),
|
||
_buildPhotoConsentRow(
|
||
context: context,
|
||
config: config,
|
||
labelFontSize: config.isMobile ? 13.0 : 16.0,
|
||
),
|
||
SizedBox(height: 10.0 * scaleFactor),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'Enfant à naître ?',
|
||
style: GoogleFonts.merienda(
|
||
fontSize: config.isMobile ? 14 : 16 * scaleFactor,
|
||
fontWeight: FontWeight.w600
|
||
)
|
||
),
|
||
Transform.scale(
|
||
scale: config.isMobile ? 0.8 : 1.0,
|
||
child: Switch(
|
||
value: widget.childData.isUnbornChild,
|
||
onChanged: config.isReadonly ? null : widget.onToggleIsUnborn,
|
||
activeColor: Theme.of(context).primaryColor,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
SizedBox(height: 8.0 * scaleFactor),
|
||
_buildGenreSegment(context, config, scaleFactor),
|
||
SizedBox(height: 8.0 * scaleFactor),
|
||
_buildField(
|
||
config: config,
|
||
scaleFactor: scaleFactor,
|
||
label: 'Prénom',
|
||
controller: _firstNameController,
|
||
hint: widget.childData.isUnbornChild ? 'Facultatif' : 'Prénom',
|
||
isRequired: !widget.childData.isUnbornChild,
|
||
focusNode: _firstNameFocus,
|
||
),
|
||
SizedBox(height: 5.0 * scaleFactor),
|
||
_buildField(
|
||
config: config,
|
||
scaleFactor: scaleFactor,
|
||
label: 'Nom',
|
||
controller: _lastNameController,
|
||
hint: 'Nom de l\'enfant',
|
||
focusNode: _lastNameFocus,
|
||
),
|
||
SizedBox(height: 8.0 * scaleFactor),
|
||
_buildField(
|
||
config: config,
|
||
scaleFactor: scaleFactor,
|
||
label: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
|
||
controller: _dobController,
|
||
hint: 'JJ/MM/AAAA',
|
||
readOnly: true,
|
||
onTap: config.isReadonly ? null : widget.onDateSelect,
|
||
suffixIcon: Icons.calendar_today,
|
||
),
|
||
],
|
||
),
|
||
if (widget.canBeRemoved && !config.isReadonly)
|
||
Positioned(
|
||
top: -5, right: -5,
|
||
child: InkWell(
|
||
onTap: widget.onRemove,
|
||
customBorder: const CircleBorder(),
|
||
child: Image.asset(
|
||
'assets/images/cross.png',
|
||
width: config.isMobile ? 30 : 36,
|
||
height: config.isMobile ? 30 : 36,
|
||
fit: BoxFit.contain,
|
||
),
|
||
),
|
||
),
|
||
|
||
if (config.isReadonly && widget.onEdit != null)
|
||
Positioned(
|
||
top: -5, right: -5,
|
||
child: IconButton(
|
||
icon: const Icon(Icons.edit, color: Colors.black54),
|
||
onPressed: widget.onEdit,
|
||
tooltip: 'Modifier',
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// Layout SPÉCIAL Readonly Desktop (Ancien Design Horizontal)
|
||
Widget _buildReadonlyDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
||
// Convertir la couleur verticale (pour mobile) en couleur horizontale (pour desktop/récap)
|
||
// On mappe les couleurs verticales vers horizontales
|
||
String horizontalCardAsset = CardColorHorizontal.lavender.path; // Par défaut
|
||
|
||
// Mapping manuel simple
|
||
if (widget.childData.cardColor.path.contains('lavender')) horizontalCardAsset = CardColorHorizontal.lavender.path;
|
||
else if (widget.childData.cardColor.path.contains('blue')) horizontalCardAsset = CardColorHorizontal.blue.path;
|
||
else if (widget.childData.cardColor.path.contains('green')) horizontalCardAsset = CardColorHorizontal.green.path;
|
||
else if (widget.childData.cardColor.path.contains('lime')) horizontalCardAsset = CardColorHorizontal.lime.path;
|
||
else if (widget.childData.cardColor.path.contains('peach')) horizontalCardAsset = CardColorHorizontal.peach.path;
|
||
else if (widget.childData.cardColor.path.contains('pink')) horizontalCardAsset = CardColorHorizontal.pink.path;
|
||
else if (widget.childData.cardColor.path.contains('red')) horizontalCardAsset = CardColorHorizontal.red.path;
|
||
|
||
final cardWidth = screenSize.width / 2.0;
|
||
|
||
return SizedBox(
|
||
width: cardWidth,
|
||
child: AspectRatio(
|
||
aspectRatio: 2.0,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
|
||
decoration: BoxDecoration(
|
||
image: DecorationImage(
|
||
image: AssetImage(horizontalCardAsset),
|
||
fit: BoxFit.cover,
|
||
),
|
||
borderRadius: BorderRadius.circular(15),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
// Titre + Edit Button
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
'Enfant ${widget.childIndex + 1}' + (widget.childData.isUnbornChild ? ' (à naître)' : ''),
|
||
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
),
|
||
if (widget.onEdit != null)
|
||
IconButton(
|
||
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
|
||
onPressed: widget.onEdit,
|
||
tooltip: 'Modifier',
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 18),
|
||
|
||
// Contenu principal : Photo + Champs
|
||
Expanded(
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
// PHOTO (1/3) + consentement sous la photo
|
||
Expanded(
|
||
flex: 1,
|
||
child: Center(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
AspectRatio(
|
||
aspectRatio: 1,
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(18),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.1),
|
||
blurRadius: 10,
|
||
offset: const Offset(0, 5),
|
||
),
|
||
],
|
||
),
|
||
child: ClipRRect(
|
||
borderRadius: BorderRadius.circular(18),
|
||
child: _hasChildPhoto(widget.childData)
|
||
? _buildChildPhotoImage(widget.childData, fit: BoxFit.cover)
|
||
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
_buildPhotoConsentRow(
|
||
context: context,
|
||
config: config,
|
||
labelFontSize: 16.0,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 32),
|
||
|
||
// CHAMPS (2/3)
|
||
Expanded(
|
||
flex: 2,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
_buildReadonlyField('Prénom :', _firstNameController.text),
|
||
const SizedBox(height: 12),
|
||
_buildReadonlyField('Nom :', _lastNameController.text),
|
||
const SizedBox(height: 12),
|
||
_buildReadonlyField(
|
||
widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :',
|
||
_dobController.text
|
||
),
|
||
const SizedBox(height: 12),
|
||
_buildReadonlyField('Genre :', _genreDisplayLabel(widget.childData.genre)),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
|
||
|
||
/// Carte en mode readonly MOBILE avec hauteur adaptative
|
||
Widget _buildReadonlyMobileCard(BuildContext context, DisplayConfig config) {
|
||
return Container(
|
||
width: double.infinity,
|
||
// Pas de height fixe
|
||
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 24.0),
|
||
decoration: BoxDecoration(
|
||
image: DecorationImage(
|
||
image: AssetImage(widget.childData.cardColor.path), // Image verticale
|
||
fit: BoxFit.fill, // Fill pour s'adapter
|
||
),
|
||
borderRadius: BorderRadius.circular(15),
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
Column(
|
||
mainAxisSize: MainAxisSize.min, // S'adapte au contenu
|
||
children: [
|
||
// Titre + Edit Button
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
'Enfant ${widget.childIndex + 1}' + (widget.childData.isUnbornChild ? ' (à naître)' : ''),
|
||
style: GoogleFonts.merienda(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
),
|
||
if (widget.onEdit != null)
|
||
const SizedBox(width: 28),
|
||
],
|
||
),
|
||
|
||
// Contenu aligné en haut
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 20.0),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.start,
|
||
children: [
|
||
// Photo
|
||
SizedBox(
|
||
height: 150,
|
||
width: 150,
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(15),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.1),
|
||
blurRadius: 10,
|
||
offset: const Offset(0, 5),
|
||
),
|
||
],
|
||
),
|
||
child: ClipRRect(
|
||
borderRadius: BorderRadius.circular(15),
|
||
child: _hasChildPhoto(widget.childData)
|
||
? _buildChildPhotoImage(widget.childData, fit: BoxFit.cover)
|
||
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 10),
|
||
_buildPhotoConsentRow(
|
||
context: context,
|
||
config: config,
|
||
labelFontSize: 14.0,
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
// Champs
|
||
_buildReadonlyField('Prénom :', _firstNameController.text),
|
||
const SizedBox(height: 8),
|
||
_buildReadonlyField('Nom :', _lastNameController.text),
|
||
const SizedBox(height: 8),
|
||
_buildReadonlyField(
|
||
widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :',
|
||
_dobController.text
|
||
),
|
||
const SizedBox(height: 8),
|
||
_buildReadonlyField('Genre :', _genreDisplayLabel(widget.childData.genre)),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
|
||
if (widget.onEdit != null)
|
||
Positioned(
|
||
top: 0,
|
||
right: 0,
|
||
child: IconButton(
|
||
padding: EdgeInsets.zero,
|
||
constraints: const BoxConstraints(),
|
||
icon: const Icon(Icons.edit, color: Colors.black54, size: 24),
|
||
onPressed: widget.onEdit,
|
||
tooltip: 'Modifier',
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
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.withOpacity(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"
|
||
Widget _buildReadonlyField(String label, String value) {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
label,
|
||
style: GoogleFonts.merienda(fontSize: 22.0, fontWeight: FontWeight.w600),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Container(
|
||
width: double.infinity,
|
||
height: 50.0,
|
||
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
|
||
decoration: BoxDecoration(
|
||
image: const DecorationImage(
|
||
image: AssetImage('assets/images/bg_beige.png'),
|
||
fit: BoxFit.fill,
|
||
),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Text(
|
||
value.isNotEmpty ? value : '-',
|
||
style: GoogleFonts.merienda(fontSize: 18.0),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildField({
|
||
required DisplayConfig config,
|
||
required double scaleFactor,
|
||
required String label,
|
||
required TextEditingController controller,
|
||
String? hint,
|
||
bool isRequired = false,
|
||
bool readOnly = false,
|
||
VoidCallback? onTap,
|
||
IconData? suffixIcon,
|
||
FocusNode? focusNode,
|
||
}) {
|
||
if (config.isReadonly) {
|
||
return FormFieldWrapper(
|
||
config: config,
|
||
label: label,
|
||
value: controller.text,
|
||
);
|
||
} else {
|
||
return CustomAppTextField(
|
||
controller: controller,
|
||
focusNode: focusNode,
|
||
labelText: label,
|
||
hintText: hint ?? label,
|
||
isRequired: isRequired,
|
||
fieldHeight: config.isMobile ? 40.0 : 50.0 * scaleFactor, // Hauteur réduite
|
||
labelFontSize: config.isMobile ? 12.0 : 18.0, // Police réduite
|
||
inputFontSize: config.isMobile ? 13.0 : 16.0, // Police réduite
|
||
readOnly: readOnly,
|
||
onTap: onTap,
|
||
suffixIcon: suffixIcon,
|
||
);
|
||
}
|
||
}
|
||
}
|