import 'dart:async'; import 'package:collection/collection.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/svg.dart'; import 'package:geolocator/geolocator.dart'; import 'package:proxibuy/core/config/api_config.dart'; import 'package:proxibuy/core/config/app_colors.dart'; import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/data/models/offer_model.dart'; import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart'; import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_event.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_event.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_state.dart'; import 'package:proxibuy/presentation/offer/bloc/widgets/category_offers_row.dart'; import 'package:proxibuy/presentation/pages/notification_preferences_page.dart'; import 'package:proxibuy/presentation/pages/reserved_list_page.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import 'package:proxibuy/presentation/widgets/gps_dialog.dart'; import 'package:proxibuy/presentation/widgets/notification_permission_dialog.dart'; import 'package:proxibuy/services/mqtt_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; class OffersPage extends StatefulWidget { final bool showDialogsOnLoad; const OffersPage({super.key, this.showDialogsOnLoad = false}); @override State createState() => _OffersPageState(); } class _OffersPageState extends State { List _selectedCategories = []; StreamSubscription? _locationServiceSubscription; StreamSubscription? _mqttMessageSubscription; StreamSubscription? _connectivitySubscription; Timer? _locationTimer; bool _isSubscribedToOffers = false; bool _isGpsEnabled = false; @override void initState() { super.initState(); _initializePage(); _initConnectivityListener(); _fetchInitialReservations(); } Future _fetchInitialReservations() async { try { const storage = FlutterSecureStorage(); final token = await storage.read(key: 'accessToken'); if (token == null) return; final dio = Dio(); final response = await dio.get( ApiConfig.baseUrl + ApiConfig.getReservations, options: Options(headers: {'Authorization': 'Bearer $token'}), ); if (response.statusCode == 200 && mounted) { final List reserves = response.data['reserves']; final List reservedIds = reserves .map( (reserveData) => (reserveData['Discount']['ID'] as String?) ?? '', ) .where((id) => id.isNotEmpty) .toList(); context.read().setReservedIds(reservedIds); } } catch (e) { debugPrint("Error fetching initial reservations: $e"); } } Future _initializePage() async { WidgetsBinding.instance.addPostFrameCallback((_) async { if (mounted) { await showNotificationPermissionDialog(context); await showGPSDialog(context); } }); await _loadPreferences(); _checkAndConnectMqtt(); _initLocationListener(); } void _initConnectivityListener() { _connectivitySubscription = Connectivity().onConnectivityChanged.listen(( List results, ) { if (!results.contains(ConnectivityResult.none)) { print("ℹ️ Network connection restored."); _checkAndConnectMqtt(); } else { print("ℹ️ Network connection lost."); } }); } Future _checkAndConnectMqtt() async { final mqttService = context.read(); if (!mqttService.isConnected) { print("--- OffersPage: MQTT not connected. Attempting to connect..."); final storage = const FlutterSecureStorage(); final token = await storage.read(key: 'accessToken'); if (token != null && token.isNotEmpty) { try { await mqttService.connect(token); if (mqttService.isConnected && mounted) { _subscribeToUserOffersOnLoad(); } } catch (e) { print("❌ OffersPage: Error connecting to MQTT: $e"); } } } else { print("--- OffersPage: MQTT already connected."); _subscribeToUserOffersOnLoad(); } } @override void dispose() { _locationServiceSubscription?.cancel(); _mqttMessageSubscription?.cancel(); _locationTimer?.cancel(); _connectivitySubscription?.cancel(); super.dispose(); } Future _subscribeToUserOffersOnLoad() async { final storage = const FlutterSecureStorage(); final userID = await storage.read(key: 'userID'); if (userID != null && mounted) { _subscribeToUserOffers(userID); } } void _initLocationListener() { _checkInitialGpsStatus(); _locationServiceSubscription = Geolocator.getServiceStatusStream().listen(( status, ) { final isEnabled = status == ServiceStatus.enabled; if (mounted && _isGpsEnabled != isEnabled) { setState(() { _isGpsEnabled = isEnabled; }); } if (isEnabled) { _startSendingLocationUpdates(); } else { print("❌ Location Service Disabled. Stopping updates."); _locationTimer?.cancel(); context.read().add(ClearOffers()); } }); } Future _checkInitialGpsStatus() async { final status = await Geolocator.isLocationServiceEnabled(); if (mounted) { setState(() { _isGpsEnabled = status; }); if (_isGpsEnabled) { _startSendingLocationUpdates(); } } } void _startSendingLocationUpdates() { print("🚀 Starting periodic location updates."); _locationTimer?.cancel(); _locationTimer = Timer.periodic(const Duration(seconds: 30), (timer) { _sendLocationUpdate(); }); _sendLocationUpdate(); } Future _sendLocationUpdate() async { final mqttService = context.read(); if (!mqttService.isConnected) { print("⚠️ MQTT not connected in OffersPage. Cannot send location."); return; } try { LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { print("🚫 Location permission denied by user."); return; } final position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, ); const storage = FlutterSecureStorage(); final userID = await storage.read(key: 'userID'); if (userID == null) { print("⚠️ UserID not found. Cannot send location."); return; } final payload = {"userID": userID, "lat": 32.6685, "lng": 51.6826}; mqttService.publish("proxybuy/sendGps", payload); } catch (e) { print("❌ Error sending location update in OffersPage: $e"); } } void _subscribeToUserOffers(String userID) { if (_isSubscribedToOffers) return; final mqttService = context.read(); final topic = 'user-proxybuy/$userID'; mqttService.subscribe(topic); _isSubscribedToOffers = true; _mqttMessageSubscription = mqttService.messages.listen((message) { final data = message['data']; if (data == null) { if (mounted) { context.read().add(const OffersReceivedFromMqtt([])); } return; } if (data is List) { try { List offers = data .whereType>() .map((json) => OfferModel.fromJson(json)) .toList(); if (mounted) { context.read().add(OffersReceivedFromMqtt(offers)); } } catch (e, stackTrace) { print("❌ Error parsing offers from MQTT: $e"); print(stackTrace); } } }); } Future _loadPreferences() async { final prefs = await SharedPreferences.getInstance(); final savedCategories = prefs.getStringList('user_selected_categories') ?? []; if (mounted) { setState(() { _selectedCategories = savedCategories; }); } } Widget _buildFavoriteCategoriesSection() { return Padding( padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'دسته‌بندی‌های مورد علاقه شما', style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold), ), TextButton( onPressed: () async { await Navigator.of(context).push( MaterialPageRoute( builder: (context) => const NotificationPreferencesPage( loadFavoritesOnStart: true, ), ), ); if (!mounted) return; context .read() .add(ResetSubmissionStatus()); _loadPreferences(); }, child: Row( children: [ SvgPicture.asset(Assets.icons.edit.path), const SizedBox(width: 4), const Text( 'ویرایش', style: TextStyle(color: AppColors.active), ), ], ), ), ], ), const Divider(height: 1), const SizedBox(height: 12), if (_selectedCategories.isEmpty) const Padding( padding: EdgeInsets.only(bottom: 8.0), child: Text( 'شما هنوز دسته‌بندی مورد علاقه خود را انتخاب نکرده‌اید.', style: TextStyle(color: Colors.grey), ), ) else Wrap( spacing: 8.0, runSpacing: 8.0, children: _selectedCategories.map((category) { return Container( padding: const EdgeInsets.symmetric( horizontal: 12.0, vertical: 6.0, ), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(20.0), ), child: Text(category), ); }).toList(), ), ], ), ); } @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.rtl, child: Scaffold( appBar: AppBar( backgroundColor: Colors.white, automaticallyImplyLeading: false, title: Padding( padding: const EdgeInsets.symmetric( horizontal: 15.0, vertical: 0.0, ), child: Assets.icons.logoWithName.svg(height: 40, width: 200), ), actions: [ IconButton(onPressed: () {}, icon: Assets.icons.notification.svg()), BlocBuilder( builder: (context, state) { final reservedCount = state.reservedProductIds.length; return Stack( alignment: Alignment.center, children: [ IconButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => const ReservedListPage(), ), ); }, icon: Assets.icons.scanBarcode.svg(), ), if (reservedCount > 0) Positioned( top: 0, right: 2, child: GestureDetector( onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => const ReservedListPage(), ), ); }, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.green, shape: BoxShape.circle, border: Border.all( color: Colors.white, width: 1.5, ), ), constraints: const BoxConstraints( minWidth: 18, minHeight: 18, ), child: Padding( padding: const EdgeInsets.fromLTRB(2, 4, 2, 2), child: Text( '$reservedCount', style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), ), ), ), ], ); }, ), const SizedBox(width: 8), ], ), body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildFavoriteCategoriesSection(), OffersView(isGpsEnabled: _isGpsEnabled), ], ), ), ), ); } } class OffersView extends StatelessWidget { final bool isGpsEnabled; const OffersView({super.key, required this.isGpsEnabled}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { if (!isGpsEnabled) { return _buildGpsActivationUI(context); } if (state is OffersInitial) { return const SizedBox( height: 300, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: 20), Text("در حال یافتن بهترین پیشنهادها برای شما..."), ], ), ), ); } if (state is OffersLoadSuccess) { if (state.offers.isEmpty) { return const SizedBox( height: 300, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text("فعلاً تخفیفی در این اطراف نیست!"), Text("کمی قدم بزنید..."), ], ), ), ); } final groupedOffers = groupBy( state.offers, (OfferModel offer) => offer.category, ); final categories = groupedOffers.keys.toList(); return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.only(top: 16), itemCount: categories.length, itemBuilder: (context, index) { final category = categories[index]; final offersForCategory = groupedOffers[category]!; return CategoryOffersRow( categoryTitle: category, offers: offersForCategory, ) .animate() .fade(duration: 500.ms) .slideY(begin: 0.3, duration: 400.ms, curve: Curves.easeOut); }, ); } if (state is OffersLoadFailure) { return SizedBox( height: 200, child: Center(child: Text("خطا در بارگذاری: ${state.error}")), ); } return const SizedBox.shrink(); }, ); } Widget _buildGpsActivationUI(BuildContext context) { return Center( child: SizedBox( child: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 85), SvgPicture.asset(Assets.images.emptyHome.path), const SizedBox(height: 60), ElevatedButton( onPressed: () async { await Geolocator.openLocationSettings(); }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.confirm, foregroundColor: Colors.white, disabledBackgroundColor: Colors.grey, padding: const EdgeInsets.symmetric( vertical: 12, horizontal: 125, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50), ), ), child: const Text( 'فعال‌سازی GPS', style: TextStyle( fontFamily: 'Dana', fontSize: 16, fontWeight: FontWeight.normal, ), ), ), const SizedBox(height: 15), const Text('جست‌وجوی تصادفی'), ], ), ), ), ); } }