Refactoring - Version OK

This commit is contained in:
dahoud
2025-11-17 16:02:04 +00:00
parent 3f00a26308
commit 3b9ffac8cd
198 changed files with 18010 additions and 11383 deletions

View File

@@ -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,
),
);
}
}
}

View File

@@ -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,
);
}
}

View File

@@ -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(', ');
}
}

View File

@@ -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(),
),
),
],
);
}
}

View File

@@ -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),
),
),
),
);
}
}

View File

@@ -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,
),
),
],
),
),
),
);
}
}