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>
This commit is contained in:
pierre
2025-08-07 11:01:45 +02:00
parent 6a609fb467
commit 599b9fcda0
662 changed files with 213221 additions and 174243 deletions

View File

@@ -0,0 +1,348 @@
import 'package:flutter/material.dart';
enum SectorActionType {
create,
update,
delete,
}
class SectorActionResultDialog extends StatelessWidget {
final SectorActionType actionType;
final String sectorName;
final Map<String, dynamic> statistics;
final Map<String, dynamic>? departmentWarning;
final VoidCallback? onConfirm;
const SectorActionResultDialog({
super.key,
required this.actionType,
required this.sectorName,
required this.statistics,
this.departmentWarning,
this.onConfirm,
});
String get _actionTitle {
switch (actionType) {
case SectorActionType.create:
return 'Secteur créé';
case SectorActionType.update:
return 'Secteur modifié';
case SectorActionType.delete:
return 'Secteur supprimé';
}
}
IconData get _actionIcon {
switch (actionType) {
case SectorActionType.create:
return Icons.add_location_alt;
case SectorActionType.update:
return Icons.edit_location_alt;
case SectorActionType.delete:
return Icons.delete_forever;
}
}
Color get _actionColor {
switch (actionType) {
case SectorActionType.create:
return Colors.green;
case SectorActionType.update:
return Colors.orange;
case SectorActionType.delete:
return Colors.red;
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Row(
children: [
Icon(_actionIcon, color: _actionColor, size: 28),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_actionTitle,
style: const TextStyle(fontSize: 20),
),
const SizedBox(height: 4),
Text(
sectorName,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
color: Colors.grey[700],
),
),
],
),
),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Warning départemental si présent
if (departmentWarning != null && departmentWarning!['intersecting_departments'] != null) ...[
_buildDepartmentWarning(),
const SizedBox(height: 16),
],
// Statistiques des passages
_buildStatisticsSection(),
],
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
onConfirm?.call();
},
child: const Text('OK'),
),
],
);
}
Widget _buildDepartmentWarning() {
final departments = departmentWarning!['intersecting_departments'] as List<dynamic>;
final isMultipleDepartments = departments.length > 1;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.warning_amber_rounded, color: Colors.orange[700], size: 24),
const SizedBox(width: 8),
Expanded(
child: Text(
isMultipleDepartments
? 'Secteur à cheval sur plusieurs départements'
: 'Secteur sur un autre département',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.orange[900],
),
),
),
],
),
const SizedBox(height: 8),
...departments.map((dept) {
final percentage = dept['percentage_overlap'] as num;
return Padding(
padding: const EdgeInsets.only(left: 32, top: 4),
child: Row(
children: [
Icon(Icons.location_on, size: 16, color: Colors.orange[700]),
const SizedBox(width: 4),
Expanded(
child: Text(
'${dept['nom_dept']} (${dept['code_dept']})',
style: TextStyle(color: Colors.orange[900]),
),
),
Text(
'${percentage.toStringAsFixed(1)}%',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.orange[900],
),
),
],
),
);
}).toList(),
],
),
);
}
Widget _buildStatisticsSection() {
final List<Widget> statisticWidgets = [];
// Titre de la section
statisticWidgets.add(
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
'Statistiques des passages',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
),
);
// CREATE : passages créés et intégrés
if (actionType == SectorActionType.create) {
final passagesCreated = statistics['passages_created'] ?? 0;
final passagesIntegrated = statistics['passages_integrated'] ?? 0;
final totalPassages = passagesCreated + passagesIntegrated;
statisticWidgets.add(_buildStatRow(
icon: Icons.add_circle_outline,
label: 'Nouveaux passages créés',
value: passagesCreated,
color: Colors.green,
));
if (passagesIntegrated > 0) {
statisticWidgets.add(_buildStatRow(
icon: Icons.merge_type,
label: 'Passages orphelins intégrés',
value: passagesIntegrated,
color: Colors.blue,
));
}
statisticWidgets.add(const Divider(height: 24));
statisticWidgets.add(_buildStatRow(
icon: Icons.functions,
label: 'Total des passages du secteur',
value: totalPassages,
color: Colors.indigo,
isBold: true,
));
}
// UPDATE : passages créés, mis à jour, orphelins, total
else if (actionType == SectorActionType.update) {
final passagesCreated = statistics['passages_created'] ?? 0;
final passagesUpdated = statistics['passages_updated'] ?? 0;
final passagesOrphaned = statistics['passages_orphaned'] ?? 0;
final passagesTotal = statistics['passages_total'] ?? 0;
if (passagesCreated > 0) {
statisticWidgets.add(_buildStatRow(
icon: Icons.add_circle_outline,
label: 'Nouveaux passages créés',
value: passagesCreated,
color: Colors.green,
));
}
if (passagesUpdated > 0) {
statisticWidgets.add(_buildStatRow(
icon: Icons.update,
label: 'Passages mis à jour',
value: passagesUpdated,
color: Colors.blue,
));
}
if (passagesOrphaned > 0) {
statisticWidgets.add(_buildStatRow(
icon: Icons.remove_circle_outline,
label: 'Passages mis en orphelin',
value: passagesOrphaned,
color: Colors.orange,
));
}
statisticWidgets.add(const Divider(height: 24));
statisticWidgets.add(_buildStatRow(
icon: Icons.functions,
label: 'Total des passages du secteur',
value: passagesTotal,
color: Colors.indigo,
isBold: true,
));
}
// DELETE : passages supprimés et conservés
else if (actionType == SectorActionType.delete) {
final passagesDeleted = statistics['passages_deleted'] ?? 0;
final passagesReassigned = statistics['passages_reassigned'] ?? 0;
if (passagesDeleted > 0) {
statisticWidgets.add(_buildStatRow(
icon: Icons.delete_outline,
label: 'Passages supprimés',
value: passagesDeleted,
color: Colors.red,
));
}
if (passagesReassigned > 0) {
statisticWidgets.add(_buildStatRow(
icon: Icons.bookmark_border,
label: 'Passages conservés (orphelins)',
value: passagesReassigned,
color: Colors.orange,
));
}
if (passagesDeleted == 0 && passagesReassigned == 0) {
statisticWidgets.add(
Text(
'Aucun passage dans ce secteur',
style: TextStyle(
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
);
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: statisticWidgets,
);
}
Widget _buildStatRow({
required IconData icon,
required String label,
required int value,
required Color color,
bool isBold = false,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
label,
style: TextStyle(
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
),
),
),
Text(
value.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: color,
fontSize: isBold ? 18 : 16,
),
),
],
),
);
}
}