Ajout de la géolocalisation automatique des casernes de pompiers et amélioration du formulaire EntiteForm
This commit is contained in:
@@ -53,6 +53,12 @@
|
||||
|
||||
- **LoginPage** : Page de connexion avec détection du type d'utilisateur
|
||||
- **SplashPage** : Page de démarrage et initialisation
|
||||
- **EntiteForm** : Formulaire de gestion des amicales avec géolocalisation automatique
|
||||
|
||||
#### Géolocalisation et Cartographie
|
||||
|
||||
- **MapboxMap** : Widget de carte pour afficher la position des amicales
|
||||
- **findFireStationCoordinates** : Service API pour localiser les casernes de pompiers via adresse.gouv.fr
|
||||
|
||||
## Technologies et Frameworks
|
||||
|
||||
@@ -69,6 +75,12 @@
|
||||
- **Dio** : Client HTTP pour les requêtes API
|
||||
- **GoRouter** : Navigation et routage dans Flutter
|
||||
- **Svelte 5** : Framework UI pour le site web
|
||||
- **Flutter Map** : Bibliothèque de cartographie pour Flutter
|
||||
|
||||
### APIs Externes
|
||||
|
||||
- **API Adresse.gouv.fr** : Utilisée pour la géolocalisation des adresses et des casernes de pompiers
|
||||
- **Stripe API** : Intégration pour les paiements par carte bancaire (commission de 1.4%)
|
||||
|
||||
### Base de Données
|
||||
|
||||
@@ -100,7 +112,9 @@
|
||||
|
||||
### Pratiques Spécifiques au Projet
|
||||
|
||||
[Toute convention ou pratique spécifique à ce projet]
|
||||
- Chiffrement des données sensibles (noms, emails, téléphones) dans la base de données
|
||||
- Mise à jour automatique des coordonnées GPS lors des changements d'adresse
|
||||
- Confirmation utilisateur pour les actions ayant un impact financier (activation Stripe)
|
||||
|
||||
## Flux de Travail et Processus de Développement
|
||||
|
||||
@@ -182,43 +196,6 @@
|
||||
## Historique des Versions
|
||||
|
||||
| Version | Date | Description |
|
||||
| ------- | ------ | ------------- |
|
||||
| 1.0.0 | [Date] | [Description] |
|
||||
|
||||
## Processus d'Authentification et Gestion des Sessions
|
||||
|
||||
### Flux de Connexion
|
||||
|
||||
1. L'utilisateur entre ses identifiants dans la page de login (username/password)
|
||||
2. L'application envoie une requête POST à `/api/login` avec les identifiants et le type de connexion (user/admin)
|
||||
3. Le serveur vérifie les identifiants, crée une session PHP et renvoie:
|
||||
- Un `session_id` (utilisé comme token Bearer)
|
||||
- Une date d'expiration de session
|
||||
- Les données de l'utilisateur et les données associées (opérations, secteurs, passages)
|
||||
4. L'application stocke ces données dans des boîtes Hive locales
|
||||
5. Le `session_id` est utilisé pour toutes les requêtes API suivantes
|
||||
|
||||
### Flux de Déconnexion
|
||||
|
||||
1. L'utilisateur demande une déconnexion
|
||||
2. L'application envoie une requête POST à `/api/logout` avec le `session_id` dans l'en-tête
|
||||
3. Le serveur détruit la session PHP avec `session_unset()` et `session_destroy()`
|
||||
4. L'application:
|
||||
- Vide toutes les boîtes Hive sauf la boîte utilisateur
|
||||
- Conserve uniquement le username et le rôle de l'utilisateur pour faciliter la reconnexion
|
||||
- Réinitialise le `session_id` à null
|
||||
|
||||
### Particularités
|
||||
|
||||
- La page de login vérifie le rôle de l'utilisateur avant de pré-remplir le champ username
|
||||
- Le type de connexion (user/admin) détermine les données chargées et les droits d'accès
|
||||
- Les utilisateurs avec rôle=1 sont des utilisateurs standards, ceux avec rôle>1 sont des administrateurs
|
||||
- Les sessions expirent après 24 heures par défaut
|
||||
|
||||
## Notes Spécifiques pour les Assistants IA
|
||||
|
||||
- Toujours vérifier les issues GitLab avant de proposer des solutions
|
||||
- Respecter strictement les conventions de code mentionnées ci-dessus
|
||||
- Lors de modifications des modèles Hive, s'assurer que les typeId sont uniques pour éviter les conflits
|
||||
- Vérifier la compatibilité des modifications avec les trois plateformes (web, iOS, Android)
|
||||
- Pour les modifications de l'API, s'assurer que la réponse reste compatible avec le format attendu par l'application
|
||||
| ------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1.0.0 | 01/03/2025 | Version initiale du projet |
|
||||
| 1.1.0 | 16/05/2025 | Ajout de la géolocalisation automatique des casernes de pompiers via API adresse.gouv.fr, amélioration du formulaire EntiteForm avec confirmation pour l'activation des paiements Stripe, mise à jour automatique des coordonnées GPS lors des changements d'adresse |
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import 'dart:convert';
|
||||
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/repositories/region_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 'package:provider/provider.dart';
|
||||
import 'custom_text_field.dart';
|
||||
|
||||
@@ -124,8 +129,113 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Appeler l'API pour mettre à jour l'entité
|
||||
Future<void> _updateEntite(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 userRepository =
|
||||
Provider.of<UserRepository>(context, listen: false);
|
||||
final userRole = 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;
|
||||
}
|
||||
|
||||
// Appeler l'API
|
||||
try {
|
||||
// Obtenir l'instance du service API
|
||||
final apiService = Provider.of<ApiService>(context, listen: false);
|
||||
|
||||
// Appeler la méthode post du service API
|
||||
await apiService.post('/entite/update', data: data);
|
||||
|
||||
// Fermer l'indicateur de chargement
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Afficher un message de succès
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Entité mise à jour avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
// Appeler la fonction onSubmit si elle existe
|
||||
if (widget.onSubmit != null) {
|
||||
widget.onSubmit!(amicale);
|
||||
}
|
||||
|
||||
// Fermer le formulaire
|
||||
Navigator.of(context).pop();
|
||||
} catch (error) {
|
||||
// Fermer l'indicateur de chargement
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Afficher un message d'erreur
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur lors de la mise à jour de l\'entité: $error'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Fermer l'indicateur de chargement
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Afficher un message d'erreur
|
||||
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,
|
||||
@@ -167,34 +277,156 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
chkActive: _chkActive,
|
||||
);
|
||||
|
||||
// Appeler l'API pour mettre à jour l'entité
|
||||
_updateEntite(amicale);
|
||||
|
||||
// Appeler la fonction onSubmit si elle existe (pour la compatibilité avec le code existant)
|
||||
if (widget.onSubmit != null) {
|
||||
widget.onSubmit!(amicale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final userRepository = Provider.of<UserRepository>(context, listen: false);
|
||||
final userRole = userRepository.getUserRole();
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
|
||||
// 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 formMaxWidth = screenWidth > 800 ? 600.0 : screenWidth;
|
||||
|
||||
return Form(
|
||||
key: _formKey,
|
||||
// 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(
|
||||
constraints: BoxConstraints(maxWidth: formMaxWidth),
|
||||
child: Column(
|
||||
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.location_on,
|
||||
color: Color(0xFF20335E),
|
||||
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 formulaire principal
|
||||
Widget _buildMainForm(ThemeData theme, bool restrictedFieldsReadOnly) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Nom
|
||||
@@ -202,6 +434,7 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
controller: _nameController,
|
||||
label: "Nom",
|
||||
readOnly: widget.readOnly,
|
||||
isRequired: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer un nom";
|
||||
@@ -226,6 +459,13 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
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),
|
||||
|
||||
@@ -253,10 +493,12 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
LengthLimitingTextInputFormatter(5),
|
||||
],
|
||||
readOnly: widget.readOnly,
|
||||
isRequired: true,
|
||||
validator: (value) {
|
||||
if (value != null &&
|
||||
value.isNotEmpty &&
|
||||
value.length < 5) {
|
||||
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;
|
||||
@@ -271,6 +513,13 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
controller: _villeController,
|
||||
label: "Ville",
|
||||
readOnly: widget.readOnly,
|
||||
isRequired: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer une ville";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -320,9 +569,7 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value != null &&
|
||||
value.isNotEmpty &&
|
||||
value.length < 10) {
|
||||
if (value != null && value.isNotEmpty && value.length < 10) {
|
||||
return "Le numéro de téléphone doit contenir 10 chiffres";
|
||||
}
|
||||
return null;
|
||||
@@ -342,9 +589,7 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value != null &&
|
||||
value.isNotEmpty &&
|
||||
value.length < 10) {
|
||||
if (value != null && value.isNotEmpty && value.length < 10) {
|
||||
return "Le numéro de mobile doit contenir 10 chiffres";
|
||||
}
|
||||
return null;
|
||||
@@ -361,6 +606,7 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
label: "Email",
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
readOnly: widget.readOnly,
|
||||
isRequired: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Veuillez entrer l'adresse email";
|
||||
@@ -374,10 +620,7 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Informations avancées (visibles uniquement pour les administrateurs)
|
||||
if (canEditRestrictedFields ||
|
||||
(_gpsLatController.text.isNotEmpty ||
|
||||
_gpsLngController.text.isNotEmpty ||
|
||||
_stripeIdController.text.isNotEmpty)) ...[
|
||||
if (_shouldShowAdvancedInfo()) ...[
|
||||
Text(
|
||||
"Informations avancées",
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
@@ -426,14 +669,53 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
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 = value!;
|
||||
_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(
|
||||
"Stripe activé",
|
||||
"Accepte les règlements en CB",
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onBackground,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -444,8 +726,10 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _stripeIdController,
|
||||
label: "Stripe ID",
|
||||
label: "ID Stripe Paiements CB",
|
||||
readOnly: restrictedFieldsReadOnly,
|
||||
helperText:
|
||||
"Les règlements par CB sont taxés d'une commission de 1.4%",
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -519,10 +803,38 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// Bouton Enregistrer
|
||||
// Boutons Fermer et Enregistrer
|
||||
if (!widget.readOnly)
|
||||
Center(
|
||||
child: ElevatedButton(
|
||||
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),
|
||||
@@ -532,7 +844,7 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
minimumSize: const Size(200, 50),
|
||||
minimumSize: const Size(150, 50),
|
||||
),
|
||||
child: const Text(
|
||||
'Enregistrer',
|
||||
@@ -542,103 +854,37 @@ class _EntiteFormState extends State<EntiteForm> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCheckboxOption({
|
||||
required String label,
|
||||
required bool value,
|
||||
required Function(bool?)? onChanged,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: const Color(0xFF20335E),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onBackground,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRegionDropdown(bool readOnly) {
|
||||
// Vérifier si les informations avancées doivent être affichées
|
||||
bool _shouldShowAdvancedInfo() {
|
||||
final userRepository = Provider.of<UserRepository>(context, listen: false);
|
||||
final userRole = 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 userRepository = Provider.of<UserRepository>(context, listen: false);
|
||||
final userRole = userRepository.getUserRole();
|
||||
|
||||
// Si en lecture seule, afficher simplement le texte
|
||||
if (readOnly && _libRegion != null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
_libRegion!,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Déterminer si l'utilisateur peut modifier les champs restreints
|
||||
final bool canEditRestrictedFields = userRole > 2;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF4F5F6).withOpacity(0.85),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF20335E).withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int>(
|
||||
value: _fkRegion,
|
||||
isExpanded: true,
|
||||
hint: const Text("Sélectionnez une région"),
|
||||
icon: const Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Color(0xFF20335E),
|
||||
),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: const Color(0xFF20335E),
|
||||
),
|
||||
dropdownColor: Colors.white,
|
||||
items: _regions
|
||||
.map<DropdownMenuItem<int>>((Map<String, dynamic> region) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: region['id'] as int,
|
||||
child: Text(region['name'] as String),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: readOnly
|
||||
? null
|
||||
: (int? newValue) {
|
||||
setState(() {
|
||||
_fkRegion = newValue;
|
||||
// Trouver le libellé correspondant
|
||||
if (newValue != null) {
|
||||
final selectedRegion = _regions.firstWhere(
|
||||
(region) => region['id'] == newValue,
|
||||
orElse: () => {'id': newValue, 'name': ''},
|
||||
);
|
||||
_libRegion = selectedRegion['name'] as String;
|
||||
} else {
|
||||
_libRegion = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user