Files
geo/app/lib/presentation/widgets/charts/payment_summary_card.dart
pierre 570a1fa1f0 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

351 lines
12 KiB
Dart
Executable File

import 'package:flutter/material.dart';
import 'package:geosector_app/presentation/widgets/charts/payment_pie_chart.dart';
import 'package:geosector_app/presentation/widgets/charts/payment_data.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 règlements
/// avec liste des types à gauche et graphique en camembert à droite
class PaymentSummaryCard 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 règlements (admin) ou seulement ceux de l'utilisateur
final bool showAllPayments;
/// Données statiques de règlements par type (utilisé si useValueListenable = false)
final Map<int, double>? paymentsByType;
/// Fonction de callback pour afficher la valeur totale personnalisée
final String Function(double totalAmount)? 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 PaymentSummaryCard({
super.key,
required this.title,
this.titleColor = AppTheme.accentColor,
this.titleIcon = Icons.payments,
this.height,
this.useValueListenable = true,
this.userId,
this.showAllPayments = false,
this.paymentsByType,
this.customTotalDisplay,
this.isDesktop = true,
this.backgroundIcon = Icons.euro_symbol,
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 paymentAmounts = _calculatePaymentAmounts(passagesBox);
final totalAmount = paymentAmounts.values.fold(0.0, (sum, amount) => sum + amount);
return _buildCardContent(
context,
totalAmount: totalAmount,
paymentAmounts: paymentAmounts,
);
},
);
} else {
// Données statiques
final totalAmount = paymentsByType?.values.fold(0.0, (sum, amount) => sum + amount) ?? 0.0;
return _buildCardContent(
context,
totalAmount: totalAmount,
paymentAmounts: paymentsByType ?? {},
);
}
}
/// Construit le contenu de la card avec les données calculées
Widget _buildCardContent(
BuildContext context, {
required double totalAmount,
required Map<int, double> paymentAmounts,
}) {
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 ?? Colors.blue)
.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, totalAmount),
const Divider(height: 24),
// Contenu principal
Expanded(
child: SizedBox(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Liste des règlements à gauche
Expanded(
flex: isDesktop ? 1 : 2,
child: _buildPaymentsList(context, paymentAmounts),
),
// 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: PaymentPieChart(
useValueListenable: false, // Utilise les données calculées
payments: _convertMapToPaymentData(paymentAmounts),
showAllPassages: showAllPayments,
userId: showAllPayments ? null : userId,
size: double.infinity,
labelSize: 12,
showPercentage: true,
showIcons: false,
showLegend: false,
isDonut: true,
innerRadius: '50%',
enable3DEffect: false,
effect3DIntensity: 0.0,
enableEnhancedExplode: false,
useGradient: false,
),
),
),
],
),
),
),
],
),
),
],
),
);
}
/// Construction du titre
Widget _buildTitle(BuildContext context, double totalAmount) {
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(totalAmount) ??
'${totalAmount.toStringAsFixed(2)}',
style: TextStyle(
fontSize: AppTheme.r(context, 20),
fontWeight: FontWeight.bold,
color: titleColor,
),
),
],
);
}
/// Construction de la liste des règlements
Widget _buildPaymentsList(BuildContext context, Map<int, double> paymentAmounts) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...AppKeys.typesReglements.entries.map((entry) {
final int typeId = entry.key;
final Map<String, dynamic> typeData = entry.value;
final double amount = paymentAmounts[typeId] ?? 0.0;
final Color color = Color(typeData['couleur'] 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['titre'] as String,
style: TextStyle(fontSize: AppTheme.r(context, 14)),
),
),
Text(
'${amount.toStringAsFixed(2)}',
style: TextStyle(
fontSize: AppTheme.r(context, 16),
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
);
}),
],
);
}
/// Calcule les montants par type de règlement
Map<int, double> _calculatePaymentAmounts(Box<PassageModel> passagesBox) {
final Map<int, double> paymentAmounts = {};
// Initialiser tous les types
for (final typeId in AppKeys.typesReglements.keys) {
paymentAmounts[typeId] = 0.0;
}
// En mode user, filtrer uniquement les passages créés par l'utilisateur (fkUser)
final currentUser = userRepository.getCurrentUser();
final int? filterUserId = showAllPayments ? null : currentUser?.id;
for (final passage in passagesBox.values) {
// En mode user, ne compter que les passages de l'utilisateur
if (filterUserId != null && passage.fkUser != filterUserId) {
continue;
}
final int typeReglement = passage.fkTypeReglement;
// Convertir la chaîne de montant en double
double montant = 0.0;
try {
String montantStr = passage.montant.replaceAll(',', '.');
montant = double.tryParse(montantStr) ?? 0.0;
} catch (e) {
// Ignorer les erreurs de conversion
}
if (montant > 0) {
if (paymentAmounts.containsKey(typeReglement)) {
paymentAmounts[typeReglement] =
(paymentAmounts[typeReglement] ?? 0.0) + montant;
} else {
paymentAmounts[0] = (paymentAmounts[0] ?? 0.0) + montant;
}
}
}
return paymentAmounts;
}
/// Convertit une Map<int, double> en List<PaymentData> pour les données statiques
List<PaymentData> _convertMapToPaymentData(Map<int, double> paymentsMap) {
final List<PaymentData> paymentDataList = [];
paymentsMap.forEach((typeReglement, montant) {
if (montant > 0) {
// Ne retourner que les types avec un montant > 0
// Récupérer les informations depuis AppKeys.typesReglements
final reglementInfo = AppKeys.typesReglements[typeReglement];
if (reglementInfo != null) {
paymentDataList.add(PaymentData(
typeId: typeReglement,
title: reglementInfo['titre'] as String,
amount: montant,
color: Color(reglementInfo['couleur'] as int),
icon: reglementInfo['icon_data'] as IconData,
));
} else {
// Fallback pour les types non définis
paymentDataList.add(PaymentData(
typeId: typeReglement,
title: 'Type inconnu',
amount: montant,
color: Colors.grey,
icon: Icons.help_outline,
));
}
}
});
return paymentDataList;
}
}