refactoring

This commit is contained in:
dahoud
2026-03-31 09:14:47 +00:00
parent 9bfffeeebe
commit 5383df6dcb
200 changed files with 11192 additions and 7063 deletions

View File

@@ -1,5 +1,4 @@
/// Page de création d'une nouvelle organisation
/// Respecte strictement le design system établi dans l'application
/// Page de création d'une nouvelle organisation — tous les champs exhaustifs
library create_organisation_page;
import 'package:flutter/material.dart';
@@ -8,8 +7,13 @@ import '../../data/models/organization_model.dart';
import '../../bloc/organizations_bloc.dart';
import '../../bloc/organizations_event.dart';
import '../../bloc/organizations_state.dart';
import '../../bloc/org_types_bloc.dart';
import '../../domain/entities/type_reference_entity.dart';
import '../../../../shared/design_system/tokens/app_colors.dart';
import '../../../../core/di/injection_container.dart';
const List<String> _devises = ['XOF', 'XAF', 'EUR', 'USD', 'GBP', 'CAD', 'CHF', 'MAD', 'GHS', 'NGN', 'CDF', 'KES'];
/// Page de création d'organisation avec design system cohérent
class CreateOrganizationPage extends StatefulWidget {
const CreateOrganizationPage({super.key});
@@ -19,54 +23,99 @@ class CreateOrganizationPage extends StatefulWidget {
class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
final _formKey = GlobalKey<FormState>();
// Informations de base
final _nomController = TextEditingController();
final _nomCourtController = TextEditingController();
final _descriptionController = TextEditingController();
// Informations légales
final _numeroEnregistrementController = TextEditingController();
DateTime? _dateFondation;
// Contact
final _emailController = TextEditingController();
final _telephoneController = TextEditingController();
final _telephoneSecondaireController = TextEditingController();
final _emailSecondaireController = TextEditingController();
final _siteWebController = TextEditingController();
final _reseauxSociauxController = TextEditingController();
// Localisation
final _adresseController = TextEditingController();
final _villeController = TextEditingController();
final _codePostalController = TextEditingController();
final _regionController = TextEditingController();
final _paysController = TextEditingController();
TypeOrganization _selectedType = TypeOrganization.association;
// Finances
String _selectedDevise = 'XOF';
final _budgetAnnuelController = TextEditingController();
bool _cotisationObligatoire = false;
final _montantCotisationAnnuelleController = TextEditingController();
// Mission & contenu
final _objectifsController = TextEditingController();
final _activitesPrincipalesController = TextEditingController();
final _certificationsController = TextEditingController();
final _partenairesController = TextEditingController();
final _notesController = TextEditingController();
// Configuration
String? _selectedTypeCode;
StatutOrganization _selectedStatut = StatutOrganization.active;
bool _accepteNouveauxMembres = true;
bool _organisationPublique = true;
late final OrgTypesBloc _orgTypesBloc;
@override
void initState() {
super.initState();
_orgTypesBloc = sl<OrgTypesBloc>()..add(const LoadOrgTypes());
}
@override
void dispose() {
_orgTypesBloc.close();
_nomController.dispose();
_nomCourtController.dispose();
_descriptionController.dispose();
_numeroEnregistrementController.dispose();
_emailController.dispose();
_telephoneController.dispose();
_telephoneSecondaireController.dispose();
_emailSecondaireController.dispose();
_siteWebController.dispose();
_reseauxSociauxController.dispose();
_adresseController.dispose();
_villeController.dispose();
_codePostalController.dispose();
_regionController.dispose();
_paysController.dispose();
_budgetAnnuelController.dispose();
_montantCotisationAnnuelleController.dispose();
_objectifsController.dispose();
_activitesPrincipalesController.dispose();
_certificationsController.dispose();
_partenairesController.dispose();
_notesController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA), // Background cohérent
backgroundColor: AppColors.lightBackground,
appBar: AppBar(
backgroundColor: const Color(0xFF6C5CE7),
backgroundColor: AppColors.primaryGreen,
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,
),
),
child: const Text('Enregistrer', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
),
],
),
@@ -74,36 +123,38 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
listener: (context, state) {
if (state is OrganizationCreated) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Organisation créée avec succès'),
backgroundColor: Color(0xFF10B981),
),
const SnackBar(content: Text('Organisation créée avec succès'), backgroundColor: AppColors.success),
);
Navigator.of(context).pop(true); // Retour avec succès
Navigator.of(context).pop(true);
} else if (state is OrganizationsError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
SnackBar(content: Text(state.message), backgroundColor: Colors.red),
);
}
},
child: Form(
key: _formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(12), // SpacingTokens cohérent
padding: const EdgeInsets.all(12),
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),
_buildSection('Informations de base', Icons.business, _buildBasicInfoFields()),
const SizedBox(height: 8),
_buildSection('Informations légales', Icons.gavel, _buildLegalFields()),
const SizedBox(height: 8),
_buildSection('Contact', Icons.contact_phone, _buildContactFields()),
const SizedBox(height: 8),
_buildSection('Localisation', Icons.location_on, _buildLocationFields()),
const SizedBox(height: 8),
_buildSection('Configuration', Icons.settings, _buildConfigurationFields()),
const SizedBox(height: 8),
_buildSection('Finances', Icons.account_balance_wallet, _buildFinancesFields()),
const SizedBox(height: 8),
_buildSection('Mission & Activités', Icons.flag, _buildMissionFields()),
const SizedBox(height: 8),
_buildSection('Informations complémentaires', Icons.info_outline, _buildSupplementaryFields()),
const SizedBox(height: 8),
_buildActionButtons(),
],
),
@@ -113,421 +164,370 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
);
}
/// Carte des informations de base
Widget _buildBasicInfoCard() {
Widget _buildSection(String title, IconData icon, List<Widget> children) {
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),
),
],
),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
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;
},
),
Row(children: [
Icon(icon, size: 16, color: AppColors.primaryGreen),
const SizedBox(width: 6),
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: AppColors.primaryGreen)),
]),
const SizedBox(height: 10),
...children,
],
),
);
}
/// 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;
},
),
],
),
);
}
List<Widget> _buildBasicInfoFields() => [
TextFormField(
controller: _nomController,
decoration: const InputDecoration(labelText: 'Nom de l\'organisation *', hintText: 'Ex: Mutuelle des Entrepreneurs', border: OutlineInputBorder(), prefixIcon: Icon(Icons.business)),
validator: (v) => v == null || v.trim().length < 3 ? 'Minimum 3 caractères' : null,
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 8),
TextFormField(
controller: _nomCourtController,
decoration: const InputDecoration(labelText: 'Sigle / Nom court', hintText: 'Ex: MUKEFI', border: OutlineInputBorder(), prefixIcon: Icon(Icons.short_text)),
validator: (v) => v != null && v.trim().isNotEmpty && v.trim().length < 2 ? 'Minimum 2 caractères' : null,
),
const SizedBox(height: 8),
BlocBuilder<OrgTypesBloc, OrgTypesState>(
bloc: _orgTypesBloc,
builder: (context, orgTypesState) {
final types = orgTypesState is OrgTypesLoaded ? orgTypesState.types
: orgTypesState is OrgTypeSuccess ? orgTypesState.types
: <TypeReferenceEntity>[];
if (orgTypesState is OrgTypesLoading || orgTypesState is OrgTypesInitial) {
return const InputDecorator(
decoration: InputDecoration(labelText: 'Type d\'organisation *', border: OutlineInputBorder(), prefixIcon: Icon(Icons.category)),
child: LinearProgressIndicator(),
);
}
return DropdownButtonFormField<String>(
value: types.any((t) => t.code == _selectedTypeCode) ? _selectedTypeCode : null,
decoration: const InputDecoration(labelText: 'Type d\'organisation *', border: OutlineInputBorder(), prefixIcon: Icon(Icons.category)),
items: types.map((t) => DropdownMenuItem(value: t.code, child: Text(t.libelle))).toList(),
onChanged: (v) => setState(() => _selectedTypeCode = v),
validator: (v) => v == null ? 'Le type est obligatoire' : null,
);
},
),
const SizedBox(height: 8),
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(labelText: 'Description', hintText: 'Décrivez brièvement l\'organisation...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.description)),
maxLines: 3,
validator: (v) => v != null && v.trim().isNotEmpty && v.trim().length < 10 ? 'Minimum 10 caractères' : 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),
),
],
List<Widget> _buildLegalFields() => [
TextFormField(
controller: _numeroEnregistrementController,
decoration: const InputDecoration(labelText: 'Numéro d\'enregistrement officiel', hintText: 'Ex: CI-ASSOC-2024-001', border: OutlineInputBorder(), prefixIcon: Icon(Icons.assignment)),
),
const SizedBox(height: 8),
InkWell(
onTap: () => _pickDateFondation(context),
child: InputDecorator(
decoration: const InputDecoration(labelText: 'Date de fondation', border: OutlineInputBorder(), prefixIcon: Icon(Icons.cake)),
child: Text(
_dateFondation != null ? _formatDate(_dateFondation) : 'Sélectionner une date',
style: TextStyle(color: _dateFondation != null ? AppColors.textPrimaryLight : AppColors.textSecondaryLight),
),
),
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;
});
}
},
),
],
),
);
}
List<Widget> _buildContactFields() => [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email principal *', hintText: 'contact@organisation.com', border: OutlineInputBorder(), prefixIcon: Icon(Icons.email)),
keyboardType: TextInputType.emailAddress,
validator: (v) {
if (v == null || v.trim().isEmpty) return 'L\'email est obligatoire';
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(v.trim())) return 'Format invalide';
return null;
},
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 8),
TextFormField(
controller: _emailSecondaireController,
decoration: const InputDecoration(labelText: 'Email secondaire', hintText: 'info@organisation.com', border: OutlineInputBorder(), prefixIcon: Icon(Icons.alternate_email)),
keyboardType: TextInputType.emailAddress,
validator: (v) {
if (v != null && v.trim().isNotEmpty && !RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(v.trim())) return 'Format invalide';
return null;
},
),
const SizedBox(height: 8),
Row(children: [
Expanded(child: TextFormField(
controller: _telephoneController,
decoration: const InputDecoration(labelText: 'Téléphone principal', hintText: '+225 XX XX XX', border: OutlineInputBorder(), prefixIcon: Icon(Icons.phone)),
keyboardType: TextInputType.phone,
validator: (v) => v != null && v.trim().isNotEmpty && v.trim().length < 8 ? 'Numéro invalide' : null,
)),
const SizedBox(width: 8),
Expanded(child: TextFormField(
controller: _telephoneSecondaireController,
decoration: const InputDecoration(labelText: 'Téléphone secondaire', hintText: '+225 XX XX XX', border: OutlineInputBorder(), prefixIcon: Icon(Icons.phone_forwarded)),
keyboardType: TextInputType.phone,
validator: (v) => v != null && v.trim().isNotEmpty && v.trim().length < 8 ? 'Numéro invalide' : null,
)),
]),
const SizedBox(height: 8),
TextFormField(
controller: _siteWebController,
decoration: const InputDecoration(labelText: 'Site web', hintText: 'https://www.organisation.com', border: OutlineInputBorder(), prefixIcon: Icon(Icons.web)),
keyboardType: TextInputType.url,
validator: (v) {
if (v != null && v.trim().isNotEmpty && !RegExp(r'^https?://[^\s]+$').hasMatch(v.trim())) return 'Doit commencer par http:// ou https://';
return null;
},
),
const SizedBox(height: 8),
TextFormField(
controller: _reseauxSociauxController,
decoration: const InputDecoration(labelText: 'Réseaux sociaux', hintText: 'Ex: Facebook, LinkedIn, Twitter...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.share)),
maxLines: 2,
),
];
List<Widget> _buildLocationFields() => [
TextFormField(
controller: _adresseController,
decoration: const InputDecoration(labelText: 'Adresse', hintText: 'Rue, quartier...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.location_on)),
maxLines: 2,
),
const SizedBox(height: 8),
Row(children: [
Expanded(child: TextFormField(
controller: _villeController,
decoration: const InputDecoration(labelText: 'Ville', hintText: 'Abidjan', border: OutlineInputBorder(), prefixIcon: Icon(Icons.location_city)),
)),
const SizedBox(width: 8),
Expanded(child: TextFormField(
controller: _codePostalController,
decoration: const InputDecoration(labelText: 'Code postal', hintText: '01 BP 1234', border: OutlineInputBorder(), prefixIcon: Icon(Icons.markunread_mailbox)),
)),
]),
const SizedBox(height: 8),
Row(children: [
Expanded(child: TextFormField(
controller: _regionController,
decoration: const InputDecoration(labelText: 'Région', hintText: 'Lagunes', border: OutlineInputBorder(), prefixIcon: Icon(Icons.map)),
)),
const SizedBox(width: 8),
Expanded(child: TextFormField(
controller: _paysController,
decoration: const InputDecoration(labelText: 'Pays', hintText: 'Côte d\'Ivoire', border: OutlineInputBorder(), prefixIcon: Icon(Icons.flag)),
)),
]),
];
List<Widget> _buildConfigurationFields() => [
DropdownButtonFormField<StatutOrganization>(
value: _selectedStatut,
decoration: const InputDecoration(labelText: 'Statut initial *', border: OutlineInputBorder(), prefixIcon: Icon(Icons.toggle_on)),
items: StatutOrganization.values.map((s) {
final color = Color(int.parse(s.color.substring(1), radix: 16) + 0xFF000000);
return DropdownMenuItem(value: s, child: Row(children: [
Container(width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
const SizedBox(width: 8),
Text(s.displayName),
]));
}).toList(),
onChanged: (v) { if (v != null) setState(() => _selectedStatut = v); },
),
const SizedBox(height: 8),
SwitchListTile(
dense: true,
contentPadding: EdgeInsets.zero,
title: const Text('Organisation publique', style: TextStyle(fontSize: 14)),
subtitle: const Text('Visible par tous les utilisateurs', style: TextStyle(fontSize: 12)),
value: _organisationPublique,
onChanged: (v) => setState(() => _organisationPublique = v),
activeColor: AppColors.primaryGreen,
),
SwitchListTile(
dense: true,
contentPadding: EdgeInsets.zero,
title: const Text('Accepte de nouveaux membres', style: TextStyle(fontSize: 14)),
subtitle: const Text('Les demandes d\'adhésion sont ouvertes', style: TextStyle(fontSize: 12)),
value: _accepteNouveauxMembres,
onChanged: (v) => setState(() => _accepteNouveauxMembres = v),
activeColor: AppColors.primaryGreen,
),
];
List<Widget> _buildFinancesFields() => [
DropdownButtonFormField<String>(
value: _selectedDevise,
decoration: const InputDecoration(labelText: 'Devise', border: OutlineInputBorder(), prefixIcon: Icon(Icons.currency_exchange)),
items: _devises.map((d) => DropdownMenuItem(value: d, child: Text(d))).toList(),
onChanged: (v) { if (v != null) setState(() => _selectedDevise = v); },
),
const SizedBox(height: 8),
TextFormField(
controller: _budgetAnnuelController,
decoration: const InputDecoration(labelText: 'Budget annuel', hintText: 'Ex: 5000000', border: OutlineInputBorder(), prefixIcon: Icon(Icons.account_balance)),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
validator: (v) {
if (v != null && v.trim().isNotEmpty && double.tryParse(v.trim()) == null) return 'Montant invalide';
return null;
},
),
const SizedBox(height: 8),
SwitchListTile(
dense: true,
contentPadding: EdgeInsets.zero,
title: const Text('Cotisation obligatoire', style: TextStyle(fontSize: 14)),
value: _cotisationObligatoire,
onChanged: (v) => setState(() => _cotisationObligatoire = v),
activeColor: AppColors.primaryGreen,
),
if (_cotisationObligatoire) ...[
const SizedBox(height: 8),
TextFormField(
controller: _montantCotisationAnnuelleController,
decoration: InputDecoration(labelText: 'Montant cotisation annuelle ($_selectedDevise)', hintText: 'Ex: 25000', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.payments)),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
validator: (v) {
if (_cotisationObligatoire && (v == null || v.trim().isEmpty)) return 'Obligatoire si cotisation requise';
if (v != null && v.trim().isNotEmpty && double.tryParse(v.trim()) == null) return 'Montant invalide';
return null;
},
),
],
];
List<Widget> _buildMissionFields() => [
TextFormField(
controller: _objectifsController,
decoration: const InputDecoration(labelText: 'Objectifs', hintText: 'Décrire les objectifs principaux...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.track_changes)),
maxLines: 3,
),
const SizedBox(height: 8),
TextFormField(
controller: _activitesPrincipalesController,
decoration: const InputDecoration(labelText: 'Activités principales', hintText: 'Lister les activités clés...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.work)),
maxLines: 3,
),
];
List<Widget> _buildSupplementaryFields() => [
TextFormField(
controller: _certificationsController,
decoration: const InputDecoration(labelText: 'Certifications / Agréments', hintText: 'Ex: ISO 9001, Agrément ministériel...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.verified)),
),
const SizedBox(height: 8),
TextFormField(
controller: _partenairesController,
decoration: const InputDecoration(labelText: 'Partenaires', hintText: 'Ex: Banque Mondiale, Ministère...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.handshake)),
maxLines: 2,
),
const SizedBox(height: 8),
TextFormField(
controller: _notesController,
decoration: const InputDecoration(labelText: 'Notes internes', hintText: 'Informations internes non visibles publiquement...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.sticky_note_2)),
maxLines: 2,
),
];
/// 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,
),
),
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: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 10),
textStyle: const TextStyle(fontSize: 14, 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,
),
),
),
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: AppColors.textSecondaryLight,
padding: const EdgeInsets.symmetric(vertical: 10),
),
),
],
),
]);
}
bool _isFormValid() =>
_nomController.text.trim().isNotEmpty &&
_emailController.text.trim().isNotEmpty;
Future<void> _pickDateFondation(BuildContext context) async {
final picked = await showDatePicker(
context: context,
initialDate: _dateFondation ?? DateTime(2000),
firstDate: DateTime(1800),
lastDate: DateTime.now(),
);
if (picked != null) setState(() => _dateFondation = picked);
}
/// Vérifie si le formulaire est valide
bool _isFormValid() {
return _nomController.text.trim().isNotEmpty;
String _formatDate(DateTime? date) {
if (date == null) return '';
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
}
/// Sauvegarde l'organisation
void _saveOrganisation() {
if (_formKey.currentState?.validate() ?? false) {
final organisation = OrganizationModel(
final org = 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,
typeOrganisation: _selectedTypeCode ?? 'ASSOCIATION',
statut: _selectedStatut,
dateFondation: _dateFondation,
numeroEnregistrement: _numeroEnregistrementController.text.trim().isEmpty ? null : _numeroEnregistrementController.text.trim(),
email: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
telephone: _telephoneController.text.trim().isEmpty ? null : _telephoneController.text.trim(),
telephoneSecondaire: _telephoneSecondaireController.text.trim().isEmpty ? null : _telephoneSecondaireController.text.trim(),
emailSecondaire: _emailSecondaireController.text.trim().isEmpty ? null : _emailSecondaireController.text.trim(),
siteWeb: _siteWebController.text.trim().isEmpty ? null : _siteWebController.text.trim(),
reseauxSociaux: _reseauxSociauxController.text.trim().isEmpty ? null : _reseauxSociauxController.text.trim(),
adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(),
ville: _villeController.text.trim().isEmpty ? null : _villeController.text.trim(),
codePostal: _codePostalController.text.trim().isEmpty ? null : _codePostalController.text.trim(),
region: _regionController.text.trim().isEmpty ? null : _regionController.text.trim(),
pays: _paysController.text.trim().isEmpty ? null : _paysController.text.trim(),
dateCreation: DateTime.now(),
devise: _selectedDevise,
budgetAnnuel: _budgetAnnuelController.text.trim().isEmpty ? null : double.tryParse(_budgetAnnuelController.text.trim()),
cotisationObligatoire: _cotisationObligatoire,
montantCotisationAnnuelle: _montantCotisationAnnuelleController.text.trim().isEmpty ? null : double.tryParse(_montantCotisationAnnuelleController.text.trim()),
objectifs: _objectifsController.text.trim().isEmpty ? null : _objectifsController.text.trim(),
activitesPrincipales: _activitesPrincipalesController.text.trim().isEmpty ? null : _activitesPrincipalesController.text.trim(),
certifications: _certificationsController.text.trim().isEmpty ? null : _certificationsController.text.trim(),
partenaires: _partenairesController.text.trim().isEmpty ? null : _partenairesController.text.trim(),
notes: _notesController.text.trim().isEmpty ? null : _notesController.text.trim(),
organisationPublique: _organisationPublique,
accepteNouveauxMembres: _accepteNouveauxMembres,
nombreMembres: 0,
);
context.read<OrganizationsBloc>().add(CreateOrganization(organisation));
context.read<OrganizationsBloc>().add(CreateOrganization(org));
}
}
}

