Initial commit: unionflow-mobile-apps

Application Flutter complète (sans build artifacts).

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 16:30:08 +00:00
commit d094d6db9c
1790 changed files with 507435 additions and 0 deletions

View File

@@ -0,0 +1,533 @@
/// Page de création d'une nouvelle organisation
/// Respecte strictement le design system établi dans l'application
library create_organisation_page;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../data/models/organization_model.dart';
import '../../bloc/organizations_bloc.dart';
import '../../bloc/organizations_event.dart';
import '../../bloc/organizations_state.dart';
/// Page de création d'organisation avec design system cohérent
class CreateOrganizationPage extends StatefulWidget {
const CreateOrganizationPage({super.key});
@override
State<CreateOrganizationPage> createState() => _CreateOrganizationPageState();
}
class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
final _formKey = GlobalKey<FormState>();
final _nomController = TextEditingController();
final _nomCourtController = TextEditingController();
final _descriptionController = TextEditingController();
final _emailController = TextEditingController();
final _telephoneController = TextEditingController();
final _siteWebController = TextEditingController();
final _adresseController = TextEditingController();
final _villeController = TextEditingController();
final _regionController = TextEditingController();
final _paysController = TextEditingController();
TypeOrganization _selectedType = TypeOrganization.association;
StatutOrganization _selectedStatut = StatutOrganization.active;
@override
void dispose() {
_nomController.dispose();
_nomCourtController.dispose();
_descriptionController.dispose();
_emailController.dispose();
_telephoneController.dispose();
_siteWebController.dispose();
_adresseController.dispose();
_villeController.dispose();
_regionController.dispose();
_paysController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA), // Background cohérent
appBar: AppBar(
backgroundColor: const Color(0xFF6C5CE7),
foregroundColor: Colors.white,
title: const Text('Nouvelle Organisation'),
elevation: 0,
actions: [
TextButton(
onPressed: _isFormValid() ? _saveOrganisation : null,
child: const Text(
'Enregistrer',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
],
),
body: BlocListener<OrganizationsBloc, OrganizationsState>(
listener: (context, state) {
if (state is OrganizationCreated) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Organisation créée avec succès'),
backgroundColor: Color(0xFF10B981),
),
);
Navigator.of(context).pop(true); // Retour avec succès
} else if (state is OrganizationsError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
);
}
},
child: Form(
key: _formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(12), // SpacingTokens cohérent
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildBasicInfoCard(),
const SizedBox(height: 16),
_buildContactCard(),
const SizedBox(height: 16),
_buildLocationCard(),
const SizedBox(height: 16),
_buildConfigurationCard(),
const SizedBox(height: 24),
_buildActionButtons(),
],
),
),
),
),
);
}
/// Carte des informations de base
Widget _buildBasicInfoCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8), // RadiusTokens cohérent
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informations de base',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _nomController,
decoration: const InputDecoration(
labelText: 'Nom de l\'organisation *',
hintText: 'Ex: Association des Développeurs',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.business),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Le nom est obligatoire';
}
if (value.trim().length < 3) {
return 'Le nom doit contenir au moins 3 caractères';
}
return null;
},
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
TextFormField(
controller: _nomCourtController,
decoration: const InputDecoration(
labelText: 'Nom court (optionnel)',
hintText: 'Ex: AsDev',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.short_text),
),
validator: (value) {
if (value != null && value.trim().isNotEmpty && value.trim().length < 2) {
return 'Le nom court doit contenir au moins 2 caractères';
}
return null;
},
),
const SizedBox(height: 16),
DropdownButtonFormField<TypeOrganization>(
value: _selectedType,
decoration: const InputDecoration(
labelText: 'Type d\'organisation *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.category),
),
items: TypeOrganization.values.map((type) {
return DropdownMenuItem(
value: type,
child: Row(
children: [
Text(type.icon, style: const TextStyle(fontSize: 16)),
const SizedBox(width: 8),
Text(type.displayName),
],
),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedType = value;
});
}
},
),
const SizedBox(height: 16),
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Description (optionnel)',
hintText: 'Décrivez brièvement l\'organisation...',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.description),
),
maxLines: 3,
validator: (value) {
if (value != null && value.trim().isNotEmpty && value.trim().length < 10) {
return 'La description doit contenir au moins 10 caractères';
}
return null;
},
),
],
),
);
}
/// Carte des informations de contact
Widget _buildContactCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Contact',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email (optionnel)',
hintText: 'contact@organisation.com',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value != null && value.trim().isNotEmpty) {
final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+$');
if (!emailRegex.hasMatch(value.trim())) {
return 'Format d\'email invalide';
}
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _telephoneController,
decoration: const InputDecoration(
labelText: 'Téléphone (optionnel)',
hintText: '+225 XX XX XX XX XX',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.phone),
),
keyboardType: TextInputType.phone,
validator: (value) {
if (value != null && value.trim().isNotEmpty && value.trim().length < 8) {
return 'Numéro de téléphone invalide';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _siteWebController,
decoration: const InputDecoration(
labelText: 'Site web (optionnel)',
hintText: 'https://www.organisation.com',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.web),
),
keyboardType: TextInputType.url,
validator: (value) {
if (value != null && value.trim().isNotEmpty) {
final urlRegex = RegExp(r'^https?://[^\s]+$');
if (!urlRegex.hasMatch(value.trim())) {
return 'Format d\'URL invalide (doit commencer par http:// ou https://)';
}
}
return null;
},
),
],
),
);
}
/// Carte de localisation
Widget _buildLocationCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Localisation',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _adresseController,
decoration: const InputDecoration(
labelText: 'Adresse (optionnel)',
hintText: 'Rue, quartier...',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.location_on),
),
maxLines: 2,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
controller: _villeController,
decoration: const InputDecoration(
labelText: 'Ville',
hintText: 'Abidjan',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.location_city),
),
),
),
const SizedBox(width: 12),
Expanded(
child: TextFormField(
controller: _regionController,
decoration: const InputDecoration(
labelText: 'Région',
hintText: 'Lagunes',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.map),
),
),
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: _paysController,
decoration: const InputDecoration(
labelText: 'Pays',
hintText: 'Côte d\'Ivoire',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.flag),
),
),
],
),
);
}
/// Carte de configuration
Widget _buildConfigurationCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Configuration',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
DropdownButtonFormField<StatutOrganization>(
value: _selectedStatut,
decoration: const InputDecoration(
labelText: 'Statut initial *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.toggle_on),
),
items: StatutOrganization.values.map((statut) {
final color = Color(int.parse(statut.color.substring(1), radix: 16) + 0xFF000000);
return DropdownMenuItem(
value: statut,
child: Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(statut.displayName),
],
),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedStatut = value;
});
}
},
),
],
),
);
}
/// Boutons d'action
Widget _buildActionButtons() {
return Column(
children: [
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isFormValid() ? _saveOrganisation : null,
icon: const Icon(Icons.save),
label: const Text('Créer l\'organisation'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C5CE7),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.cancel),
label: const Text('Annuler'),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF6B7280),
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
);
}
/// Vérifie si le formulaire est valide
bool _isFormValid() {
return _nomController.text.trim().isNotEmpty;
}
/// Sauvegarde l'organisation
void _saveOrganisation() {
if (_formKey.currentState?.validate() ?? false) {
final organisation = OrganizationModel(
nom: _nomController.text.trim(),
nomCourt: _nomCourtController.text.trim().isEmpty ? null : _nomCourtController.text.trim(),
description: _descriptionController.text.trim().isEmpty ? null : _descriptionController.text.trim(),
typeOrganisation: _selectedType,
statut: _selectedStatut,
email: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
telephone: _telephoneController.text.trim().isEmpty ? null : _telephoneController.text.trim(),
siteWeb: _siteWebController.text.trim().isEmpty ? null : _siteWebController.text.trim(),
adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(),
ville: _villeController.text.trim().isEmpty ? null : _villeController.text.trim(),
region: _regionController.text.trim().isEmpty ? null : _regionController.text.trim(),
pays: _paysController.text.trim().isEmpty ? null : _paysController.text.trim(),
dateCreation: DateTime.now(),
nombreMembres: 0,
);
context.read<OrganizationsBloc>().add(CreateOrganization(organisation));
}
}
}

