feat: Version 3.3.4 - Nouvelle architecture pages, optimisations widgets Flutter et API
- Mise à jour VERSION vers 3.3.4 - Optimisations et révisions architecture API (deploy-api.sh, scripts de migration) - Ajout documentation Stripe Tap to Pay complète - Migration vers polices Inter Variable pour Flutter - Optimisations build Android et nettoyage fichiers temporaires - Amélioration système de déploiement avec gestion backups - Ajout scripts CRON et migrations base de données 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
899
app/lib/presentation/widgets/members_board_passages.dart
Normal file
899
app/lib/presentation/widgets/members_board_passages.dart
Normal file
@@ -0,0 +1,899 @@
|
||||
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(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: widget.height ?? 700, // Hauteur max, sinon s'adapte au contenu
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
|
||||
boxShadow: AppTheme.cardShadow,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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
|
||||
Expanded(
|
||||
child: 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),
|
||||
];
|
||||
|
||||
// Utilise seulement le scroll vertical, le tableau s'adapte à la largeur
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: 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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user