Files
geo/app/lib/presentation/widgets/sector_distribution_card.dart

486 lines
17 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.withOpacity(0.1) : Colors.grey.withOpacity(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: 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(
height: widget.height,
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,
children: [
// Ligne du titre avec boutons de tri
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 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),
Expanded(
child: _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(
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
final bool isAdmin = CurrentUserService.instance.canAccessAdmin;
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: isAdmin
? InkWell(
onTap: () {
// Sauvegarder le secteur sélectionné et l'index de la page carte dans Hive
final settingsBox = Hive.box(AppKeys.settingsBoxName);
settingsBox.put('selectedSectorId', sectorId);
settingsBox.put('selectedPageIndex', 4); // Index de la page carte
// Naviguer vers le dashboard admin qui chargera la page carte
context.go('/admin');
},
child: Text(
name,
style: TextStyle(
fontSize: 14,
color: textColor,
fontWeight: hasPassages ? FontWeight.w600 : FontWeight.w300,
decoration: TextDecoration.underline,
decorationColor: textColor.withOpacity(0.5),
),
overflow: TextOverflow.ellipsis,
),
)
: Text(
name,
style: TextStyle(
fontSize: 14,
color: textColor,
fontWeight: hasPassages ? FontWeight.w600 : FontWeight.w300,
),
overflow: TextOverflow.ellipsis,
),
),
Text(
hasPassages
? '$count passages ($progressPercentage% d\'avancement)'
: '0 passage',
style: TextStyle(
fontWeight: hasPassages ? FontWeight.bold : FontWeight.normal,
fontSize: 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;
// Vérifier si l'utilisateur est admin pour les clics
final bool isAdmin = CurrentUserService.instance.canAccessAdmin;
return Expanded(
flex: count,
child: isAdmin
? InkWell(
onTap: () {
// Sauvegarder les filtres dans Hive pour la page historique
final settingsBox = Hive.box(AppKeys.settingsBoxName);
settingsBox.put('history_selectedSectorId', sectorId);
settingsBox.put('history_selectedSectorName', sectorName);
settingsBox.put('history_selectedTypeId', typeId);
settingsBox.put('selectedPageIndex', 2); // Index de la page historique
// Naviguer vers le dashboard admin qui chargera la page historique
context.go('/admin');
},
child: Container(
color: color,
child: Center(
child: percentage >= 5 // N'afficher le texte que si >= 5%
? Text(
'$count (${percentage.toInt()}%)',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
shadows: [
Shadow(
offset: Offset(0.5, 0.5),
blurRadius: 1.0,
color: Colors.black45,
),
],
),
)
: null,
),
),
)
: Container(
color: color,
child: Center(
child: percentage >= 5 // N'afficher le texte que si >= 5%
? Text(
'$count (${percentage.toInt()}%)',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
shadows: [
Shadow(
offset: Offset(0.5, 0.5),
blurRadius: 1.0,
color: Colors.black45,
),
],
),
)
: null,
),
),
);
}).toList(),
),
],
),
),
);
}
}