Files
geo/app/lib/presentation/user/user_dashboard_home_page.dart
pierre 1018b86537 feat: Gestion des secteurs et migration v3.0.4+304
- Ajout système complet de gestion des secteurs avec contours géographiques
- Import des contours départementaux depuis GeoJSON
- API REST pour la gestion des secteurs (/api/sectors)
- Service de géolocalisation pour déterminer les secteurs
- Migration base de données avec tables x_departements_contours et sectors_adresses
- Interface Flutter pour visualisation et gestion des secteurs
- Ajout thème sombre dans l'application
- Corrections diverses et optimisations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 11:01:45 +02:00

285 lines
10 KiB
Dart
Executable File

import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
import 'package:geosector_app/presentation/widgets/charts/charts.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
class UserDashboardHomePage extends StatefulWidget {
const UserDashboardHomePage({super.key});
@override
State<UserDashboardHomePage> createState() => _UserDashboardHomePageState();
}
class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
// Formater une date au format JJ/MM/YYYY
String _formatDate(DateTime date) {
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final size = MediaQuery.of(context).size;
final isDesktop = size.width > 900;
return Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Builder(builder: (context) {
// Récupérer l'opération actuelle
final operation = userRepository.getCurrentOperation();
if (operation != null) {
return Text(
'${operation.name} (${_formatDate(operation.dateDebut)}-${_formatDate(operation.dateFin)})',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
);
} else {
return Text(
'Tableau de bord',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
);
}
}),
const SizedBox(height: 24),
// Synthèse des passages
_buildSummaryCards(isDesktop),
const SizedBox(height: 24),
// Graphique des passages
_buildPassagesChart(context, theme),
const SizedBox(height: 24),
// Derniers passages
_buildRecentPassages(context, theme),
],
),
),
),
);
}
// Construction des cartes de synthèse
Widget _buildSummaryCards(bool isDesktop) {
return Column(
children: [
_buildCombinedPassagesCard(context, isDesktop),
const SizedBox(height: 16),
_buildCombinedPaymentsCard(isDesktop),
],
);
}
// Construction d'une carte combinée pour les règlements (liste + graphique)
Widget _buildCombinedPaymentsCard(bool isDesktop) {
return PaymentSummaryCard(
title: 'Mes règlements',
titleColor: AppTheme.accentColor,
titleIcon: Icons.payments,
height: 300,
useValueListenable: true,
userId: userRepository.getCurrentUser()?.id,
showAllPayments: false,
isDesktop: isDesktop,
backgroundIcon: Icons.euro_symbol,
backgroundIconColor: Colors.blue,
backgroundIconOpacity: 0.07,
backgroundIconSize: 180,
customTotalDisplay: (totalAmount) {
// Calculer le nombre de passages avec règlement pour le titre personnalisé
final currentUser = userRepository.getCurrentUser();
if (currentUser == null) return '${totalAmount.toStringAsFixed(2)}';
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
int passagesCount = 0;
for (final passage in passagesBox.values) {
if (passage.fkUser == currentUser.id) {
double montant = 0.0;
try {
String montantStr = passage.montant.replaceAll(',', '.');
montant = double.tryParse(montantStr) ?? 0.0;
} catch (e) {
// Ignorer les erreurs
}
if (montant > 0) passagesCount++;
}
}
return '${totalAmount.toStringAsFixed(2)} € sur $passagesCount passages';
},
);
}
// Construction d'une carte combinée pour les passages (liste + graphique)
Widget _buildCombinedPassagesCard(BuildContext context, bool isDesktop) {
return PassageSummaryCard(
title: 'Mes passages',
titleColor: AppTheme.primaryColor,
titleIcon: Icons.route,
height: 300,
useValueListenable: true,
userId: userRepository.getCurrentUser()?.id,
showAllPassages: false,
excludePassageTypes: const [2], // Exclure "À finaliser"
isDesktop: isDesktop,
);
}
// Construction du graphique des passages
Widget _buildPassagesChart(BuildContext context, ThemeData theme) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 350,
child: ActivityChart(
useValueListenable: true, // Utiliser le système réactif
excludePassageTypes: const [2], // Exclure les passages "À finaliser"
daysToShow: 15,
periodType: 'Jour',
height: 350,
userId: userRepository.getCurrentUser()?.id,
title: 'Dernière activité enregistrée sur 15 jours',
),
),
],
),
),
);
}
// Construction de la liste des derniers passages
Widget _buildRecentPassages(BuildContext context, ThemeData theme) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Derniers passages',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () {
// Naviguer vers la page d'historique
},
child: const Text('Voir tout'),
),
],
),
),
// Utilisation du widget commun PassagesListWidget avec ValueListenableBuilder
ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
final recentPassages = _getRecentPassages(passagesBox);
return PassagesListWidget(
passages: recentPassages,
showFilters: false,
showSearch: false,
showActions: true,
maxPassages: 10,
excludePassageTypes: const [2],
filterByUserId: userRepository.getCurrentUser()?.id,
periodFilter: 'last15',
onPassageSelected: (passage) {
debugPrint('Passage sélectionné: ${passage['id']}');
},
onDetailsView: (passage) {
debugPrint('Affichage des détails: ${passage['id']}');
},
onPassageEdit: (passage) {
debugPrint('Modification du passage: ${passage['id']}');
},
onReceiptView: (passage) {
debugPrint('Affichage du reçu pour le passage: ${passage['id']}');
},
);
},
),
],
),
);
}
/// Récupère les passages récents pour la liste
List<Map<String, dynamic>> _getRecentPassages(Box<PassageModel> passagesBox) {
final allPassages = passagesBox.values.where((p) => p.passedAt != null).toList();
allPassages.sort((a, b) => b.passedAt!.compareTo(a.passedAt!));
// Limiter aux 10 passages les plus récents
final recentPassagesModels = allPassages.take(10).toList();
// Convertir les modèles de passage au format attendu par le widget PassagesListWidget
return recentPassagesModels.map((passage) {
// Construire l'adresse complète à partir des champs disponibles
final String address =
'${passage.numero} ${passage.rue}${passage.rueBis.isNotEmpty ? ' ${passage.rueBis}' : ''}, ${passage.ville}';
// Convertir le montant en double
double amount = 0.0;
try {
if (passage.montant.isNotEmpty) {
// Gérer les formats possibles (virgule ou point)
String montantStr = passage.montant.replaceAll(',', '.');
amount = double.tryParse(montantStr) ?? 0.0;
}
} catch (e) {
debugPrint('Erreur de conversion du montant: ${passage.montant}');
amount = 0.0;
}
return {
'id': passage.id, // Garder l'ID comme int, pas besoin de toString()
'address': address,
'amount': amount,
'date': passage.passedAt ?? DateTime.now(),
'type': passage.fkType,
'payment': passage.fkTypeReglement,
'name': passage.name,
'notes': passage.remarque,
'hasReceipt': passage.nomRecu.isNotEmpty,
'hasError': passage.emailErreur.isNotEmpty,
'fkUser': passage.fkUser,
};
}).toList();
}
}