View File

@@ -0,0 +1,500 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/widgets/core_card.dart';
import '../../../../core/di/injection.dart';
import '../../bloc/org_types_bloc.dart';
import '../../domain/entities/type_reference_entity.dart';
class OrgTypesPage extends StatelessWidget {
const OrgTypesPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => getIt<OrgTypesBloc>()..add(const LoadOrgTypes()),
child: const _OrgTypesView(),
);
}
}
class _OrgTypesView extends StatefulWidget {
const _OrgTypesView();
@override
State<_OrgTypesView> createState() => _OrgTypesViewState();
}
class _OrgTypesViewState extends State<_OrgTypesView> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.lightBackground,
appBar: const UFAppBar(
title: 'TYPES D\'ORGANISATIONS',
automaticallyImplyLeading: true,
),
body: BlocConsumer<OrgTypesBloc, OrgTypesState>(
listener: (context, state) {
if (state is OrgTypeSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
} else if (state is OrgTypeOperationError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
},
builder: (context, state) {
if (state is OrgTypesLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is OrgTypesError) {
return _buildErrorState(context, state.message);
}
final types = _getTypes(state);
if (types.isEmpty && state is! OrgTypeOperating) {
return _buildEmptyState(context);
}
return RefreshIndicator(
onRefresh: () async => context.read<OrgTypesBloc>().add(const LoadOrgTypes()),
child: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: types.length,
separatorBuilder: (_, __) => const SizedBox(height: 6),
itemBuilder: (context, index) => _buildTypeCard(context, types[index], state),
),
);
},
),
floatingActionButton: FloatingActionButton.small(
onPressed: () => _showTypeForm(context, null),
backgroundColor: AppColors.primaryGreen,
child: const Icon(Icons.add, color: Colors.white),
),
);
}
List<TypeReferenceEntity> _getTypes(OrgTypesState state) {
if (state is OrgTypesLoaded) return state.types;
if (state is OrgTypeOperating) return state.types;
if (state is OrgTypeSuccess) return state.types;
if (state is OrgTypeOperationError) return state.types;
return [];
}
Widget _buildTypeCard(BuildContext context, TypeReferenceEntity type, OrgTypesState state) {
final isOperating = state is OrgTypeOperating;
final color = _parseColor(type.couleur) ?? AppColors.primaryGreen;
return Opacity(
opacity: isOperating ? 0.6 : 1.0,
child: CoreCard(
margin: EdgeInsets.zero,
onTap: (!type.estSysteme && !isOperating) ? () => _showTypeForm(context, type) : null,
child: Container(
decoration: BoxDecoration(
border: Border(left: BorderSide(color: color, width: 3)),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.fromLTRB(10, 8, 8, 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.12),
borderRadius: BorderRadius.circular(4),
),
child: Text(
type.code,
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w800,
color: color,
letterSpacing: 0.5,
),
),
),
if (type.estDefaut) ...[
const SizedBox(width: 6),
const Icon(Icons.star_rounded, size: 13, color: Color(0xFFF59E0B)),
],
if (type.estSysteme) ...[
const SizedBox(width: 6),
Icon(Icons.lock_outline, size: 12, color: Colors.grey[500]),
],
],
),
const SizedBox(height: 4),
Text(
type.libelle,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppColors.textPrimaryLight,
),
),
if (type.description != null && type.description!.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
type.description!,
style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
if (!type.estSysteme && !isOperating) ...[
IconButton(
icon: const Icon(Icons.edit_outlined, size: 16),
color: AppColors.textSecondaryLight,
onPressed: () => _showTypeForm(context, type),
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 16),
color: Colors.red[400],
onPressed: () => _confirmDelete(context, type),
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
),
],
],
),
),
),
);
}
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.category_outlined, size: 48, color: Colors.grey[400]),
const SizedBox(height: 12),
const Text(
'Aucun type défini',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.textPrimaryLight),
),
const SizedBox(height: 6),
Text(
'Créez votre premier type d\'organisation',
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => _showTypeForm(context, null),
icon: const Icon(Icons.add, size: 16),
label: const Text('Créer un type'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
],
),
),
);
}
Widget _buildErrorState(BuildContext context, String message) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 40, color: Colors.red[400]),
const SizedBox(height: 12),
const Text('Erreur de chargement', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700)),
const SizedBox(height: 6),
Text(message, style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight), textAlign: TextAlign.center),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => context.read<OrgTypesBloc>().add(const LoadOrgTypes()),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Réessayer'),
),
],
),
),
);
}
void _showTypeForm(BuildContext context, TypeReferenceEntity? existing) {
final bloc = context.read<OrgTypesBloc>();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => _OrgTypeFormSheet(existing: existing, bloc: bloc),
);
}
void _confirmDelete(BuildContext context, TypeReferenceEntity type) {
final bloc = context.read<OrgTypesBloc>();
showDialog(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
title: const Text('Supprimer ce type ?', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700)),
content: Text(
'Supprimer "${type.libelle}" (${type.code}) ?\nLes organisations associées devront être mises à jour.',
style: const TextStyle(fontSize: 12),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
bloc.add(DeleteOrgTypeEvent(type.id));
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
),
child: const Text('Supprimer', style: TextStyle(fontSize: 12)),
),
],
),
);
}
Color? _parseColor(String? hex) {
if (hex == null || hex.isEmpty) return null;
try {
final clean = hex.replaceAll('#', '');
return Color(int.parse('FF$clean', radix: 16));
} catch (_) {
return null;
}
}
}
/// Bottom sheet form for create / edit
class _OrgTypeFormSheet extends StatefulWidget {
final TypeReferenceEntity? existing;
final OrgTypesBloc bloc;
const _OrgTypeFormSheet({this.existing, required this.bloc});
@override
State<_OrgTypeFormSheet> createState() => _OrgTypeFormSheetState();
}
class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
late final TextEditingController _codeCtrl;
late final TextEditingController _libelleCtrl;
late final TextEditingController _descCtrl;
String _selectedColor = '#22C55E';
final _formKey = GlobalKey<FormState>();
static const _colorOptions = [
'#22C55E', '#3B82F6', '#F59E0B', '#EF4444',
'#8B5CF6', '#EC4899', '#14B8A6', '#F97316',
'#64748B', '#A16207',
];
@override
void initState() {
super.initState();
_codeCtrl = TextEditingController(text: widget.existing?.code ?? '');
_libelleCtrl = TextEditingController(text: widget.existing?.libelle ?? '');
_descCtrl = TextEditingController(text: widget.existing?.description ?? '');
_selectedColor = widget.existing?.couleur ?? '#22C55E';
}
@override
void dispose() {
_codeCtrl.dispose();
_libelleCtrl.dispose();
_descCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isEdit = widget.existing != null;
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Handle bar
Center(
child: Container(
width: 36,
height: 4,
decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)),
),
),
const SizedBox(height: 14),
Text(
isEdit ? 'Modifier le type' : 'Nouveau type d\'organisation',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: AppColors.textPrimaryLight),
),
const SizedBox(height: 14),
// Code
TextFormField(
controller: _codeCtrl,
decoration: InputDecoration(
labelText: 'Code technique *',
hintText: 'Ex: ASSOCIATION',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
isDense: true,
),
textCapitalization: TextCapitalization.characters,
style: const TextStyle(fontSize: 12),
validator: (v) => (v == null || v.trim().isEmpty) ? 'Code requis' : null,
),
const SizedBox(height: 10),
// Libellé
TextFormField(
controller: _libelleCtrl,
decoration: InputDecoration(
labelText: 'Libellé *',
hintText: 'Ex: Association',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
isDense: true,
),
style: const TextStyle(fontSize: 12),
validator: (v) => (v == null || v.trim().isEmpty) ? 'Libellé requis' : null,
),
const SizedBox(height: 10),
// Description
TextFormField(
controller: _descCtrl,
decoration: InputDecoration(
labelText: 'Description',
hintText: 'Optionnelle',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
isDense: true,
),
style: const TextStyle(fontSize: 12),
maxLines: 2,
),
const SizedBox(height: 12),
// Color picker
const Text('Couleur', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.textSecondaryLight)),
const SizedBox(height: 6),
Wrap(
spacing: 8,
runSpacing: 8,
children: _colorOptions.map((hex) {
final color = Color(int.parse('FF${hex.replaceAll('#', '')}', radix: 16));
final selected = _selectedColor == hex;
return GestureDetector(
onTap: () => setState(() => _selectedColor = hex),
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: selected ? Border.all(color: Colors.black, width: 2) : null,
),
child: selected ? const Icon(Icons.check, size: 14, color: Colors.white) : null,
),
);
}).toList(),
),
const SizedBox(height: 16),
// Submit
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Text(
isEdit ? 'Enregistrer' : 'Créer le type',
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700),
),
),
),
],
),
),
),
);
}
void _submit() {
if (!_formKey.currentState!.validate()) return;
Navigator.pop(context);
if (widget.existing != null) {
widget.bloc.add(UpdateOrgTypeEvent(
id: widget.existing!.id,
code: _codeCtrl.text.trim(),
libelle: _libelleCtrl.text.trim(),
description: _descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(),
couleur: _selectedColor,
ordreAffichage: widget.existing!.ordreAffichage,
));
} else {
widget.bloc.add(CreateOrgTypeEvent(
code: _codeCtrl.text.trim(),
libelle: _libelleCtrl.text.trim(),
description: _descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(),
couleur: _selectedColor,
));
}
}
}

