feat: Livraison version 3.0.6

- Amélioration de la gestion des entités et des utilisateurs
- Mise à jour des modèles Amicale et Client avec champs supplémentaires
- Ajout du service de logging et amélioration du chargement UI
- Refactoring des formulaires utilisateur et amicale
- Intégration de file_picker et image_picker pour la gestion des fichiers
- Amélioration de la gestion des membres et de leur suppression
- Optimisation des performances de l'API
- Mise à jour de la documentation technique

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-08 20:33:54 +02:00
parent 599b9fcda0
commit 206c76c7db
69 changed files with 203569 additions and 174972 deletions

View File

@@ -1,11 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:typed_data';
import 'dart:convert';
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 'package:image_picker/image_picker.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io';
import 'package:geosector_app/presentation/widgets/loading_spin_overlay.dart';
import 'custom_text_field.dart';
class AmicaleForm extends StatefulWidget {
@@ -52,6 +58,13 @@ class _AmicaleFormState extends State<AmicaleForm> {
bool _chkAcceptSms = false;
bool _chkActive = true;
bool _chkStripe = false;
bool _chkMdpManuel = false;
bool _chkUsernameManuel = false;
// Pour l'upload du logo
final ImagePicker _picker = ImagePicker();
XFile? _selectedImage;
String? _logoUrl;
@override
void initState() {
@@ -78,6 +91,10 @@ class _AmicaleFormState extends State<AmicaleForm> {
_chkAcceptSms = amicale?.chkAcceptSms ?? false;
_chkActive = amicale?.chkActive ?? true;
_chkStripe = amicale?.chkStripe ?? false;
_chkMdpManuel = amicale?.chkMdpManuel ?? false;
_chkUsernameManuel = amicale?.chkUsernameManuel ?? false;
// Note : Le logo sera chargé dynamiquement depuis l'API
}
@override
@@ -133,6 +150,8 @@ class _AmicaleFormState extends State<AmicaleForm> {
'chk_copie_mail_recu': amicale.chkCopieMailRecu ? 1 : 0,
'chk_accept_sms': amicale.chkAcceptSms ? 1 : 0,
'chk_stripe': amicale.chkStripe ? 1 : 0,
'chk_mdp_manuel': amicale.chkMdpManuel ? 1 : 0,
'chk_username_manuel': amicale.chkUsernameManuel ? 1 : 0,
};
// Ajouter les champs réservés aux administrateurs si l'utilisateur est admin
@@ -232,6 +251,115 @@ class _AmicaleFormState extends State<AmicaleForm> {
}
}
// Méthode pour sélectionner une image
Future<void> _selectImage() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1024,
maxHeight: 1024,
imageQuality: 85,
);
if (image != null) {
// Vérifier la taille du fichier (limite 5 Mo)
final int fileSize = await image.length();
const int maxSize = 5 * 1024 * 1024; // 5 Mo en octets
if (fileSize > maxSize) {
// Fichier trop volumineux
final double sizeMB = fileSize / (1024 * 1024);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Le fichier est trop volumineux (${sizeMB.toStringAsFixed(2)} Mo). '
'La taille maximale autorisée est de 5 Mo.',
),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 5),
),
);
}
return;
}
setState(() {
_selectedImage = image;
});
// Upload immédiatement après sélection
if (widget.amicale?.id != null) {
await _uploadLogo();
}
}
} catch (e) {
debugPrint('Erreur lors de la sélection de l\'image: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la sélection de l\'image: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
// Méthode pour uploader le logo
Future<void> _uploadLogo() async {
if (_selectedImage == null || widget.amicale?.id == null) return;
OverlayEntry? spinOverlay;
try {
// Afficher le spinner
spinOverlay = LoadingSpinOverlayUtils.show(
context: context,
message: 'Upload du logo en cours...',
blurAmount: 10.0,
showCard: true,
);
// Appeler l'API pour uploader le logo
final response = await widget.apiService?.uploadLogo(
widget.amicale!.id,
_selectedImage!,
);
if (response != null && response['status'] == 'success') {
// Succès
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Logo uploadé avec succès'),
backgroundColor: Colors.green,
),
);
}
// Mettre à jour l'amicale avec le nouveau logo en base64
// Note : Le serveur devrait aussi mettre à jour le logo dans la session
// Pour l'instant on garde l'image sélectionnée en preview
setState(() {
// L'image reste en preview jusqu'au prochain login
});
}
} catch (e) {
debugPrint('Erreur lors de l\'upload du logo: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de l\'upload: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
} finally {
// Fermer le spinner
LoadingSpinOverlayUtils.hideSpecific(spinOverlay);
}
}
void _submitForm() {
debugPrint('🔧 _submitForm appelée');
@@ -271,6 +399,8 @@ class _AmicaleFormState extends State<AmicaleForm> {
chkAcceptSms: _chkAcceptSms,
chkActive: _chkActive,
chkStripe: _chkStripe,
chkMdpManuel: _chkMdpManuel,
chkUsernameManuel: _chkUsernameManuel,
) ??
AmicaleModel(
id: 0, // Sera remplacé par l'API
@@ -291,6 +421,9 @@ class _AmicaleFormState extends State<AmicaleForm> {
chkCopieMailRecu: _chkCopieMailRecu,
chkAcceptSms: _chkAcceptSms,
chkActive: _chkActive,
chkStripe: _chkStripe,
chkMdpManuel: _chkMdpManuel,
chkUsernameManuel: _chkUsernameManuel,
);
debugPrint('🔧 AmicaleModel créé: ${amicale.name}');
@@ -305,6 +438,10 @@ class _AmicaleFormState extends State<AmicaleForm> {
// Construire la section logo
Widget _buildLogoSection() {
// Vérifier si on est admin d'amicale (role 2)
final userRole = widget.userRepository.getUserRole();
final canUploadLogo = userRole == 2 && !widget.readOnly;
return Container(
width: 150,
height: 150,
@@ -323,40 +460,40 @@ class _AmicaleFormState extends State<AmicaleForm> {
borderRadius: BorderRadius.circular(8),
child: Stack(
children: [
// Image par défaut
// Afficher l'image sélectionnée, ou le logo depuis l'API, ou l'image par défaut
Center(
child: Image.asset(
'assets/images/logo_recu.png',
width: 150,
height: 150,
fit: BoxFit.contain,
),
child: _buildLogoImage(),
),
// Overlay pour indiquer que l'image est modifiable (si non en lecture seule)
if (!widget.readOnly)
// Overlay pour indiquer que l'image est modifiable (si admin d'amicale)
if (canUploadLogo)
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'),
),
);
},
onTap: _selectImage,
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.1),
color: Colors.black.withOpacity(0.3),
),
child: const Center(
child: Icon(
Icons.camera_alt,
color: Colors.white,
size: 40,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(
Icons.camera_alt,
color: Colors.white,
size: 32,
),
SizedBox(height: 4),
Text(
'Modifier',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
@@ -367,6 +504,102 @@ class _AmicaleFormState extends State<AmicaleForm> {
),
);
}
// Méthode pour construire l'image du logo
Widget _buildLogoImage() {
// 1. Si une image a été sélectionnée localement (preview)
if (_selectedImage != null) {
if (kIsWeb) {
return FutureBuilder<Uint8List>(
future: _selectedImage!.readAsBytes(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Image.memory(
snapshot.data!,
width: 150,
height: 150,
fit: BoxFit.contain,
);
}
return const CircularProgressIndicator();
},
);
} else {
return Image.file(
File(_selectedImage!.path),
width: 150,
height: 150,
fit: BoxFit.contain,
);
}
}
// 2. Si l'amicale a un logo en base64 stocké dans Hive
if (widget.amicale?.logoBase64 != null && widget.amicale!.logoBase64!.isNotEmpty) {
try {
// Le logoBase64 contient déjà le data URL complet (data:image/png;base64,...)
final dataUrl = widget.amicale!.logoBase64!;
// Extraire le base64 du data URL
final base64Data = dataUrl.split(',').last;
final bytes = base64Decode(base64Data);
return Image.memory(
bytes,
width: 150,
height: 150,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
debugPrint('Erreur affichage logo base64: $error');
// En cas d'erreur, essayer l'API
return _buildLogoFromApi();
},
);
} catch (e) {
debugPrint('Erreur décodage base64: $e');
// En cas d'erreur, essayer l'API
return _buildLogoFromApi();
}
}
// 3. Sinon, essayer de charger depuis l'API
return _buildLogoFromApi();
}
// Méthode pour charger le logo depuis l'API
Widget _buildLogoFromApi() {
if (widget.amicale?.id != null && widget.apiService != null) {
// Construire l'URL complète du logo
final logoUrl = '${widget.apiService!.baseUrl}/entites/${widget.amicale!.id}/logo';
return Image.network(
logoUrl,
width: 150,
height: 150,
fit: BoxFit.contain,
headers: {
'Authorization': 'Bearer ${widget.apiService!.sessionId ?? ""}',
},
errorBuilder: (context, error, stackTrace) {
// En cas d'erreur, afficher l'image par défaut
return Image.asset(
'assets/images/logo_recu.png',
width: 150,
height: 150,
fit: BoxFit.contain,
);
},
);
}
// Par défaut, afficher l'image locale
return Image.asset(
'assets/images/logo_recu.png',
width: 150,
height: 150,
fit: BoxFit.contain,
);
}
// Construire la minimap
Widget _buildMiniMap() {
@@ -825,59 +1058,106 @@ class _AmicaleFormState extends State<AmicaleForm> {
),
const SizedBox(height: 8),
// Checkbox Demo
_buildCheckboxOption(
label: "Mode démo",
value: _chkDemo,
onChanged: restrictedFieldsReadOnly
? null
: (value) {
setState(() {
_chkDemo = value!;
});
},
),
const SizedBox(height: 8),
// Options organisées sur 2 colonnes
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Colonne de gauche
Expanded(
child: Column(
children: [
// 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 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 Accept SMS
_buildCheckboxOption(
label: "Accepte les SMS",
value: _chkAcceptSms,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_chkAcceptSms = value!;
});
},
),
],
),
),
const SizedBox(width: 32),
// Colonne de droite
Expanded(
child: Column(
children: [
// Checkbox Active
_buildCheckboxOption(
label: "Actif",
value: _chkActive,
onChanged: restrictedFieldsReadOnly
? null
: (value) {
setState(() {
_chkActive = value!;
});
},
),
const SizedBox(height: 8),
// Checkbox Active
_buildCheckboxOption(
label: "Actif",
value: _chkActive,
onChanged: restrictedFieldsReadOnly
? null
: (value) {
setState(() {
_chkActive = value!;
});
},
// Checkbox Mot de passe manuel
_buildCheckboxOption(
label: "Saisie manuelle des mots de passe",
value: _chkMdpManuel,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_chkMdpManuel = value!;
});
},
),
const SizedBox(height: 8),
// Checkbox Username manuel
_buildCheckboxOption(
label: "Saisie manuelle des identifiants",
value: _chkUsernameManuel,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_chkUsernameManuel = value!;
});
},
),
],
),
),
],
),
const SizedBox(height: 25),