🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
475 lines
16 KiB
Dart
Executable File
475 lines
16 KiB
Dart
Executable File
import 'package:flutter/material.dart';
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'package:go_router/go_router.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/constants/app_keys.dart';
|
|
import 'package:geosector_app/core/theme/app_theme.dart';
|
|
import 'package:geosector_app/core/services/current_user_service.dart';
|
|
|
|
// Enum pour les types de tri
|
|
enum SortType { name, count, progress }
|
|
|
|
enum SortOrder { none, asc, desc }
|
|
|
|
class SectorDistributionCard extends StatefulWidget {
|
|
final String title;
|
|
final double? height;
|
|
final EdgeInsetsGeometry? padding;
|
|
|
|
const SectorDistributionCard({
|
|
super.key,
|
|
this.title = 'Répartition par secteur',
|
|
this.height,
|
|
this.padding,
|
|
});
|
|
|
|
@override
|
|
State<SectorDistributionCard> createState() => _SectorDistributionCardState();
|
|
}
|
|
|
|
class _SectorDistributionCardState extends State<SectorDistributionCard> {
|
|
SortType? _currentSortType;
|
|
SortOrder _currentSortOrder = SortOrder.none;
|
|
|
|
void _onSortPressed(SortType sortType) {
|
|
setState(() {
|
|
if (_currentSortType == sortType) {
|
|
// Cycle through: none -> asc -> desc -> none
|
|
if (_currentSortOrder == SortOrder.none) {
|
|
_currentSortOrder = SortOrder.asc;
|
|
} else if (_currentSortOrder == SortOrder.asc) {
|
|
_currentSortOrder = SortOrder.desc;
|
|
} else {
|
|
_currentSortOrder = SortOrder.none;
|
|
_currentSortType = null;
|
|
}
|
|
} else {
|
|
_currentSortType = sortType;
|
|
_currentSortOrder = SortOrder.asc;
|
|
}
|
|
});
|
|
}
|
|
|
|
Widget _buildSortButton(String label, SortType sortType) {
|
|
final isActive =
|
|
_currentSortType == sortType && _currentSortOrder != SortOrder.none;
|
|
final isAsc =
|
|
_currentSortType == sortType && _currentSortOrder == SortOrder.asc;
|
|
|
|
return InkWell(
|
|
onTap: () => _onSortPressed(sortType),
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: isActive
|
|
? Colors.blue.withValues(alpha: 0.1)
|
|
: Colors.grey.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(
|
|
color: isActive ? Colors.blue : Colors.grey[400]!,
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: AppTheme.r(context, 12),
|
|
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
|
|
color: isActive ? Colors.blue : Colors.grey[700],
|
|
),
|
|
),
|
|
if (isActive) ...[
|
|
const SizedBox(width: 2),
|
|
Icon(
|
|
isAsc ? Icons.arrow_upward : Icons.arrow_downward,
|
|
size: 12,
|
|
color: Colors.blue,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: widget.padding ?? const EdgeInsets.all(AppTheme.spacingM),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
|
|
boxShadow: AppTheme.cardShadow,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Ligne du titre avec boutons de tri
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
widget.title,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: AppTheme.r(context, 16),
|
|
),
|
|
),
|
|
// Boutons de tri groupés
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_buildSortButton('Nom', SortType.name),
|
|
const SizedBox(width: 4),
|
|
_buildSortButton('Nb', SortType.count),
|
|
const SizedBox(width: 4),
|
|
_buildSortButton('%', SortType.progress),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: AppTheme.spacingM),
|
|
_buildAutoRefreshContent(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAutoRefreshContent() {
|
|
// Écouter les changements des deux boîtes
|
|
return ValueListenableBuilder(
|
|
valueListenable:
|
|
Hive.box<SectorModel>(AppKeys.sectorsBoxName).listenable(),
|
|
builder: (context, Box<SectorModel> sectorsBox, child) {
|
|
return ValueListenableBuilder(
|
|
valueListenable:
|
|
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
|
|
builder: (context, Box<PassageModel> passagesBox, child) {
|
|
return _buildContent(sectorsBox, passagesBox);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildContent(
|
|
Box<SectorModel> sectorsBox, Box<PassageModel> passagesBox) {
|
|
try {
|
|
// Calculer les statistiques
|
|
final sectorStats = _calculateSectorStats(sectorsBox, passagesBox);
|
|
|
|
if (sectorStats.isEmpty) {
|
|
return const Center(
|
|
child: Text('Aucune donnée de secteur disponible'),
|
|
);
|
|
}
|
|
|
|
// Appliquer le tri
|
|
_applySorting(sectorStats);
|
|
|
|
// Trouver le maximum de passages pour un secteur
|
|
final maxCount = sectorStats.fold<int>(
|
|
0,
|
|
(max, sector) => sector['count'] > max ? sector['count'] : max,
|
|
);
|
|
|
|
// Liste des secteurs directement sans sous-titre
|
|
return ListView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: sectorStats.length,
|
|
itemBuilder: (context, index) {
|
|
final sector = sectorStats[index];
|
|
return _buildSectorItem(
|
|
sector['name'],
|
|
sector['count'],
|
|
Color(sector['color']),
|
|
sectorStats,
|
|
maxCount, // Passer le max pour calculer les proportions
|
|
);
|
|
},
|
|
);
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du calcul des statistiques: $e');
|
|
return Center(
|
|
child: Text('Erreur: ${e.toString()}'),
|
|
);
|
|
}
|
|
}
|
|
|
|
List<Map<String, dynamic>> _calculateSectorStats(
|
|
Box<SectorModel> sectorsBox,
|
|
Box<PassageModel> passagesBox,
|
|
) {
|
|
// Récupérer tous les secteurs et passages
|
|
final List<SectorModel> sectors = sectorsBox.values.toList();
|
|
final List<PassageModel> passages = passagesBox.values.toList();
|
|
|
|
// Préparer les données pour l'affichage - AFFICHER TOUS LES SECTEURS
|
|
List<Map<String, dynamic>> stats = [];
|
|
|
|
for (final sector in sectors) {
|
|
// Compter les passages par type pour ce secteur
|
|
Map<int, int> passagesByType = {};
|
|
int totalCount = 0;
|
|
int passagesNotType2 = 0;
|
|
|
|
// Compter tous les passages pour ce secteur
|
|
for (final passage in passages) {
|
|
if (passage.fkSector == sector.id) {
|
|
final type = passage.fkType;
|
|
passagesByType[type] = (passagesByType[type] ?? 0) + 1;
|
|
totalCount++;
|
|
if (type != 2) {
|
|
passagesNotType2++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculer le pourcentage d'avancement
|
|
final int progressPercentage =
|
|
totalCount > 0 ? ((passagesNotType2 / totalCount) * 100).round() : 0;
|
|
|
|
stats.add({
|
|
'id': sector.id,
|
|
'name': sector.libelle,
|
|
'count': totalCount,
|
|
'passagesByType': passagesByType,
|
|
'progressPercentage': progressPercentage,
|
|
'color': sector.color.isEmpty
|
|
? 0xFF4B77BE
|
|
: int.tryParse(sector.color.replaceAll('#', '0xFF')) ?? 0xFF4B77BE,
|
|
});
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
void _applySorting(List<Map<String, dynamic>> stats) {
|
|
if (_currentSortType == null || _currentSortOrder == SortOrder.none) {
|
|
// Tri par défaut : par nombre de passages décroissant, puis par nom
|
|
stats.sort((a, b) {
|
|
int countCompare = (b['count'] as int).compareTo(a['count'] as int);
|
|
if (countCompare != 0) return countCompare;
|
|
return (a['name'] as String).compareTo(b['name'] as String);
|
|
});
|
|
return;
|
|
}
|
|
|
|
switch (_currentSortType!) {
|
|
case SortType.name:
|
|
stats.sort((a, b) {
|
|
final result = (a['name'] as String).compareTo(b['name'] as String);
|
|
return _currentSortOrder == SortOrder.asc ? result : -result;
|
|
});
|
|
break;
|
|
case SortType.count:
|
|
stats.sort((a, b) {
|
|
final result = (a['count'] as int).compareTo(b['count'] as int);
|
|
return _currentSortOrder == SortOrder.asc ? result : -result;
|
|
});
|
|
break;
|
|
case SortType.progress:
|
|
stats.sort((a, b) {
|
|
final result = (a['progressPercentage'] as int)
|
|
.compareTo(b['progressPercentage'] as int);
|
|
return _currentSortOrder == SortOrder.asc ? result : -result;
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
Widget _buildSectorItem(
|
|
String name,
|
|
int count,
|
|
Color color,
|
|
List<Map<String, dynamic>> allStats,
|
|
int maxCount,
|
|
) {
|
|
// Récupérer les données du secteur actuel
|
|
final sectorData = allStats.firstWhere((s) => s['name'] == name);
|
|
final Map<int, int> passagesByType = sectorData['passagesByType'] ?? {};
|
|
final int progressPercentage = sectorData['progressPercentage'] ?? 0;
|
|
final int sectorId = sectorData['id'] ?? 0;
|
|
|
|
// Calculer le ratio par rapport au maximum (éviter division par zéro)
|
|
final double widthRatio = maxCount > 0 ? count / maxCount : 0;
|
|
|
|
// Style différent pour les secteurs sans passages
|
|
final bool hasPassages = count > 0;
|
|
final textColor = hasPassages ? Colors.black87 : Colors.grey;
|
|
|
|
// Vérifier si l'utilisateur est admin (prend en compte le mode d'affichage)
|
|
final bool isAdmin = CurrentUserService.instance.shouldShowAdminUI;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: AppTheme.spacingM),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Nom du secteur et total
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: InkWell(
|
|
onTap: () {
|
|
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
|
|
|
if (isAdmin) {
|
|
// Admin : naviguer vers la page carte
|
|
settingsBox.put('selectedSectorId', sectorId);
|
|
settingsBox.put('selectedPageIndex', 4); // Index de la page carte
|
|
context.go('/admin');
|
|
} else {
|
|
// User : naviguer vers la page historique avec le secteur sélectionné
|
|
settingsBox.delete('history_selectedTypeId');
|
|
settingsBox.delete('history_selectedPaymentTypeId');
|
|
settingsBox.delete('history_selectedMemberId');
|
|
settingsBox.delete('history_startDate');
|
|
settingsBox.delete('history_endDate');
|
|
|
|
settingsBox.put('history_selectedSectorId', sectorId);
|
|
settingsBox.put('history_selectedSectorName', name);
|
|
context.go('/user/history');
|
|
}
|
|
},
|
|
child: Text(
|
|
name,
|
|
style: TextStyle(
|
|
fontSize: AppTheme.r(context, 14),
|
|
color: textColor,
|
|
fontWeight:
|
|
hasPassages ? FontWeight.w600 : FontWeight.w300,
|
|
decoration: TextDecoration.underline,
|
|
decorationColor: textColor.withValues(alpha: 0.5),
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
hasPassages
|
|
? '$count passages ($progressPercentage% d\'avancement)'
|
|
: '0 passage',
|
|
style: TextStyle(
|
|
fontWeight: hasPassages ? FontWeight.bold : FontWeight.normal,
|
|
fontSize: AppTheme.r(context, 13),
|
|
color: textColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
// Barre horizontale cumulée avec largeur proportionnelle
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: FractionallySizedBox(
|
|
widthFactor: widthRatio,
|
|
child: _buildStackedBar(passagesByType, count, sectorId, name),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStackedBar(Map<int, int> passagesByType, int totalCount,
|
|
int sectorId, String sectorName) {
|
|
if (totalCount == 0) {
|
|
// Barre vide pour les secteurs sans passages
|
|
return Container(
|
|
height: 24,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[200],
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Ordre des types : 1, 3, 4, 5, 6, 7, 8, 9, puis 2 en dernier
|
|
final typeOrder = [1, 3, 4, 5, 6, 7, 8, 9, 2];
|
|
|
|
return Container(
|
|
height: 24,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: Colors.grey[300]!, width: 0.5),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: Stack(
|
|
children: [
|
|
// Barre de fond
|
|
Container(color: Colors.grey[100]),
|
|
// Barres empilées
|
|
Row(
|
|
children: typeOrder.map((typeId) {
|
|
final count = passagesByType[typeId] ?? 0;
|
|
if (count == 0) return const SizedBox.shrink();
|
|
|
|
final percentage = (count / totalCount) * 100;
|
|
final typeInfo = AppKeys.typesPassages[typeId];
|
|
final color = typeInfo != null
|
|
? Color(typeInfo['couleur2'] as int)
|
|
: Colors.grey;
|
|
|
|
return Expanded(
|
|
flex: count,
|
|
child: InkWell(
|
|
onTap: () {
|
|
// Réinitialiser TOUS les filtres avant de sauvegarder les nouveaux
|
|
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
|
settingsBox.delete('history_selectedPaymentTypeId');
|
|
settingsBox.delete('history_selectedMemberId');
|
|
settingsBox.delete('history_startDate');
|
|
settingsBox.delete('history_endDate');
|
|
|
|
// Sauvegarder uniquement le secteur et le type de passage sélectionnés
|
|
settingsBox.put('history_selectedSectorId', sectorId);
|
|
settingsBox.put('history_selectedSectorName', sectorName);
|
|
settingsBox.put('history_selectedTypeId', typeId);
|
|
|
|
// Naviguer directement vers la page historique
|
|
final bool isAdmin = CurrentUserService.instance.shouldShowAdminUI;
|
|
context.go(isAdmin ? '/admin/history' : '/user/history');
|
|
},
|
|
child: Container(
|
|
color: color,
|
|
child: Center(
|
|
child: percentage >= 5
|
|
? Text(
|
|
'$count (${percentage.toInt()}%)',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: AppTheme.r(context, 10),
|
|
fontWeight: FontWeight.bold,
|
|
shadows: [
|
|
Shadow(
|
|
offset: Offset(0.5, 0.5),
|
|
blurRadius: 1.0,
|
|
color: Colors.black45,
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: null,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|