Refactoring - Version OK
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
/// Dialogue de création d'organisation (mutuelle)
|
||||
/// Formulaire complet pour créer une nouvelle mutuelle
|
||||
library create_organisation_dialog;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../bloc/organizations_bloc.dart';
|
||||
import '../../bloc/organizations_event.dart';
|
||||
import '../../data/models/organization_model.dart';
|
||||
|
||||
/// Dialogue de création d'organisation
|
||||
class CreateOrganizationDialog extends StatefulWidget {
|
||||
const CreateOrganizationDialog({super.key});
|
||||
|
||||
@override
|
||||
State<CreateOrganizationDialog> createState() => _CreateOrganizationDialogState();
|
||||
}
|
||||
|
||||
class _CreateOrganizationDialogState extends State<CreateOrganizationDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Contrôleurs de texte
|
||||
final _nomController = TextEditingController();
|
||||
final _nomCourtController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _telephoneController = TextEditingController();
|
||||
final _adresseController = TextEditingController();
|
||||
final _villeController = TextEditingController();
|
||||
final _codePostalController = TextEditingController();
|
||||
final _regionController = TextEditingController();
|
||||
final _paysController = TextEditingController();
|
||||
final _siteWebController = TextEditingController();
|
||||
final _objectifsController = TextEditingController();
|
||||
|
||||
// Valeurs sélectionnées
|
||||
TypeOrganization _selectedType = TypeOrganization.association;
|
||||
bool _accepteNouveauxMembres = true;
|
||||
bool _organisationPublique = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nomController.dispose();
|
||||
_nomCourtController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_emailController.dispose();
|
||||
_telephoneController.dispose();
|
||||
_adresseController.dispose();
|
||||
_villeController.dispose();
|
||||
_codePostalController.dispose();
|
||||
_regionController.dispose();
|
||||
_paysController.dispose();
|
||||
_siteWebController.dispose();
|
||||
_objectifsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
constraints: const BoxConstraints(maxHeight: 600),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// En-tête
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF8B5CF6),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
topRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.business, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'Créer une mutuelle',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Formulaire
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Informations de base
|
||||
_buildSectionTitle('Informations de base'),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextFormField(
|
||||
controller: _nomController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom de la mutuelle *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.business),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Le nom est obligatoire';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextFormField(
|
||||
controller: _nomCourtController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom court / Sigle',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.short_text),
|
||||
hintText: 'Ex: MUTEC, MUPROCI',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.description),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
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),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Contact
|
||||
_buildSectionTitle('Contact'),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.email),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'L\'email est obligatoire';
|
||||
}
|
||||
if (!value.contains('@')) {
|
||||
return 'Email invalide';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextFormField(
|
||||
controller: _telephoneController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Téléphone',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextFormField(
|
||||
controller: _siteWebController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Site web',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.language),
|
||||
hintText: 'https://www.exemple.com',
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Adresse
|
||||
_buildSectionTitle('Adresse'),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextFormField(
|
||||
controller: _adresseController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Adresse',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.home),
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _villeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Ville',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _codePostalController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Code postal',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextFormField(
|
||||
controller: _regionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Région',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextFormField(
|
||||
controller: _paysController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pays',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Objectifs
|
||||
_buildSectionTitle('Objectifs et mission'),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextFormField(
|
||||
controller: _objectifsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Objectifs',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.flag),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Paramètres
|
||||
_buildSectionTitle('Paramètres'),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Accepte de nouveaux membres'),
|
||||
subtitle: const Text('Permet l\'adhésion de nouveaux membres'),
|
||||
value: _accepteNouveauxMembres,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_accepteNouveauxMembres = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Organisation publique'),
|
||||
subtitle: const Text('Visible dans l\'annuaire public'),
|
||||
value: _organisationPublique,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_organisationPublique = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Boutons d'action
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
border: Border(top: BorderSide(color: Colors.grey[300]!)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: _submitForm,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF8B5CF6),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Créer la mutuelle'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF8B5CF6),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _submitForm() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Créer le modèle d'organisation
|
||||
final organisation = OrganizationModel(
|
||||
nom: _nomController.text,
|
||||
nomCourt: _nomCourtController.text.isNotEmpty ? _nomCourtController.text : null,
|
||||
description: _descriptionController.text.isNotEmpty ? _descriptionController.text : null,
|
||||
email: _emailController.text,
|
||||
telephone: _telephoneController.text.isNotEmpty ? _telephoneController.text : null,
|
||||
adresse: _adresseController.text.isNotEmpty ? _adresseController.text : null,
|
||||
ville: _villeController.text.isNotEmpty ? _villeController.text : null,
|
||||
codePostal: _codePostalController.text.isNotEmpty ? _codePostalController.text : null,
|
||||
region: _regionController.text.isNotEmpty ? _regionController.text : null,
|
||||
pays: _paysController.text.isNotEmpty ? _paysController.text : null,
|
||||
siteWeb: _siteWebController.text.isNotEmpty ? _siteWebController.text : null,
|
||||
objectifs: _objectifsController.text.isNotEmpty ? _objectifsController.text : null,
|
||||
typeOrganisation: _selectedType,
|
||||
statut: StatutOrganization.active,
|
||||
accepteNouveauxMembres: _accepteNouveauxMembres,
|
||||
organisationPublique: _organisationPublique,
|
||||
);
|
||||
|
||||
// Envoyer l'événement au BLoC
|
||||
context.read<OrganizationsBloc>().add(CreateOrganization(organisation));
|
||||
|
||||
// Fermer le dialogue
|
||||
Navigator.pop(context);
|
||||
|
||||
// Afficher un message de succès
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Mutuelle créée avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
/// Dialogue de modification d'organisation (mutuelle)
|
||||
library edit_organisation_dialog;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../bloc/organizations_bloc.dart';
|
||||
import '../../bloc/organizations_event.dart';
|
||||
import '../../data/models/organization_model.dart';
|
||||
|
||||
class EditOrganizationDialog extends StatefulWidget {
|
||||
final OrganizationModel organization;
|
||||
|
||||
const EditOrganizationDialog({
|
||||
super.key,
|
||||
required this.organization,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EditOrganizationDialog> createState() => _EditOrganizationDialogState();
|
||||
}
|
||||
|
||||
class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
|
||||
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 _adresseController;
|
||||
late final TextEditingController _villeController;
|
||||
late final TextEditingController _codePostalController;
|
||||
late final TextEditingController _regionController;
|
||||
late final TextEditingController _paysController;
|
||||
late final TextEditingController _siteWebController;
|
||||
late final TextEditingController _objectifsController;
|
||||
|
||||
late TypeOrganization _selectedType;
|
||||
late StatutOrganization _selectedStatut;
|
||||
late bool _accepteNouveauxMembres;
|
||||
late bool _organisationPublique;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_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 ?? '');
|
||||
_adresseController = TextEditingController(text: widget.organization.adresse ?? '');
|
||||
_villeController = TextEditingController(text: widget.organization.ville ?? '');
|
||||
_codePostalController = TextEditingController(text: widget.organization.codePostal ?? '');
|
||||
_regionController = TextEditingController(text: widget.organization.region ?? '');
|
||||
_paysController = TextEditingController(text: widget.organization.pays ?? '');
|
||||
_siteWebController = TextEditingController(text: widget.organization.siteWeb ?? '');
|
||||
_objectifsController = TextEditingController(text: widget.organization.objectifs ?? '');
|
||||
|
||||
_selectedType = widget.organization.typeOrganisation;
|
||||
_selectedStatut = widget.organization.statut;
|
||||
_accepteNouveauxMembres = widget.organization.accepteNouveauxMembres;
|
||||
_organisationPublique = widget.organization.organisationPublique;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nomController.dispose();
|
||||
_nomCourtController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_emailController.dispose();
|
||||
_telephoneController.dispose();
|
||||
_adresseController.dispose();
|
||||
_villeController.dispose();
|
||||
_codePostalController.dispose();
|
||||
_regionController.dispose();
|
||||
_paysController.dispose();
|
||||
_siteWebController.dispose();
|
||||
_objectifsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
constraints: const BoxConstraints(maxHeight: 600),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('Informations de base'),
|
||||
const SizedBox(height: 12),
|
||||
_buildNomField(),
|
||||
const SizedBox(height: 12),
|
||||
_buildNomCourtField(),
|
||||
const SizedBox(height: 12),
|
||||
_buildDescriptionField(),
|
||||
const SizedBox(height: 12),
|
||||
_buildTypeDropdown(),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatutDropdown(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSectionTitle('Contact'),
|
||||
const SizedBox(height: 12),
|
||||
_buildEmailField(),
|
||||
const SizedBox(height: 12),
|
||||
_buildTelephoneField(),
|
||||
const SizedBox(height: 12),
|
||||
_buildSiteWebField(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSectionTitle('Adresse'),
|
||||
const SizedBox(height: 12),
|
||||
_buildAdresseField(),
|
||||
const SizedBox(height: 12),
|
||||
_buildVilleCodePostalRow(),
|
||||
const SizedBox(height: 12),
|
||||
_buildRegionField(),
|
||||
const SizedBox(height: 12),
|
||||
_buildPaysField(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSectionTitle('Objectifs et mission'),
|
||||
const SizedBox(height: 12),
|
||||
_buildObjectifsField(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSectionTitle('Paramètres'),
|
||||
const SizedBox(height: 12),
|
||||
_buildAccepteNouveauxMembresSwitch(),
|
||||
_buildOrganisationPubliqueSwitch(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF8B5CF6),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
topRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.edit, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'Modifier la mutuelle',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF8B5CF6),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNomField() {
|
||||
return TextFormField(
|
||||
controller: _nomController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom de la mutuelle *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.business),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Le nom est obligatoire';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNomCourtField() {
|
||||
return TextFormField(
|
||||
controller: _nomCourtController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom court / Sigle',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.short_text),
|
||||
hintText: 'Ex: MUTEC, MUPROCI',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionField() {
|
||||
return TextFormField(
|
||||
controller: _descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.description),
|
||||
),
|
||||
maxLines: 3,
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedType = value!;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatutDropdown() {
|
||||
return DropdownButtonFormField<StatutOrganization>(
|
||||
value: _selectedStatut,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Statut *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.info),
|
||||
),
|
||||
items: StatutOrganization.values.map((statut) {
|
||||
return DropdownMenuItem(
|
||||
value: statut,
|
||||
child: Text(statut.displayName),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedStatut = value!;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailField() {
|
||||
return TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.email),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'L\'email est obligatoire';
|
||||
}
|
||||
if (!value.contains('@')) {
|
||||
return 'Email invalide';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSiteWebField() {
|
||||
return TextFormField(
|
||||
controller: _siteWebController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Site web',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.language),
|
||||
hintText: 'https://www.exemple.com',
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAdresseField() {
|
||||
return TextFormField(
|
||||
controller: _adresseController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Adresse',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.home),
|
||||
),
|
||||
maxLines: 2,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVilleCodePostalRow() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _villeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Ville',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _codePostalController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Code postal',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRegionField() {
|
||||
return TextFormField(
|
||||
controller: _regionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Région',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaysField() {
|
||||
return TextFormField(
|
||||
controller: _paysController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pays',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildObjectifsField() {
|
||||
return TextFormField(
|
||||
controller: _objectifsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Objectifs',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.flag),
|
||||
),
|
||||
maxLines: 3,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAccepteNouveauxMembresSwitch() {
|
||||
return SwitchListTile(
|
||||
title: const Text('Accepte de nouveaux membres'),
|
||||
subtitle: const Text('Permet l\'adhésion de nouveaux membres'),
|
||||
value: _accepteNouveauxMembres,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_accepteNouveauxMembres = value;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOrganisationPubliqueSwitch() {
|
||||
return SwitchListTile(
|
||||
title: const Text('Organisation publique'),
|
||||
subtitle: const Text('Visible dans l\'annuaire public'),
|
||||
value: _organisationPublique,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_organisationPublique = value;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
border: Border(top: BorderSide(color: Colors.grey[300]!)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: _submitForm,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF8B5CF6),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Enregistrer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _submitForm() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
final updatedOrganisation = widget.organization.copyWith(
|
||||
nom: _nomController.text,
|
||||
nomCourt: _nomCourtController.text.isNotEmpty ? _nomCourtController.text : null,
|
||||
description: _descriptionController.text.isNotEmpty ? _descriptionController.text : null,
|
||||
email: _emailController.text,
|
||||
telephone: _telephoneController.text.isNotEmpty ? _telephoneController.text : null,
|
||||
adresse: _adresseController.text.isNotEmpty ? _adresseController.text : null,
|
||||
ville: _villeController.text.isNotEmpty ? _villeController.text : null,
|
||||
codePostal: _codePostalController.text.isNotEmpty ? _codePostalController.text : null,
|
||||
region: _regionController.text.isNotEmpty ? _regionController.text : null,
|
||||
pays: _paysController.text.isNotEmpty ? _paysController.text : null,
|
||||
siteWeb: _siteWebController.text.isNotEmpty ? _siteWebController.text : null,
|
||||
objectifs: _objectifsController.text.isNotEmpty ? _objectifsController.text : null,
|
||||
typeOrganisation: _selectedType,
|
||||
statut: _selectedStatut,
|
||||
accepteNouveauxMembres: _accepteNouveauxMembres,
|
||||
organisationPublique: _organisationPublique,
|
||||
);
|
||||
|
||||
context.read<OrganizationsBloc>().add(UpdateOrganization(widget.organization.id!, updatedOrganisation));
|
||||
Navigator.pop(context);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Mutuelle modifiée avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTelephoneField() {
|
||||
return TextFormField(
|
||||
controller: _telephoneController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Téléphone',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/// Widget de carte d'organisation
|
||||
/// Respecte le design system établi avec les mêmes patterns que les autres cartes
|
||||
library organization_card;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../data/models/organization_model.dart';
|
||||
|
||||
/// Carte d'organisation avec design cohérent
|
||||
class OrganizationCard extends StatelessWidget {
|
||||
final OrganizationModel organization;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onEdit;
|
||||
final VoidCallback? onDelete;
|
||||
final bool showActions;
|
||||
|
||||
const OrganizationCard({
|
||||
super.key,
|
||||
required this.organization,
|
||||
this.onTap,
|
||||
this.onEdit,
|
||||
this.onDelete,
|
||||
this.showActions = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
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: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12), // SpacingTokens cohérent
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 8),
|
||||
_buildContent(),
|
||||
const SizedBox(height: 8),
|
||||
_buildFooter(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Header avec nom et statut
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
// Icône du type d'organisation
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF6C5CE7).withOpacity(0.1), // ColorTokens cohérent
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
organization.typeOrganisation.icon,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Nom et nom court
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
organization.nom,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF374151), // ColorTokens cohérent
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (organization.nomCourt?.isNotEmpty == true) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
organization.nomCourt!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
// Badge de statut
|
||||
_buildStatusBadge(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Badge de statut
|
||||
Widget _buildStatusBadge() {
|
||||
final color = Color(int.parse(organization.statut.color.substring(1), radix: 16) + 0xFF000000);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
organization.statut.displayName,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Contenu principal
|
||||
Widget _buildContent() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Type d'organisation
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.category_outlined,
|
||||
size: 14,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
organization.typeOrganisation.displayName,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// Localisation
|
||||
if (organization.ville?.isNotEmpty == true || organization.region?.isNotEmpty == true)
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on_outlined,
|
||||
size: 14,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_buildLocationText(),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// Description si disponible
|
||||
if (organization.description?.isNotEmpty == true) ...[
|
||||
Text(
|
||||
organization.description!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Footer avec statistiques et actions
|
||||
Widget _buildFooter() {
|
||||
return Row(
|
||||
children: [
|
||||
// Statistiques
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
_buildStatItem(
|
||||
icon: Icons.people_outline,
|
||||
value: organization.nombreMembres.toString(),
|
||||
label: 'membres',
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
if (organization.ancienneteAnnees > 0)
|
||||
_buildStatItem(
|
||||
icon: Icons.access_time,
|
||||
value: organization.ancienneteAnnees.toString(),
|
||||
label: 'ans',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Actions
|
||||
if (showActions) _buildActions(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Item de statistique
|
||||
Widget _buildStatItem({
|
||||
required IconData icon,
|
||||
required String value,
|
||||
required String label,
|
||||
}) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 14,
|
||||
color: const Color(0xFF6C5CE7),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'$value $label',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF374151),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Actions (éditer, supprimer)
|
||||
Widget _buildActions() {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (onEdit != null)
|
||||
IconButton(
|
||||
onPressed: onEdit,
|
||||
icon: const Icon(
|
||||
Icons.edit_outlined,
|
||||
size: 18,
|
||||
color: Color(0xFF6C5CE7),
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
tooltip: 'Modifier',
|
||||
),
|
||||
if (onDelete != null)
|
||||
IconButton(
|
||||
onPressed: onDelete,
|
||||
icon: Icon(
|
||||
Icons.delete_outline,
|
||||
size: 18,
|
||||
color: Colors.red.shade400,
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
tooltip: 'Supprimer',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit le texte de localisation
|
||||
String _buildLocationText() {
|
||||
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.join(', ');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/// Widget de filtres pour les organisations
|
||||
/// Respecte le design system établi
|
||||
library organization_filter_widget;
|
||||
|
||||
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';
|
||||
|
||||
/// Widget de filtres avec design cohérent
|
||||
class OrganizationFilterWidget extends StatelessWidget {
|
||||
const OrganizationFilterWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<OrganizationsBloc, OrganizationsState>(
|
||||
builder: (context, state) {
|
||||
if (state is! OrganizationsLoaded) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
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: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.filter_list,
|
||||
size: 16,
|
||||
color: Color(0xFF6C5CE7),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'Filtres',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF374151),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (state.hasFilters)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<OrganizationsBloc>().add(
|
||||
const ClearOrganizationsFilters(),
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: const Text(
|
||||
'Effacer',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6C5CE7),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatusFilter(context, state),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _buildTypeFilter(context, state),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildSortOptions(context, state),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Filtre par statut
|
||||
Widget _buildStatusFilter(BuildContext context, OrganizationsLoaded state) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: const Color(0xFFE5E7EB),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<StatutOrganization?>(
|
||||
value: state.statusFilter,
|
||||
hint: const Text(
|
||||
'Statut',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
isExpanded: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF374151),
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem<StatutOrganization?>(
|
||||
value: null,
|
||||
child: Text('Tous les statuts'),
|
||||
),
|
||||
...StatutOrganization.values.map((statut) {
|
||||
return DropdownMenuItem<StatutOrganization?>(
|
||||
value: statut,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(int.parse(statut.color.substring(1), radix: 16) + 0xFF000000),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(statut.displayName),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
onChanged: (value) {
|
||||
context.read<OrganizationsBloc>().add(
|
||||
FilterOrganizationsByStatus(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Filtre par type
|
||||
Widget _buildTypeFilter(BuildContext context, OrganizationsLoaded state) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: const Color(0xFFE5E7EB),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<TypeOrganization?>(
|
||||
value: state.typeFilter,
|
||||
hint: const Text(
|
||||
'Type',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
isExpanded: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF374151),
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem<TypeOrganization?>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
onChanged: (value) {
|
||||
context.read<OrganizationsBloc>().add(
|
||||
FilterOrganizationsByType(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Options de tri
|
||||
Widget _buildSortOptions(BuildContext context, OrganizationsLoaded state) {
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.sort,
|
||||
size: 14,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'Trier par:',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
children: OrganizationSortType.values.map((sortType) {
|
||||
final isSelected = state.sortType == sortType;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
final ascending = isSelected ? !state.sortAscending : true;
|
||||
context.read<OrganizationsBloc>().add(
|
||||
SortOrganizations(sortType, ascending: ascending),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? const Color(0xFF6C5CE7).withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? const Color(0xFF6C5CE7)
|
||||
: const Color(0xFFE5E7EB),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
sortType.displayName,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: isSelected
|
||||
? const Color(0xFF6C5CE7)
|
||||
: const Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
if (isSelected) ...[
|
||||
const SizedBox(width: 2),
|
||||
Icon(
|
||||
state.sortAscending
|
||||
? Icons.arrow_upward
|
||||
: Icons.arrow_downward,
|
||||
size: 10,
|
||||
color: const Color(0xFF6C5CE7),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/// Widget de barre de recherche pour les organisations
|
||||
/// Respecte le design system établi
|
||||
library organisation_search_bar;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Barre de recherche avec design cohérent
|
||||
class OrganisationSearchBar extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
final Function(String) onSearch;
|
||||
final VoidCallback? onClear;
|
||||
final String hintText;
|
||||
final bool enabled;
|
||||
|
||||
const OrganisationSearchBar({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.onSearch,
|
||||
this.onClear,
|
||||
this.hintText = 'Rechercher une organisation...',
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OrganisationSearchBar> createState() => _OrganisationSearchBarState();
|
||||
}
|
||||
|
||||
class _OrganisationSearchBarState extends State<OrganisationSearchBar> {
|
||||
bool _hasText = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_onTextChanged);
|
||||
_hasText = widget.controller.text.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_onTextChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTextChanged() {
|
||||
final hasText = widget.controller.text.isNotEmpty;
|
||||
if (hasText != _hasText) {
|
||||
setState(() {
|
||||
_hasText = hasText;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
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: Material(
|
||||
color: Colors.transparent,
|
||||
child: TextField(
|
||||
controller: widget.controller,
|
||||
enabled: widget.enabled,
|
||||
onChanged: widget.onSearch,
|
||||
onSubmitted: widget.onSearch,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xFF6B7280),
|
||||
fontSize: 14,
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF6C5CE7), // ColorTokens cohérent
|
||||
size: 20,
|
||||
),
|
||||
suffixIcon: _hasText
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
widget.controller.clear();
|
||||
widget.onClear?.call();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.clear,
|
||||
color: Color(0xFF6B7280),
|
||||
size: 20,
|
||||
),
|
||||
tooltip: 'Effacer',
|
||||
)
|
||||
: null,
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF374151),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/// Widget des statistiques des organisations
|
||||
/// Respecte le design system avec les mêmes patterns que les autres stats
|
||||
library organisation_stats_widget;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Widget des statistiques avec design cohérent
|
||||
class OrganisationStatsWidget extends StatelessWidget {
|
||||
final Map<String, int> stats;
|
||||
final Function(String)? onStatTap;
|
||||
|
||||
const OrganisationStatsWidget({
|
||||
super.key,
|
||||
required this.stats,
|
||||
this.onStatTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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(
|
||||
'Statistiques',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6C5CE7), // ColorTokens cohérent
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
title: 'Total',
|
||||
value: stats['total']?.toString() ?? '0',
|
||||
icon: Icons.business,
|
||||
color: const Color(0xFF6C5CE7),
|
||||
onTap: () => onStatTap?.call('total'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
title: 'Actives',
|
||||
value: stats['actives']?.toString() ?? '0',
|
||||
icon: Icons.check_circle,
|
||||
color: const Color(0xFF10B981),
|
||||
onTap: () => onStatTap?.call('actives'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
title: 'Inactives',
|
||||
value: stats['inactives']?.toString() ?? '0',
|
||||
icon: Icons.pause_circle,
|
||||
color: const Color(0xFF6B7280),
|
||||
onTap: () => onStatTap?.call('inactives'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
title: 'Membres',
|
||||
value: stats['totalMembres']?.toString() ?? '0',
|
||||
icon: Icons.people,
|
||||
color: const Color(0xFF3B82F6),
|
||||
onTap: () => onStatTap?.call('membres'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Carte de statistique individuelle
|
||||
Widget _buildStatCard({
|
||||
required String title,
|
||||
required String value,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: 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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: color,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user