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:
pierre
2025-10-05 20:11:15 +02:00
parent 2786252307
commit 570a1fa1f0
212 changed files with 24275 additions and 11321 deletions

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