- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD) * DEV: Clés TEST Pierre (mode test) * REC: Clés TEST Client (mode test) * PROD: Clés LIVE Client (mode live) - Ajout de la gestion des bases de données immeubles/bâtiments * Configuration buildings_database pour DEV/REC/PROD * Service BuildingService pour enrichissement des adresses - Optimisations pages et améliorations ergonomie - Mises à jour des dépendances Composer - Nettoyage des fichiers obsolètes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1323 lines
48 KiB
Dart
1323 lines
48 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:geosector_app/core/constants/app_keys.dart';
|
|
import 'package:geosector_app/core/theme/app_theme.dart';
|
|
import 'package:geosector_app/core/data/models/membre_model.dart';
|
|
import 'package:geosector_app/core/data/models/passage_model.dart';
|
|
import 'package:geosector_app/core/data/models/sector_model.dart';
|
|
import 'package:geosector_app/core/repositories/operation_repository.dart';
|
|
import 'package:geosector_app/core/services/current_user_service.dart';
|
|
import 'package:geosector_app/app.dart';
|
|
|
|
/// Widget affichant un tableau détaillé des membres avec leurs statistiques de passages
|
|
/// Uniquement visible sur plateforme Web
|
|
class MembersBoardPassages extends StatefulWidget {
|
|
final String title;
|
|
final double? height;
|
|
|
|
const MembersBoardPassages({
|
|
super.key,
|
|
this.title = 'Détails par membre',
|
|
this.height,
|
|
});
|
|
|
|
@override
|
|
State<MembersBoardPassages> createState() => _MembersBoardPassagesState();
|
|
}
|
|
|
|
class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
|
// Repository pour récupérer l'opération courante uniquement
|
|
final OperationRepository _operationRepository = operationRepository;
|
|
|
|
// Variables pour le tri (valeurs par défaut, seront restaurées depuis settings)
|
|
int? _sortColumnIndex;
|
|
bool _sortAscending = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadSortSettings();
|
|
}
|
|
|
|
// Charger les paramètres de tri depuis la box settings
|
|
void _loadSortSettings() {
|
|
try {
|
|
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
|
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
|
_sortColumnIndex = settingsBox.get('membersBoardSortColumn', defaultValue: 2); // 2 = Effectués par défaut
|
|
_sortAscending = settingsBox.get('membersBoardSortAscending', defaultValue: false); // Descendant par défaut
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du chargement des paramètres de tri: $e');
|
|
// Valeurs par défaut en cas d'erreur
|
|
_sortColumnIndex = 2;
|
|
_sortAscending = false;
|
|
}
|
|
}
|
|
|
|
// Sauvegarder les paramètres de tri dans la box settings
|
|
void _saveSortSettings() {
|
|
try {
|
|
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
|
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
|
settingsBox.put('membersBoardSortColumn', _sortColumnIndex);
|
|
settingsBox.put('membersBoardSortAscending', _sortAscending);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la sauvegarde des paramètres de tri: $e');
|
|
}
|
|
}
|
|
|
|
// Vérifier si le type Lot doit être affiché
|
|
bool _shouldShowLotType() {
|
|
final currentUser = CurrentUserService.instance.currentUser;
|
|
if (currentUser != null && currentUser.fkEntite != null) {
|
|
final userAmicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!);
|
|
if (userAmicale != null) {
|
|
return userAmicale.chkLotActif;
|
|
}
|
|
}
|
|
return true; // Par défaut, on affiche
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
|
|
boxShadow: AppTheme.cardShadow,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// En-tête de la card
|
|
Container(
|
|
padding: const EdgeInsets.all(AppTheme.spacingM),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.primary.withOpacity(0.05),
|
|
borderRadius: const BorderRadius.only(
|
|
topLeft: Radius.circular(AppTheme.borderRadiusMedium),
|
|
topRight: Radius.circular(AppTheme.borderRadiusMedium),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.people_outline,
|
|
color: theme.colorScheme.primary,
|
|
size: 24,
|
|
),
|
|
const SizedBox(width: AppTheme.spacingS),
|
|
Text(
|
|
widget.title,
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Corps avec le tableau
|
|
ValueListenableBuilder<Box<MembreModel>>(
|
|
valueListenable: Hive.box<MembreModel>(AppKeys.membresBoxName).listenable(),
|
|
builder: (context, membresBox, child) {
|
|
final membres = membresBox.values.toList();
|
|
|
|
// Récupérer l'opération courante
|
|
final currentOperation = _operationRepository.getCurrentOperation();
|
|
if (currentOperation == null) {
|
|
return const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(AppTheme.spacingL),
|
|
child: Text('Aucune opération en cours'),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Trier les membres selon la colonne sélectionnée
|
|
_sortMembers(membres, currentOperation.id);
|
|
|
|
// Construire les lignes : TOTAL en première position + détails membres
|
|
final allRows = [
|
|
_buildTotalRow(membres, currentOperation.id, theme),
|
|
..._buildRows(membres, currentOperation.id, theme),
|
|
];
|
|
|
|
// Afficher le tableau complet sans scroll interne
|
|
return SizedBox(
|
|
width: double.infinity, // Prendre toute la largeur disponible
|
|
child: Theme(
|
|
data: Theme.of(context).copyWith(
|
|
dataTableTheme: DataTableThemeData(
|
|
headingRowColor: WidgetStateProperty.resolveWith<Color?>(
|
|
(Set<WidgetState> states) {
|
|
return theme.colorScheme.primary.withOpacity(0.08);
|
|
},
|
|
),
|
|
dataRowColor: WidgetStateProperty.resolveWith<Color?>(
|
|
(Set<WidgetState> states) {
|
|
if (states.contains(WidgetState.selected)) {
|
|
return theme.colorScheme.primary.withOpacity(0.08);
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
child: DataTable(
|
|
columnSpacing: 4, // Espacement minimal entre colonnes
|
|
horizontalMargin: 4, // Marges horizontales minimales
|
|
headingRowHeight: 42, // Hauteur de l'en-tête optimisée
|
|
dataRowMinHeight: 42,
|
|
dataRowMaxHeight: 42,
|
|
// Utiliser les flèches natives de DataTable
|
|
sortColumnIndex: _sortColumnIndex,
|
|
sortAscending: _sortAscending,
|
|
columns: _buildColumns(theme),
|
|
rows: allRows,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Trie les membres selon la colonne sélectionnée
|
|
void _sortMembers(List<MembreModel> membres, int operationId) {
|
|
if (_sortColumnIndex == null) {
|
|
// Tri par défaut : par nom
|
|
membres.sort((a, b) {
|
|
final nameA = '${a.firstName ?? ''} ${a.name ?? ''}'.trim();
|
|
final nameB = '${b.firstName ?? ''} ${b.name ?? ''}'.trim();
|
|
return nameA.compareTo(nameB);
|
|
});
|
|
return;
|
|
}
|
|
|
|
final passageBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
|
final allPassages = passageBox.values.where((p) => p.fkOperation == operationId).toList();
|
|
final showLotType = _shouldShowLotType();
|
|
|
|
// Fonction helper pour obtenir les stats d'un membre
|
|
Map<String, dynamic> getMemberStats(MembreModel membre) {
|
|
final memberPassages = allPassages.where((p) => p.fkUser == membre.opeUserId).toList();
|
|
int totalCount = memberPassages.length;
|
|
int effectueCount = 0;
|
|
double effectueMontant = 0.0;
|
|
int aFinaliserCount = 0;
|
|
int refuseCount = 0;
|
|
int donCount = 0;
|
|
int lotsCount = 0;
|
|
int videCount = 0;
|
|
|
|
for (final passage in memberPassages) {
|
|
switch (passage.fkType) {
|
|
case 1:
|
|
effectueCount++;
|
|
if (passage.montant.isNotEmpty) {
|
|
effectueMontant += double.tryParse(passage.montant) ?? 0.0;
|
|
}
|
|
break;
|
|
case 2:
|
|
aFinaliserCount++;
|
|
break;
|
|
case 3:
|
|
refuseCount++;
|
|
break;
|
|
case 4:
|
|
donCount++;
|
|
break;
|
|
case 5:
|
|
if (showLotType) lotsCount++;
|
|
break;
|
|
case 6:
|
|
videCount++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
double montantMoyen = effectueCount > 0 ? effectueMontant / effectueCount : 0.0;
|
|
final memberSectorIds = memberPassages.where((p) => p.fkSector != null).map((p) => p.fkSector!).toSet();
|
|
final sectorCount = memberSectorIds.length;
|
|
final passagesNonAFinaliser = totalCount - aFinaliserCount;
|
|
double tauxAvancement = totalCount > 0 ? passagesNonAFinaliser / totalCount : 0.0;
|
|
|
|
return {
|
|
'total': totalCount,
|
|
'effectue': effectueCount,
|
|
'effectueMontant': effectueMontant,
|
|
'montantMoyen': montantMoyen,
|
|
'aFinaliser': aFinaliserCount,
|
|
'refuse': refuseCount,
|
|
'don': donCount,
|
|
'lots': lotsCount,
|
|
'vide': videCount,
|
|
'tauxAvancement': tauxAvancement,
|
|
'secteurs': sectorCount,
|
|
};
|
|
}
|
|
|
|
membres.sort((a, b) {
|
|
final statsA = getMemberStats(a);
|
|
final statsB = getMemberStats(b);
|
|
int result = 0;
|
|
|
|
switch (_sortColumnIndex) {
|
|
case 0: // Nom
|
|
final nameA = '${a.firstName ?? ''} ${a.name ?? ''}'.trim();
|
|
final nameB = '${b.firstName ?? ''} ${b.name ?? ''}'.trim();
|
|
result = nameA.compareTo(nameB);
|
|
break;
|
|
case 1: // Total
|
|
result = statsA['total'].compareTo(statsB['total']);
|
|
break;
|
|
case 2: // Effectués
|
|
result = statsA['effectue'].compareTo(statsB['effectue']);
|
|
break;
|
|
case 3: // Montant moyen
|
|
result = statsA['montantMoyen'].compareTo(statsB['montantMoyen']);
|
|
break;
|
|
case 4: // À finaliser
|
|
result = statsA['aFinaliser'].compareTo(statsB['aFinaliser']);
|
|
break;
|
|
case 5: // Refusés
|
|
result = statsA['refuse'].compareTo(statsB['refuse']);
|
|
break;
|
|
case 6: // Dons
|
|
result = statsA['don'].compareTo(statsB['don']);
|
|
break;
|
|
case 7: // Lots (si affiché)
|
|
if (showLotType) {
|
|
result = statsA['lots'].compareTo(statsB['lots']);
|
|
} else {
|
|
result = statsA['vide'].compareTo(statsB['vide']);
|
|
}
|
|
break;
|
|
case 8: // Vides ou Taux d'avancement (dépend si Lots affiché)
|
|
if (showLotType) {
|
|
result = statsA['vide'].compareTo(statsB['vide']);
|
|
} else {
|
|
result = statsA['tauxAvancement'].compareTo(statsB['tauxAvancement']);
|
|
}
|
|
break;
|
|
case 9: // Taux d'avancement ou Secteurs (dépend si Lots affiché)
|
|
if (showLotType) {
|
|
result = statsA['tauxAvancement'].compareTo(statsB['tauxAvancement']);
|
|
} else {
|
|
result = statsA['secteurs'].compareTo(statsB['secteurs']);
|
|
}
|
|
break;
|
|
case 10: // Secteurs (si Lots affiché)
|
|
result = statsA['secteurs'].compareTo(statsB['secteurs']);
|
|
break;
|
|
}
|
|
|
|
return _sortAscending ? result : -result;
|
|
});
|
|
}
|
|
|
|
/// Helper pour construire le texte du header
|
|
Widget _buildHeaderText(String text, int columnIndex, TextStyle? style) {
|
|
return Text(
|
|
text,
|
|
style: style,
|
|
);
|
|
}
|
|
|
|
/// Helper pour gérer le tri de colonne
|
|
void _handleSort(int columnIndex) {
|
|
setState(() {
|
|
if (_sortColumnIndex == columnIndex) {
|
|
// Même colonne : basculer l'ordre
|
|
_sortAscending = !_sortAscending;
|
|
} else {
|
|
// Nouvelle colonne : tri ascendant par défaut
|
|
_sortColumnIndex = columnIndex;
|
|
_sortAscending = true;
|
|
}
|
|
// Sauvegarder les nouveaux paramètres de tri
|
|
_saveSortSettings();
|
|
});
|
|
}
|
|
|
|
/// Construit les colonnes du tableau
|
|
List<DataColumn> _buildColumns(ThemeData theme) {
|
|
// Utilise le thème pour une meilleure lisibilité
|
|
final headerStyle = theme.textTheme.labelLarge?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
) ?? const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
);
|
|
|
|
final showLotType = _shouldShowLotType();
|
|
|
|
final columns = [
|
|
// Nom
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: _buildHeaderText('Nom', 0, headerStyle),
|
|
),
|
|
onSort: (columnIndex, _) => _handleSort(columnIndex),
|
|
),
|
|
// Total
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Center(
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.route,
|
|
size: 16,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
const SizedBox(width: 4),
|
|
_buildHeaderText('Total', 1, headerStyle),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
numeric: true,
|
|
onSort: (columnIndex, _) => _handleSort(columnIndex),
|
|
),
|
|
// Effectués
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
color: Colors.green.withOpacity(0.2),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.task_alt,
|
|
size: 16,
|
|
color: Colors.green,
|
|
),
|
|
const SizedBox(width: 4),
|
|
_buildHeaderText('Effectués', 2, headerStyle),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
numeric: true,
|
|
onSort: (columnIndex, _) => _handleSort(columnIndex),
|
|
),
|
|
// Montant moyen
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Center(
|
|
child: _buildHeaderText('Moy./passage', 3, headerStyle),
|
|
),
|
|
),
|
|
numeric: true,
|
|
onSort: (columnIndex, _) => _handleSort(columnIndex),
|
|
),
|
|
// À finaliser
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
color: Colors.orange.withOpacity(0.2),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.refresh,
|
|
size: 16,
|
|
color: Colors.orange,
|
|
),
|
|
const SizedBox(width: 4),
|
|
_buildHeaderText('À finaliser', 4, headerStyle),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
numeric: true,
|
|
onSort: (columnIndex, _) => _handleSort(columnIndex),
|
|
),
|
|
// Refusés
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
color: Colors.red.withOpacity(0.2),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.block,
|
|
size: 16,
|
|
color: Colors.red,
|
|
),
|
|
const SizedBox(width: 4),
|
|
_buildHeaderText('Refusés', 5, headerStyle),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
numeric: true,
|
|
onSort: (columnIndex, _) => _handleSort(columnIndex),
|
|
),
|
|
// Dons
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
color: Colors.lightBlue.withOpacity(0.2),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.volunteer_activism,
|
|
size: 16,
|
|
color: Colors.lightBlue,
|
|
),
|
|
const SizedBox(width: 4),
|
|
_buildHeaderText('Dons', 6, headerStyle),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
numeric: true,
|
|
onSort: (columnIndex, _) => _handleSort(columnIndex),
|
|
),
|
|
// Lots - affiché seulement si chkLotActif = true
|
|
if (showLotType)
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
color: Colors.blue.withOpacity(0.2),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.layers,
|
|
size: 16,
|
|
color: Colors.blue,
|
|
),
|
|
const SizedBox(width: 4),
|
|
_buildHeaderText('Lots', 7, headerStyle),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
numeric: true,
|
|
onSort: (columnIndex, _) => _handleSort(columnIndex),
|
|
),
|
|
// Vides
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
color: Colors.grey.withOpacity(0.2),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.home_outlined,
|
|
size: 16,
|
|
color: Colors.grey,
|
|
),
|
|
const SizedBox(width: 4),
|
|
_buildHeaderText('Vides', showLotType ? 8 : 7, headerStyle),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
numeric: true,
|
|
onSort: (columnIndex, _) => _handleSort(columnIndex),
|
|
),
|
|
// Taux d'avancement
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Center(
|
|
child: _buildHeaderText('Avancement', showLotType ? 9 : 8, headerStyle),
|
|
),
|
|
),
|
|
onSort: (columnIndex, _) => _handleSort(columnIndex),
|
|
),
|
|
// Secteurs
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Center(
|
|
child: _buildHeaderText('Secteurs', showLotType ? 10 : 9, headerStyle),
|
|
),
|
|
),
|
|
numeric: true,
|
|
onSort: (columnIndex, _) => _handleSort(columnIndex),
|
|
),
|
|
];
|
|
|
|
return columns;
|
|
}
|
|
|
|
/// Navigation vers l'historique avec un type de passage
|
|
void _navigateToHistoryWithType(int typeId, {int? memberId}) {
|
|
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
|
|
|
// Réinitialiser TOUS les filtres
|
|
settingsBox.delete('history_selectedSectorId');
|
|
settingsBox.delete('history_selectedSectorName');
|
|
settingsBox.delete('history_selectedPaymentTypeId');
|
|
settingsBox.delete('history_startDate');
|
|
settingsBox.delete('history_endDate');
|
|
|
|
// Sélectionner le type de passage
|
|
settingsBox.put('history_selectedTypeId', typeId);
|
|
|
|
// Sélectionner le membre si spécifié, sinon réinitialiser (Tous les membres)
|
|
if (memberId != null) {
|
|
settingsBox.put('history_selectedMemberId', memberId);
|
|
} else {
|
|
settingsBox.delete('history_selectedMemberId');
|
|
}
|
|
|
|
// Naviguer vers la page historique
|
|
debugPrint('MembersBoardPassages: Navigation vers /admin/history avec typeId=$typeId${memberId != null ? ', memberId=$memberId' : ' (tous les membres)'}');
|
|
if (mounted) {
|
|
context.go('/admin/history');
|
|
}
|
|
}
|
|
|
|
/// Construit la ligne de totaux
|
|
DataRow _buildTotalRow(List<MembreModel> membres, int operationId, ThemeData theme) {
|
|
final showLotType = _shouldShowLotType();
|
|
|
|
// Récupérer directement depuis les boxes Hive (déjà ouvertes)
|
|
final passageBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
|
final allPassages = passageBox.values.where((p) => p.fkOperation == operationId).toList();
|
|
|
|
// Calculer les totaux globaux
|
|
int totalCount = allPassages.length;
|
|
int effectueCount = 0;
|
|
double effectueMontant = 0.0;
|
|
int aFinaliserCount = 0;
|
|
int refuseCount = 0;
|
|
int donCount = 0;
|
|
int lotsCount = 0;
|
|
double lotsMontant = 0.0;
|
|
int videCount = 0;
|
|
|
|
for (final passage in allPassages) {
|
|
switch (passage.fkType) {
|
|
case 1: // Effectué
|
|
effectueCount++;
|
|
if (passage.montant.isNotEmpty) {
|
|
effectueMontant += double.tryParse(passage.montant) ?? 0.0;
|
|
}
|
|
break;
|
|
case 2: // À finaliser
|
|
aFinaliserCount++;
|
|
break;
|
|
case 3: // Refusé
|
|
refuseCount++;
|
|
break;
|
|
case 4: // Don
|
|
donCount++;
|
|
break;
|
|
case 5: // Lots
|
|
if (showLotType) {
|
|
lotsCount++;
|
|
if (passage.montant.isNotEmpty) {
|
|
lotsMontant += double.tryParse(passage.montant) ?? 0.0;
|
|
}
|
|
}
|
|
break;
|
|
case 6: // Vide
|
|
videCount++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Calculer le montant moyen global
|
|
double montantMoyen = effectueCount > 0 ? effectueMontant / effectueCount : 0.0;
|
|
|
|
// Compter les secteurs uniques
|
|
final Set<int> uniqueSectorIds = {};
|
|
for (final passage in allPassages) {
|
|
if (passage.fkSector != null) {
|
|
uniqueSectorIds.add(passage.fkSector!);
|
|
}
|
|
}
|
|
final sectorCount = uniqueSectorIds.length;
|
|
|
|
// Calculer le taux d'avancement global (passages != 2 / total passages)
|
|
double tauxAvancement = 0.0;
|
|
if (totalCount > 0) {
|
|
final passagesNonAFinaliser = totalCount - aFinaliserCount;
|
|
tauxAvancement = passagesNonAFinaliser / totalCount;
|
|
}
|
|
|
|
return DataRow(
|
|
color: WidgetStateProperty.all(theme.colorScheme.primary.withOpacity(0.15)),
|
|
cells: [
|
|
// Nom
|
|
DataCell(
|
|
Container(
|
|
alignment: Alignment.centerLeft,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Text(
|
|
'TOTAL',
|
|
style: theme.textTheme.bodyLarge?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
) ??
|
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
|
),
|
|
),
|
|
),
|
|
// Total
|
|
DataCell(
|
|
Container(
|
|
alignment: Alignment.center,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Text(
|
|
totalCount.toString(),
|
|
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ??
|
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
|
),
|
|
),
|
|
),
|
|
// Effectués - Cliquable pour naviguer vers l'historique
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _navigateToHistoryWithType(1), // Type 1 = Effectués, tous les membres
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.green.withOpacity(0.2),
|
|
alignment: Alignment.center,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
effectueCount.toString(),
|
|
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ??
|
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
|
),
|
|
Text(
|
|
'(${effectueMontant.toStringAsFixed(2)}€)',
|
|
style: theme.textTheme.bodySmall ?? const TextStyle(fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Montant moyen
|
|
DataCell(
|
|
Center(
|
|
child: Text(
|
|
montantMoyen > 0 ? '${montantMoyen.toStringAsFixed(2)}€' : '-',
|
|
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ??
|
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
|
),
|
|
),
|
|
),
|
|
// À finaliser - Cliquable pour naviguer vers l'historique
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _navigateToHistoryWithType(2), // Type 2 = À finaliser, tous les membres
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.orange.withOpacity(0.2),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
aFinaliserCount.toString(),
|
|
style: (theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14))
|
|
.copyWith(fontWeight: FontWeight.bold, fontStyle: FontStyle.italic),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Refusés - Cliquable pour naviguer vers l'historique
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _navigateToHistoryWithType(3), // Type 3 = Refusés, tous les membres
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.red.withOpacity(0.2),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
refuseCount.toString(),
|
|
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ??
|
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Dons - Cliquable pour naviguer vers l'historique
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _navigateToHistoryWithType(4), // Type 4 = Dons, tous les membres
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.lightBlue.withOpacity(0.2),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
donCount.toString(),
|
|
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ??
|
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Lots - affiché seulement si chkLotActif = true, cliquable pour naviguer vers l'historique
|
|
if (showLotType)
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _navigateToHistoryWithType(5), // Type 5 = Lots, tous les membres
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.blue.withOpacity(0.2),
|
|
alignment: Alignment.center,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
lotsCount.toString(),
|
|
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ??
|
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
|
),
|
|
Text(
|
|
'(${lotsMontant.toStringAsFixed(2)}€)',
|
|
style: theme.textTheme.bodySmall ?? const TextStyle(fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Vides - Cliquable pour naviguer vers l'historique
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _navigateToHistoryWithType(6), // Type 6 = Vides, tous les membres
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.grey.withOpacity(0.2),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
videCount.toString(),
|
|
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ??
|
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Taux d'avancement
|
|
DataCell(
|
|
SizedBox(
|
|
width: 100,
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: LinearProgressIndicator(
|
|
value: tauxAvancement,
|
|
backgroundColor: Colors.grey.shade300,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Colors.blue.shade600,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'${(tauxAvancement * 100).toStringAsFixed(1)}%',
|
|
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ??
|
|
const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Secteurs
|
|
DataCell(
|
|
Center(
|
|
child: Text(
|
|
sectorCount.toString(),
|
|
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ??
|
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Construit les lignes du tableau
|
|
List<DataRow> _buildRows(List<MembreModel> membres, int operationId, ThemeData theme) {
|
|
final List<DataRow> rows = [];
|
|
final showLotType = _shouldShowLotType();
|
|
|
|
// Récupérer directement depuis les boxes Hive (déjà ouvertes)
|
|
final passageBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
|
final allPassages = passageBox.values.where((p) => p.fkOperation == operationId).toList();
|
|
|
|
// Récupérer tous les secteurs directement depuis la box
|
|
final sectorBox = Hive.box<SectorModel>(AppKeys.sectorsBoxName);
|
|
final allSectors = sectorBox.values.toList();
|
|
|
|
for (int index = 0; index < membres.length; index++) {
|
|
final membre = membres[index];
|
|
final isEvenRow = index % 2 == 0;
|
|
|
|
// Récupérer les passages du membre (via ope_user_id)
|
|
final memberPassages = allPassages.where((p) => p.fkUser == membre.opeUserId).toList();
|
|
|
|
// Calculer les statistiques par type
|
|
int totalCount = memberPassages.length;
|
|
int effectueCount = 0;
|
|
double effectueMontant = 0.0;
|
|
int aFinaliserCount = 0;
|
|
int refuseCount = 0;
|
|
int donCount = 0;
|
|
int lotsCount = 0;
|
|
double lotsMontant = 0.0;
|
|
int videCount = 0;
|
|
|
|
for (final passage in memberPassages) {
|
|
switch (passage.fkType) {
|
|
case 1: // Effectué
|
|
effectueCount++;
|
|
if (passage.montant.isNotEmpty) {
|
|
effectueMontant += double.tryParse(passage.montant) ?? 0.0;
|
|
}
|
|
break;
|
|
case 2: // À finaliser
|
|
aFinaliserCount++;
|
|
break;
|
|
case 3: // Refusé
|
|
refuseCount++;
|
|
break;
|
|
case 4: // Don
|
|
donCount++;
|
|
break;
|
|
case 5: // Lots
|
|
if (showLotType) { // Compter seulement si Lots est activé
|
|
lotsCount++;
|
|
if (passage.montant.isNotEmpty) {
|
|
lotsMontant += double.tryParse(passage.montant) ?? 0.0;
|
|
}
|
|
}
|
|
break;
|
|
case 6: // Vide
|
|
videCount++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Calculer le montant moyen
|
|
double montantMoyen = effectueCount > 0 ? effectueMontant / effectueCount : 0.0;
|
|
|
|
// Récupérer les secteurs uniques du membre via ses passages
|
|
final Set<int> memberSectorIds = {};
|
|
for (final passage in memberPassages) {
|
|
if (passage.fkSector != null) {
|
|
memberSectorIds.add(passage.fkSector!);
|
|
}
|
|
}
|
|
final sectorCount = memberSectorIds.length;
|
|
final memberSectors = allSectors.where((s) => memberSectorIds.contains(s.id)).toList();
|
|
|
|
// Calculer le taux d'avancement (passages != 2 / total passages)
|
|
double tauxAvancement = 0.0;
|
|
|
|
if (totalCount > 0) {
|
|
// Compter les passages dont le type != 2 (tous sauf "À finaliser")
|
|
final passagesNonAFinaliser = totalCount - aFinaliserCount;
|
|
tauxAvancement = passagesNonAFinaliser / totalCount;
|
|
}
|
|
|
|
rows.add(
|
|
DataRow(
|
|
color: WidgetStateProperty.all(
|
|
isEvenRow ? Colors.white : Colors.grey.shade50,
|
|
),
|
|
cells: [
|
|
// Nom - Cliquable pour naviguer vers l'historique avec le membre sélectionné
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
debugPrint('MembersBoardPassages: Clic sur membre ${membre.id} (opeUserId: ${membre.opeUserId})');
|
|
|
|
// Réinitialiser TOUS les filtres sauf le membre
|
|
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
|
settingsBox.delete('history_selectedSectorId');
|
|
settingsBox.delete('history_selectedSectorName');
|
|
settingsBox.delete('history_selectedTypeId');
|
|
settingsBox.delete('history_selectedPaymentTypeId');
|
|
settingsBox.delete('history_startDate');
|
|
settingsBox.delete('history_endDate');
|
|
|
|
// Sélectionner uniquement le membre (via opeUserId)
|
|
settingsBox.put('history_selectedMemberId', membre.opeUserId);
|
|
|
|
// Naviguer vers la page historique
|
|
debugPrint('MembersBoardPassages: Navigation vers /admin/history avec opeUserId ${membre.opeUserId}');
|
|
if (mounted) {
|
|
context.go('/admin/history');
|
|
}
|
|
},
|
|
child: Container(
|
|
alignment: Alignment.centerLeft,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Text(
|
|
_buildMemberDisplayName(membre),
|
|
style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600) ??
|
|
const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Total - Cliquable pour naviguer vers l'historique avec le membre sélectionné
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
debugPrint('MembersBoardPassages: Clic sur Total pour membre ${membre.id} (opeUserId: ${membre.opeUserId})');
|
|
|
|
// Réinitialiser TOUS les filtres sauf le membre
|
|
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
|
settingsBox.delete('history_selectedSectorId');
|
|
settingsBox.delete('history_selectedSectorName');
|
|
settingsBox.delete('history_selectedTypeId');
|
|
settingsBox.delete('history_selectedPaymentTypeId');
|
|
settingsBox.delete('history_startDate');
|
|
settingsBox.delete('history_endDate');
|
|
|
|
// Sélectionner uniquement le membre (via opeUserId)
|
|
settingsBox.put('history_selectedMemberId', membre.opeUserId);
|
|
|
|
// Naviguer vers la page historique
|
|
debugPrint('MembersBoardPassages: Navigation vers /admin/history avec opeUserId ${membre.opeUserId}');
|
|
if (mounted) {
|
|
context.go('/admin/history');
|
|
}
|
|
},
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Text(
|
|
totalCount.toString(),
|
|
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ??
|
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Effectués - Cliquable pour naviguer vers l'historique avec ce membre
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _navigateToHistoryWithType(1, memberId: membre.opeUserId), // Type 1 = Effectués
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.green.withOpacity(0.1),
|
|
alignment: Alignment.center,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
effectueCount.toString(),
|
|
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ??
|
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
|
),
|
|
Text(
|
|
'(${effectueMontant.toStringAsFixed(2)}€)',
|
|
style: theme.textTheme.bodySmall ?? const TextStyle(fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Montant moyen
|
|
DataCell(Center(child: Text(
|
|
montantMoyen > 0 ? '${montantMoyen.toStringAsFixed(2)}€' : '-',
|
|
style: theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14),
|
|
))),
|
|
// À finaliser - Cliquable pour naviguer vers l'historique avec ce membre
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _navigateToHistoryWithType(2, memberId: membre.opeUserId), // Type 2 = À finaliser
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.orange.withOpacity(0.1),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
aFinaliserCount.toString(),
|
|
style: (theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14))
|
|
.copyWith(fontStyle: FontStyle.italic),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Refusés - Cliquable pour naviguer vers l'historique avec ce membre
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _navigateToHistoryWithType(3, memberId: membre.opeUserId), // Type 3 = Refusés
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.red.withOpacity(0.1),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
refuseCount.toString(),
|
|
style: theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Dons - Cliquable pour naviguer vers l'historique avec ce membre
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _navigateToHistoryWithType(4, memberId: membre.opeUserId), // Type 4 = Dons
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.lightBlue.withOpacity(0.1),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
donCount.toString(),
|
|
style: theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Lots - affiché seulement si chkLotActif = true, cliquable pour naviguer vers l'historique avec ce membre
|
|
if (showLotType)
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _navigateToHistoryWithType(5, memberId: membre.opeUserId), // Type 5 = Lots
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.blue.withOpacity(0.1),
|
|
alignment: Alignment.center,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
lotsCount.toString(),
|
|
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ??
|
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
|
),
|
|
Text(
|
|
'(${lotsMontant.toStringAsFixed(2)}€)',
|
|
style: theme.textTheme.bodySmall ?? const TextStyle(fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Vides - Cliquable pour naviguer vers l'historique avec ce membre
|
|
DataCell(
|
|
MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _navigateToHistoryWithType(6, memberId: membre.opeUserId), // Type 6 = Vides
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.grey.withOpacity(0.1),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
videCount.toString(),
|
|
style: theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Taux d'avancement
|
|
DataCell(
|
|
SizedBox(
|
|
width: 100,
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: LinearProgressIndicator(
|
|
value: tauxAvancement,
|
|
backgroundColor: Colors.grey.shade300,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Colors.blue.shade600,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'${(tauxAvancement * 100).toStringAsFixed(1)}%',
|
|
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ??
|
|
const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Secteurs
|
|
DataCell(
|
|
Row(
|
|
children: [
|
|
if (sectorCount == 0)
|
|
Icon(
|
|
Icons.warning,
|
|
color: Colors.red.shade400,
|
|
size: 16,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
sectorCount.toString(),
|
|
style: TextStyle(
|
|
fontSize: theme.textTheme.bodyMedium?.fontSize ?? 14,
|
|
fontWeight: sectorCount > 0 ? FontWeight.bold : FontWeight.normal,
|
|
color: sectorCount > 0 ? Colors.green.shade700 : Colors.red.shade700,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
IconButton(
|
|
icon: const Icon(Icons.map_outlined, size: 16),
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(),
|
|
onPressed: () {
|
|
_showMemberSectorsDialog(context, membre, memberSectors.toList());
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
/// Construit le nom d'affichage d'un membre avec son sectName si disponible
|
|
String _buildMemberDisplayName(MembreModel membre) {
|
|
String displayName = '${membre.firstName ?? ''} ${membre.name ?? ''}'.trim();
|
|
|
|
// Ajouter le sectName entre parenthèses s'il existe
|
|
if (membre.sectName != null && membre.sectName!.isNotEmpty) {
|
|
displayName += ' (${membre.sectName})';
|
|
}
|
|
|
|
return displayName;
|
|
}
|
|
|
|
/// Affiche un dialogue avec les secteurs du membre
|
|
void _showMemberSectorsDialog(BuildContext context, MembreModel membre, List<SectorModel> memberSectors) {
|
|
final theme = Theme.of(context);
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text('Secteurs de ${membre.firstName} ${membre.name}'),
|
|
content: SizedBox(
|
|
width: 400,
|
|
child: memberSectors.isEmpty
|
|
? const Text('Aucun secteur attribué')
|
|
: ListView.builder(
|
|
shrinkWrap: true,
|
|
itemCount: memberSectors.length,
|
|
itemBuilder: (context, index) {
|
|
final sector = memberSectors[index];
|
|
return ListTile(
|
|
leading: Icon(
|
|
Icons.map,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
title: Text(sector.libelle),
|
|
subtitle: Text('Secteur #${sector.id}'),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Fermer'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
} |