feat(session): ajout du sélecteur de date et persistance dans l'historique de tir

This commit is contained in:
qguillaume
2026-05-27 00:57:26 +02:00
parent e570bd4296
commit 78c9fc96c9
6 changed files with 111 additions and 54 deletions

View File

@@ -1,3 +1,4 @@
import 'dart:io';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import '../models/session.dart'; import '../models/session.dart';
@@ -285,9 +286,14 @@ class DatabaseHelper {
Future<int> insertSession(Session session) async { Future<int> insertSession(Session session) async {
final db = await database; final db = await database;
return await db.transaction((txn) async { 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<String, dynamic> sessionMap = Map.from(session.toMap());
sessionMap['created_at'] = session.createdAt.toIso8601String();
await txn.insert( await txn.insert(
AppConstants.sessionsTable, AppConstants.sessionsTable,
session.toMap(), sessionMap, // On utilise notre map corrigée au lieu de session.toMap() direct !
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
); );

View File

@@ -27,6 +27,7 @@ class SessionRepository {
required List<TargetAnalysis> analyses, required List<TargetAnalysis> analyses,
String? notes, String? notes,
int distance = 25, int distance = 25,
DateTime? date,
}) async { }) async {
final session = Session( final session = Session(
id: id ?? _uuid.v4(), id: id ?? _uuid.v4(),
@@ -34,7 +35,7 @@ class SessionRepository {
weaponId: weaponId, weaponId: weaponId,
maxShotsPerTarget: maxShotsPerTarget, maxShotsPerTarget: maxShotsPerTarget,
analyses: analyses, analyses: analyses,
createdAt: DateTime.now(), createdAt: date ?? DateTime.now(),
notes: notes, notes: notes,
distance: distance, distance: distance,
); );

View File

@@ -174,7 +174,7 @@ class AnalysisProvider extends ChangeNotifier {
x: impact.x, x: impact.x,
y: impact.y, y: impact.y,
score: impact.suggestedScore, score: impact.suggestedScore,
analysisId: '', // Will be set when saving analysisId: '',
); );
}).toList(); }).toList();
@@ -537,8 +537,7 @@ class AnalysisProvider extends ChangeNotifier {
_imagePath = correctedPath; _imagePath = correctedPath;
_correctedImagePath = correctedPath; _correctedImagePath = correctedPath;
_distortionCorrectionEnabled = true; _distortionCorrectionEnabled = true;
_imageAspectRatio = _imageAspectRatio = 1.0;
1.0; // The corrected image is always square (side x side)
notifyListeners(); notifyListeners();
} }
@@ -616,18 +615,18 @@ class AnalysisProvider extends ChangeNotifier {
// 3. Application immédiate de la transformation (méthode asynchrone) // 3. Application immédiate de la transformation (méthode asynchrone)
await applyDistortionCorrection(); await applyDistortionCorrection();
} else { } else {
notifyListeners(); // On prévient quand même si pas de correction notifyListeners();
} }
} }
Future<void> runFullDistortionWorkflow() async { Future<void> runFullDistortionWorkflow() async {
_state = AnalysisState.loading; // Affiche un spinner sur votre UI _state = AnalysisState.loading;
notifyListeners(); notifyListeners();
try { try {
calculateDistortion(); // Calcule les paramètres calculateDistortion();
await applyDistortionCorrection(); // Génère le fichier corrigé await applyDistortionCorrection();
_distortionCorrectionEnabled = true; // Active l'affichage _distortionCorrectionEnabled = true;
_state = AnalysisState.success; _state = AnalysisState.success;
} catch (e) { } catch (e) {
_errorMessage = "Erreur de rendu : $e"; _errorMessage = "Erreur de rendu : $e";
@@ -688,12 +687,7 @@ class AnalysisProvider extends ChangeNotifier {
return false; return false;
} }
// Identifiant d'analyse temporaire final service = AiExportService();
final analysisId = _shots.isNotEmpty && _shots.first.analysisId.isNotEmpty
? _shots.first.analysisId
: 'analysis_${DateTime.now().millisecondsSinceEpoch}';
final service = AiExportService(); // Local instanciation for simplicity
_state = AnalysisState.loading; _state = AnalysisState.loading;
notifyListeners(); notifyListeners();
@@ -716,13 +710,14 @@ class AnalysisProvider extends ChangeNotifier {
return success; return success;
} }
/// Save the session (legacy standalone flow) /// Save the session (Refactorisé pour appliquer le paramètre de date personnalisé)
Future<TargetAnalysis> saveSession({ Future<TargetAnalysis> saveSession({
String? notes, String? notes,
String? sessionId, String? sessionId,
String? weaponName, String? weaponName,
String? weaponId, String? weaponId,
int? distance, int? distance,
DateTime? date, // <-- REÇOIT PROPREMENT LA DATE DE DEBUT DE SESSION
}) async { }) async {
if (_imagePath == null || _targetType == null) { if (_imagePath == null || _targetType == null) {
throw Exception('Cannot save: missing image or target type'); throw Exception('Cannot save: missing image or target type');
@@ -748,6 +743,7 @@ class AnalysisProvider extends ChangeNotifier {
if (existingSession != null) { if (existingSession != null) {
await _sessionRepository.addAnalysisToSession(sessionId, targetAnalysis); await _sessionRepository.addAnalysisToSession(sessionId, targetAnalysis);
} else { } else {
// CORRECTION : Utilise la date injectée plutôt que DateTime.now()
await _sessionRepository.createSession( await _sessionRepository.createSession(
id: sessionId, id: sessionId,
weapon: weaponName ?? 'Inconnue', weapon: weaponName ?? 'Inconnue',
@@ -756,9 +752,11 @@ class AnalysisProvider extends ChangeNotifier {
analyses: [targetAnalysis], analyses: [targetAnalysis],
notes: notes, notes: notes,
distance: distance ?? 25, distance: distance ?? 25,
date: date ?? DateTime.now(),
); );
} }
} else { } else {
// CORRECTION : S'applique aussi au flux alternatif sans ID préalable
await _sessionRepository.createSession( await _sessionRepository.createSession(
weapon: weaponName ?? 'Inconnue', weapon: weaponName ?? 'Inconnue',
weaponId: weaponId, weaponId: weaponId,
@@ -766,6 +764,7 @@ class AnalysisProvider extends ChangeNotifier {
analyses: [targetAnalysis], analyses: [targetAnalysis],
notes: notes, notes: notes,
distance: distance ?? 25, distance: distance ?? 25,
date: date ?? DateTime.now(),
); );
} }

View File

@@ -568,7 +568,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
), ),
); );
} }
void _showSaveSessionDialog(BuildContext context, AnalysisProvider provider) { void _showSaveSessionDialog(BuildContext context, AnalysisProvider provider) {
showDialog( showDialog(
context: context, context: context,
@@ -614,6 +613,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
weaponName: sessionProvider.currentWeapon, weaponName: sessionProvider.currentWeapon,
weaponId: sessionProvider.currentWeaponId, weaponId: sessionProvider.currentWeaponId,
distance: sessionProvider.distance, distance: sessionProvider.distance,
date: sessionProvider.sessionDate,
); );
sessionProvider.addAnalysis(analysis); sessionProvider.addAnalysis(analysis);
@@ -645,6 +645,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
weaponName: sessionProvider.currentWeapon, weaponName: sessionProvider.currentWeapon,
weaponId: sessionProvider.currentWeaponId, weaponId: sessionProvider.currentWeaponId,
distance: sessionProvider.distance, distance: sessionProvider.distance,
date: sessionProvider.sessionDate, // <-- CORRECTION : Prise en compte de la date ici aussi !
); );
if (context.mounted) { if (context.mounted) {

View File

@@ -4,6 +4,8 @@ import '../../data/models/target_analysis.dart';
import '../../data/models/weapon.dart'; import '../../data/models/weapon.dart';
class SessionProvider extends ChangeNotifier { class SessionProvider extends ChangeNotifier {
DateTime? _sessionDate;
DateTime? get sessionDate => _sessionDate;
String? _currentWeaponName; String? _currentWeaponName;
String? _currentWeaponId; String? _currentWeaponId;
int _shotsPerTarget = 5; int _shotsPerTarget = 5;
@@ -23,11 +25,12 @@ class SessionProvider extends ChangeNotifier {
int get totalSessionScore => _currentAnalyses.fold(0, (sum, a) => sum + a.totalScore); int get totalSessionScore => _currentAnalyses.fold(0, (sum, a) => sum + a.totalScore);
int get targetCount => _currentAnalyses.length; 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; _currentWeaponName = weaponName;
_currentWeaponId = weaponId; _currentWeaponId = weaponId;
_shotsPerTarget = shots; _shotsPerTarget = shots;
_distance = distance; _distance = distance;
_sessionDate = date ?? DateTime.now();
_activeSessionId = sessionId; _activeSessionId = sessionId;
_currentAnalyses.clear(); _currentAnalyses.clear();
_isSessionActive = true; _isSessionActive = true;

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.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/constants/app_constants.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../data/models/weapon.dart'; import '../../data/models/weapon.dart';
@@ -18,13 +19,15 @@ class SessionSetupScreen extends StatefulWidget {
class _SessionSetupScreenState extends State<SessionSetupScreen> { class _SessionSetupScreenState extends State<SessionSetupScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
// Plus besoin du _weaponController, on force la sélection
int _shotsPerTarget = 5; int _shotsPerTarget = 5;
int _distance = 25; int _distance = 25;
List<Weapon> _availableWeapons = []; List<Weapon> _availableWeapons = [];
Weapon? _selectedWeapon; Weapon? _selectedWeapon;
bool _isLoadingWeapons = true; bool _isLoadingWeapons = true;
// AJOUT DE LA VARIABLE DATE : Initialisée par défaut à maintenant
DateTime _selectedDate = DateTime.now();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -39,7 +42,6 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
if (mounted) { if (mounted) {
setState(() { setState(() {
_availableWeapons = weapons; _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) { if (_availableWeapons.isNotEmpty && _selectedWeapon == null) {
_selectedWeapon = _availableWeapons.first; _selectedWeapon = _availableWeapons.first;
_updateSettingsForWeapon(_selectedWeapon!); _updateSettingsForWeapon(_selectedWeapon!);
@@ -49,25 +51,47 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
} }
} }
// Petite méthode pour mettre à jour les curseurs selon l'arme sélectionnée
void _updateSettingsForWeapon(Weapon weapon) { void _updateSettingsForWeapon(Weapon weapon) {
_shotsPerTarget = weapon.magazineCapacity; _shotsPerTarget = weapon.magazineCapacity;
_distance = (weapon.type == WeaponType.handgun) ? 25 : 50; _distance = (weapon.type == WeaponType.handgun) ? 25 : 50;
} }
// Fonction pour ouvrir le calendrier si l'utilisateur clique sur le champ
Future<void> _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() { void _startSession() {
// Sécurité supplémentaire : impossible de démarrer sans arme
if (_formKey.currentState!.validate() && _selectedWeapon != null) { if (_formKey.currentState!.validate() && _selectedWeapon != null) {
final repository = context.read<SessionRepository>(); final repository = context.read<SessionRepository>();
final sessionProvider = context.read<SessionProvider>(); final sessionProvider = context.read<SessionProvider>();
final sessionId = repository.generateId(); 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( sessionProvider.startSession(
_selectedWeapon!.displayName, _selectedWeapon!.displayName,
_shotsPerTarget, _shotsPerTarget,
sessionId, sessionId,
weaponId: _selectedWeapon!.id, weaponId: _selectedWeapon!.id,
distance: _distance, distance: _distance,
date: _selectedDate,
); );
Navigator.pushReplacement( Navigator.pushReplacement(
@@ -79,6 +103,9 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
@override @override
Widget build(BuildContext context) { 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Configuration de la session'), title: const Text('Configuration de la session'),
@@ -93,7 +120,7 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Text( const Text(
'Choix de l\'arme', 'Informations générales',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -128,7 +155,6 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
context, context,
MaterialPageRoute(builder: (_) => const WeaponListScreen()), MaterialPageRoute(builder: (_) => const WeaponListScreen()),
).then((_) { ).then((_) {
// Quand on revient de l'armurerie, on recharge la liste !
_loadWeapons(); _loadWeapons();
}); });
}, },
@@ -139,6 +165,8 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
), ),
) )
else else
Column(
children: [
DropdownButtonFormField<Weapon>( DropdownButtonFormField<Weapon>(
value: _selectedWeapon, value: _selectedWeapon,
decoration: InputDecoration( decoration: InputDecoration(
@@ -148,7 +176,6 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
borderRadius: BorderRadius.circular(AppConstants.borderRadius), borderRadius: BorderRadius.circular(AppConstants.borderRadius),
), ),
), ),
// On a retiré le choix "null / Autre", l'utilisateur DOIT choisir une arme existante
items: _availableWeapons.map((w) => DropdownMenuItem( items: _availableWeapons.map((w) => DropdownMenuItem(
value: w, value: w,
child: Text(w.displayName), child: Text(w.displayName),
@@ -162,6 +189,27 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
} }
}, },
), ),
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), const SizedBox(height: 24),
@@ -252,7 +300,6 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
const SizedBox(height: 48), const SizedBox(height: 48),
ElevatedButton.icon( 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) onPressed: (_availableWeapons.isEmpty || _selectedWeapon == null)
? null ? null
: _startSession, : _startSession,