View File

@@ -0,0 +1,705 @@
/// Page d'édition d'une organisation existante
/// Respecte strictement le design system établi dans l'application
library edit_organisation_page;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../data/models/organization_model.dart';
import '../../bloc/organizations_bloc.dart';
import '../../bloc/organizations_event.dart';
import '../../bloc/organizations_state.dart';
/// Page d'édition d'organisation avec design system cohérent
class EditOrganizationPage extends StatefulWidget {
final OrganizationModel organization;
const EditOrganizationPage({
super.key,
required this.organization,
});
@override
State<EditOrganizationPage> createState() => _EditOrganizationPageState();
}
class _EditOrganizationPageState extends State<EditOrganizationPage> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nomController;
late final TextEditingController _nomCourtController;
late final TextEditingController _descriptionController;
late final TextEditingController _emailController;
late final TextEditingController _telephoneController;
late final TextEditingController _siteWebController;
late final TextEditingController _adresseController;
late final TextEditingController _villeController;
late final TextEditingController _regionController;
late final TextEditingController _paysController;
late TypeOrganization _selectedType;
late StatutOrganization _selectedStatut;
@override
void initState() {
super.initState();
// Initialiser les contrôleurs avec les valeurs existantes
_nomController = TextEditingController(text: widget.organization.nom);
_nomCourtController = TextEditingController(text: widget.organization.nomCourt ?? '');
_descriptionController = TextEditingController(text: widget.organization.description ?? '');
_emailController = TextEditingController(text: widget.organization.email ?? '');
_telephoneController = TextEditingController(text: widget.organization.telephone ?? '');
_siteWebController = TextEditingController(text: widget.organization.siteWeb ?? '');
_adresseController = TextEditingController(text: widget.organization.adresse ?? '');
_villeController = TextEditingController(text: widget.organization.ville ?? '');
_regionController = TextEditingController(text: widget.organization.region ?? '');
_paysController = TextEditingController(text: widget.organization.pays ?? '');
_selectedType = widget.organization.typeOrganisation;
_selectedStatut = widget.organization.statut;
}
@override
void dispose() {
_nomController.dispose();
_nomCourtController.dispose();
_descriptionController.dispose();
_emailController.dispose();
_telephoneController.dispose();
_siteWebController.dispose();
_adresseController.dispose();
_villeController.dispose();
_regionController.dispose();
_paysController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA), // Background cohérent
appBar: AppBar(
backgroundColor: const Color(0xFF6C5CE7),
foregroundColor: Colors.white,
title: const Text('Modifier Organisation'),
elevation: 0,
actions: [
TextButton(
onPressed: _hasChanges() ? _saveChanges : null,
child: const Text(
'Enregistrer',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
],
),
body: BlocListener<OrganizationsBloc, OrganizationsState>(
listener: (context, state) {
if (state is OrganizationUpdated) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Organisation modifiée avec succès'),
backgroundColor: Color(0xFF10B981),
),
);
Navigator.of(context).pop(true); // Retour avec succès
} else if (state is OrganizationsError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
);
}
},
child: Form(
key: _formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(12), // SpacingTokens cohérent
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildBasicInfoCard(),
const SizedBox(height: 16),
_buildContactCard(),
const SizedBox(height: 16),
_buildLocationCard(),
const SizedBox(height: 16),
_buildConfigurationCard(),
const SizedBox(height: 16),
_buildMetadataCard(),
const SizedBox(height: 24),
_buildActionButtons(),
],
),
),
),
),
);
}
/// Carte des informations de base
Widget _buildBasicInfoCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8), // RadiusTokens cohérent
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informations de base',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _nomController,
decoration: const InputDecoration(
labelText: 'Nom de l\'organisation *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.business),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Le nom est obligatoire';
}
if (value.trim().length < 3) {
return 'Le nom doit contenir au moins 3 caractères';
}
return null;
},
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
TextFormField(
controller: _nomCourtController,
decoration: const InputDecoration(
labelText: 'Nom court (optionnel)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.short_text),
),
validator: (value) {
if (value != null && value.trim().isNotEmpty && value.trim().length < 2) {
return 'Le nom court doit contenir au moins 2 caractères';
}
return null;
},
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
DropdownButtonFormField<TypeOrganization>(
value: _selectedType,
decoration: const InputDecoration(
labelText: 'Type d\'organisation *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.category),
),
items: TypeOrganization.values.map((type) {
return DropdownMenuItem(
value: type,
child: Row(
children: [
Text(type.icon, style: const TextStyle(fontSize: 16)),
const SizedBox(width: 8),
Text(type.displayName),
],
),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedType = value;
});
}
},
),
const SizedBox(height: 16),
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Description (optionnel)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.description),
),
maxLines: 3,
validator: (value) {
if (value != null && value.trim().isNotEmpty && value.trim().length < 10) {
return 'La description doit contenir au moins 10 caractères';
}
return null;
},
onChanged: (_) => setState(() {}),
),
],
),
);
}
/// Carte des informations de contact
Widget _buildContactCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Contact',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email (optionnel)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value != null && value.trim().isNotEmpty) {
final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+$');
if (!emailRegex.hasMatch(value.trim())) {
return 'Format d\'email invalide';
}
}
return null;
},
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
TextFormField(
controller: _telephoneController,
decoration: const InputDecoration(
labelText: 'Téléphone (optionnel)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.phone),
),
keyboardType: TextInputType.phone,
validator: (value) {
if (value != null && value.trim().isNotEmpty && value.trim().length < 8) {
return 'Numéro de téléphone invalide';
}
return null;
},
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
TextFormField(
controller: _siteWebController,
decoration: const InputDecoration(
labelText: 'Site web (optionnel)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.web),
),
keyboardType: TextInputType.url,
validator: (value) {
if (value != null && value.trim().isNotEmpty) {
final urlRegex = RegExp(r'^https?://[^\s]+$');
if (!urlRegex.hasMatch(value.trim())) {
return 'Format d\'URL invalide (doit commencer par http:// ou https://)';
}
}
return null;
},
onChanged: (_) => setState(() {}),
),
],
),
);
}
/// Carte de localisation
Widget _buildLocationCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Localisation',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _adresseController,
decoration: const InputDecoration(
labelText: 'Adresse (optionnel)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.location_on),
),
maxLines: 2,
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
controller: _villeController,
decoration: const InputDecoration(
labelText: 'Ville',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.location_city),
),
onChanged: (_) => setState(() {}),
),
),
const SizedBox(width: 12),
Expanded(
child: TextFormField(
controller: _regionController,
decoration: const InputDecoration(
labelText: 'Région',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.map),
),
onChanged: (_) => setState(() {}),
),
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: _paysController,
decoration: const InputDecoration(
labelText: 'Pays',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.flag),
),
onChanged: (_) => setState(() {}),
),
],
),
);
}
/// Carte de configuration
Widget _buildConfigurationCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Configuration',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
DropdownButtonFormField<StatutOrganization>(
value: _selectedStatut,
decoration: const InputDecoration(
labelText: 'Statut *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.toggle_on),
),
items: StatutOrganization.values.map((statut) {
final color = Color(int.parse(statut.color.substring(1), radix: 16) + 0xFF000000);
return DropdownMenuItem(
value: statut,
child: Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(statut.displayName),
],
),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedStatut = value;
});
}
},
),
],
),
);
}
/// Carte des métadonnées (lecture seule)
Widget _buildMetadataCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informations système',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
_buildReadOnlyField(
icon: Icons.fingerprint,
label: 'ID',
value: widget.organization.id ?? 'Non défini',
),
const SizedBox(height: 12),
_buildReadOnlyField(
icon: Icons.calendar_today,
label: 'Date de création',
value: _formatDate(widget.organization.dateCreation),
),
const SizedBox(height: 12),
_buildReadOnlyField(
icon: Icons.people,
label: 'Nombre de membres',
value: widget.organization.nombreMembres.toString(),
),
if (widget.organization.ancienneteAnnees > 0) ...[
const SizedBox(height: 12),
_buildReadOnlyField(
icon: Icons.access_time,
label: 'Ancienneté',
value: '${widget.organization.ancienneteAnnees} ans',
),
],
],
),
);
}
/// Champ en lecture seule
Widget _buildReadOnlyField({
required IconData icon,
required String label,
required String value,
}) {
return Row(
children: [
Icon(
icon,
size: 20,
color: const Color(0xFF6B7280),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF374151),
fontWeight: FontWeight.w600,
),
),
],
),
),
],
);
}
/// Boutons d'action
Widget _buildActionButtons() {
return Column(
children: [
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _hasChanges() ? _saveChanges : null,
icon: const Icon(Icons.save),
label: const Text('Enregistrer les modifications'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C5CE7),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () => _showDiscardDialog(),
icon: const Icon(Icons.cancel),
label: const Text('Annuler'),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF6B7280),
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
);
}
/// Vérifie s'il y a des changements
bool _hasChanges() {
return _nomController.text.trim() != widget.organization.nom ||
_nomCourtController.text.trim() != (widget.organization.nomCourt ?? '') ||
_descriptionController.text.trim() != (widget.organization.description ?? '') ||
_emailController.text.trim() != (widget.organization.email ?? '') ||
_telephoneController.text.trim() != (widget.organization.telephone ?? '') ||
_siteWebController.text.trim() != (widget.organization.siteWeb ?? '') ||
_adresseController.text.trim() != (widget.organization.adresse ?? '') ||
_villeController.text.trim() != (widget.organization.ville ?? '') ||
_regionController.text.trim() != (widget.organization.region ?? '') ||
_paysController.text.trim() != (widget.organization.pays ?? '') ||
_selectedType != widget.organization.typeOrganisation ||
_selectedStatut != widget.organization.statut;
}
/// Sauvegarde les modifications
void _saveChanges() {
if (_formKey.currentState?.validate() ?? false) {
final updatedOrganisation = widget.organization.copyWith(
nom: _nomController.text.trim(),
nomCourt: _nomCourtController.text.trim().isEmpty ? null : _nomCourtController.text.trim(),
description: _descriptionController.text.trim().isEmpty ? null : _descriptionController.text.trim(),
typeOrganisation: _selectedType,
statut: _selectedStatut,
email: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
telephone: _telephoneController.text.trim().isEmpty ? null : _telephoneController.text.trim(),
siteWeb: _siteWebController.text.trim().isEmpty ? null : _siteWebController.text.trim(),
adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(),
ville: _villeController.text.trim().isEmpty ? null : _villeController.text.trim(),
region: _regionController.text.trim().isEmpty ? null : _regionController.text.trim(),
pays: _paysController.text.trim().isEmpty ? null : _paysController.text.trim(),
);
if (widget.organization.id != null) {
context.read<OrganizationsBloc>().add(
UpdateOrganization(widget.organization.id!, updatedOrganisation),
);
}
}
}
/// Affiche le dialog de confirmation d'annulation
void _showDiscardDialog() {
if (_hasChanges()) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Annuler les modifications'),
content: const Text('Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir les abandonner ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Continuer l\'édition'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(); // Fermer le dialog
Navigator.of(context).pop(); // Retour à la page précédente
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Abandonner', style: TextStyle(color: Colors.white)),
),
],
),
);
} else {
Navigator.of(context).pop();
}
}
/// Formate une date
String _formatDate(DateTime? date) {
if (date == null) return 'Non spécifiée';
return '${date.day}/${date.month}/${date.year}';
}
}

