// lib/presentation/pages/offers_page.dart 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_background_service/flutter_background_service.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: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? _connectivitySubscription; bool _isGpsEnabled = false; bool _isConnectedToInternet = true; @override void initState() { super.initState(); _checkInitialConnectivity(); _initializePage(); _initConnectivityListener(); _fetchInitialReservations(); } @override void dispose() { _locationServiceSubscription?.cancel(); _connectivitySubscription?.cancel(); super.dispose(); } Future _checkInitialConnectivity() async { final connectivityResult = await Connectivity().checkConnectivity(); if (!mounted) return; setState(() { _isConnectedToInternet = !connectivityResult.contains(ConnectivityResult.none); }); } Future _initializePage() async { if (widget.showDialogsOnLoad) { WidgetsBinding.instance.addPostFrameCallback((_) async { if (mounted) { await showNotificationPermissionDialog(context); await showGPSDialog(context); } }); } await _loadPreferences(); _initLocationListener(); _listenToBackgroundService(); } void _listenToBackgroundService() { FlutterBackgroundService().on('update').listen((event) { if (event == null || event['offers'] == null) return; final data = event['offers']['data']; if (data == null || data is! List) { if (mounted) { context.read().add(const OffersReceivedFromMqtt([])); } return; } try { List offers = data .whereType>() .map((json) => OfferModel.fromJson(json)) .toList(); if (mounted) { context.read().add(OffersReceivedFromMqtt(offers)); } } catch (e, stackTrace) { debugPrint("❌ Error parsing offers from Background Service: $e"); debugPrint(stackTrace.toString()); } }); } void _initConnectivityListener() { _connectivitySubscription = Connectivity().onConnectivityChanged.listen((results) { final hasConnection = !results.contains(ConnectivityResult.none); if (mounted && _isConnectedToInternet != hasConnection) { setState(() { _isConnectedToInternet = hasConnection; }); if (hasConnection) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('اتصال به اینترنت برقرار شد.'), backgroundColor: Colors.green, ), ); } else { context.read().add(ClearOffers()); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('اتصال به اینترنت قطع شد.'), backgroundColor: Colors.red, ), ); } } }); } Future _fetchInitialReservations() async { if (!_isConnectedToInternet) return; 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"); } } void _initLocationListener() { _checkInitialGpsStatus(); _locationServiceSubscription = Geolocator.getServiceStatusStream().listen((status) { final isEnabled = status == ServiceStatus.enabled; if (mounted && _isGpsEnabled != isEnabled) { setState(() { _isGpsEnabled = isEnabled; }); if (!isEnabled) { context.read().add(ClearOffers()); } } }); } Future _checkInitialGpsStatus() async { final status = await Geolocator.isLocationServiceEnabled(); if (mounted) { setState(() { _isGpsEnabled = status; }); } } 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, isConnectedToInternet: _isConnectedToInternet, ), ], ), ), ), ); } } class OffersView extends StatelessWidget { final bool isGpsEnabled; final bool isConnectedToInternet; const OffersView({ super.key, required this.isGpsEnabled, required this.isConnectedToInternet, }); @override Widget build(BuildContext context) { if (!isConnectedToInternet) { return _buildNoInternetUI(context); } if (!isGpsEnabled) { return _buildGpsActivationUI(context); } return BlocBuilder( builder: (context, state) { 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('جست‌وجوی تصادفی'), ], ), ), ), ); } Widget _buildNoInternetUI(BuildContext context) { return SizedBox( height: 300, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.wifi_off_rounded, size: 80, color: Colors.grey[400]), const SizedBox(height: 20), const Text( "اتصال به اینترنت برقرار نیست", style: TextStyle(fontSize: 18, color: Colors.grey), ), const SizedBox(height: 10), const Text( "لطفاً اتصال خود را بررسی کرده و دوباره تلاش کنید.", style: TextStyle(color: Colors.grey), ), ], ), ), ); } }