🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
892 lines
31 KiB
Dart
892 lines
31 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;
|
|
|
|
// 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.withValues(alpha: 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 par nom
|
|
membres.sort((a, b) {
|
|
final nameA = '${a.firstName ?? ''} ${a.name ?? ''}'.trim();
|
|
final nameB = '${b.firstName ?? ''} ${b.name ?? ''}'.trim();
|
|
return nameA.compareTo(nameB);
|
|
});
|
|
|
|
// 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: 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,
|
|
headingRowColor: WidgetStateProperty.all(
|
|
theme.colorScheme.primary.withValues(alpha: 0.08),
|
|
),
|
|
columns: _buildColumns(theme),
|
|
rows: allRows,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 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: Text('Nom', style: headerStyle),
|
|
),
|
|
),
|
|
// Total
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Center(
|
|
child: Text('Total', style: headerStyle),
|
|
),
|
|
),
|
|
numeric: true,
|
|
),
|
|
// Effectués
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Container(
|
|
color: Colors.green.withValues(alpha: 0.2),
|
|
alignment: Alignment.center,
|
|
child: Text('Effectués', style: headerStyle),
|
|
),
|
|
),
|
|
numeric: true,
|
|
),
|
|
// Montant moyen
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Center(
|
|
child: Text('Moy./passage', style: headerStyle),
|
|
),
|
|
),
|
|
numeric: true,
|
|
),
|
|
// À finaliser
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Container(
|
|
color: Colors.orange.withValues(alpha: 0.2),
|
|
alignment: Alignment.center,
|
|
child: Text('À finaliser', style: headerStyle),
|
|
),
|
|
),
|
|
numeric: true,
|
|
),
|
|
// Refusés
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Container(
|
|
color: Colors.red.withValues(alpha: 0.2),
|
|
alignment: Alignment.center,
|
|
child: Text('Refusés', style: headerStyle),
|
|
),
|
|
),
|
|
numeric: true,
|
|
),
|
|
// Dons
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Container(
|
|
color: Colors.lightBlue.withValues(alpha: 0.2),
|
|
alignment: Alignment.center,
|
|
child: Text('Dons', style: headerStyle),
|
|
),
|
|
),
|
|
numeric: true,
|
|
),
|
|
// Lots - affiché seulement si chkLotActif = true
|
|
if (showLotType)
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Container(
|
|
color: Colors.blue.withValues(alpha: 0.2),
|
|
alignment: Alignment.center,
|
|
child: Text('Lots', style: headerStyle),
|
|
),
|
|
),
|
|
numeric: true,
|
|
),
|
|
// Vides
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Container(
|
|
color: Colors.grey.withValues(alpha: 0.2),
|
|
alignment: Alignment.center,
|
|
child: Text('Vides', style: headerStyle),
|
|
),
|
|
),
|
|
numeric: true,
|
|
),
|
|
// Taux d'avancement
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Center(
|
|
child: Text('Avancement', style: headerStyle),
|
|
),
|
|
),
|
|
),
|
|
// Secteurs
|
|
DataColumn(
|
|
label: Expanded(
|
|
child: Center(
|
|
child: Text('Secteurs', style: headerStyle),
|
|
),
|
|
),
|
|
numeric: true,
|
|
),
|
|
];
|
|
|
|
return columns;
|
|
}
|
|
|
|
/// 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
|
|
double tauxAvancement = 0.0;
|
|
if (sectorCount > 0 && membres.isNotEmpty) {
|
|
tauxAvancement = effectueCount / (sectorCount * membres.length);
|
|
if (tauxAvancement > 1) tauxAvancement = 1.0;
|
|
}
|
|
|
|
return DataRow(
|
|
color: WidgetStateProperty.all(theme.colorScheme.primary.withValues(alpha: 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
|
|
DataCell(
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.green.withValues(alpha: 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
|
|
DataCell(
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.orange.withValues(alpha: 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
|
|
DataCell(
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.red.withValues(alpha: 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
|
|
DataCell(
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.lightBlue.withValues(alpha: 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
|
|
if (showLotType)
|
|
DataCell(
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.blue.withValues(alpha: 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
|
|
DataCell(
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.grey.withValues(alpha: 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).toInt()}%',
|
|
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
|
|
final memberPassages = allPassages.where((p) => p.fkUser == membre.id).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 effectués / secteurs attribués)
|
|
double tauxAvancement = 0.0;
|
|
bool hasWarning = false;
|
|
|
|
if (sectorCount > 0) {
|
|
// On considère que chaque secteur devrait avoir au moins un passage effectué
|
|
tauxAvancement = effectueCount / sectorCount;
|
|
if (tauxAvancement > 1) tauxAvancement = 1.0; // Limiter à 100%
|
|
hasWarning = tauxAvancement < 0.5; // Avertissement si moins de 50%
|
|
} else {
|
|
hasWarning = true; // Avertissement si aucun secteur attribué
|
|
}
|
|
|
|
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}');
|
|
|
|
// Naviguer directement vers la page history avec memberId
|
|
debugPrint('MembersBoardPassages: Navigation vers /admin/history?memberId=${membre.id}');
|
|
if (mounted) {
|
|
context.go('/admin/history?memberId=${membre.id}');
|
|
}
|
|
},
|
|
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 membre ${membre.id}');
|
|
|
|
// Naviguer directement vers la page history avec memberId
|
|
debugPrint('MembersBoardPassages: Navigation vers /admin/history?memberId=${membre.id}');
|
|
if (mounted) {
|
|
context.go('/admin/history?memberId=${membre.id}');
|
|
}
|
|
},
|
|
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
|
|
DataCell(
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.green.withValues(alpha: 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
|
|
DataCell(
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.orange.withValues(alpha: 0.1),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
aFinaliserCount.toString(),
|
|
style: (theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14))
|
|
.copyWith(fontStyle: FontStyle.italic),
|
|
),
|
|
),
|
|
),
|
|
// Refusés
|
|
DataCell(
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.red.withValues(alpha: 0.1),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
refuseCount.toString(),
|
|
style: theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14),
|
|
),
|
|
),
|
|
),
|
|
// Dons
|
|
DataCell(
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.lightBlue.withValues(alpha: 0.1),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
donCount.toString(),
|
|
style: theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14),
|
|
),
|
|
),
|
|
),
|
|
// Lots - affiché seulement si chkLotActif = true
|
|
if (showLotType)
|
|
DataCell(
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.blue.withValues(alpha: 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
|
|
DataCell(
|
|
Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
color: Colors.grey.withValues(alpha: 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>(
|
|
hasWarning ? Colors.red.shade400 : Colors.green.shade400,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
if (hasWarning)
|
|
Icon(
|
|
Icons.warning,
|
|
color: Colors.red.shade400,
|
|
size: 16,
|
|
)
|
|
else
|
|
Text(
|
|
'${(tauxAvancement * 100).toInt()}%',
|
|
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'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
} |