463 lines
15 KiB
Dart
463 lines
15 KiB
Dart
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) {
|
|
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).withOpacity(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
|
|
useValueListenable
|
|
? _buildTitleWithValueListenable()
|
|
: _buildTitleWithStaticData(),
|
|
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: useValueListenable
|
|
? _buildPaymentsListWithValueListenable()
|
|
: _buildPaymentsListWithStaticData(),
|
|
),
|
|
|
|
// 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: useValueListenable,
|
|
payments: useValueListenable ? [] : _convertMapToPaymentData(paymentsByType ?? {}),
|
|
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 avec ValueListenableBuilder
|
|
Widget _buildTitleWithValueListenable() {
|
|
return ValueListenableBuilder(
|
|
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
|
|
builder: (context, Box<PassageModel> passagesBox, child) {
|
|
final paymentStats = _calculatePaymentStats(passagesBox);
|
|
|
|
return Row(
|
|
children: [
|
|
if (titleIcon != null) ...[
|
|
Icon(
|
|
titleIcon,
|
|
color: titleColor,
|
|
size: 24,
|
|
),
|
|
const SizedBox(width: 8),
|
|
],
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
customTotalDisplay?.call(paymentStats['totalAmount']) ??
|
|
'${paymentStats['totalAmount'].toStringAsFixed(2)} €',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: titleColor,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Construction du titre avec données statiques
|
|
Widget _buildTitleWithStaticData() {
|
|
final totalAmount = paymentsByType?.values.fold(0.0, (sum, amount) => sum + amount) ?? 0.0;
|
|
|
|
return Row(
|
|
children: [
|
|
if (titleIcon != null) ...[
|
|
Icon(
|
|
titleIcon,
|
|
color: titleColor,
|
|
size: 24,
|
|
),
|
|
const SizedBox(width: 8),
|
|
],
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
customTotalDisplay?.call(totalAmount) ?? '${totalAmount.toStringAsFixed(2)} €',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: titleColor,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Construction de la liste des règlements avec ValueListenableBuilder
|
|
Widget _buildPaymentsListWithValueListenable() {
|
|
return ValueListenableBuilder(
|
|
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
|
|
builder: (context, Box<PassageModel> passagesBox, child) {
|
|
final paymentAmounts = _calculatePaymentAmounts(passagesBox);
|
|
|
|
return _buildPaymentsList(paymentAmounts);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Construction de la liste des règlements avec données statiques
|
|
Widget _buildPaymentsListWithStaticData() {
|
|
return _buildPaymentsList(paymentsByType ?? {});
|
|
}
|
|
|
|
/// Construction de la liste des règlements
|
|
Widget _buildPaymentsList(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: const TextStyle(fontSize: 14),
|
|
),
|
|
),
|
|
Text(
|
|
'${amount.toStringAsFixed(2)} €',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: color,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Calcule les statistiques de règlement
|
|
Map<String, dynamic> _calculatePaymentStats(Box<PassageModel> passagesBox) {
|
|
if (showAllPayments) {
|
|
// Pour les administrateurs : tous les règlements
|
|
int passagesWithPaymentCount = 0;
|
|
double totalAmount = 0.0;
|
|
|
|
for (final passage in passagesBox.values) {
|
|
// 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) {
|
|
passagesWithPaymentCount++;
|
|
totalAmount += montant;
|
|
}
|
|
}
|
|
|
|
return {
|
|
'passagesCount': passagesWithPaymentCount,
|
|
'totalAmount': totalAmount,
|
|
};
|
|
} else {
|
|
// Pour les utilisateurs : seulement leurs règlements
|
|
final currentUser = userRepository.getCurrentUser();
|
|
final targetUserId = userId ?? currentUser?.id;
|
|
|
|
if (targetUserId == null) {
|
|
return {'passagesCount': 0, 'totalAmount': 0.0};
|
|
}
|
|
|
|
int passagesWithPaymentCount = 0;
|
|
double totalAmount = 0.0;
|
|
|
|
for (final passage in passagesBox.values) {
|
|
if (passage.fkUser == targetUserId) {
|
|
// 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) {
|
|
passagesWithPaymentCount++;
|
|
totalAmount += montant;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
'passagesCount': passagesWithPaymentCount,
|
|
'totalAmount': totalAmount,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
if (showAllPayments) {
|
|
// Pour les administrateurs : compter tous les règlements
|
|
for (final passage in passagesBox.values) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Pour les utilisateurs : compter seulement leurs règlements
|
|
final currentUser = userRepository.getCurrentUser();
|
|
final targetUserId = userId ?? currentUser?.id;
|
|
|
|
if (targetUserId != null) {
|
|
for (final passage in passagesBox.values) {
|
|
if (passage.fkUser == targetUserId) {
|
|
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;
|
|
}
|
|
} |