Files
geo/app/lib/presentation/widgets/amicale_form.dart
d6soft e5ab857913 feat: création branche singletons - début refactorisation
- Sauvegarde des fichiers critiques
- Préparation transformation ApiService en singleton
- Préparation création CurrentUserService et CurrentAmicaleService
- Objectif: renommer Box users -> user
2025-06-05 15:22:29 +02:00

974 lines
32 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/presentation/widgets/mapbox_map.dart';
import 'package:latlong2/latlong.dart';
import 'custom_text_field.dart';
class AmicaleForm extends StatefulWidget {
final AmicaleModel? amicale;
final Function(AmicaleModel)? onSubmit;
final bool readOnly;
final UserRepository userRepository; // Nouveau paramètre
final ApiService? apiService; // Nouveau paramètre optionnel
const AmicaleForm({
super.key,
this.amicale,
this.onSubmit,
this.readOnly = false,
required this.userRepository, // Requis
this.apiService, // Optionnel
});
@override
State<AmicaleForm> createState() => _AmicaleFormState();
}
class _AmicaleFormState extends State<AmicaleForm> {
final _formKey = GlobalKey<FormState>();
// Controllers
late final TextEditingController _nameController;
late final TextEditingController _adresse1Controller;
late final TextEditingController _adresse2Controller;
late final TextEditingController _codePostalController;
late final TextEditingController _villeController;
late final TextEditingController _phoneController;
late final TextEditingController _mobileController;
late final TextEditingController _emailController;
late final TextEditingController _gpsLatController;
late final TextEditingController _gpsLngController;
late final TextEditingController _stripeIdController;
// Form values
int? _fkRegion;
String? _libRegion;
bool _chkDemo = false;
bool _chkCopieMailRecu = false;
bool _chkAcceptSms = false;
bool _chkActive = true;
bool _chkStripe = false;
@override
void initState() {
super.initState();
// Initialize controllers with amicale data if available
final amicale = widget.amicale;
_nameController = TextEditingController(text: amicale?.name ?? '');
_adresse1Controller = TextEditingController(text: amicale?.adresse1 ?? '');
_adresse2Controller = TextEditingController(text: amicale?.adresse2 ?? '');
_codePostalController = TextEditingController(text: amicale?.codePostal ?? '');
_villeController = TextEditingController(text: amicale?.ville ?? '');
_phoneController = TextEditingController(text: amicale?.phone ?? '');
_mobileController = TextEditingController(text: amicale?.mobile ?? '');
_emailController = TextEditingController(text: amicale?.email ?? '');
_gpsLatController = TextEditingController(text: amicale?.gpsLat ?? '');
_gpsLngController = TextEditingController(text: amicale?.gpsLng ?? '');
_stripeIdController = TextEditingController(text: amicale?.stripeId ?? '');
_fkRegion = amicale?.fkRegion;
_libRegion = amicale?.libRegion;
_chkDemo = amicale?.chkDemo ?? false;
_chkCopieMailRecu = amicale?.chkCopieMailRecu ?? false;
_chkAcceptSms = amicale?.chkAcceptSms ?? false;
_chkActive = amicale?.chkActive ?? true;
_chkStripe = amicale?.chkStripe ?? false;
}
@override
void dispose() {
_nameController.dispose();
_adresse1Controller.dispose();
_adresse2Controller.dispose();
_codePostalController.dispose();
_villeController.dispose();
_phoneController.dispose();
_mobileController.dispose();
_emailController.dispose();
_gpsLatController.dispose();
_gpsLngController.dispose();
_stripeIdController.dispose();
super.dispose();
}
// Appeler l'API pour mettre à jour l'entité
Future<void> _updateAmicale(AmicaleModel amicale) async {
try {
// Afficher un indicateur de chargement
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
},
);
// Préparer les données pour l'API
final Map<String, dynamic> data = {
'id': amicale.id,
'name': amicale.name,
'adresse1': amicale.adresse1,
'adresse2': amicale.adresse2,
'code_postal': amicale.codePostal,
'ville': amicale.ville,
'phone': amicale.phone,
'mobile': amicale.mobile,
'email': amicale.email,
'chk_copie_mail_recu': amicale.chkCopieMailRecu,
'chk_accept_sms': amicale.chkAcceptSms,
'chk_stripe': amicale.chkStripe,
};
// Ajouter les champs réservés aux administrateurs si l'utilisateur est admin
final userRole = widget.userRepository.getUserRole();
if (userRole > 2) {
data['gps_lat'] = amicale.gpsLat;
data['gps_lng'] = amicale.gpsLng;
data['stripe_id'] = amicale.stripeId;
data['chk_demo'] = amicale.chkDemo;
data['chk_active'] = amicale.chkActive;
}
// Fermer l'indicateur de chargement
Navigator.of(context).pop();
// Appeler l'API si le service est disponible
if (widget.apiService != null) {
try {
await widget.apiService!.post('/entite/update', data: data);
// Afficher un message de succès
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Amicale mise à jour avec succès'),
backgroundColor: Colors.green,
),
);
}
} catch (error) {
// Afficher un message d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la mise à jour de l\'amicale: $error'),
backgroundColor: Colors.red,
),
);
}
return; // Sortir de la fonction en cas d'erreur
}
} else {
// Pas d'API service, afficher un message d'information
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Modifications enregistrées localement'),
backgroundColor: Colors.blue,
),
);
}
}
// Appeler la fonction onSubmit si elle existe
if (widget.onSubmit != null) {
widget.onSubmit!(amicale);
}
// Fermer le formulaire
if (mounted) {
Navigator.of(context).pop();
}
} catch (e) {
// Fermer l'indicateur de chargement si encore ouvert
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
// Afficher un message d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
// Vérifier qu'au moins un numéro de téléphone est renseigné
if (_phoneController.text.isEmpty && _mobileController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez renseigner au moins un numéro de téléphone'),
backgroundColor: Colors.red,
),
);
return;
}
final amicale = widget.amicale?.copyWith(
name: _nameController.text,
adresse1: _adresse1Controller.text,
adresse2: _adresse2Controller.text,
codePostal: _codePostalController.text,
ville: _villeController.text,
fkRegion: _fkRegion,
libRegion: _libRegion,
phone: _phoneController.text,
mobile: _mobileController.text,
email: _emailController.text,
gpsLat: _gpsLatController.text,
gpsLng: _gpsLngController.text,
stripeId: _stripeIdController.text,
chkDemo: _chkDemo,
chkCopieMailRecu: _chkCopieMailRecu,
chkAcceptSms: _chkAcceptSms,
chkActive: _chkActive,
chkStripe: _chkStripe,
) ??
AmicaleModel(
id: 0, // Sera remplacé par l'API
name: _nameController.text,
adresse1: _adresse1Controller.text,
adresse2: _adresse2Controller.text,
codePostal: _codePostalController.text,
ville: _villeController.text,
fkRegion: _fkRegion,
libRegion: _libRegion,
phone: _phoneController.text,
mobile: _mobileController.text,
email: _emailController.text,
gpsLat: _gpsLatController.text,
gpsLng: _gpsLngController.text,
stripeId: _stripeIdController.text,
chkDemo: _chkDemo,
chkCopieMailRecu: _chkCopieMailRecu,
chkAcceptSms: _chkAcceptSms,
chkActive: _chkActive,
);
// Appeler l'API pour mettre à jour l'amicale
_updateAmicale(amicale);
// Ne pas appeler widget.onSubmit ici car c'est fait dans _updateAmicale
}
}
// Construire la section logo
Widget _buildLogoSection() {
return Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
children: [
// Image par défaut
Center(
child: Image.asset(
'assets/images/logo_recu.png',
width: 150,
height: 150,
fit: BoxFit.contain,
),
),
// Overlay pour indiquer que l'image est modifiable (si non en lecture seule)
if (!widget.readOnly)
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
// TODO: Implémenter la sélection d'image
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Fonctionnalité de modification du logo à venir'),
),
);
},
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.1),
),
child: const Center(
child: Icon(
Icons.camera_alt,
color: Colors.white,
size: 40,
),
),
),
),
),
),
],
),
),
);
}
// Construire la minimap
Widget _buildMiniMap() {
// Vérifier si les coordonnées GPS sont valides
double? lat = double.tryParse(_gpsLatController.text);
double? lng = double.tryParse(_gpsLngController.text);
// Si les coordonnées ne sont pas valides, afficher un message
if (lat == null || lng == null) {
return Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Text(
'Aucune coordonnée GPS',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
),
);
}
// Créer la position pour la carte
final position = LatLng(lat, lng);
// Créer un marqueur pour la position de l'amicale
final markers = [
Marker(
point: position,
width: 20,
height: 20,
child: const Icon(
Icons.fireplace_rounded,
color: Color.fromARGB(255, 212, 34, 31),
size: 20,
),
),
];
// Retourner la minimap
return Container(
width: 150,
height: 150,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: MapboxMap(
initialPosition: position,
initialZoom: 15.0,
markers: markers,
showControls: false,
),
),
);
}
// Construire le dropdown pour la région
Widget _buildRegionDropdown(bool restrictedFieldsReadOnly) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Afficher le libellé de la région en lecture seule
if (_libRegion != null && _libRegion!.isNotEmpty)
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
color: Theme.of(context).inputDecorationTheme.fillColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
_libRegion!,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
)
else
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(
color: Theme.of(context).inputDecorationTheme.fillColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Aucune région définie',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).hintColor,
),
),
),
],
);
}
// Construire une option checkbox
Widget _buildCheckboxOption({
required String label,
required bool value,
required void Function(bool?)? onChanged,
}) {
return Row(
children: [
Checkbox(
value: value,
onChanged: onChanged,
activeColor: Theme.of(context).colorScheme.primary,
),
Expanded(
child: Text(
label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
);
}
// Construire le formulaire principal
Widget _buildMainForm(ThemeData theme, bool restrictedFieldsReadOnly) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Nom
CustomTextField(
controller: _nameController,
label: "Nom",
readOnly: widget.readOnly,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return "Veuillez entrer un nom";
}
return null;
},
),
const SizedBox(height: 16),
// Bloc Adresse
Text(
"Adresse",
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
// Adresse 1
CustomTextField(
controller: _adresse1Controller,
label: "Adresse ligne 1",
readOnly: widget.readOnly,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return "Veuillez entrer une adresse";
}
return null;
},
),
const SizedBox(height: 16),
// Adresse 2
CustomTextField(
controller: _adresse2Controller,
label: "Adresse ligne 2",
readOnly: widget.readOnly,
),
const SizedBox(height: 16),
// Code Postal et Ville
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Code Postal
Expanded(
flex: 1,
child: CustomTextField(
controller: _codePostalController,
label: "Code Postal",
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(5),
],
readOnly: widget.readOnly,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return "Veuillez entrer un code postal";
}
if (value.length < 5) {
return "Le code postal doit contenir 5 chiffres";
}
return null;
},
),
),
const SizedBox(width: 16),
// Ville
Expanded(
flex: 2,
child: CustomTextField(
controller: _villeController,
label: "Ville",
readOnly: widget.readOnly,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return "Veuillez entrer une ville";
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
// Région
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Région",
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
_buildRegionDropdown(restrictedFieldsReadOnly),
],
),
const SizedBox(height: 16),
// Contact
Text(
"Contact",
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
// Téléphone fixe et mobile sur la même ligne
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Téléphone fixe
Expanded(
child: CustomTextField(
controller: _phoneController,
label: "Téléphone fixe",
keyboardType: TextInputType.phone,
readOnly: widget.readOnly,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
],
validator: (value) {
if (value != null && value.isNotEmpty && value.length < 10) {
return "Le numéro de téléphone doit contenir 10 chiffres";
}
return null;
},
),
),
const SizedBox(width: 16),
// Téléphone mobile
Expanded(
child: CustomTextField(
controller: _mobileController,
label: "Téléphone mobile",
keyboardType: TextInputType.phone,
readOnly: widget.readOnly,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
],
validator: (value) {
if (value != null && value.isNotEmpty && value.length < 10) {
return "Le numéro de mobile doit contenir 10 chiffres";
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
// Email
CustomTextField(
controller: _emailController,
label: "Email",
keyboardType: TextInputType.emailAddress,
readOnly: widget.readOnly,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return "Veuillez entrer l'adresse email";
}
if (!value.contains('@') || !value.contains('.')) {
return "Veuillez entrer une adresse email valide";
}
return null;
},
),
const SizedBox(height: 16),
// Informations avancées (visibles uniquement pour les administrateurs)
if (_shouldShowAdvancedInfo()) ...[
Text(
"Informations avancées",
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
// GPS Latitude et Longitude
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// GPS Latitude
Expanded(
child: CustomTextField(
controller: _gpsLatController,
label: "GPS Latitude",
keyboardType: const TextInputType.numberWithOptions(decimal: true),
readOnly: restrictedFieldsReadOnly,
),
),
const SizedBox(width: 16),
// GPS Longitude
Expanded(
child: CustomTextField(
controller: _gpsLngController,
label: "GPS Longitude",
keyboardType: const TextInputType.numberWithOptions(decimal: true),
readOnly: restrictedFieldsReadOnly,
),
),
],
),
const SizedBox(height: 16),
// Stripe Checkbox et Stripe ID sur la même ligne
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Checkbox Stripe
Checkbox(
value: _chkStripe,
onChanged: restrictedFieldsReadOnly
? null
: (value) {
if (value == true) {
// Afficher une boîte de dialogue de confirmation
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmation'),
content: const Text(
'En acceptant les règlements par carte bancaire, des commissions de 1.4% seront prélevées sur les montants encaissés. Souhaitez-vous continuer ?'),
actions: [
TextButton(
onPressed: () {
// L'utilisateur a répondu "non"
setState(() {
_chkStripe = false;
});
Navigator.of(context).pop();
},
child: const Text('Non'),
),
ElevatedButton(
onPressed: () {
// L'utilisateur a répondu "oui"
setState(() {
_chkStripe = true;
});
Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF20335E),
foregroundColor: Colors.white,
),
child: const Text('Oui'),
),
],
),
);
} else {
// Si l'utilisateur décoche la case, pas besoin de confirmation
setState(() {
_chkStripe = false;
});
}
},
activeColor: const Color(0xFF20335E),
),
Text(
"Accepte les règlements en CB",
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 16),
// Stripe ID
Expanded(
child: CustomTextField(
controller: _stripeIdController,
label: "ID Stripe Paiements CB",
readOnly: restrictedFieldsReadOnly,
helperText: "Les règlements par CB sont taxés d'une commission de 1.4%",
),
),
],
),
const SizedBox(height: 16),
],
// Options
Text(
"Options",
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
// Checkbox Demo
_buildCheckboxOption(
label: "Mode démo",
value: _chkDemo,
onChanged: restrictedFieldsReadOnly
? null
: (value) {
setState(() {
_chkDemo = value!;
});
},
),
const SizedBox(height: 8),
// Checkbox Copie Mail Reçu
_buildCheckboxOption(
label: "Copie des mails reçus",
value: _chkCopieMailRecu,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_chkCopieMailRecu = value!;
});
},
),
const SizedBox(height: 8),
// Checkbox Accept SMS
_buildCheckboxOption(
label: "Accepte les SMS",
value: _chkAcceptSms,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_chkAcceptSms = value!;
});
},
),
const SizedBox(height: 8),
// Checkbox Active
_buildCheckboxOption(
label: "Actif",
value: _chkActive,
onChanged: restrictedFieldsReadOnly
? null
: (value) {
setState(() {
_chkActive = value!;
});
},
),
const SizedBox(height: 25),
// Boutons Fermer et Enregistrer
if (!widget.readOnly)
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Bouton Fermer
OutlinedButton(
onPressed: () {
Navigator.of(context).pop();
},
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF20335E),
side: const BorderSide(color: Color(0xFF20335E)),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
minimumSize: const Size(150, 50),
),
child: const Text(
'Fermer',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: 20),
// Bouton Enregistrer
ElevatedButton(
onPressed: _submitForm,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF20335E),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
minimumSize: const Size(150, 50),
),
child: const Text(
'Enregistrer',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
],
);
}
// Vérifier si les informations avancées doivent être affichées
bool _shouldShowAdvancedInfo() {
final userRole = widget.userRepository.getUserRole();
final bool canEditRestrictedFields = userRole > 2;
return canEditRestrictedFields || _gpsLatController.text.isNotEmpty || _gpsLngController.text.isNotEmpty || _stripeIdController.text.isNotEmpty;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final userRole = widget.userRepository.getUserRole();
// Déterminer si l'utilisateur peut modifier les champs restreints
final bool canEditRestrictedFields = userRole > 2;
// Lecture seule pour les champs restreints si l'utilisateur n'a pas les droits
final bool restrictedFieldsReadOnly = widget.readOnly || !canEditRestrictedFields;
// Calculer la largeur maximale du formulaire pour les écrans larges
final screenWidth = MediaQuery.of(context).size.width;
final maxFormWidth = screenWidth > 800 ? 800.0 : screenWidth;
final formContent = Container(
width: maxFormWidth,
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec logo et minimap
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Section Logo
_buildLogoSection(),
// Section MiniMap
_buildMiniMap(),
],
),
const SizedBox(height: 24),
// Formulaire principal
_buildMainForm(theme, restrictedFieldsReadOnly),
],
),
),
),
);
// Vérifier si on est dans une Dialog en regardant le type du widget parent
final route = ModalRoute.of(context);
final isInDialog = route?.settings.name == null;
// Si on est dans une Dialog, ne pas utiliser Scaffold
if (isInDialog) {
return Center(child: formContent);
}
// Sinon, utiliser Scaffold pour les pages complètes
return Scaffold(
appBar: AppBar(
title: Text(widget.readOnly ? 'Détails de l\'amicale' : 'Modifier l\'amicale'),
backgroundColor: theme.appBarTheme.backgroundColor,
foregroundColor: theme.appBarTheme.foregroundColor,
),
body: Center(child: formContent),
);
}
}