diff --git a/lib/data/database/database_helper.dart b/lib/data/database/database_helper.dart index 45c4f686..bcbdd0c1 100644 --- a/lib/data/database/database_helper.dart +++ b/lib/data/database/database_helper.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import '../models/session.dart'; @@ -123,10 +124,10 @@ class DatabaseHelper { Future _onUpgrade(Database db, int oldVersion, int newVersion) async { if (oldVersion < 2) { // Migration from single-target sessions to multi-target sessions - + // 1. Rename existing sessions table to a temporary name await db.execute('ALTER TABLE ${AppConstants.sessionsTable} RENAME TO sessions_old'); - + // 2. Create the new sessions table await db.execute(''' CREATE TABLE ${AppConstants.sessionsTable} ( @@ -220,7 +221,7 @@ class DatabaseHelper { // 6. Cleanup await db.execute('DROP TABLE sessions_old'); await db.execute('DROP TABLE shots_old'); - + // 7. Re-create indexes await db.execute(''' CREATE INDEX idx_target_analyses_session_id ON ${AppConstants.targetAnalysesTable}(session_id) @@ -285,9 +286,14 @@ class DatabaseHelper { Future insertSession(Session session) async { final db = await database; return await db.transaction((txn) async { + // INTERCEPTION ET FIX DE LA DATE ICI : + // On extrait la map générée et on force la colonne created_at avec la vraie date de l'objet + final Map sessionMap = Map.from(session.toMap()); + sessionMap['created_at'] = session.createdAt.toIso8601String(); + await txn.insert( AppConstants.sessionsTable, - session.toMap(), + sessionMap, // On utilise notre map corrigée au lieu de session.toMap() direct ! conflictAlgorithm: ConflictAlgorithm.replace, ); @@ -447,7 +453,7 @@ class DatabaseHelper { Future updateWeapon(Weapon weapon) async { final db = await database; - + // Start a transaction to ensure both updates succeed return await db.transaction((txn) async { // 1. Update weapon in armory @@ -472,7 +478,7 @@ class DatabaseHelper { Future deleteWeapon(String id) async { final db = await database; - + return await db.transaction((txn) async { // 1. Unlink sessions from this weapon but KEEP the name (gravé à jamais) await txn.update( @@ -538,4 +544,4 @@ class DatabaseHelper { await db.close(); _database = null; } -} +} \ No newline at end of file diff --git a/lib/data/repositories/session_repository.dart b/lib/data/repositories/session_repository.dart index dee0089b..41172d11 100644 --- a/lib/data/repositories/session_repository.dart +++ b/lib/data/repositories/session_repository.dart @@ -27,6 +27,7 @@ class SessionRepository { required List analyses, String? notes, int distance = 25, + DateTime? date, }) async { final session = Session( id: id ?? _uuid.v4(), @@ -34,7 +35,7 @@ class SessionRepository { weaponId: weaponId, maxShotsPerTarget: maxShotsPerTarget, analyses: analyses, - createdAt: DateTime.now(), + createdAt: date ?? DateTime.now(), notes: notes, distance: distance, ); diff --git a/lib/features/analysis/analysis_provider.dart b/lib/features/analysis/analysis_provider.dart index b8b882ad..156a5dbb 100644 --- a/lib/features/analysis/analysis_provider.dart +++ b/lib/features/analysis/analysis_provider.dart @@ -174,7 +174,7 @@ class AnalysisProvider extends ChangeNotifier { x: impact.x, y: impact.y, score: impact.suggestedScore, - analysisId: '', // Will be set when saving + analysisId: '', ); }).toList(); @@ -537,8 +537,7 @@ class AnalysisProvider extends ChangeNotifier { _imagePath = correctedPath; _correctedImagePath = correctedPath; _distortionCorrectionEnabled = true; - _imageAspectRatio = - 1.0; // The corrected image is always square (side x side) + _imageAspectRatio = 1.0; notifyListeners(); } @@ -616,18 +615,18 @@ class AnalysisProvider extends ChangeNotifier { // 3. Application immédiate de la transformation (méthode asynchrone) await applyDistortionCorrection(); } else { - notifyListeners(); // On prévient quand même si pas de correction + notifyListeners(); } } Future runFullDistortionWorkflow() async { - _state = AnalysisState.loading; // Affiche un spinner sur votre UI + _state = AnalysisState.loading; notifyListeners(); try { - calculateDistortion(); // Calcule les paramètres - await applyDistortionCorrection(); // Génère le fichier corrigé - _distortionCorrectionEnabled = true; // Active l'affichage + calculateDistortion(); + await applyDistortionCorrection(); + _distortionCorrectionEnabled = true; _state = AnalysisState.success; } catch (e) { _errorMessage = "Erreur de rendu : $e"; @@ -688,12 +687,7 @@ class AnalysisProvider extends ChangeNotifier { return false; } - // Identifiant d'analyse temporaire - final analysisId = _shots.isNotEmpty && _shots.first.analysisId.isNotEmpty - ? _shots.first.analysisId - : 'analysis_${DateTime.now().millisecondsSinceEpoch}'; - - final service = AiExportService(); // Local instanciation for simplicity + final service = AiExportService(); _state = AnalysisState.loading; notifyListeners(); @@ -716,13 +710,14 @@ class AnalysisProvider extends ChangeNotifier { return success; } - /// Save the session (legacy standalone flow) + /// Save the session (Refactorisé pour appliquer le paramètre de date personnalisé) Future saveSession({ String? notes, String? sessionId, String? weaponName, String? weaponId, int? distance, + DateTime? date, // <-- REÇOIT PROPREMENT LA DATE DE DEBUT DE SESSION }) async { if (_imagePath == null || _targetType == null) { throw Exception('Cannot save: missing image or target type'); @@ -748,6 +743,7 @@ class AnalysisProvider extends ChangeNotifier { if (existingSession != null) { await _sessionRepository.addAnalysisToSession(sessionId, targetAnalysis); } else { + // CORRECTION : Utilise la date injectée plutôt que DateTime.now() await _sessionRepository.createSession( id: sessionId, weapon: weaponName ?? 'Inconnue', @@ -756,9 +752,11 @@ class AnalysisProvider extends ChangeNotifier { analyses: [targetAnalysis], notes: notes, distance: distance ?? 25, + date: date ?? DateTime.now(), ); } } else { + // CORRECTION : S'applique aussi au flux alternatif sans ID préalable await _sessionRepository.createSession( weapon: weaponName ?? 'Inconnue', weaponId: weaponId, @@ -766,6 +764,7 @@ class AnalysisProvider extends ChangeNotifier { analyses: [targetAnalysis], notes: notes, distance: distance ?? 25, + date: date ?? DateTime.now(), ); } diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index b275973d..bb463aac 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -568,7 +568,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), ); } - void _showSaveSessionDialog(BuildContext context, AnalysisProvider provider) { showDialog( context: context, @@ -614,6 +613,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { weaponName: sessionProvider.currentWeapon, weaponId: sessionProvider.currentWeaponId, distance: sessionProvider.distance, + date: sessionProvider.sessionDate, ); sessionProvider.addAnalysis(analysis); @@ -645,6 +645,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { weaponName: sessionProvider.currentWeapon, weaponId: sessionProvider.currentWeaponId, distance: sessionProvider.distance, + date: sessionProvider.sessionDate, // <-- CORRECTION : Prise en compte de la date ici aussi ! ); if (context.mounted) { @@ -666,4 +667,4 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), ); } -} \ No newline at end of file + } \ No newline at end of file diff --git a/lib/features/session/session_provider.dart b/lib/features/session/session_provider.dart index d82ca021..6b58129b 100644 --- a/lib/features/session/session_provider.dart +++ b/lib/features/session/session_provider.dart @@ -4,6 +4,8 @@ import '../../data/models/target_analysis.dart'; import '../../data/models/weapon.dart'; class SessionProvider extends ChangeNotifier { + DateTime? _sessionDate; + DateTime? get sessionDate => _sessionDate; String? _currentWeaponName; String? _currentWeaponId; int _shotsPerTarget = 5; @@ -23,11 +25,12 @@ class SessionProvider extends ChangeNotifier { int get totalSessionScore => _currentAnalyses.fold(0, (sum, a) => sum + a.totalScore); int get targetCount => _currentAnalyses.length; - void startSession(String weaponName, int shots, String sessionId, {String? weaponId, int distance = 25}) { + void startSession(String weaponName, int shots, String sessionId, {String? weaponId, int distance = 25, DateTime? date}) { _currentWeaponName = weaponName; _currentWeaponId = weaponId; _shotsPerTarget = shots; _distance = distance; + _sessionDate = date ?? DateTime.now(); _activeSessionId = sessionId; _currentAnalyses.clear(); _isSessionActive = true; diff --git a/lib/features/session/session_setup_screen.dart b/lib/features/session/session_setup_screen.dart index 9c54af7d..733705ec 100644 --- a/lib/features/session/session_setup_screen.dart +++ b/lib/features/session/session_setup_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:intl/intl.dart'; // Utile pour formater proprement la date en français import '../../core/constants/app_constants.dart'; import '../../core/theme/app_theme.dart'; import '../../data/models/weapon.dart'; @@ -18,13 +19,15 @@ class SessionSetupScreen extends StatefulWidget { class _SessionSetupScreenState extends State { final _formKey = GlobalKey(); - // Plus besoin du _weaponController, on force la sélection int _shotsPerTarget = 5; int _distance = 25; List _availableWeapons = []; Weapon? _selectedWeapon; bool _isLoadingWeapons = true; + // AJOUT DE LA VARIABLE DATE : Initialisée par défaut à maintenant + DateTime _selectedDate = DateTime.now(); + @override void initState() { super.initState(); @@ -39,7 +42,6 @@ class _SessionSetupScreenState extends State { if (mounted) { setState(() { _availableWeapons = weapons; - // Si la liste n'est pas vide et qu'aucune arme n'est sélectionnée, on sélectionne la première par défaut if (_availableWeapons.isNotEmpty && _selectedWeapon == null) { _selectedWeapon = _availableWeapons.first; _updateSettingsForWeapon(_selectedWeapon!); @@ -49,25 +51,47 @@ class _SessionSetupScreenState extends State { } } - // Petite méthode pour mettre à jour les curseurs selon l'arme sélectionnée void _updateSettingsForWeapon(Weapon weapon) { _shotsPerTarget = weapon.magazineCapacity; _distance = (weapon.type == WeaponType.handgun) ? 25 : 50; } + // Fonction pour ouvrir le calendrier si l'utilisateur clique sur le champ + Future _pickDate() async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedDate, + firstDate: DateTime(2020), + lastDate: DateTime(2100), + locale: const Locale('fr', 'FR'), // Force le calendrier en français + ); + if (picked != null && picked != _selectedDate) { + setState(() { + // On garde aussi l'heure actuelle lors du changement de date + final now = DateTime.now(); + _selectedDate = DateTime(picked.year, picked.month, picked.day, now.hour, now.minute); + }); + } + } + void _startSession() { - // Sécurité supplémentaire : impossible de démarrer sans arme if (_formKey.currentState!.validate() && _selectedWeapon != null) { final repository = context.read(); final sessionProvider = context.read(); final sessionId = repository.generateId(); + + // PASSE-PARTOUT : On envoie la session au provider. + // Note : Si ton `sessionProvider.startSession` ne prend pas encore la date en paramètre, + // pas de panique, la compilation passera car Dart tolère les arguments nommés optionnels + // s'ils sont déjà présents, ou tu pourras l'ajouter à ta méthode startSession. sessionProvider.startSession( _selectedWeapon!.displayName, _shotsPerTarget, sessionId, weaponId: _selectedWeapon!.id, distance: _distance, + date: _selectedDate, ); Navigator.pushReplacement( @@ -79,6 +103,9 @@ class _SessionSetupScreenState extends State { @override Widget build(BuildContext context) { + // Petit formatage sympa en français (ex: "27 mai 2026") + final String formattedDate = DateFormat('dd MMMM yyyy', 'fr_FR').format(_selectedDate); + return Scaffold( appBar: AppBar( title: const Text('Configuration de la session'), @@ -93,7 +120,7 @@ class _SessionSetupScreenState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( - 'Choix de l\'arme', + 'Informations générales', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), @@ -128,7 +155,6 @@ class _SessionSetupScreenState extends State { context, MaterialPageRoute(builder: (_) => const WeaponListScreen()), ).then((_) { - // Quand on revient de l'armurerie, on recharge la liste ! _loadWeapons(); }); }, @@ -139,28 +165,50 @@ class _SessionSetupScreenState extends State { ), ) else - DropdownButtonFormField( - value: _selectedWeapon, - decoration: InputDecoration( - labelText: 'Sélectionner une arme', - prefixIcon: const Icon(Icons.shield), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppConstants.borderRadius), + Column( + children: [ + DropdownButtonFormField( + value: _selectedWeapon, + decoration: InputDecoration( + labelText: 'Sélectionner une arme', + prefixIcon: const Icon(Icons.shield), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + ), + items: _availableWeapons.map((w) => DropdownMenuItem( + value: w, + child: Text(w.displayName), + )).toList(), + onChanged: (value) { + if (value != null) { + setState(() { + _selectedWeapon = value; + _updateSettingsForWeapon(value); + }); + } + }, ), - ), - // On a retiré le choix "null / Autre", l'utilisateur DOIT choisir une arme existante - items: _availableWeapons.map((w) => DropdownMenuItem( - value: w, - child: Text(w.displayName), - )).toList(), - onChanged: (value) { - if (value != null) { - setState(() { - _selectedWeapon = value; - _updateSettingsForWeapon(value); - }); - } - }, + const SizedBox(height: 16), + + // NOUVEAU CHAMP : Sélecteur de date interactif + InkWell( + onTap: _availableWeapons.isEmpty ? null : _pickDate, + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + child: IgnorePointer( + child: TextFormField( + decoration: InputDecoration( + labelText: 'Date de la session', + prefixIcon: const Icon(Icons.calendar_today), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + ), + controller: TextEditingController(text: formattedDate), + ), + ), + ), + ], ), const SizedBox(height: 24), @@ -252,7 +300,6 @@ class _SessionSetupScreenState extends State { const SizedBox(height: 48), ElevatedButton.icon( - // On désactive le bouton si la liste est vide ou si aucune arme n'est sélectionnée onPressed: (_availableWeapons.isEmpty || _selectedWeapon == null) ? null : _startSession,