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 3bbc599ab4
commit 1018b86537
620 changed files with 120502 additions and 91396 deletions

View File

@@ -0,0 +1,232 @@
import 'package:flutter/material.dart';
import 'package:geosector_app/core/services/theme_service.dart';
/// Widget pour basculer entre les thèmes clair/sombre/automatique
class ThemeSwitcher extends StatelessWidget {
/// Style d'affichage du sélecteur
final ThemeSwitcherStyle style;
/// Afficher le texte descriptif
final bool showLabel;
/// Callback optionnel appelé après changement de thème
final VoidCallback? onThemeChanged;
const ThemeSwitcher({
super.key,
this.style = ThemeSwitcherStyle.iconButton,
this.showLabel = false,
this.onThemeChanged,
});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: ThemeService.instance,
builder: (context, child) {
switch (style) {
case ThemeSwitcherStyle.iconButton:
return _buildIconButton(context);
case ThemeSwitcherStyle.dropdown:
return _buildDropdown(context);
case ThemeSwitcherStyle.segmentedButton:
return _buildSegmentedButton(context);
case ThemeSwitcherStyle.toggleButtons:
return _buildToggleButtons(context);
}
},
);
}
/// Bouton icône simple (bascule entre clair/sombre)
Widget _buildIconButton(BuildContext context) {
final themeService = ThemeService.instance;
return IconButton(
icon: Icon(themeService.themeModeIcon),
tooltip: 'Changer le thème (${themeService.themeModeDescription})',
onPressed: () async {
await themeService.toggleTheme();
onThemeChanged?.call();
},
);
}
/// Dropdown avec toutes les options
Widget _buildDropdown(BuildContext context) {
final themeService = ThemeService.instance;
final theme = Theme.of(context);
return DropdownButton<ThemeMode>(
value: themeService.themeMode,
icon: Icon(Icons.arrow_drop_down, color: theme.colorScheme.onSurface),
underline: Container(),
items: [
DropdownMenuItem(
value: ThemeMode.system,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.brightness_auto, size: 20),
const SizedBox(width: 8),
const Text('Automatique'),
if (showLabel) ...[
const SizedBox(width: 4),
Text(
'(${themeService.isSystemDark ? 'sombre' : 'clair'})',
style: theme.textTheme.bodySmall,
),
],
],
),
),
const DropdownMenuItem(
value: ThemeMode.light,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.light_mode, size: 20),
SizedBox(width: 8),
Text('Clair'),
],
),
),
const DropdownMenuItem(
value: ThemeMode.dark,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.dark_mode, size: 20),
SizedBox(width: 8),
Text('Sombre'),
],
),
),
],
onChanged: (ThemeMode? mode) async {
if (mode != null) {
await themeService.setThemeMode(mode);
onThemeChanged?.call();
}
},
);
}
/// Boutons segmentés (Material 3)
Widget _buildSegmentedButton(BuildContext context) {
final themeService = ThemeService.instance;
return SegmentedButton<ThemeMode>(
segments: const [
ButtonSegment(
value: ThemeMode.light,
icon: Icon(Icons.light_mode, size: 16),
label: Text('Clair'),
),
ButtonSegment(
value: ThemeMode.system,
icon: Icon(Icons.brightness_auto, size: 16),
label: Text('Auto'),
),
ButtonSegment(
value: ThemeMode.dark,
icon: Icon(Icons.dark_mode, size: 16),
label: Text('Sombre'),
),
],
selected: {themeService.themeMode},
onSelectionChanged: (Set<ThemeMode> selection) async {
if (selection.isNotEmpty) {
await themeService.setThemeMode(selection.first);
onThemeChanged?.call();
}
},
);
}
/// Boutons à bascule
Widget _buildToggleButtons(BuildContext context) {
final themeService = ThemeService.instance;
final theme = Theme.of(context);
return ToggleButtons(
borderRadius: BorderRadius.circular(8),
constraints: const BoxConstraints(minHeight: 40, minWidth: 60),
isSelected: [
themeService.themeMode == ThemeMode.light,
themeService.themeMode == ThemeMode.system,
themeService.themeMode == ThemeMode.dark,
],
onPressed: (int index) async {
final modes = [ThemeMode.light, ThemeMode.system, ThemeMode.dark];
await themeService.setThemeMode(modes[index]);
onThemeChanged?.call();
},
children: const [
Icon(Icons.light_mode, size: 20),
Icon(Icons.brightness_auto, size: 20),
Icon(Icons.dark_mode, size: 20),
],
);
}
}
/// Widget d'information sur le thème actuel
class ThemeInfo extends StatelessWidget {
const ThemeInfo({super.key});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: ThemeService.instance,
builder: (context, child) {
final themeService = ThemeService.instance;
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
themeService.themeModeIcon,
size: 16,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Text(
themeService.themeModeDescription,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
],
),
);
},
);
}
}
/// Styles d'affichage pour le ThemeSwitcher
enum ThemeSwitcherStyle {
/// Bouton icône simple qui bascule entre clair/sombre
iconButton,
/// Menu déroulant avec toutes les options
dropdown,
/// Boutons segmentés (Material 3)
segmentedButton,
/// Boutons à bascule
toggleButtons,
}