feat: Version 3.3.5 - Optimisations pages, améliorations ergonomie et affichages dynamiques stats
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/presentation/widgets/charts/payment_pie_chart.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.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:geosector_app/core/services/current_user_service.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/app.dart';
|
||||
import 'package:go_router/go_router.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 {
|
||||
class PaymentSummaryCard extends StatefulWidget {
|
||||
/// Titre de la carte
|
||||
final String title;
|
||||
|
||||
@@ -70,10 +72,49 @@ class PaymentSummaryCard extends StatelessWidget {
|
||||
this.backgroundIconSize = 180,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PaymentSummaryCard> createState() => _PaymentSummaryCardState();
|
||||
}
|
||||
|
||||
class _PaymentSummaryCardState extends State<PaymentSummaryCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
);
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(PaymentSummaryCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
// Relancer l'animation si les paramètres importants ont changé
|
||||
final bool shouldResetAnimation = oldWidget.userId != widget.userId ||
|
||||
oldWidget.showAllPayments != widget.showAllPayments ||
|
||||
oldWidget.useValueListenable != widget.useValueListenable;
|
||||
|
||||
if (shouldResetAnimation) {
|
||||
_animationController.reset();
|
||||
_animationController.forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Si useValueListenable, construire avec ValueListenableBuilder centralisé
|
||||
if (useValueListenable) {
|
||||
if (widget.useValueListenable) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
|
||||
builder: (context, Box<PassageModel> passagesBox, child) {
|
||||
@@ -90,11 +131,11 @@ class PaymentSummaryCard extends StatelessWidget {
|
||||
);
|
||||
} else {
|
||||
// Données statiques
|
||||
final totalAmount = paymentsByType?.values.fold(0.0, (sum, amount) => sum + amount) ?? 0.0;
|
||||
final totalAmount = widget.paymentsByType?.values.fold(0.0, (sum, amount) => sum + amount) ?? 0.0;
|
||||
return _buildCardContent(
|
||||
context,
|
||||
totalAmount: totalAmount,
|
||||
paymentAmounts: paymentsByType ?? {},
|
||||
paymentAmounts: widget.paymentsByType ?? {},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -113,20 +154,20 @@ class PaymentSummaryCard extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
// Icône d'arrière-plan (optionnelle)
|
||||
if (backgroundIcon != null)
|
||||
if (widget.backgroundIcon != null)
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Icon(
|
||||
backgroundIcon,
|
||||
size: backgroundIconSize,
|
||||
color: (backgroundIconColor ?? Colors.blue)
|
||||
.withValues(alpha: backgroundIconOpacity),
|
||||
widget.backgroundIcon,
|
||||
size: widget.backgroundIconSize,
|
||||
color: (widget.backgroundIconColor ?? Colors.blue)
|
||||
.withValues(alpha: widget.backgroundIconOpacity),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Contenu principal
|
||||
Container(
|
||||
height: height,
|
||||
height: widget.height,
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -142,35 +183,19 @@ class PaymentSummaryCard extends StatelessWidget {
|
||||
children: [
|
||||
// Liste des règlements à gauche
|
||||
Expanded(
|
||||
flex: isDesktop ? 1 : 2,
|
||||
flex: widget.isDesktop ? 1 : 2,
|
||||
child: _buildPaymentsList(context, paymentAmounts),
|
||||
),
|
||||
|
||||
// Séparateur vertical
|
||||
if (isDesktop) const VerticalDivider(width: 24),
|
||||
if (widget.isDesktop) const VerticalDivider(width: 24),
|
||||
|
||||
// Graphique en camembert à droite
|
||||
Expanded(
|
||||
flex: isDesktop ? 1 : 2,
|
||||
flex: widget.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,
|
||||
),
|
||||
child: _buildPieChart(paymentAmounts),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -189,17 +214,17 @@ class PaymentSummaryCard extends StatelessWidget {
|
||||
Widget _buildTitle(BuildContext context, double totalAmount) {
|
||||
return Row(
|
||||
children: [
|
||||
if (titleIcon != null) ...[
|
||||
if (widget.titleIcon != null) ...[
|
||||
Icon(
|
||||
titleIcon,
|
||||
color: titleColor,
|
||||
widget.titleIcon,
|
||||
color: widget.titleColor,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
widget.title,
|
||||
style: TextStyle(
|
||||
fontSize: AppTheme.r(context, 16),
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -207,19 +232,38 @@ class PaymentSummaryCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
customTotalDisplay?.call(totalAmount) ??
|
||||
widget.customTotalDisplay?.call(totalAmount) ??
|
||||
'${totalAmount.toStringAsFixed(2)} €',
|
||||
style: TextStyle(
|
||||
fontSize: AppTheme.r(context, 20),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: titleColor,
|
||||
color: widget.titleColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Construction de la liste des règlements
|
||||
/// Gérer le clic sur un type de règlement
|
||||
void _handlePaymentTypeClick(int typeId) {
|
||||
// Réinitialiser TOUS les filtres avant de sauvegarder le nouveau
|
||||
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
||||
settingsBox.delete('history_selectedTypeId');
|
||||
settingsBox.delete('history_selectedSectorId');
|
||||
settingsBox.delete('history_selectedSectorName');
|
||||
settingsBox.delete('history_selectedMemberId');
|
||||
settingsBox.delete('history_startDate');
|
||||
settingsBox.delete('history_endDate');
|
||||
|
||||
// Sauvegarder uniquement le type de règlement sélectionné
|
||||
settingsBox.put('history_selectedPaymentTypeId', typeId);
|
||||
|
||||
// Naviguer directement vers la page historique
|
||||
final bool isAdmin = CurrentUserService.instance.shouldShowAdminUI;
|
||||
context.go(isAdmin ? '/admin/history' : '/user/history');
|
||||
}
|
||||
|
||||
/// Construction de la liste des règlements (avec clics)
|
||||
Widget _buildPaymentsList(BuildContext context, Map<int, double> paymentAmounts) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -233,37 +277,44 @@ class PaymentSummaryCard extends StatelessWidget {
|
||||
|
||||
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,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => _handlePaymentTypeClick(typeId),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 4.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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
@@ -271,6 +322,95 @@ class PaymentSummaryCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Construction du graphique en camembert (avec clics)
|
||||
Widget _buildPieChart(Map<int, double> paymentAmounts) {
|
||||
final chartData = _prepareChartDataFromMap(paymentAmounts);
|
||||
|
||||
// Si aucune donnée, afficher un message
|
||||
if (chartData.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('Aucune donnée disponible'),
|
||||
);
|
||||
}
|
||||
|
||||
// Créer des animations
|
||||
final progressAnimation = CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
|
||||
final explodeAnimation = CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.7, 1.0, curve: Curves.elasticOut),
|
||||
);
|
||||
|
||||
final opacityAnimation = CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.1, 0.5, curve: Curves.easeIn),
|
||||
);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return SfCircularChart(
|
||||
margin: EdgeInsets.zero,
|
||||
legend: Legend(
|
||||
isVisible: false,
|
||||
position: LegendPosition.bottom,
|
||||
overflowMode: LegendItemOverflowMode.wrap,
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
onSelectionChanged: (SelectionArgs args) {
|
||||
// Gérer le clic sur un segment du graphique
|
||||
final pointIndex = args.pointIndex;
|
||||
if (pointIndex < chartData.length) {
|
||||
final selectedData = chartData[pointIndex];
|
||||
_handlePaymentTypeClick(selectedData.typeId);
|
||||
}
|
||||
},
|
||||
series: <CircularSeries>[
|
||||
DoughnutSeries<PaymentData, String>(
|
||||
dataSource: chartData,
|
||||
xValueMapper: (PaymentData data, _) => data.title,
|
||||
yValueMapper: (PaymentData data, _) => data.amount,
|
||||
pointColorMapper: (PaymentData data, _) => data.color,
|
||||
enableTooltip: true,
|
||||
selectionBehavior: SelectionBehavior(
|
||||
enable: true,
|
||||
selectedColor: null, // Garde la couleur d'origine
|
||||
unselectedOpacity: 0.5,
|
||||
),
|
||||
dataLabelMapper: (PaymentData data, _) {
|
||||
// Calculer le pourcentage avec une décimale
|
||||
final total = chartData.fold(0.0, (sum, item) => sum + item.amount);
|
||||
final percentage = (data.amount / total * 100);
|
||||
return '${percentage.toStringAsFixed(1)}%';
|
||||
},
|
||||
dataLabelSettings: DataLabelSettings(
|
||||
isVisible: true,
|
||||
labelPosition: ChartDataLabelPosition.inside,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
innerRadius: '50%',
|
||||
explode: true,
|
||||
explodeIndex: 0,
|
||||
explodeOffset: '${(5 * explodeAnimation.value).toStringAsFixed(1)}%',
|
||||
opacity: opacityAnimation.value,
|
||||
animationDuration: 0,
|
||||
startAngle: 270,
|
||||
endAngle: 270 + (360 * progressAnimation.value).toInt(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Calcule les montants par type de règlement
|
||||
Map<int, double> _calculatePaymentAmounts(Box<PassageModel> passagesBox) {
|
||||
final Map<int, double> paymentAmounts = {};
|
||||
@@ -282,7 +422,7 @@ class PaymentSummaryCard extends StatelessWidget {
|
||||
|
||||
// En mode user, filtrer uniquement les passages créés par l'utilisateur (fkUser)
|
||||
final currentUser = userRepository.getCurrentUser();
|
||||
final int? filterUserId = showAllPayments ? null : currentUser?.id;
|
||||
final int? filterUserId = widget.showAllPayments ? null : currentUser?.id;
|
||||
|
||||
for (final passage in passagesBox.values) {
|
||||
// En mode user, ne compter que les passages de l'utilisateur
|
||||
@@ -314,8 +454,8 @@ class PaymentSummaryCard extends StatelessWidget {
|
||||
return paymentAmounts;
|
||||
}
|
||||
|
||||
/// Convertit une Map<int, double> en List<PaymentData> pour les données statiques
|
||||
List<PaymentData> _convertMapToPaymentData(Map<int, double> paymentsMap) {
|
||||
/// Prépare les données pour le graphique en camembert à partir d'une Map
|
||||
List<PaymentData> _prepareChartDataFromMap(Map<int, double> paymentsMap) {
|
||||
final List<PaymentData> paymentDataList = [];
|
||||
|
||||
paymentsMap.forEach((typeReglement, montant) {
|
||||
|
||||
Reference in New Issue
Block a user