diff --git a/lib/core/config/api_config.dart b/lib/core/config/api_config.dart index aa65fe4..07756dc 100644 --- a/lib/core/config/api_config.dart +++ b/lib/core/config/api_config.dart @@ -55,4 +55,10 @@ class ApiConfig { /// Body: {'Discount': discountId, 'User': userId} /// Headers: {'Authorization': 'Bearer '} static const String addOrder = '$baseUrl/order/add'; + + // ========== Reservation Endpoints ========== + /// Endpoint to get reservations. + /// Method: GET + /// Headers: {'Authorization': 'Bearer '} + static const String getReservations = '$baseUrl/reservation/get'; } \ No newline at end of file diff --git a/lib/domain/entities/reservation_entity.dart b/lib/domain/entities/reservation_entity.dart new file mode 100644 index 0000000..128084f --- /dev/null +++ b/lib/domain/entities/reservation_entity.dart @@ -0,0 +1,76 @@ +class ReservationEntity { + final String id; + final String shop; + final ReservationDiscount discount; + final String user; + final bool status; + + ReservationEntity({ + required this.id, + required this.shop, + required this.discount, + required this.user, + required this.status, + }); + + factory ReservationEntity.fromJson(Map json) { + return ReservationEntity( + id: json['ID'] ?? '', + shop: json['Shop'] ?? '', + discount: ReservationDiscount.fromJson(json['Discount'] ?? {}), + user: json['User'] ?? '', + status: json['Status'] ?? false, + ); + } +} + +class ReservationDiscount { + final String id; + final String name; + final String typeName; + final DateTime? endDate; + final List images; + + + ReservationDiscount({ + required this.id, + required this.name, + this.endDate, + required this.images, + required this.typeName, + }); + + factory ReservationDiscount.fromJson(Map json) { + List _parseImages(dynamic imageList) { + if (imageList is List) { + return imageList.map((img) => ReservationImage.fromJson(img)).toList(); + } + return []; + } + + return ReservationDiscount( + id: json['ID'] ?? '', + name: json['Name'] ?? 'بدون نام', + endDate: json['EndDate'] != null ? DateTime.tryParse(json['EndDate']) : null, + images: _parseImages(json['Images']), + typeName: json['Type']?['Name'] ?? 'نامشخص', + ); + } +} + +class ReservationImage { + final String id; + final String url; + + ReservationImage({ + required this.id, + required this.url, + }); + + factory ReservationImage.fromJson(Map json) { + return ReservationImage( + id: json['_id'] ?? '', + url: json['Url'] ?? '', + ); + } +} diff --git a/lib/presentation/pages/barcode_scanner_page.dart b/lib/presentation/pages/barcode_scanner_page.dart index 78543ae..5adc1a2 100644 --- a/lib/presentation/pages/barcode_scanner_page.dart +++ b/lib/presentation/pages/barcode_scanner_page.dart @@ -79,7 +79,6 @@ class _BarcodeScannerPageState extends State { if (await Vibration.hasVibrator() ?? false) { Vibration.vibrate(duration: 200); } - // Ensure you have a success sound file at this path await _audioPlayer.play(AssetSource('sounds/short-success-sound-glockenspiel-treasure-video-game-6346.mp3')); await showSuccessDialog(context, message: state.message); Navigator.of(context).pop(); diff --git a/lib/presentation/pages/reserve_manegment_page.dart b/lib/presentation/pages/reserve_manegment_page.dart index 06f2fe3..9e57098 100644 --- a/lib/presentation/pages/reserve_manegment_page.dart +++ b/lib/presentation/pages/reserve_manegment_page.dart @@ -1,12 +1,31 @@ -import 'dart:async'; // اضافه کردن کتابخانه برای استفاده از Timer -import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; +import 'dart:async'; +import 'package:business_panel/presentation/reservation/bloc/reservation_bloc.dart'; import 'package:business_panel/presentation/widgets/active_discount_card.dart'; import 'package:business_panel/presentation/widgets/custom_app_bar_single.dart'; +import 'package:business_panel/domain/entities/discount_entity.dart'; +import 'package:business_panel/domain/entities/reservation_entity.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:business_panel/gen/assets.gen.dart'; +extension ReservationToDiscount on ReservationEntity { + DiscountEntity toDiscountEntity() { + return DiscountEntity( + id: id, + name: discount.name, + shopName: 'فروشگاه', + images: discount.images.map((img) => img.url).toList(), + type: discount.typeName, + description: 'رزرو شده', + price: 0.0, + nPrice: 0.0, + startDate: null, + endDate: discount.endDate, + ); + } +} + class ReserveManegmment extends StatefulWidget { const ReserveManegmment({super.key}); @@ -15,23 +34,35 @@ class ReserveManegmment extends StatefulWidget { } class _ReserveManegmmentState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ReservationBloc()..add(FetchReservations()), + child: const _ReserveManegmmentView(), + ); + } +} + +class _ReserveManegmmentView extends StatefulWidget { + const _ReserveManegmmentView(); + + @override + State<_ReserveManegmmentView> createState() => _ReserveManegmmentViewState(); +} + +class _ReserveManegmmentViewState extends State<_ReserveManegmmentView> { final TextEditingController _searchController = TextEditingController(); Timer? _debounce; @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - context.read().add(FetchDiscounts()); - } - }); } @override void dispose() { _searchController.dispose(); - _debounce?.cancel(); // کنسل کردن تایمر برای جلوگیری از نشت حافظه + _debounce?.cancel(); super.dispose(); } @@ -39,7 +70,7 @@ class _ReserveManegmmentState extends State { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 500), () { if (mounted) { - context.read().add(SearchDiscounts(query: query)); + context.read().add(SearchReservations(query: query)); } }); } @@ -73,30 +104,31 @@ class _ReserveManegmmentState extends State { ), contentPadding: const EdgeInsets.symmetric(vertical: 0), ), - onChanged: _onSearchChanged, // استفاده از متد جدید + onChanged: _onSearchChanged, ), ), Expanded( - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { - if (state is HomeError) { + if (state is ReservationError) { return Center(child: Text('خطا: ${state.message}')); } - if (state is HomeLoaded) { - if (state.discounts.isEmpty) { - return const Center(child: Text("هیچ تخفیفی با این مشخصات یافت نشد.")); + if (state is ReservationLoaded) { + if (state.reservations.isEmpty) { + return const Center(child: Text("هیچ رزروی با این مشخصات یافت نشد.")); } return RefreshIndicator( onRefresh: () async { - context.read().add(FetchDiscounts()); + context.read().add(FetchReservations()); _searchController.clear(); }, child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: state.discounts.length, + itemCount: state.reservations.length, itemBuilder: (context, index) { - final discount = state.discounts[index]; + final reservation = state.reservations[index]; + final discount = reservation.toDiscountEntity(); return ActiveDiscountCard(discount: discount); }, ), diff --git a/lib/presentation/reservation/bloc/reservation_bloc.dart b/lib/presentation/reservation/bloc/reservation_bloc.dart new file mode 100644 index 0000000..e3d689a --- /dev/null +++ b/lib/presentation/reservation/bloc/reservation_bloc.dart @@ -0,0 +1,75 @@ +import 'package:bloc/bloc.dart'; +import 'package:business_panel/core/config/api_config.dart'; +import 'package:business_panel/core/services/token_storage_service.dart'; +import 'package:business_panel/core/utils/logging_interceptor.dart'; +import 'package:business_panel/domain/entities/reservation_entity.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + +part 'reservation_event.dart'; +part 'reservation_state.dart'; + +class ReservationBloc extends Bloc { + final Dio _dio = Dio(); + final TokenStorageService _tokenStorage = TokenStorageService(); + + ReservationBloc() : super(ReservationInitial()) { + _dio.interceptors.add(LoggingInterceptor()); + + on((event, emit) async { + await _fetchReservations(emit); + }); + + on((event, emit) async { + await _fetchReservations(emit, searchQuery: event.query); + }); + } + + Future _fetchReservations(Emitter emit, {String? searchQuery}) async { + emit(ReservationLoading()); + try { + final token = await _tokenStorage.getAccessToken(); + if (token == null || token.isEmpty) { + emit(ReservationError("خطای احراز هویت. لطفا دوباره وارد شوید.")); + return; + } + + String url = ApiConfig.getReservations; + if (searchQuery != null && searchQuery.isNotEmpty) { + url = '$url?search=$searchQuery'; + } + + final response = await _dio.get( + url, + options: Options( + headers: {'Authorization': 'Bearer $token'}, + ), + ); + + if (response.statusCode == 200 && response.data['data'] != null) { + final List data = response.data['data']['items'] ?? []; + final reservations = data + .map((json) => ReservationEntity.fromJson(json)) + .where((reservation) { + if (reservation.discount.endDate == null) return true; + return reservation.discount.endDate!.isAfter(DateTime.now()); + }) + .toList(); + emit(ReservationLoaded(reservations)); + } else { + emit(ReservationError(response.data['message'] ?? 'خطا در دریافت اطلاعات')); + } + } on DioException catch (e) { + if (kDebugMode) { + print('DioException in ReservationBloc: ${e.response?.data}'); + } + emit(ReservationError(e.response?.data['message'] ?? 'خطای شبکه')); + } catch (e, stackTrace) { + if (kDebugMode) { + print('Error in ReservationBloc: $e'); + print(stackTrace); + } + emit(ReservationError('خطای پیش‌بینی نشده رخ داد: ${e.toString()}')); + } + } +} diff --git a/lib/presentation/reservation/bloc/reservation_event.dart b/lib/presentation/reservation/bloc/reservation_event.dart new file mode 100644 index 0000000..fd82a80 --- /dev/null +++ b/lib/presentation/reservation/bloc/reservation_event.dart @@ -0,0 +1,11 @@ +part of 'reservation_bloc.dart'; + +abstract class ReservationEvent {} + +class FetchReservations extends ReservationEvent {} + +class SearchReservations extends ReservationEvent { + final String query; + + SearchReservations({required this.query}); +} diff --git a/lib/presentation/reservation/bloc/reservation_state.dart b/lib/presentation/reservation/bloc/reservation_state.dart new file mode 100644 index 0000000..bb9cff0 --- /dev/null +++ b/lib/presentation/reservation/bloc/reservation_state.dart @@ -0,0 +1,19 @@ +part of 'reservation_bloc.dart'; + +abstract class ReservationState {} + +class ReservationInitial extends ReservationState {} + +class ReservationLoading extends ReservationState {} + +class ReservationLoaded extends ReservationState { + final List reservations; + + ReservationLoaded(this.reservations); +} + +class ReservationError extends ReservationState { + final String message; + + ReservationError(this.message); +} diff --git a/lib/presentation/widgets/active_discount_card.dart b/lib/presentation/widgets/active_discount_card.dart index 32ad109..6a87be5 100644 --- a/lib/presentation/widgets/active_discount_card.dart +++ b/lib/presentation/widgets/active_discount_card.dart @@ -148,7 +148,7 @@ class ActiveDiscountCard extends StatelessWidget { ], ), const SizedBox(height: 16), - InkWell( // <-- ویجت را در InkWell قرار دهید + InkWell( onTap: () { Navigator.of(context).push( MaterialPageRoute( diff --git a/lib/presentation/widgets/analytics_discount_card.dart b/lib/presentation/widgets/analytics_discount_card.dart index 9212616..6f3a19c 100644 --- a/lib/presentation/widgets/analytics_discount_card.dart +++ b/lib/presentation/widgets/analytics_discount_card.dart @@ -1,5 +1,3 @@ -// lib/presentation/widgets/analytics_discount_card.dart - import 'package:business_panel/core/config/app_colors.dart'; import 'package:business_panel/domain/entities/discount_entity.dart'; import 'package:business_panel/gen/assets.gen.dart'; @@ -99,7 +97,7 @@ class AnalyticsDiscountCard extends StatelessWidget { textColor: Colors.grey.shade600), const SizedBox(height: 5), if (discount.endDate == null) - _buildStatusText('تاریخ نامعتبر', Colors.orange) + _buildStatusText('تاریخ نامعتبر', Colors.orange) else if (remaining.isNegative) _buildExpiredDateRange() else @@ -228,7 +226,7 @@ class AnalyticsDiscountCard extends StatelessWidget { if (value == true) { final bloc = context.read(); bloc.add( - FetchManagedDiscounts(status: 1)); // Defaulting to active + FetchManagedDiscounts(status: 1)); } }); },