Files
geo/app/lib/presentation/widgets/charts/passage_summary_card.dart
pierre b6584c83fa 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>
2025-10-05 20:11:15 +02:00

334 lines
11 KiB
Dart
Executable File

import 'package:flutter/material.dart';
import 'package:geosector_app/presentation/widgets/charts/passage_pie_chart.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/app.dart';
/// Widget commun pour afficher une carte de synthèse des passages
/// avec liste des types à gauche et graphique en camembert à droite
class PassageSummaryCard extends StatelessWidget {
/// Titre de la carte
final String title;
/// Couleur de l'icône et du titre
final Color titleColor;
/// Icône à afficher dans le titre
final IconData? titleIcon;
/// Hauteur totale de la carte
final double? height;
/// Utiliser ValueListenableBuilder pour mise à jour automatique
final bool useValueListenable;
/// ID de l'utilisateur pour filtrer les passages (si null, tous les utilisateurs)
final int? userId;
/// Afficher tous les passages (admin) ou seulement ceux de l'utilisateur
final bool showAllPassages;
/// Types de passages à exclure du graphique
final List<int> excludePassageTypes;
/// Données statiques de passages par type (utilisé si useValueListenable = false)
final Map<int, int>? passagesByType;
/// Fonction de callback pour afficher la valeur totale personnalisée
final String Function(int totalPassages)? customTotalDisplay;
/// Afficher le graphique en mode desktop ou mobile
final bool isDesktop;
/// Icône d'arrière-plan (optionnelle)
final IconData? backgroundIcon;
/// Couleur de l'icône d'arrière-plan
final Color? backgroundIconColor;
/// Opacité de l'icône d'arrière-plan
final double backgroundIconOpacity;
/// Taille de l'icône d'arrière-plan
final double backgroundIconSize;
const PassageSummaryCard({
super.key,
required this.title,
this.titleColor = AppTheme.primaryColor,
this.titleIcon = Icons.route,
this.height,
this.useValueListenable = true,
this.userId,
this.showAllPassages = false,
this.excludePassageTypes = const [2], // Exclure "À finaliser" par défaut
this.passagesByType,
this.customTotalDisplay,
this.isDesktop = true,
this.backgroundIcon = Icons.route,
this.backgroundIconColor,
this.backgroundIconOpacity = 0.07,
this.backgroundIconSize = 180,
});
@override
Widget build(BuildContext context) {
// Si useValueListenable, construire avec ValueListenableBuilder centralisé
if (useValueListenable) {
return ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
// Calculer les données une seule fois
final passagesCounts = _calculatePassagesCounts(passagesBox);
final totalUserPassages = passagesCounts.values.fold(0, (sum, count) => sum + count);
return _buildCardContent(
context,
totalUserPassages: totalUserPassages,
passagesCounts: passagesCounts,
);
},
);
} else {
// Données statiques
final totalPassages = passagesByType?.values.fold(0, (sum, count) => sum + count) ?? 0;
return _buildCardContent(
context,
totalUserPassages: totalPassages,
passagesCounts: passagesByType ?? {},
);
}
}
/// Construit le contenu de la card avec les données calculées
Widget _buildCardContent(
BuildContext context, {
required int totalUserPassages,
required Map<int, int> passagesCounts,
}) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Stack(
children: [
// Icône d'arrière-plan (optionnelle)
if (backgroundIcon != null)
Positioned.fill(
child: Center(
child: Icon(
backgroundIcon,
size: backgroundIconSize,
color: (backgroundIconColor ?? AppTheme.primaryColor)
.withValues(alpha: backgroundIconOpacity),
),
),
),
// Contenu principal
Container(
height: height,
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre avec comptage
_buildTitle(context, totalUserPassages),
const Divider(height: 24),
// Contenu principal
Expanded(
child: SizedBox(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Liste des passages à gauche
Expanded(
flex: isDesktop ? 1 : 2,
child: _buildPassagesList(context, passagesCounts),
),
// Séparateur vertical
if (isDesktop) const VerticalDivider(width: 24),
// Graphique en camembert à droite
Expanded(
flex: isDesktop ? 1 : 2,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: PassagePieChart(
useValueListenable: false, // Utilise les données calculées
passagesByType: passagesCounts,
excludePassageTypes: excludePassageTypes,
showAllPassages: showAllPassages,
userId: showAllPassages ? null : userId,
size: double.infinity,
labelSize: 12,
showPercentage: true,
showIcons: false,
showLegend: false,
isDonut: true,
innerRadius: '50%',
),
),
),
],
),
),
),
],
),
),
],
),
);
}
/// Construction du titre
Widget _buildTitle(BuildContext context, int totalUserPassages) {
return Row(
children: [
if (titleIcon != null) ...[
Icon(
titleIcon,
color: titleColor,
size: 24,
),
const SizedBox(width: 8),
],
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: AppTheme.r(context, 16),
fontWeight: FontWeight.bold,
),
),
),
Text(
customTotalDisplay?.call(totalUserPassages) ??
totalUserPassages.toString(),
style: TextStyle(
fontSize: AppTheme.r(context, 20),
fontWeight: FontWeight.bold,
color: titleColor,
),
),
],
);
}
/// Construction de la liste des passages
Widget _buildPassagesList(BuildContext context, Map<int, int> passagesCounts) {
// Vérifier si le type Lot doit être affiché
bool showLotType = true;
final currentUser = userRepository.getCurrentUser();
if (currentUser != null && currentUser.fkEntite != null) {
final userAmicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!);
if (userAmicale != null) {
showLotType = userAmicale.chkLotActif;
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...AppKeys.typesPassages.entries.where((entry) {
// Exclure le type Lot (5) si chkLotActif = false
if (entry.key == 5 && !showLotType) {
return false;
}
return true;
}).map((entry) {
final int typeId = entry.key;
final Map<String, dynamic> typeData = entry.value;
final int count = passagesCounts[typeId] ?? 0;
final Color color = Color(typeData['couleur2'] as int);
final IconData iconData = typeData['icon_data'] as IconData;
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
child: Icon(
iconData,
color: Colors.white,
size: 16,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
typeData['titres'] as String,
style: TextStyle(fontSize: AppTheme.r(context, 14)),
),
),
Text(
count.toString(),
style: TextStyle(
fontSize: AppTheme.r(context, 16),
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
);
}),
],
);
}
/// Calcule les compteurs de passages par type
Map<int, int> _calculatePassagesCounts(Box<PassageModel> passagesBox) {
final Map<int, int> counts = {};
// Vérifier si le type Lot doit être affiché
bool showLotType = true;
final currentUser = userRepository.getCurrentUser();
if (currentUser != null && currentUser.fkEntite != null) {
final userAmicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!);
if (userAmicale != null) {
showLotType = userAmicale.chkLotActif;
}
}
// Initialiser tous les types
for (final typeId in AppKeys.typesPassages.keys) {
// Exclure le type Lot (5) si chkLotActif = false
if (typeId == 5 && !showLotType) {
continue;
}
// Exclure les types non désirés
if (excludePassageTypes.contains(typeId)) {
continue;
}
counts[typeId] = 0;
}
// L'API filtre déjà les passages côté serveur
// On compte simplement tous les passages de la box
for (final passage in passagesBox.values) {
// Exclure le type Lot (5) si chkLotActif = false
if (passage.fkType == 5 && !showLotType) {
continue;
}
// Exclure les types non désirés
if (excludePassageTypes.contains(passage.fkType)) {
continue;
}
counts[passage.fkType] = (counts[passage.fkType] ?? 0) + 1;
}
return counts;
}
}