- 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>
186 lines
5.4 KiB
Dart
Executable File
186 lines
5.4 KiB
Dart
Executable File
import 'package:flutter/material.dart';
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'package:geosector_app/core/data/models/passage_model.dart';
|
|
import 'package:geosector_app/core/data/models/sector_model.dart';
|
|
import 'package:geosector_app/core/constants/app_keys.dart';
|
|
import 'package:geosector_app/core/theme/app_theme.dart';
|
|
|
|
class SectorDistributionCard extends StatelessWidget {
|
|
final String title;
|
|
final double? height;
|
|
final EdgeInsetsGeometry? padding;
|
|
|
|
const SectorDistributionCard({
|
|
super.key,
|
|
this.title = 'Répartition par secteur',
|
|
this.height,
|
|
this.padding,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
height: height,
|
|
padding: padding ?? const EdgeInsets.all(AppTheme.spacingM),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
|
|
boxShadow: AppTheme.cardShadow,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
const SizedBox(height: AppTheme.spacingM),
|
|
Expanded(
|
|
child: _buildAutoRefreshContent(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAutoRefreshContent() {
|
|
// Écouter les changements des deux boîtes
|
|
return ValueListenableBuilder(
|
|
valueListenable:
|
|
Hive.box<SectorModel>(AppKeys.sectorsBoxName).listenable(),
|
|
builder: (context, Box<SectorModel> sectorsBox, child) {
|
|
return ValueListenableBuilder(
|
|
valueListenable:
|
|
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
|
|
builder: (context, Box<PassageModel> passagesBox, child) {
|
|
return _buildContent(sectorsBox, passagesBox);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildContent(
|
|
Box<SectorModel> sectorsBox, Box<PassageModel> passagesBox) {
|
|
try {
|
|
// Calculer les statistiques
|
|
final sectorStats = _calculateSectorStats(sectorsBox, passagesBox);
|
|
|
|
if (sectorStats.isEmpty) {
|
|
return const Center(
|
|
child: Text('Aucune donnée de secteur disponible'),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
itemCount: sectorStats.length,
|
|
itemBuilder: (context, index) {
|
|
final sector = sectorStats[index];
|
|
return _buildSectorItem(
|
|
sector['name'],
|
|
sector['count'],
|
|
Color(sector['color']),
|
|
sectorStats,
|
|
);
|
|
},
|
|
);
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du calcul des statistiques: $e');
|
|
return Center(
|
|
child: Text('Erreur: ${e.toString()}'),
|
|
);
|
|
}
|
|
}
|
|
|
|
List<Map<String, dynamic>> _calculateSectorStats(
|
|
Box<SectorModel> sectorsBox,
|
|
Box<PassageModel> passagesBox,
|
|
) {
|
|
// Récupérer tous les secteurs et passages
|
|
final List<SectorModel> sectors = sectorsBox.values.toList();
|
|
final List<PassageModel> passages = passagesBox.values.toList();
|
|
|
|
// Compter les passages par secteur (en excluant ceux où fkType==2)
|
|
final Map<int, int> sectorCounts = {};
|
|
|
|
for (final passage in passages) {
|
|
// Exclure les passages où fkType==2 et ceux sans secteur
|
|
if (passage.fkType != 2 && passage.fkSector != null) {
|
|
sectorCounts[passage.fkSector!] =
|
|
(sectorCounts[passage.fkSector!] ?? 0) + 1;
|
|
}
|
|
}
|
|
|
|
// Préparer les données pour l'affichage
|
|
List<Map<String, dynamic>> stats = [];
|
|
for (final sector in sectors) {
|
|
final count = sectorCounts[sector.id] ?? 0;
|
|
if (count > 0) {
|
|
stats.add({
|
|
'name': sector.libelle,
|
|
'count': count,
|
|
'color': sector.color.isEmpty
|
|
? 0xFF4B77BE
|
|
: int.tryParse(sector.color.replaceAll('#', '0xFF')) ??
|
|
0xFF4B77BE,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Trier par nombre de passages (décroissant)
|
|
stats.sort((a, b) => (b['count'] as int).compareTo(a['count'] as int));
|
|
|
|
return stats;
|
|
}
|
|
|
|
Widget _buildSectorItem(
|
|
String name,
|
|
int count,
|
|
Color color,
|
|
List<Map<String, dynamic>> allStats,
|
|
) {
|
|
final totalCount =
|
|
allStats.fold(0, (sum, item) => sum + (item['count'] as int));
|
|
final percentage = totalCount > 0 ? (count / totalCount) * 100 : 0;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: AppTheme.spacingS),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
name,
|
|
style: const TextStyle(fontSize: 14),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
Text(
|
|
'$count (${percentage.toInt()}%)',
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
LinearProgressIndicator(
|
|
value: percentage / 100,
|
|
backgroundColor: Colors.grey[200],
|
|
valueColor: AlwaysStoppedAnimation<Color>(color),
|
|
minHeight: 8,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|