View File

@@ -0,0 +1,832 @@
/// Page de détail d'une organisation
/// Respecte strictement le design system établi dans l'application
library organisation_detail_page;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../data/models/organization_model.dart';
import '../../bloc/organizations_bloc.dart';
import '../../bloc/organizations_event.dart';
import '../../bloc/organizations_state.dart';
import 'edit_organization_page.dart';
/// Page de détail d'une organisation avec design system cohérent
class OrganizationDetailPage extends StatefulWidget {
final String organizationId;
const OrganizationDetailPage({
super.key,
required this.organizationId,
});
@override
State<OrganizationDetailPage> createState() => _OrganizationDetailPageState();
}
class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
@override
void initState() {
super.initState();
// Charger les détails de l'organisation
context.read<OrganizationsBloc>().add(LoadOrganizationById(widget.organizationId));
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA), // Background cohérent
appBar: AppBar(
backgroundColor: const Color(0xFF6C5CE7),
foregroundColor: Colors.white,
title: const Text('Détail Organisation'),
elevation: 0,
actions: [
IconButton(
onPressed: () => _showEditDialog(),
icon: const Icon(Icons.edit),
tooltip: 'Modifier',
),
PopupMenuButton<String>(
onSelected: (value) => _handleMenuAction(value),
itemBuilder: (context) => [
const PopupMenuItem(
value: 'activate',
child: Row(
children: [
Icon(Icons.check_circle, color: Color(0xFF10B981)),
SizedBox(width: 8),
Text('Activer'),
],
),
),
const PopupMenuItem(
value: 'deactivate',
child: Row(
children: [
Icon(Icons.pause_circle, color: Color(0xFF6B7280)),
SizedBox(width: 8),
Text('Désactiver'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
Text('Supprimer'),
],
),
),
],
),
],
),
body: BlocBuilder<OrganizationsBloc, OrganizationsState>(
builder: (context, state) {
if (state is OrganizationLoading) {
return _buildLoadingState();
} else if (state is OrganizationLoaded) {
return _buildDetailContent(state.organization);
} else if (state is OrganizationsError) {
return _buildErrorState(state);
}
return _buildEmptyState();
},
),
);
}
/// État de chargement
Widget _buildLoadingState() {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6C5CE7)),
),
SizedBox(height: 16),
Text(
'Chargement des détails...',
style: TextStyle(
fontSize: 16,
color: Color(0xFF6B7280),
),
),
],
),
);
}
/// Contenu principal avec les détails
Widget _buildDetailContent(OrganizationModel organization) {
return SingleChildScrollView(
padding: const EdgeInsets.all(12), // SpacingTokens cohérent
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderCard(organization),
const SizedBox(height: 16),
_buildInfoCard(organization),
const SizedBox(height: 16),
_buildStatsCard(organization),
const SizedBox(height: 16),
_buildContactCard(organization),
const SizedBox(height: 16),
_buildActionsCard(organization),
],
),
);
}
/// Carte d'en-tête avec informations principales
Widget _buildHeaderCard(OrganizationModel organization) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF6C5CE7),
const Color(0xFF6C5CE7).withOpacity(0.8),
],
),
borderRadius: BorderRadius.circular(8), // RadiusTokens cohérent
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
organization.typeOrganisation.icon,
style: const TextStyle(fontSize: 24),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
organization.nom,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
if (organization.nomCourt?.isNotEmpty == true) ...[
const SizedBox(height: 4),
Text(
organization.nomCourt!,
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
),
),
],
const SizedBox(height: 8),
_buildStatusBadge(organization.statut),
],
),
),
],
),
if (organization.description?.isNotEmpty == true) ...[
const SizedBox(height: 16),
Text(
organization.description!,
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
height: 1.4,
),
),
],
],
),
);
}
/// Badge de statut
Widget _buildStatusBadge(StatutOrganization statut) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
),
child: Text(
statut.displayName,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
);
}
/// Carte d'informations générales
Widget _buildInfoCard(OrganizationModel organization) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informations générales',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
_buildInfoRow(
icon: Icons.category,
label: 'Type',
value: organization.typeOrganisation.displayName,
),
const SizedBox(height: 12),
_buildInfoRow(
icon: Icons.location_on,
label: 'Localisation',
value: _buildLocationText(organization),
),
const SizedBox(height: 12),
_buildInfoRow(
icon: Icons.calendar_today,
label: 'Date de création',
value: _formatDate(organization.dateCreation),
),
if (organization.ancienneteAnnees > 0) ...[
const SizedBox(height: 12),
_buildInfoRow(
icon: Icons.access_time,
label: 'Ancienneté',
value: '${organization.ancienneteAnnees} ans',
),
],
],
),
);
}
/// Ligne d'information
Widget _buildInfoRow({
required IconData icon,
required String label,
required String value,
}) {
return Row(
children: [
Icon(
icon,
size: 20,
color: const Color(0xFF6C5CE7),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF374151),
fontWeight: FontWeight.w600,
),
),
],
),
),
],
);
}
/// Carte de statistiques
Widget _buildStatsCard(OrganizationModel organization) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Statistiques',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatItem(
icon: Icons.people,
label: 'Membres',
value: organization.nombreMembres.toString(),
color: const Color(0xFF3B82F6),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatItem(
icon: Icons.event,
label: 'Événements',
value: (organization.nombreEvenements ?? 0).toString(),
color: const Color(0xFF10B981),
),
),
],
),
],
),
);
}
/// Item de statistique
Widget _buildStatItem({
required IconData icon,
required String label,
required String value,
required Color color,
}) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.05),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: color.withOpacity(0.1),
width: 1,
),
),
child: Column(
children: [
Icon(
icon,
size: 24,
color: color,
),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: color,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
/// Carte de contact
Widget _buildContactCard(OrganizationModel organization) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Contact',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
if (organization.email?.isNotEmpty == true)
_buildContactRow(
icon: Icons.email,
label: 'Email',
value: organization.email!,
onTap: () => _launchEmail(organization.email!),
),
if (organization.telephone?.isNotEmpty == true) ...[
const SizedBox(height: 12),
_buildContactRow(
icon: Icons.phone,
label: 'Téléphone',
value: organization.telephone!,
onTap: () => _launchPhone(organization.telephone!),
),
],
if (organization.siteWeb?.isNotEmpty == true) ...[
const SizedBox(height: 12),
_buildContactRow(
icon: Icons.web,
label: 'Site web',
value: organization.siteWeb!,
onTap: () => _launchWebsite(organization.siteWeb!),
),
],
],
),
);
}
/// Ligne de contact
Widget _buildContactRow({
required IconData icon,
required String label,
required String value,
VoidCallback? onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(6),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(
icon,
size: 20,
color: const Color(0xFF6C5CE7),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
fontSize: 14,
color: onTap != null ? const Color(0xFF6C5CE7) : const Color(0xFF374151),
fontWeight: FontWeight.w600,
decoration: onTap != null ? TextDecoration.underline : null,
),
),
],
),
),
if (onTap != null)
const Icon(
Icons.open_in_new,
size: 16,
color: Color(0xFF6C5CE7),
),
],
),
),
);
}
/// Carte d'actions
Widget _buildActionsCard(OrganizationModel organization) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Actions',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _showEditDialog(organization),
icon: const Icon(Icons.edit),
label: const Text('Modifier'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C5CE7),
foregroundColor: Colors.white,
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: () => _showDeleteConfirmation(organization),
icon: const Icon(Icons.delete),
label: const Text('Supprimer'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: const BorderSide(color: Colors.red),
),
),
),
],
),
],
),
);
}
/// État d'erreur
Widget _buildErrorState(OrganizationsError state) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Colors.red.shade400,
),
const SizedBox(height: 16),
Text(
'Erreur',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.red.shade700,
),
),
const SizedBox(height: 8),
Text(
state.message,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF6B7280),
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
context.read<OrganizationsBloc>().add(LoadOrganizationById(widget.organizationId));
},
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C5CE7),
foregroundColor: Colors.white,
),
),
],
),
);
}
/// État vide
Widget _buildEmptyState() {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.business_outlined,
size: 64,
color: Color(0xFF6B7280),
),
SizedBox(height: 16),
Text(
'Organisation non trouvée',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF374151),
),
),
],
),
);
}
/// Construit le texte de localisation
String _buildLocationText(OrganizationModel organization) {
final parts = <String>[];
if (organization.ville?.isNotEmpty == true) {
parts.add(organization.ville!);
}
if (organization.region?.isNotEmpty == true) {
parts.add(organization.region!);
}
if (organization.pays?.isNotEmpty == true) {
parts.add(organization.pays!);
}
return parts.isEmpty ? 'Non spécifiée' : parts.join(', ');
}
/// Formate une date
String _formatDate(DateTime? date) {
if (date == null) return 'Non spécifiée';
return '${date.day}/${date.month}/${date.year}';
}
/// Actions du menu
void _handleMenuAction(String action) {
switch (action) {
case 'activate':
context.read<OrganizationsBloc>().add(ActivateOrganization(widget.organizationId));
break;
case 'deactivate':
context.read<OrganizationsBloc>().add(SuspendOrganization(widget.organizationId));
break;
case 'delete':
_showDeleteConfirmation(null);
break;
}
}
/// Ouvre la page d'édition ou le dialog selon le contexte
void _showEditDialog([OrganizationModel? organization]) {
if (organization == null) {
final state = context.read<OrganizationsBloc>().state;
if (state is OrganizationLoaded) {
organization = state.organization;
}
}
if (organization == null || !context.mounted) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chargement de l\'organisation en cours...')),
);
}
return;
}
final org = organization;
final bloc = context.read<OrganizationsBloc>();
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => BlocProvider.value(
value: bloc,
child: EditOrganizationPage(organization: org),
),
),
).then((_) {
if (context.mounted) {
bloc.add(LoadOrganizationById(widget.organizationId));
}
});
}
/// Affiche la confirmation de suppression
void _showDeleteConfirmation(OrganizationModel? organization) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmer la suppression'),
content: Text(
organization != null
? 'Êtes-vous sûr de vouloir supprimer "${organization.nom}" ?'
: 'Êtes-vous sûr de vouloir supprimer cette organisation ?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
context.read<OrganizationsBloc>().add(DeleteOrganization(widget.organizationId));
Navigator.of(context).pop(); // Retour à la liste
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Supprimer', style: TextStyle(color: Colors.white)),
),
],
),
);
}
/// Lance l'application email
Future<void> _launchEmail(String email) async {
final uri = Uri(scheme: 'mailto', path: email);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Impossible d\'ouvrir l\'email: $email')),
);
}
}
}
/// Lance l'application téléphone
Future<void> _launchPhone(String phone) async {
final uri = Uri(scheme: 'tel', path: phone);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Impossible d\'appeler: $phone')),
);
}
}
}
/// Lance le navigateur web
Future<void> _launchWebsite(String url) async {
final uri = Uri.parse(url.startsWith('http') ? url : 'https://$url');
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Impossible d\'ouvrir: $url')),
);
}
}
}
}