View File

@@ -8,13 +8,14 @@ import '../widgets/organization_card.dart';
import '../widgets/create_organization_dialog.dart';
import '../widgets/edit_organization_dialog.dart';
import 'organization_detail_page.dart';
import 'org_types_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';
import '../../../../features/authentication/presentation/bloc/auth_bloc.dart';
import '../../../../features/authentication/data/models/user_role.dart';
/// Page de gestion des organisations - Interface sophistiquée et exhaustive
///
@@ -36,7 +37,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
final TextEditingController _searchController = TextEditingController();
TabController? _tabController;
final ScrollController _scrollController = ScrollController();
List<TypeOrganization?> _availableTypes = [];
List<String?> _availableTypes = [];
@override
void initState() {
@@ -62,21 +63,21 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
}
/// Calcule les types d'organisations disponibles dans les données
List<TypeOrganization?> _calculateAvailableTypes(List<OrganizationModel> organizations) {
List<String?> _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));
final types = typesSet.toList()..sort((a, b) => a.compareTo(b));
// 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) {
void _updateTabController(List<String?> newTypes) {
if (_availableTypes.length != newTypes.length ||
!_availableTypes.every((type) => newTypes.contains(type))) {
_availableTypes = newTypes;
@@ -94,7 +95,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: UnionFlowColors.error,
backgroundColor: AppColors.error,
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: 'Réessayer',
@@ -109,7 +110,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Organisation créée avec succès'),
backgroundColor: UnionFlowColors.success,
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
@@ -117,7 +118,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Organisation mise à jour avec succès'),
backgroundColor: UnionFlowColors.success,
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
@@ -125,7 +126,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Organisation supprimée avec succès'),
backgroundColor: UnionFlowColors.success,
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
@@ -137,10 +138,21 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
backgroundColor: Colors.transparent,
appBar: UFAppBar(
title: 'Gestion des Organisations',
backgroundColor: UnionFlowColors.surface,
foregroundColor: UnionFlowColors.textPrimary,
backgroundColor: AppColors.lightSurface,
foregroundColor: AppColors.textPrimaryLight,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.category_outlined),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const OrgTypesPage(),
),
);
},
tooltip: 'Types d\'organisations',
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
@@ -246,7 +258,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
padding: EdgeInsets.all(SpacingTokens.md),
child: Center(
child: CircularProgressIndicator(
color: UnionFlowColors.unionGreen,
color: AppColors.primaryGreen,
),
),
),
@@ -287,10 +299,16 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
if (state is! OrganizationsLoaded && state is! OrganizationsLoadingMore) {
return null;
}
// Réservé au Super Admin uniquement
final authState = context.read<AuthBloc>().state;
if (authState is! AuthAuthenticated ||
authState.effectiveRole != UserRole.superAdmin) {
return null;
}
return FloatingActionButton.extended(
onPressed: _showCreateOrganizationDialog,
backgroundColor: UnionFlowColors.unionGreen,
backgroundColor: AppColors.primaryGreen,
elevation: 8,
icon: const Icon(Icons.add, color: Colors.white),
label: const Text(
@@ -308,9 +326,19 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
gradient: const LinearGradient(
colors: [AppColors.brandGreen, AppColors.primaryGreen],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: UnionFlowColors.greenGlowShadow,
boxShadow: [
BoxShadow(
color: AppColors.primaryGreen.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
@@ -334,7 +362,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
const Text(
'Gestion des Organisations',
style: TextStyle(
fontSize: 20,
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
@@ -370,9 +398,9 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
color: AppColors.lightSurface,
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: UnionFlowColors.softShadow,
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -381,16 +409,16 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
children: [
const Icon(
Icons.analytics_outlined,
color: UnionFlowColors.textSecondary,
color: AppColors.textSecondaryLight,
size: 20,
),
const SizedBox(width: SpacingTokens.xs),
const Text(
'Statistiques',
style: TextStyle(
fontSize: 16,
fontSize: 13,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
color: AppColors.textPrimaryLight,
),
),
],
@@ -403,7 +431,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
'Total',
totalOrgs.toString(),
Icons.business_outlined,
UnionFlowColors.unionGreen,
AppColors.primaryGreen,
),
),
const SizedBox(width: SpacingTokens.sm),
@@ -412,7 +440,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
'Actives',
activeOrgs.toString(),
Icons.check_circle_outline,
UnionFlowColors.success,
AppColors.success,
),
),
const SizedBox(width: SpacingTokens.sm),
@@ -421,7 +449,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
'Membres',
totalMembers.toString(),
Icons.people_outline,
UnionFlowColors.info,
AppColors.primaryGreen,
),
),
],
@@ -452,14 +480,14 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: UnionFlowColors.textPrimary,
color: AppColors.textPrimaryLight,
),
),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: UnionFlowColors.textSecondary,
color: AppColors.textSecondaryLight,
fontWeight: FontWeight.w500,
),
),
@@ -473,16 +501,16 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
return Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
color: AppColors.lightSurface,
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: UnionFlowColors.softShadow,
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))],
),
child: Container(
decoration: BoxDecoration(
color: UnionFlowColors.surfaceVariant,
color: AppColors.lightBackground,
borderRadius: BorderRadius.circular(RadiusTokens.md),
border: Border.all(
color: UnionFlowColors.border,
color: AppColors.lightBorder,
width: 1,
),
),
@@ -494,17 +522,17 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
decoration: InputDecoration(
hintText: 'Rechercher par nom, type, localisation...',
hintStyle: const TextStyle(
color: UnionFlowColors.textSecondary,
color: AppColors.textSecondaryLight,
fontSize: 14,
),
prefixIcon: const Icon(Icons.search, color: UnionFlowColors.unionGreen),
prefixIcon: const Icon(Icons.search, color: AppColors.primaryGreen),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
context.read<OrganizationsBloc>().add(const SearchOrganizations(''));
},
icon: const Icon(Icons.clear, color: UnionFlowColors.textSecondary),
icon: const Icon(Icons.clear, color: AppColors.textSecondaryLight),
)
: null,
border: InputBorder.none,
@@ -519,7 +547,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
}
/// Onglets de catégories générés dynamiquement selon les types disponibles
Widget _buildCategoryTabs(List<TypeOrganization?> availableTypes) {
Widget _buildCategoryTabs(List<String?> availableTypes) {
if (_tabController == null || availableTypes.isEmpty) {
return const SizedBox.shrink();
}
@@ -528,16 +556,16 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
builder: (context, state) {
return Container(
decoration: BoxDecoration(
color: UnionFlowColors.surface,
color: AppColors.lightSurface,
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: UnionFlowColors.softShadow,
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))],
),
child: TabBar(
controller: _tabController!,
isScrollable: availableTypes.length > 4, // Scrollable si plus de 4 types
labelColor: UnionFlowColors.unionGreen,
unselectedLabelColor: UnionFlowColors.textSecondary,
indicatorColor: UnionFlowColors.unionGreen,
labelColor: AppColors.primaryGreen,
unselectedLabelColor: AppColors.textSecondaryLight,
indicatorColor: AppColors.primaryGreen,
indicatorWeight: 3,
indicatorSize: TabBarIndicatorSize.tab,
labelStyle: const TextStyle(
@@ -560,22 +588,8 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
}
},
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),
],
),
);
final label = type == null ? 'Toutes' : type;
return Tab(text: label);
}).toList(),
),
);
@@ -623,13 +637,13 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
Container(
padding: const EdgeInsets.all(SpacingTokens.xl),
decoration: BoxDecoration(
color: UnionFlowColors.unionGreenPale,
color: AppColors.primaryGreen.withOpacity(0.12),
shape: BoxShape.circle,
),
child: const Icon(
Icons.business_outlined,
size: 64,
color: UnionFlowColors.unionGreen,
color: AppColors.primaryGreen,
),
),
const SizedBox(height: SpacingTokens.lg),
@@ -638,7 +652,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
color: AppColors.textPrimaryLight,
),
),
const SizedBox(height: SpacingTokens.xs),
@@ -646,7 +660,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
'Essayez de modifier vos critères de recherche\nou créez une nouvelle organisation',
style: TextStyle(
fontSize: 14,
color: UnionFlowColors.textSecondary,
color: AppColors.textSecondaryLight,
),
textAlign: TextAlign.center,
),
@@ -659,7 +673,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
icon: const Icon(Icons.clear_all),
label: const Text('Réinitialiser les filtres'),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.lg,
@@ -683,7 +697,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: UnionFlowColors.unionGreen,
color: AppColors.primaryGreen,
strokeWidth: 3,
),
const SizedBox(height: SpacingTokens.md),
@@ -691,7 +705,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
'Chargement des organisations...',
style: TextStyle(
fontSize: 14,
color: UnionFlowColors.textSecondary,
color: AppColors.textSecondaryLight,
),
),
],
@@ -706,10 +720,10 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
margin: const EdgeInsets.all(SpacingTokens.xl),
padding: const EdgeInsets.all(SpacingTokens.xl),
decoration: BoxDecoration(
color: UnionFlowColors.errorPale,
color: AppColors.error.withOpacity(0.08),
borderRadius: BorderRadius.circular(RadiusTokens.lg),
border: Border.all(
color: UnionFlowColors.errorLight,
color: AppColors.error.withOpacity(0.3),
width: 1,
),
),
@@ -719,7 +733,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
const Icon(
Icons.error_outline,
size: 64,
color: UnionFlowColors.error,
color: AppColors.error,
),
const SizedBox(height: SpacingTokens.md),
Text(
@@ -727,7 +741,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
color: AppColors.textPrimaryLight,
),
textAlign: TextAlign.center,
),
@@ -737,7 +751,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
state.details!,
style: const TextStyle(
fontSize: 12,
color: UnionFlowColors.textSecondary,
color: AppColors.textSecondaryLight,
),
textAlign: TextAlign.center,
),
@@ -750,7 +764,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.lg,
@@ -771,10 +785,11 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
void _showOrganizationDetails(OrganizationModel org) {
final orgId = org.id;
if (orgId == null || orgId.isEmpty) return;
final bloc = context.read<OrganizationsBloc>();
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => BlocProvider.value(
value: context.read<OrganizationsBloc>(),
builder: (_) => BlocProvider.value(
value: bloc,
child: OrganizationDetailPage(organizationId: orgId),
),
),
@@ -782,39 +797,48 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
}
void _showCreateOrganizationDialog() {
final bloc = context.read<OrganizationsBloc>();
showDialog(
context: context,
builder: (context) => const CreateOrganizationDialog(),
builder: (_) => BlocProvider.value(
value: bloc,
child: const CreateOrganizationDialog(),
),
);
}
void _showEditOrganizationDialog(OrganizationModel org) {
final bloc = context.read<OrganizationsBloc>();
showDialog(
context: context,
builder: (context) => EditOrganizationDialog(organization: org),
builder: (_) => BlocProvider.value(
value: bloc,
child: EditOrganizationDialog(organization: org),
),
);
}
void _confirmDeleteOrganization(OrganizationModel org) {
final bloc = context.read<OrganizationsBloc>();
showDialog(
context: context,
builder: (context) => AlertDialog(
builder: (dialogContext) => AlertDialog(
title: const Text('Supprimer l\'organisation'),
content: Text('Voulez-vous vraiment supprimer "${org.nom}" ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
if (org.id != null) {
context.read<OrganizationsBloc>().add(DeleteOrganization(org.id!));
bloc.add(DeleteOrganization(org.id!));
}
Navigator.of(context).pop();
Navigator.of(dialogContext).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.error,
backgroundColor: AppColors.error,
foregroundColor: Colors.white,
),
child: const Text('Supprimer'),

View File

@@ -6,7 +6,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/organizations_bloc.dart';
import '../../bloc/organizations_event.dart';
import '../../bloc/org_types_bloc.dart';
import '../../domain/entities/type_reference_entity.dart';
import '../../data/models/organization_model.dart';
import '../../../../core/di/injection_container.dart';
/// Dialogue de création d'organisation
class CreateOrganizationDialog extends StatefulWidget {
@@ -34,12 +37,20 @@ class _CreateOrganizationDialogState extends State<CreateOrganizationDialog> {
final _objectifsController = TextEditingController();
// Valeurs sélectionnées
TypeOrganization _selectedType = TypeOrganization.association;
String? _selectedTypeCode;
bool _accepteNouveauxMembres = true;
late final OrgTypesBloc _orgTypesBloc;
bool _organisationPublique = true;
@override
void initState() {
super.initState();
_orgTypesBloc = sl<OrgTypesBloc>()..add(const LoadOrgTypes());
}
@override
void dispose() {
_orgTypesBloc.close();
_nomController.dispose();
_nomCourtController.dispose();
_descriptionController.dispose();
@@ -146,24 +157,39 @@ class _CreateOrganizationDialogState extends State<CreateOrganizationDialog> {
),
const SizedBox(height: 12),
// Type d'organisation
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: Text(type.displayName),
// Type d'organisation dynamique
BlocBuilder<OrgTypesBloc, OrgTypesState>(
bloc: _orgTypesBloc,
builder: (context, orgTypesState) {
final types = orgTypesState is OrgTypesLoaded
? orgTypesState.types
: orgTypesState is OrgTypeSuccess
? orgTypesState.types
: <TypeReferenceEntity>[];
if (orgTypesState is OrgTypesLoading || orgTypesState is OrgTypesInitial) {
return const InputDecorator(
decoration: InputDecoration(
labelText: 'Type d\'organisation *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.category),
),
child: LinearProgressIndicator(),
);
}
return DropdownButtonFormField<String>(
value: types.any((t) => t.code == _selectedTypeCode) ? _selectedTypeCode : null,
decoration: const InputDecoration(
labelText: 'Type d\'organisation *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.category),
),
items: types.map((type) => DropdownMenuItem<String>(
value: type.code,
child: Text(type.libelle),
)).toList(),
onChanged: (value) => setState(() => _selectedTypeCode = value),
validator: (value) => value == null ? 'Le type est obligatoire' : null,
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedType = value!;
});
},
),
const SizedBox(height: 16),
@@ -378,7 +404,7 @@ class _CreateOrganizationDialogState extends State<CreateOrganizationDialog> {
pays: _paysController.text.isNotEmpty ? _paysController.text : null,
siteWeb: _siteWebController.text.isNotEmpty ? _siteWebController.text : null,
objectifs: _objectifsController.text.isNotEmpty ? _objectifsController.text : null,
typeOrganisation: _selectedType,
typeOrganisation: _selectedTypeCode ?? 'ASSOCIATION',
statut: StatutOrganization.active,
accepteNouveauxMembres: _accepteNouveauxMembres,
organisationPublique: _organisationPublique,

View File

@@ -5,7 +5,11 @@ 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 '../../bloc/org_types_bloc.dart';
import '../../domain/entities/type_reference_entity.dart';
import '../../data/models/organization_model.dart';
import '../../../../core/di/injection_container.dart';
class EditOrganizationDialog extends StatefulWidget {
final OrganizationModel organization;
@@ -35,8 +39,10 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
late final TextEditingController _siteWebController;
late final TextEditingController _objectifsController;
late TypeOrganization _selectedType;
late String _selectedTypeCode;
late StatutOrganization _selectedStatut;
late final OrgTypesBloc _orgTypesBloc;
late final OrganizationsBloc _detailBloc;
late bool _accepteNouveauxMembres;
late bool _organisationPublique;
@@ -57,14 +63,44 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
_siteWebController = TextEditingController(text: widget.organization.siteWeb ?? '');
_objectifsController = TextEditingController(text: widget.organization.objectifs ?? '');
_selectedType = widget.organization.typeOrganisation;
_selectedTypeCode = widget.organization.typeOrganisation;
_selectedStatut = widget.organization.statut;
_orgTypesBloc = sl<OrgTypesBloc>()..add(const LoadOrgTypes());
_accepteNouveauxMembres = widget.organization.accepteNouveauxMembres;
_organisationPublique = widget.organization.organisationPublique;
// Charge le détail complet depuis l'API (la liste retourne un DTO allégé)
_detailBloc = sl<OrganizationsBloc>();
if (widget.organization.id != null) {
_detailBloc.add(LoadOrganizationById(widget.organization.id!));
}
}
void _refillForm(OrganizationModel org) {
_nomController.text = org.nom;
_nomCourtController.text = org.nomCourt ?? '';
_descriptionController.text = org.description ?? '';
_emailController.text = org.email ?? '';
_telephoneController.text = org.telephone ?? '';
_siteWebController.text = org.siteWeb ?? '';
_adresseController.text = org.adresse ?? '';
_villeController.text = org.ville ?? '';
_codePostalController.text = org.codePostal ?? '';
_regionController.text = org.region ?? '';
_paysController.text = org.pays ?? '';
_objectifsController.text = org.objectifs ?? '';
setState(() {
_selectedTypeCode = org.typeOrganisation;
_selectedStatut = org.statut;
_accepteNouveauxMembres = org.accepteNouveauxMembres;
_organisationPublique = org.organisationPublique;
});
}
@override
void dispose() {
_orgTypesBloc.close();
_detailBloc.close();
_nomController.dispose();
_nomCourtController.dispose();
_descriptionController.dispose();
@@ -82,8 +118,15 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
@override
Widget build(BuildContext context) {
return Dialog(
child: Container(
return BlocListener<OrganizationsBloc, OrganizationsState>(
bloc: _detailBloc,
listener: (context, state) {
if (state is OrganizationLoaded) {
_refillForm(state.organization);
}
},
child: Dialog(
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
constraints: const BoxConstraints(maxHeight: 600),
child: Column(
@@ -149,6 +192,7 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
],
),
),
),
);
}
@@ -237,23 +281,40 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
}
Widget _buildTypeDropdown() {
return 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: Text(type.displayName),
return BlocBuilder<OrgTypesBloc, OrgTypesState>(
bloc: _orgTypesBloc,
builder: (context, orgTypesState) {
final types = orgTypesState is OrgTypesLoaded
? orgTypesState.types
: orgTypesState is OrgTypeSuccess
? orgTypesState.types
: <TypeReferenceEntity>[];
if (orgTypesState is OrgTypesLoading || orgTypesState is OrgTypesInitial) {
return const InputDecorator(
decoration: InputDecoration(
labelText: 'Type d\'organisation *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.category),
),
child: LinearProgressIndicator(),
);
}
return DropdownButtonFormField<String>(
value: types.any((t) => t.code == _selectedTypeCode) ? _selectedTypeCode : null,
decoration: const InputDecoration(
labelText: 'Type d\'organisation *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.category),
),
items: types.map((type) => DropdownMenuItem<String>(
value: type.code,
child: Text(type.libelle),
)).toList(),
onChanged: (value) {
if (value != null) setState(() => _selectedTypeCode = value);
},
validator: (value) => value == null ? 'Le type est obligatoire' : null,
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedType = value!;
});
},
);
}
@@ -453,7 +514,7 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
pays: _paysController.text.isNotEmpty ? _paysController.text : null,
siteWeb: _siteWebController.text.isNotEmpty ? _siteWebController.text : null,
objectifs: _objectifsController.text.isNotEmpty ? _objectifsController.text : null,
typeOrganisation: _selectedType,
typeOrganisation: _selectedTypeCode,
statut: _selectedStatut,
accepteNouveauxMembres: _accepteNouveauxMembres,
organisationPublique: _organisationPublique,

View File

@@ -68,10 +68,7 @@ class OrganizationCard extends StatelessWidget {
color: const Color(0xFF6C5CE7).withOpacity(0.1), // ColorTokens cohérent
borderRadius: BorderRadius.circular(6),
),
child: Text(
organization.typeOrganisation.icon,
style: const TextStyle(fontSize: 16),
),
child: const Icon(Icons.business_outlined, size: 18, color: Color(0xFF6C5CE7)),
),
const SizedBox(width: 12),
// Nom et nom court
@@ -144,7 +141,7 @@ class OrganizationCard extends StatelessWidget {
),
const SizedBox(width: 6),
Text(
organization.typeOrganisation.displayName,
organization.typeOrganisation,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),

View File

@@ -169,7 +169,7 @@ class OrganizationFilterWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(6),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<TypeOrganization?>(
child: DropdownButton<String?>(
value: state.typeFilter,
hint: const Text(
'Type',
@@ -185,30 +185,17 @@ class OrganizationFilterWidget extends StatelessWidget {
color: Color(0xFF374151),
),
items: [
const DropdownMenuItem<TypeOrganization?>(
const DropdownMenuItem<String?>(
value: null,
child: Text('Tous les types'),
),
...TypeOrganization.values.map((type) {
return DropdownMenuItem<TypeOrganization?>(
value: type,
child: Row(
children: [
Text(
type.icon,
style: const TextStyle(fontSize: 12),
),
const SizedBox(width: 6),
Expanded(
child: Text(
type.displayName,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}),
...state.organizations
.map((org) => org.typeOrganisation)
.toSet()
.map((code) => DropdownMenuItem<String?>(
value: code,
child: Text(code, overflow: TextOverflow.ellipsis),
)),
],
onChanged: (value) {
context.read<OrganizationsBloc>().add(