feat(frontend): Refonte infrastructure formulaires multi-modes
- Support des modes Desktop/Mobile et Édition/Lecture seule - Refactoring des widgets de formulaire (PersonalInfo, ProfessionalInfo, Presentation, ChildCard) - Mise à jour des écrans de récapitulatif (ParentStep5, AmStep4) - Ajout de navigation (Précédent/Soumettre) sur mobile Closes #78 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../config/display_config.dart';
|
||||
import 'custom_app_text_field.dart';
|
||||
|
||||
/// Widget générique pour afficher un champ de formulaire
|
||||
/// S'adapte automatiquement selon le DisplayConfig (editable/readonly, mobile/desktop)
|
||||
class FormFieldWrapper extends StatelessWidget {
|
||||
/// Configuration d'affichage
|
||||
final DisplayConfig config;
|
||||
|
||||
/// Label du champ
|
||||
final String label;
|
||||
|
||||
/// Valeur actuelle
|
||||
final String value;
|
||||
|
||||
/// Controller pour le mode éditable
|
||||
final TextEditingController? controller;
|
||||
|
||||
/// Callback de changement (mode éditable)
|
||||
final ValueChanged<String>? onChanged;
|
||||
|
||||
/// Hint du champ (mode éditable)
|
||||
final String? hint;
|
||||
|
||||
/// Nombre de lignes (pour textarea)
|
||||
final int? maxLines;
|
||||
|
||||
/// Type de clavier
|
||||
final TextInputType? keyboardType;
|
||||
|
||||
/// Widget personnalisé à afficher (override le champ standard)
|
||||
final Widget? customWidget;
|
||||
|
||||
const FormFieldWrapper({
|
||||
super.key,
|
||||
required this.config,
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.controller,
|
||||
this.onChanged,
|
||||
this.hint,
|
||||
this.maxLines,
|
||||
this.keyboardType,
|
||||
this.customWidget,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (config.isReadonly) {
|
||||
return _buildReadonlyField(context);
|
||||
} else {
|
||||
return _buildEditableField(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit un champ en mode lecture seule
|
||||
Widget _buildReadonlyField(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: LayoutHelper.getSpacing(context,
|
||||
mobileSpacing: 8.0,
|
||||
desktopSpacing: 12.0,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Label
|
||||
Text(
|
||||
label,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: config.isMobile ? 14 : 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: const Color(0xFF4A4A4A),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// Valeur avec fond beige
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
image: const DecorationImage(
|
||||
image: AssetImage('assets/images/bg_beige.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
value.isEmpty ? '-' : value,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: config.isMobile ? 14 : 16,
|
||||
color: const Color(0xFF2C2C2C),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un champ en mode éditable
|
||||
Widget _buildEditableField(BuildContext context) {
|
||||
// Si un widget personnalisé est fourni, l'utiliser
|
||||
if (customWidget != null) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: LayoutHelper.getSpacing(context,
|
||||
mobileSpacing: 8.0,
|
||||
desktopSpacing: 12.0,
|
||||
),
|
||||
),
|
||||
child: customWidget,
|
||||
);
|
||||
}
|
||||
|
||||
// Sinon, utiliser le champ standard
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: LayoutHelper.getSpacing(context,
|
||||
mobileSpacing: 8.0,
|
||||
desktopSpacing: 12.0,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Label
|
||||
Text(
|
||||
label,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: config.isMobile ? 14 : 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF4A4A4A),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Champ de saisie
|
||||
CustomAppTextField(
|
||||
controller: controller!,
|
||||
labelText: label,
|
||||
hintText: hint ?? label,
|
||||
keyboardType: keyboardType ?? TextInputType.text,
|
||||
fieldWidth: double.infinity,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget générique pour afficher une ligne de champs
|
||||
/// S'adapte automatiquement: horizontal sur desktop, vertical sur mobile
|
||||
class FormFieldRow extends StatelessWidget {
|
||||
/// Configuration d'affichage
|
||||
final DisplayConfig config;
|
||||
|
||||
/// Liste des champs à afficher
|
||||
final List<Widget> fields;
|
||||
|
||||
/// Espacement entre les champs
|
||||
final double? spacing;
|
||||
|
||||
const FormFieldRow({
|
||||
super.key,
|
||||
required this.config,
|
||||
required this.fields,
|
||||
this.spacing,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveSpacing = spacing ??
|
||||
LayoutHelper.getSpacing(context,
|
||||
mobileSpacing: 12.0,
|
||||
desktopSpacing: 20.0,
|
||||
);
|
||||
|
||||
if (config.isMobile) {
|
||||
// Layout vertical sur mobile
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: fields,
|
||||
);
|
||||
} else {
|
||||
// Layout horizontal sur desktop
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int i = 0; i < fields.length; i++) ...[
|
||||
Expanded(child: fields[i]),
|
||||
if (i < fields.length - 1) SizedBox(width: effectiveSpacing),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user