View File

@@ -0,0 +1,826 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/organizations_bloc.dart';
import '../../bloc/organizations_event.dart';
import '../../bloc/organizations_state.dart';
import '../../data/models/organization_model.dart';
import '../widgets/organization_card.dart';
import '../widgets/create_organization_dialog.dart';
import '../widgets/edit_organization_dialog.dart';
import 'organization_detail_page.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
import '../../../../shared/design_system/unionflow_design_v2.dart';
import '../../../../shared/design_system/components/animated_fade_in.dart';
import '../../../../shared/design_system/components/animated_slide_in.dart';
import '../../../../shared/design_system/components/african_pattern_background.dart';
import '../../../../shared/design_system/components/uf_app_bar.dart';
/// Page de gestion des organisations - Interface sophistiquée et exhaustive
///
/// Cette page offre une interface complète pour la gestion des organisations
/// avec des fonctionnalités avancées de recherche, filtrage, statistiques
/// et actions de gestion basées sur les permissions utilisateur.
///
/// **Design System V2** - Utilise UnionFlowColors et composants standardisés
/// **Backend connecté** - Toutes les données proviennent d'OrganizationsBloc
class OrganizationsPage extends StatefulWidget {
const OrganizationsPage({super.key});
@override
State<OrganizationsPage> createState() => _OrganizationsPageState();
}
class _OrganizationsPageState extends State<OrganizationsPage> with TickerProviderStateMixin {
// Controllers et état
final TextEditingController _searchController = TextEditingController();
TabController? _tabController;
final ScrollController _scrollController = ScrollController();
List<TypeOrganization?> _availableTypes = [];
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
// Les organisations sont déjà chargées par OrganizationsPageWrapper
// Le TabController sera initialisé dans didChangeDependencies
}
@override
void dispose() {
_tabController?.dispose();
_searchController.dispose();
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.9) {
// Charger plus d'organisations quand on approche du bas
context.read<OrganizationsBloc>().add(const LoadMoreOrganizations());
}
}
/// Calcule les types d'organisations disponibles dans les données
List<TypeOrganization?> _calculateAvailableTypes(List<OrganizationModel> organizations) {
if (organizations.isEmpty) {
return [null]; // Seulement "Toutes"
}
// Extraire tous les types uniques
final typesSet = organizations.map((org) => org.typeOrganisation).toSet();
final types = typesSet.toList()..sort((a, b) => a.displayName.compareTo(b.displayName));
// null en premier pour "Toutes", puis les types triés alphabétiquement
return [null, ...types];
}
/// Initialise ou met à jour le TabController si les types ont changé
void _updateTabController(List<TypeOrganization?> newTypes) {
if (_availableTypes.length != newTypes.length ||
!_availableTypes.every((type) => newTypes.contains(type))) {
_availableTypes = newTypes;
_tabController?.dispose();
_tabController = TabController(length: _availableTypes.length, vsync: this);
}
}
@override
Widget build(BuildContext context) {
return BlocConsumer<OrganizationsBloc, OrganizationsState>(
listener: (context, state) {
// Gestion des messages de succès et erreurs
if (state is OrganizationsError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: UnionFlowColors.error,
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: 'Réessayer',
textColor: Colors.white,
onPressed: () {
context.read<OrganizationsBloc>().add(const LoadOrganizations(refresh: true));
},
),
),
);
} else if (state is OrganizationCreated) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Organisation créée avec succès'),
backgroundColor: UnionFlowColors.success,
duration: Duration(seconds: 2),
),
);
} else if (state is OrganizationUpdated) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Organisation mise à jour avec succès'),
backgroundColor: UnionFlowColors.success,
duration: Duration(seconds: 2),
),
);
} else if (state is OrganizationDeleted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Organisation supprimée avec succès'),
backgroundColor: UnionFlowColors.success,
duration: Duration(seconds: 2),
),
);
}
},
builder: (context, state) {
return AfricanPatternBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: UFAppBar(
title: 'Gestion des Organisations',
backgroundColor: UnionFlowColors.surface,
foregroundColor: UnionFlowColors.textPrimary,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
context.read<OrganizationsBloc>().add(const RefreshOrganizations());
},
tooltip: 'Rafraîchir',
),
],
),
body: SafeArea(
child: _buildBody(state),
),
floatingActionButton: _buildActionButton(state),
),
);
},
);
}
Widget _buildBody(OrganizationsState state) {
if (state is OrganizationsInitial || state is OrganizationsLoading) {
return _buildLoadingState();
}
if (state is OrganizationsLoaded) {
final loadedState = state;
// Calculer les types disponibles et mettre à jour le TabController
final availableTypes = _calculateAvailableTypes(loadedState.organizations);
_updateTabController(availableTypes);
return RefreshIndicator(
onRefresh: () async {
context.read<OrganizationsBloc>().add(const RefreshOrganizations());
},
child: SingleChildScrollView(
controller: _scrollController,
padding: const EdgeInsets.all(SpacingTokens.md),
physics: const AlwaysScrollableScrollPhysics(),
child: AnimatedFadeIn(
duration: const Duration(milliseconds: 400),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec design system
AnimatedSlideIn(
duration: const Duration(milliseconds: 500),
curve: Curves.easeOut,
child: _buildHeader(loadedState),
),
const SizedBox(height: SpacingTokens.md),
// Section statistiques
AnimatedSlideIn(
duration: const Duration(milliseconds: 600),
curve: Curves.easeOut,
child: _buildStatsSection(loadedState),
),
const SizedBox(height: SpacingTokens.md),
// Barre de recherche
AnimatedSlideIn(
duration: const Duration(milliseconds: 700),
curve: Curves.easeOut,
child: _buildSearchBar(loadedState),
),
const SizedBox(height: SpacingTokens.md),
// Onglets de catégories dynamiques
AnimatedSlideIn(
duration: const Duration(milliseconds: 800),
curve: Curves.easeOut,
child: _buildCategoryTabs(availableTypes),
),
const SizedBox(height: SpacingTokens.md),
// Liste des organisations
AnimatedSlideIn(
duration: const Duration(milliseconds: 900),
curve: Curves.easeOut,
child: _buildOrganizationsList(loadedState),
),
const SizedBox(height: 80), // Espace pour le FAB
],
),
),
),
);
}
if (state is OrganizationsLoadingMore) {
// Show current organizations with loading indicator at bottom
return SingleChildScrollView(
controller: _scrollController,
padding: const EdgeInsets.all(SpacingTokens.md),
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLoadingMorePlaceholder(state.currentOrganizations),
const Padding(
padding: EdgeInsets.all(SpacingTokens.md),
child: Center(
child: CircularProgressIndicator(
color: UnionFlowColors.unionGreen,
),
),
),
const SizedBox(height: 80),
],
),
);
}
if (state is OrganizationsError) {
return _buildErrorState(state);
}
return _buildLoadingState();
}
/// Placeholder pour affichage pendant le chargement de plus d'organisations
Widget _buildLoadingMorePlaceholder(List<OrganizationModel> currentOrganizations) {
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: currentOrganizations.length,
separatorBuilder: (context, index) => const SizedBox(height: SpacingTokens.sm),
itemBuilder: (context, index) {
final org = currentOrganizations[index];
return OrganizationCard(
organization: org,
onTap: () => _showOrganizationDetails(org),
showActions: false,
);
},
);
}
/// Bouton d'action harmonisé avec Design System V2
Widget? _buildActionButton(OrganizationsState state) {
// Afficher le FAB seulement si les données sont chargées
if (state is! OrganizationsLoaded && state is! OrganizationsLoadingMore) {
return null;
}
return FloatingActionButton.extended(
onPressed: _showCreateOrganizationDialog,
backgroundColor: UnionFlowColors.unionGreen,
elevation: 8,
icon: const Icon(Icons.add, color: Colors.white),
label: const Text(
'Nouvelle organisation',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
);
}
/// Header épuré avec Design System V2
Widget _buildHeader(OrganizationsLoaded state) {
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: UnionFlowColors.greenGlowShadow,
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(RadiusTokens.md),
),
child: const Icon(
Icons.business,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: SpacingTokens.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Gestion des Organisations',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'${state.filteredOrganizations.length} organisation(s)',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
),
),
],
),
),
IconButton(
onPressed: () {
context.read<OrganizationsBloc>().add(const RefreshOrganizations());
},
icon: const Icon(Icons.refresh, color: Colors.white),
tooltip: 'Rafraîchir',
),
],
),
);
}
/// Section statistiques avec données réelles et Design System V2
Widget _buildStatsSection(OrganizationsLoaded state) {
final totalOrgs = state.organizations.length;
final activeOrgs = state.organizations.where((o) => o.statut == StatutOrganization.active).length;
final totalMembers = state.organizations.fold<int>(0, (sum, o) => sum + o.nombreMembres);
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: UnionFlowColors.softShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
Icons.analytics_outlined,
color: UnionFlowColors.textSecondary,
size: 20,
),
const SizedBox(width: SpacingTokens.xs),
const Text(
'Statistiques',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
),
],
),
const SizedBox(height: SpacingTokens.md),
Row(
children: [
Expanded(
child: _buildStatCard(
'Total',
totalOrgs.toString(),
Icons.business_outlined,
UnionFlowColors.unionGreen,
),
),
const SizedBox(width: SpacingTokens.sm),
Expanded(
child: _buildStatCard(
'Actives',
activeOrgs.toString(),
Icons.check_circle_outline,
UnionFlowColors.success,
),
),
const SizedBox(width: SpacingTokens.sm),
Expanded(
child: _buildStatCard(
'Membres',
totalMembers.toString(),
Icons.people_outline,
UnionFlowColors.info,
),
),
],
),
],
),
);
}
/// Carte de statistique avec Design System V2
Widget _buildStatCard(String label, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(RadiusTokens.md),
border: Border.all(
color: color.withOpacity(0.2),
width: 1,
),
),
child: Column(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: SpacingTokens.xs),
Text(
value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: UnionFlowColors.textPrimary,
),
),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: UnionFlowColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
/// Barre de recherche avec Design System V2
Widget _buildSearchBar(OrganizationsLoaded state) {
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: UnionFlowColors.softShadow,
),
child: Container(
decoration: BoxDecoration(
color: UnionFlowColors.surfaceVariant,
borderRadius: BorderRadius.circular(RadiusTokens.md),
border: Border.all(
color: UnionFlowColors.border,
width: 1,
),
),
child: TextField(
controller: _searchController,
onChanged: (value) {
context.read<OrganizationsBloc>().add(SearchOrganizations(value));
},
decoration: InputDecoration(
hintText: 'Rechercher par nom, type, localisation...',
hintStyle: const TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 14,
),
prefixIcon: const Icon(Icons.search, color: UnionFlowColors.unionGreen),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
context.read<OrganizationsBloc>().add(const SearchOrganizations(''));
},
icon: const Icon(Icons.clear, color: UnionFlowColors.textSecondary),
)
: null,
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.md,
vertical: SpacingTokens.sm,
),
),
),
),
);
}
/// Onglets de catégories générés dynamiquement selon les types disponibles
Widget _buildCategoryTabs(List<TypeOrganization?> availableTypes) {
if (_tabController == null || availableTypes.isEmpty) {
return const SizedBox.shrink();
}
return BlocBuilder<OrganizationsBloc, OrganizationsState>(
builder: (context, state) {
return Container(
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: UnionFlowColors.softShadow,
),
child: TabBar(
controller: _tabController!,
isScrollable: availableTypes.length > 4, // Scrollable si plus de 4 types
labelColor: UnionFlowColors.unionGreen,
unselectedLabelColor: UnionFlowColors.textSecondary,
indicatorColor: UnionFlowColors.unionGreen,
indicatorWeight: 3,
indicatorSize: TabBarIndicatorSize.tab,
labelStyle: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
unselectedLabelStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
onTap: (index) {
// Filtrer par type selon l'onglet sélectionné
final selectedType = availableTypes[index];
if (selectedType != null) {
context.read<OrganizationsBloc>().add(FilterOrganizationsByType(selectedType));
} else {
// null = "Toutes" → effacer les filtres
context.read<OrganizationsBloc>().add(const ClearOrganizationsFilters());
}
},
tabs: availableTypes.map((type) {
// null = "Toutes", sinon utiliser le displayName du type
final label = type == null ? 'Toutes' : type.displayName;
final icon = type?.icon; // Emoji du type
return Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Text(icon, style: const TextStyle(fontSize: 16)),
const SizedBox(width: SpacingTokens.xs),
],
Text(label),
],
),
);
}).toList(),
),
);
},
);
}
/// Liste des organisations avec données réelles et OrganizationCard
Widget _buildOrganizationsList(OrganizationsLoaded state) {
final organizations = state.filteredOrganizations;
if (organizations.isEmpty) {
return _buildEmptyState();
}
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: organizations.length,
separatorBuilder: (context, index) => const SizedBox(height: SpacingTokens.sm),
itemBuilder: (context, index) {
final org = organizations[index];
return AnimatedFadeIn(
duration: Duration(milliseconds: 300 + (index * 50)),
child: OrganizationCard(
organization: org,
onTap: () => _showOrganizationDetails(org),
onEdit: () => _showEditOrganizationDialog(org),
onDelete: () => _confirmDeleteOrganization(org),
showActions: true,
),
);
},
);
}
/// État vide avec Design System V2
Widget _buildEmptyState() {
return Container(
padding: const EdgeInsets.all(SpacingTokens.xl),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(SpacingTokens.xl),
decoration: BoxDecoration(
color: UnionFlowColors.unionGreenPale,
shape: BoxShape.circle,
),
child: const Icon(
Icons.business_outlined,
size: 64,
color: UnionFlowColors.unionGreen,
),
),
const SizedBox(height: SpacingTokens.lg),
const Text(
'Aucune organisation trouvée',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
),
const SizedBox(height: SpacingTokens.xs),
const Text(
'Essayez de modifier vos critères de recherche\nou créez une nouvelle organisation',
style: TextStyle(
fontSize: 14,
color: UnionFlowColors.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: SpacingTokens.lg),
ElevatedButton.icon(
onPressed: () {
context.read<OrganizationsBloc>().add(const ClearOrganizationsFilters());
_searchController.clear();
},
icon: const Icon(Icons.clear_all),
label: const Text('Réinitialiser les filtres'),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.lg,
vertical: SpacingTokens.sm,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(RadiusTokens.md),
),
),
),
],
),
),
);
}
/// État de chargement avec Design System V2
Widget _buildLoadingState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: UnionFlowColors.unionGreen,
strokeWidth: 3,
),
const SizedBox(height: SpacingTokens.md),
const Text(
'Chargement des organisations...',
style: TextStyle(
fontSize: 14,
color: UnionFlowColors.textSecondary,
),
),
],
),
);
}
/// État d'erreur avec Design System V2
Widget _buildErrorState(OrganizationsError state) {
return Center(
child: Container(
margin: const EdgeInsets.all(SpacingTokens.xl),
padding: const EdgeInsets.all(SpacingTokens.xl),
decoration: BoxDecoration(
color: UnionFlowColors.errorPale,
borderRadius: BorderRadius.circular(RadiusTokens.lg),
border: Border.all(
color: UnionFlowColors.errorLight,
width: 1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: UnionFlowColors.error,
),
const SizedBox(height: SpacingTokens.md),
Text(
state.message,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
textAlign: TextAlign.center,
),
if (state.details != null) ...[
const SizedBox(height: SpacingTokens.xs),
Text(
state.details!,
style: const TextStyle(
fontSize: 12,
color: UnionFlowColors.textSecondary,
),
textAlign: TextAlign.center,
),
],
const SizedBox(height: SpacingTokens.lg),
ElevatedButton.icon(
onPressed: () {
context.read<OrganizationsBloc>().add(const LoadOrganizations(refresh: true));
},
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.lg,
vertical: SpacingTokens.sm,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(RadiusTokens.md),
),
),
),
],
),
),
);
}
// Méthodes d'actions
void _showOrganizationDetails(OrganizationModel org) {
final orgId = org.id;
if (orgId == null || orgId.isEmpty) return;
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => BlocProvider.value(
value: context.read<OrganizationsBloc>(),
child: OrganizationDetailPage(organizationId: orgId),
),
),
);
}
void _showCreateOrganizationDialog() {
showDialog(
context: context,
builder: (context) => const CreateOrganizationDialog(),
);
}
void _showEditOrganizationDialog(OrganizationModel org) {
showDialog(
context: context,
builder: (context) => EditOrganizationDialog(organization: org),
);
}
void _confirmDeleteOrganization(OrganizationModel org) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Supprimer l\'organisation'),
content: Text('Voulez-vous vraiment supprimer "${org.nom}" ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
if (org.id != null) {
context.read<OrganizationsBloc>().add(DeleteOrganization(org.id!));
}
Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.error,
foregroundColor: Colors.white,
),
child: const Text('Supprimer'),
),
],
),
);
}
}

View File

@@ -0,0 +1,34 @@
/// Wrapper pour la page des organisations avec BLoC Provider
library organisations_page_wrapper;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import '../../../authentication/data/models/user_role.dart';
import '../../../authentication/presentation/bloc/auth_bloc.dart';
import '../../bloc/organizations_bloc.dart';
import '../../bloc/organizations_event.dart';
import 'organizations_page.dart';
final _getIt = GetIt.instance;
/// Wrapper qui fournit le BLoC pour la page des organisations
class OrganizationsPageWrapper extends StatelessWidget {
const OrganizationsPageWrapper({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<OrganizationsBloc>(
create: (context) {
final bloc = _getIt<OrganizationsBloc>();
// Admin d'organisation : ne charger que son/ses organisation(s)
final authState = context.read<AuthBloc>().state;
final useMesOnly = authState is AuthAuthenticated &&
authState.effectiveRole == UserRole.orgAdmin;
bloc.add(LoadOrganizations(useMesOnly: useMesOnly));
return bloc;
},
child: const OrganizationsPage(),
);
}
}