import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:latlong2/latlong.dart'; import 'package:manager_api_new/api.dart'; import 'dart:async'; import 'package:mymuseum_visitapp/Helpers/translationHelper.dart'; import 'package:mymuseum_visitapp/Models/ResponseSubDTO.dart'; import 'package:mymuseum_visitapp/Models/visitContext.dart'; import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_step_timer.dart'; import 'package:mymuseum_visitapp/Screens/Sections/Quiz/questions_list.dart'; /// Vue de progression centrée contenu (escape game, ou parcours sans carte). class GuidedPathContentProgressionPage extends StatefulWidget { final GuidedPathDTO path; final VisitAppContext visitAppContext; const GuidedPathContentProgressionPage({ Key? key, required this.path, required this.visitAppContext, }) : super(key: key); @override State createState() => _GuidedPathContentProgressionPageState(); } class _GuidedPathContentProgressionPageState extends State { late List _steps; int _currentStepIndex = 0; final Set _completedStepIds = {}; List _stepQuestions = []; bool _quizCompleted = false; bool _quizPassed = false; bool _inGeoZone = false; StreamSubscription? _positionSub; @override void initState() { super.initState(); _steps = [...(widget.path.steps ?? [])] ..sort((a, b) => (a.order ?? 0).compareTo(b.order ?? 0)); _initStepState(); _startLocationTracking(); } @override void dispose() { _positionSub?.cancel(); super.dispose(); } // ─── State ──────────────────────────────────────────────────────────────── void _initStepState() { final step = _currentStep; if (step == null) return; _quizCompleted = false; _quizPassed = false; _inGeoZone = false; if (step.quizQuestions?.isNotEmpty == true) { _stepQuestions = step.quizQuestions! .map((q) => QuestionSubDTO( chosen: null, label: q.label, responsesSubDTO: ResponseSubDTO().fromJSON(q.responses), resourceId: q.resourceId, resourceUrl: q.resource?.url, order: q.order, )) .toList(); } else { _stepQuestions = []; } } GuidedStepDTO? get _currentStep => _steps.isEmpty ? null : _steps[_currentStepIndex]; String _translate(List? list) => TranslationHelper.get(list, widget.visitAppContext); bool _hasGeoTrigger(GuidedStepDTO step) => (step.geometry?.type == 'Point') || (step.triggerGeoPointId != null && step.triggerGeoPoint != null); bool get _canAdvance { final step = _currentStep; if (step == null || step.isStepLocked == true) return false; if (_hasGeoTrigger(step) && !_inGeoZone) return false; if ((widget.path.requireSuccessToAdvance ?? false) && step.quizQuestions?.isNotEmpty == true && !_quizPassed) return false; return true; } void _advance() { final step = _currentStep; if (step == null || !_canAdvance) return; if (step.id != null) _completedStepIds.add(step.id!); if (_currentStepIndex < _steps.length - 1) { setState(() { _currentStepIndex++; _initStepState(); }); } else { _showCompletionDialog(); } } void _goBack() { if (_currentStepIndex > 0 && !(widget.path.isLinear ?? false)) { setState(() { _currentStepIndex--; _initStepState(); }); } } void _showCompletionDialog() { showDialog( context: context, builder: (_) => AlertDialog( title: const Text('Parcours terminé !'), content: const Text('Vous avez complété toutes les étapes.'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); Navigator.of(context).pop(); }, child: const Text('Fermer'), ), ], ), ); } // ─── Géoloc ─────────────────────────────────────────────────────────────── Future _startLocationTracking() async { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) return; LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) return; } if (permission == LocationPermission.deniedForever) return; _positionSub = Geolocator.getPositionStream( locationSettings: const LocationSettings(accuracy: LocationAccuracy.high, distanceFilter: 5), ).listen((pos) => _checkGeoZone(LatLng(pos.latitude, pos.longitude))); } void _checkGeoZone(LatLng position) { final step = _currentStep; if (step == null || !_hasGeoTrigger(step)) return; LatLng? center; double radius = step.zoneRadiusMeters ?? 30; if (step.geometry?.type == 'Point') { final c = step.geometry!.coordinates as List; center = LatLng((c[1] as num).toDouble(), (c[0] as num).toDouble()); } else if (step.triggerGeoPoint?.geometry?.type == 'Point') { final c = step.triggerGeoPoint!.geometry!.coordinates as List; center = LatLng((c[1] as num).toDouble(), (c[0] as num).toDouble()); } if (center == null) return; final dist = const Distance().distance(position, center); if ((dist <= radius) != _inGeoZone) { setState(() => _inGeoZone = dist <= radius); } } // ─── Build ──────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { final step = _currentStep; if (step == null) return const Scaffold(body: Center(child: Text('Aucune étape'))); final title = _translate(step.title); final desc = _translate(step.description); final hasQuiz = step.quizQuestions?.isNotEmpty == true; final hasTimer = step.isStepTimer == true && (step.timerSeconds ?? 0) > 0; final hasGeo = _hasGeoTrigger(step); final isLocked = step.isStepLocked == true; return Scaffold( body: SafeArea( child: Column( children: [ // ── Header ────────────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(8, 8, 16, 0), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.of(context).pop(), ), Expanded( child: Text( title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), // Progress indicator Text( 'Étape ${_currentStepIndex + 1} / ${_steps.length}', style: TextStyle(color: Colors.grey[600], fontSize: 13), ), ], ), ), // Dot progress Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(_steps.length, (i) { final done = _completedStepIds.contains(_steps[i].id); return Padding( padding: const EdgeInsets.symmetric(horizontal: 3), child: Container( width: 8, height: 8, decoration: BoxDecoration( shape: BoxShape.circle, color: done ? Colors.green : i == _currentStepIndex ? Colors.blue : Colors.grey[300], ), ), ); }), ), ), // ── Contenu scrollable ─────────────────────────────────────── Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Image if (step.imageUrl != null) ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.network( step.imageUrl!, height: 200, width: double.infinity, fit: BoxFit.cover, ), ), if (step.imageUrl != null) const SizedBox(height: 16), // Locked if (isLocked) Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Row( children: [ const Icon(Icons.lock, color: Colors.grey), const SizedBox(width: 8), Text('Étape verrouillée', style: TextStyle(color: Colors.grey[600])), ], ), ), // Description if (desc.isNotEmpty && !isLocked) Text(desc, style: const TextStyle(fontSize: 15, height: 1.6)), if (desc.isNotEmpty) const SizedBox(height: 16), // Timer (barre) if (hasTimer && !isLocked) GuidedStepTimer( key: ValueKey(step.id), seconds: step.timerSeconds!, expiredMessage: _translate(step.timerExpiredMessage), showAsBar: true, onExpired: () => setState(() {}), ), if (hasTimer) const SizedBox(height: 16), // Géo if (hasGeo && !isLocked) Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: _inGeoZone ? Colors.green.withOpacity(0.1) : Colors.orange.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all( color: _inGeoZone ? Colors.green : Colors.orange), ), child: Row( children: [ Icon( _inGeoZone ? Icons.location_on : Icons.location_searching, color: _inGeoZone ? Colors.green : Colors.orange, size: 18, ), const SizedBox(width: 8), Text( _inGeoZone ? 'Vous êtes dans la zone ✓' : 'Approchez-vous du point indiqué', style: TextStyle( color: _inGeoZone ? Colors.green[700] : Colors.orange[700], fontSize: 13, ), ), ], ), ), if (hasGeo) const SizedBox(height: 16), // Quiz if (hasQuiz && !isLocked && !_quizCompleted) SizedBox( height: 340, child: QuestionsListWidget( questionsSubDTO: _stepQuestions, isShowResponse: false, onShowResponse: () { final good = _stepQuestions .where((q) => q.chosen != null && q.chosen == q.responsesSubDTO! .indexWhere((r) => r.isGood == true)) .length; setState(() { _quizCompleted = true; _quizPassed = good == _stepQuestions.length; }); }, orientation: MediaQuery.of(context).orientation, ), ), if (hasQuiz && _quizCompleted) Row( children: [ Icon( _quizPassed ? Icons.check_circle : Icons.cancel, color: _quizPassed ? Colors.green : Colors.red, ), const SizedBox(width: 8), Text( _quizPassed ? 'Quiz réussi !' : 'Quiz échoué', style: TextStyle( color: _quizPassed ? Colors.green : Colors.red, fontWeight: FontWeight.w500, ), ), if (!_quizPassed) ...[ const Spacer(), TextButton( onPressed: () => setState(() { _quizCompleted = false; _quizPassed = false; _stepQuestions = _currentStep!.quizQuestions! .map((q) => QuestionSubDTO( chosen: null, label: q.label, responsesSubDTO: ResponseSubDTO().fromJSON(q.responses), resourceId: q.resourceId, resourceUrl: q.resource?.url, order: q.order, )) .toList(); }), child: const Text('Réessayer'), ), ], ], ), ], ), ), ), // ── Navigation ─────────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Row( children: [ if (_currentStepIndex > 0 && !(widget.path.isLinear ?? false)) OutlinedButton.icon( onPressed: _goBack, icon: const Icon(Icons.arrow_back, size: 16), label: const Text('Précédent'), ), const Spacer(), FilledButton.icon( onPressed: _canAdvance ? _advance : null, icon: Icon( _currentStepIndex < _steps.length - 1 ? Icons.arrow_forward : Icons.flag, size: 16, ), label: Text( _currentStepIndex < _steps.length - 1 ? 'Suivant' : 'Terminer', ), ), ], ), ), ], ), ), ); } }