diff --git a/assets/icons/empty_home.svg b/assets/icons/empty_home.svg new file mode 100644 index 0000000..3aa4aa6 --- /dev/null +++ b/assets/icons/empty_home.svg @@ -0,0 +1,307 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ri_search-2-line.svg b/assets/icons/ri_search-2-line.svg new file mode 100644 index 0000000..1b1471e --- /dev/null +++ b/assets/icons/ri_search-2-line.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/config/api_config.dart b/lib/core/config/api_config.dart index 775a665..13b2fc7 100644 --- a/lib/core/config/api_config.dart +++ b/lib/core/config/api_config.dart @@ -1,3 +1,5 @@ +// lib/core/config/api_config.dart + class ApiConfig { // Private constructor to prevent instantiation ApiConfig._(); @@ -35,4 +37,16 @@ class ApiConfig { /// Headers: {'Authorization': 'Bearer '} static const String addDiscount = '$baseUrl/discount/add'; static const String getDiscounts = '$baseUrl/discount/get'; -} + static const String getActiveDiscounts = '$baseUrl/discount/get?status=1'; + + /// Endpoint to get a single discount by its ID. + /// Method: GET + /// Headers: {'Authorization': 'Bearer '} + static String getDiscountById(String id) => '$baseUrl/discount/get/$id'; + + /// Endpoint to edit an existing discount. + /// Method: POST + /// Body: FormData + /// Headers: {'Authorization': 'Bearer '} + static String editDiscount(String id) => '$baseUrl/discount/edit/$id'; +} \ No newline at end of file diff --git a/lib/domain/entities/discount_entity.dart b/lib/domain/entities/discount_entity.dart index 621331c..42ef9db 100644 --- a/lib/domain/entities/discount_entity.dart +++ b/lib/domain/entities/discount_entity.dart @@ -1,3 +1,5 @@ +// lib/domain/entities/discount_entity.dart + class DiscountEntity { final String id; final String name; @@ -29,19 +31,23 @@ class DiscountEntity { } return []; } - + return DiscountEntity( - id: json['_id'] ?? '', + // --- FIX IS HERE: Reading "ID" instead of "_id" --- + id: json['ID'] ?? '', // Changed from '_id' to 'ID' name: json['Name'] ?? 'بدون نام', // Safely access nested properties - shopName: (json['Shop'] != null ? json['Shop']['Name'] : 'فروشگاه') ?? 'فروشگاه', + shopName: + (json['Shop'] != null ? json['Shop']['Name'] : 'فروشگاه') ?? 'فروشگاه', images: _parseImages(json['Images']), - type: (json['Type'] != null ? json['Type']['Description'] : 'عمومی') ?? 'عمومی', + type: (json['Type'] != null ? json['Type']['Description'] : 'عمومی') ?? + 'عمومی', description: json['Description'] ?? '', price: (json['Price'] as num? ?? 0).toDouble(), nPrice: (json['NPrice'] as num? ?? 0).toDouble(), // Handle potential null or invalid date - endDate: json['EndDate'] != null ? DateTime.tryParse(json['EndDate']) : null, + endDate: + json['EndDate'] != null ? DateTime.tryParse(json['EndDate']) : null, ); } } \ No newline at end of file diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 90d88bc..0c3962e 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -129,6 +129,9 @@ class $AssetsIconsGen { /// File path: assets/icons/edit.svg String get edit => 'assets/icons/edit.svg'; + /// File path: assets/icons/empty_home.svg + String get emptyHome => 'assets/icons/empty_home.svg'; + /// File path: assets/icons/error.svg String get error => 'assets/icons/error.svg'; @@ -171,6 +174,9 @@ class $AssetsIconsGen { /// File path: assets/icons/resturan.svg String get resturan => 'assets/icons/resturan.svg'; + /// File path: assets/icons/ri_search-2-line.svg + String get riSearch2Line => 'assets/icons/ri_search-2-line.svg'; + /// File path: assets/icons/routing.svg String get routing => 'assets/icons/routing.svg'; @@ -266,6 +272,7 @@ class $AssetsIconsGen { documentText, edit02, edit, + emptyHome, error, fastfood, galleryAdd, @@ -280,6 +287,7 @@ class $AssetsIconsGen { radar2, receiptDisscount, resturan, + riSearch2Line, routing, scanBarcode, shop, diff --git a/lib/main.dart b/lib/main.dart index ecc251b..8dbd866 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,10 @@ import 'package:business_panel/core/config/app_colors.dart'; import 'package:business_panel/presentation/auth/bloc/auth_bloc.dart'; -import 'package:business_panel/presentation/pages/onboarding_page.dart'; +import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; import 'package:business_panel/presentation/pages/splash_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +// ignore: depend_on_referenced_packages import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:persian_datetime_picker/persian_datetime_picker.dart'; @@ -16,8 +17,13 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => AuthBloc(), + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => AuthBloc()), + BlocProvider( + create: (context) => HomeBloc()..add(FetchDiscounts()), + ), + ], child: MaterialApp( title: 'Proxibuy', debugShowCheckedModeBanner: false, @@ -29,9 +35,9 @@ class MyApp extends StatelessWidget { GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [ - Locale("fa", "IR"), - Locale("en", "US"), - ], + Locale("fa", "IR"), + Locale("en", "US"), + ], locale: const Locale("fa", "IR"), theme: ThemeData( fontFamily: 'Dana', @@ -104,5 +110,4 @@ class MyApp extends StatelessWidget { ), ); } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/lib/presentation/auth/bloc/auth_bloc.dart b/lib/presentation/auth/bloc/auth_bloc.dart index c9acc22..5bd65d1 100644 --- a/lib/presentation/auth/bloc/auth_bloc.dart +++ b/lib/presentation/auth/bloc/auth_bloc.dart @@ -1,5 +1,6 @@ +import 'dart:io'; // کتابخانه برای SocketException اضافه شد import 'package:bloc/bloc.dart'; -import 'package:business_panel/core/config/api_config.dart'; // <-- این خط را اضافه کنید +import 'package:business_panel/core/config/api_config.dart'; import 'package:business_panel/core/services/token_storage_service.dart'; import 'package:dio/dio.dart'; import 'package:business_panel/core/utils/logging_interceptor.dart'; @@ -29,18 +30,26 @@ class AuthBloc extends Bloc { return; } - // Use the ApiConfig class await _dio.get( - ApiConfig.checkShopStatus, // <-- تغییر در این خط + ApiConfig.checkShopStatus, options: Options( headers: {'Authorization': 'Bearer $token'}, ), ); - + emit(ShopExists()); } on DioException catch (e) { - if (e.response?.statusCode == 404) { + // --- منطق تشخیص آفلاین بودن بهبود یافت --- + final isOffline = e.error is SocketException || + e.type == DioExceptionType.connectionError || + e.type == DioExceptionType.sendTimeout || + e.type == DioExceptionType.receiveTimeout; + + if (isOffline) { + // اگر کاربر آفلاین باشد، این حالت صادر می‌شود + emit(AuthOffline()); + } else if (e.response?.statusCode == 404) { emit(NoShop()); } else { emit(AuthFailure(e.response?.data?['message'] ?? 'An error occurred while checking shop status.')); @@ -53,9 +62,8 @@ class AuthBloc extends Bloc { on((event, emit) async { emit(AuthLoading()); try { - // Use the ApiConfig class final response = await _dio.post( - ApiConfig.sendOtp, // <-- تغییر در این خط + ApiConfig.sendOtp, data: {'Phone': event.phoneNumber, 'Code': event.countryCode}, ); @@ -72,9 +80,8 @@ class AuthBloc extends Bloc { on((event, emit) async { emit(AuthLoading()); try { - // Use the ApiConfig class final response = await _dio.post( - ApiConfig.verifyOtp, // <-- تغییر در این خط + ApiConfig.verifyOtp, data: { 'Phone': event.phoneNumber, 'Code': event.countryCode, diff --git a/lib/presentation/auth/bloc/auth_state.dart b/lib/presentation/auth/bloc/auth_state.dart index 5e7e52a..46fea13 100644 --- a/lib/presentation/auth/bloc/auth_state.dart +++ b/lib/presentation/auth/bloc/auth_state.dart @@ -18,6 +18,10 @@ class ShopExists extends AuthState {} // ADDED: State for when the user is logged in but has no shop class NoShop extends AuthState {} +// *** CHANGE IS HERE: Added state for offline mode *** +// ADDED: State for when the user is authenticated but offline +class AuthOffline extends AuthState {} + class AuthCodeSentSuccess extends AuthState {} class AuthVerified extends AuthState {} diff --git a/lib/presentation/discount/bloc/discount_bloc.dart b/lib/presentation/discount/bloc/discount_bloc.dart index ba2c9cf..045464f 100644 --- a/lib/presentation/discount/bloc/discount_bloc.dart +++ b/lib/presentation/discount/bloc/discount_bloc.dart @@ -1,23 +1,114 @@ +// lib/presentation/discount/bloc/discount_bloc.dart + +import 'dart:developer'; 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:dio/dio.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'discount_event.dart'; import 'discount_state.dart'; +import 'dart:convert'; // Import for jsonEncode class DiscountBloc extends Bloc { + final Dio _dio = Dio(); + final TokenStorageService _tokenStorage = TokenStorageService(); + DiscountBloc() : super(const DiscountState()) { - - final Dio _dio = Dio(); - final TokenStorageService _tokenStorage = TokenStorageService(); + _dio.interceptors.add(LoggingInterceptor()); + + on((event, emit) async { + emit(state.copyWith(isLoadingDetails: true, errorMessage: null)); + try { + final token = await _tokenStorage.getAccessToken(); + if (token == null) { + emit( + state.copyWith( + isLoadingDetails: false, + errorMessage: "خطای احراز هویت.", + ), + ); + return; + } + + final response = await _dio.get( + ApiConfig.getDiscountById(event.discountId), + options: Options(headers: {'Authorization': 'Bearer $token'}), + ); + + if (response.statusCode == 200 && response.data['data'] != null) { + final data = response.data['data']; + + List> images = []; + if (data['Images'] is List) { + images = (data['Images'] as List) + .map>( + (img) => { + 'id': img['_id'] as String?, + 'url': img['Url'] as String?, + }, + ) + .where((img) => img['url'] != null) + .toList(); + } + + final name = data['Name'] ?? ''; + final typeId = data['Type']?['ID']; + final description = data['Description'] ?? ''; + final price = data['Price']?.toString() ?? '0'; + final nPrice = data['NPrice']?.toString() ?? '0'; + final startDate = + data['StartDate'] != null ? DateTime.tryParse(data['StartDate']) : null; + final endDate = + data['EndDate'] != null ? DateTime.tryParse(data['EndDate']) : null; + final startTime = data['StartTime']; + final endTime = data['EndTime']; + final radius = (data['Radius'] as num?)?.toDouble() ?? 0.0; + + emit( + state.copyWith( + isLoadingDetails: false, + discountId: data['_id'], + productName: name, + productImages: images, + discountTypeId: typeId, + description: description, + price: price, + discountedPrice: nPrice, + startDate: startDate, + endDate: endDate, + startTime: startTime, + endTime: endTime, + notificationRadius: radius, + ), + ); + } else { + emit( + state.copyWith( + isLoadingDetails: false, + errorMessage: "تخفیف یافت نشد.", + ), + ); + } + } catch (e) { + emit( + state.copyWith( + isLoadingDetails: false, + errorMessage: 'خطا در دریافت اطلاعات.', + ), + ); + } + }); on((event, emit) { - List updatedImages = List.from(state.productImages); + List> updatedImages = List.from(state.productImages); + final newImageMap = {'id': null, 'url': event.imagePath}; + if (updatedImages.length > event.index) { - updatedImages[event.index] = event.imagePath; + updatedImages[event.index] = newImageMap; } else { - updatedImages.add(event.imagePath); + updatedImages.add(newImageMap); } emit(state.copyWith(productImages: updatedImages)); }); @@ -55,57 +146,99 @@ class DiscountBloc extends Bloc { }); on((event, emit) async { - emit(state.copyWith(isSubmitting: true, errorMessage: null)); + await _submitDiscountForm(emit, isEdit: false); + }); - try { - final token = await _tokenStorage.getAccessToken(); - if (token == null) { - emit(state.copyWith(isSubmitting: false, errorMessage: "خطای احراز هویت.")); - return; - } - - List imageFiles = []; - for (var imagePath in state.productImages) { - imageFiles.add(await MultipartFile.fromFile( - imagePath, - filename: imagePath.split('/').last, - )); - } - - final data = { - 'Name' : state.productName, - 'Images': imageFiles, - 'Type': state.discountTypeId, - 'Description': state.description, - 'Price': double.tryParse(state.price), - 'NPrice': double.tryParse(state.discountedPrice), - 'Start': state.startDate != null ? DateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(state.startDate!) : null, - 'End': state.endDate != null ? DateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(state.endDate!) : null, - 'StartTime': state.startTime, - 'EndTime': state.endTime, - 'Radius': state.notificationRadius, - }; - - final formData = FormData.fromMap(data); - - await _dio.post( - ApiConfig.addDiscount, - data: formData, - options: Options( - headers: {'Authorization': 'Bearer $token'}, - ), - ); - - emit(state.copyWith(isSubmitting: false, isSuccess: true)); - - } on DioException catch (e) { - emit(state.copyWith( - isSubmitting: false, - errorMessage: e.response?.data['message'] ?? 'خطای سرور.', - )); - } catch (e) { - emit(state.copyWith(isSubmitting: false, errorMessage: 'خطای ناشناخته: ${e.toString()}')); - } + on((event, emit) async { + await _submitDiscountForm( + emit, + isEdit: true, + discountId: event.discountId, + ); }); } -} + + Future _submitDiscountForm( + Emitter emit, { + required bool isEdit, + String? discountId, + }) async { + emit(state.copyWith(isSubmitting: true, errorMessage: null)); + + try { + final token = await _tokenStorage.getAccessToken(); + if (token == null) { + emit( + state.copyWith(isSubmitting: false, errorMessage: "خطای احراز هویت."), + ); + return; + } + + final Map data = { + 'Name': state.productName, + 'Type': state.discountTypeId, + 'Description': state.description, + 'Price': double.tryParse(state.price), + 'NPrice': double.tryParse(state.discountedPrice), + 'Start': state.startDate?.toUtc().toIso8601String(), + 'End': state.endDate?.toUtc().toIso8601String(), + 'StartTime': state.startTime, + 'EndTime': state.endTime, + 'Radius': state.notificationRadius, + }; + + List newImageFiles = []; + List existingImageIds = []; + + for (var imageMap in state.productImages) { + final id = imageMap['id']; + final url = imageMap['url']; + + if (id != null && url != null && url.startsWith('http')) { + existingImageIds.add(id); + } else if (url != null && !url.startsWith('http')) { + newImageFiles.add( + await MultipartFile.fromFile(url, filename: url.split('/').last), + ); + } + } + + if (newImageFiles.isNotEmpty) { + data['Images'] = newImageFiles; + } + + if (isEdit) { + data['ExistingImages'] = jsonEncode(existingImageIds); + } + + // *** CHANGE IS HERE: Logging the data before sending *** + log('Submitting Discount Data: $data', name: 'DiscountBloc'); + + final formData = FormData.fromMap(data); + final url = + isEdit ? ApiConfig.editDiscount(discountId!) : ApiConfig.addDiscount; + + await _dio.post( + url, + data: formData, + options: Options(headers: {'Authorization': 'Bearer $token'}), + ); + + emit(state.copyWith(isSubmitting: false, isSuccess: true)); + } on DioException catch (e) { + emit( + state.copyWith( + isSubmitting: false, + errorMessage: e.response?.data['message'] ?? 'خطای سرور.', + ), + ); + } catch (e) { + emit( + state.copyWith( + isSubmitting: false, + errorMessage: 'خطای ناشناخته: ${e.toString()}', + ), + ); + } + } +} \ No newline at end of file diff --git a/lib/presentation/discount/bloc/discount_event.dart b/lib/presentation/discount/bloc/discount_event.dart index 3b9a29a..fa28db6 100644 --- a/lib/presentation/discount/bloc/discount_event.dart +++ b/lib/presentation/discount/bloc/discount_event.dart @@ -1,6 +1,13 @@ +// lib/presentation/discount/bloc/discount_event.dart abstract class DiscountEvent {} +// Fetch details for editing +class FetchDiscountDetails extends DiscountEvent { + final String discountId; + FetchDiscountDetails(this.discountId); +} + class ProductImageAdded extends DiscountEvent { final String imagePath; final int index; @@ -15,7 +22,7 @@ class ProductNameChanged extends DiscountEvent { class DiscountTypeChanged extends DiscountEvent { final String typeId; DiscountTypeChanged(this.typeId); - } +} class DescriptionChanged extends DiscountEvent { final String description; @@ -49,4 +56,10 @@ class NotificationRadiusChanged extends DiscountEvent { NotificationRadiusChanged(this.radius); } -class SubmitDiscount extends DiscountEvent {} \ No newline at end of file +class SubmitDiscount extends DiscountEvent {} + +// Event for updating an existing discount +class UpdateDiscount extends DiscountEvent { + final String discountId; + UpdateDiscount(this.discountId); +} \ No newline at end of file diff --git a/lib/presentation/discount/bloc/discount_state.dart b/lib/presentation/discount/bloc/discount_state.dart index 4a00a36..769ad64 100644 --- a/lib/presentation/discount/bloc/discount_state.dart +++ b/lib/presentation/discount/bloc/discount_state.dart @@ -1,22 +1,28 @@ +// lib/presentation/discount/bloc/discount_state.dart + import 'package:equatable/equatable.dart'; class DiscountState extends Equatable { - final List productImages; + final String? discountId; + // *** CHANGE IS HERE: Storing image ID and URL together *** + final List> productImages; final String productName; final String? discountTypeId; final String description; final DateTime? startDate; final DateTime? endDate; - final String? startTime; - final String? endTime; + final String? startTime; + final String? endTime; final String price; final String discountedPrice; final double notificationRadius; final bool isSubmitting; + final bool isLoadingDetails; final bool isSuccess; final String? errorMessage; const DiscountState({ + this.discountId, this.productImages = const [], this.productName = '', this.discountTypeId, @@ -29,12 +35,14 @@ class DiscountState extends Equatable { this.discountedPrice = '', this.notificationRadius = 0.0, this.isSubmitting = false, + this.isLoadingDetails = false, this.isSuccess = false, this.errorMessage, }); DiscountState copyWith({ - List? productImages, + String? discountId, + List>? productImages, String? productName, String? discountTypeId, String? description, @@ -46,10 +54,12 @@ class DiscountState extends Equatable { String? discountedPrice, double? notificationRadius, bool? isSubmitting, + bool? isLoadingDetails, bool? isSuccess, String? errorMessage, }) { return DiscountState( + discountId: discountId ?? this.discountId, productImages: productImages ?? this.productImages, productName: productName ?? this.productName, discountTypeId: discountTypeId ?? this.discountTypeId, @@ -62,6 +72,7 @@ class DiscountState extends Equatable { discountedPrice: discountedPrice ?? this.discountedPrice, notificationRadius: notificationRadius ?? this.notificationRadius, isSubmitting: isSubmitting ?? this.isSubmitting, + isLoadingDetails: isLoadingDetails ?? this.isLoadingDetails, isSuccess: isSuccess ?? this.isSuccess, errorMessage: errorMessage ?? this.errorMessage, ); @@ -69,6 +80,7 @@ class DiscountState extends Equatable { @override List get props => [ + discountId, productImages, productName, discountTypeId, @@ -81,6 +93,7 @@ class DiscountState extends Equatable { discountedPrice, notificationRadius, isSubmitting, + isLoadingDetails, isSuccess, errorMessage, ]; diff --git a/lib/presentation/home/bloc/home_bloc.dart b/lib/presentation/home/bloc/home_bloc.dart index b0107f7..57129f4 100644 --- a/lib/presentation/home/bloc/home_bloc.dart +++ b/lib/presentation/home/bloc/home_bloc.dart @@ -1,6 +1,7 @@ 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/discount_entity.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; @@ -13,43 +14,57 @@ class HomeBloc extends Bloc { final TokenStorageService _tokenStorage = TokenStorageService(); HomeBloc() : super(HomeInitial()) { + _dio.interceptors.add(LoggingInterceptor()); + on((event, emit) async { - emit(HomeLoading()); - try { - final token = await _tokenStorage.getAccessToken(); - if (token == null || token.isEmpty) { - emit(HomeError("خطای احراز هویت. لطفا دوباره وارد شوید.")); - return; - } + await _fetchDiscounts(emit); + }); - final response = await _dio.get( - ApiConfig.getDiscounts, - options: Options( - headers: {'Authorization': 'Bearer $token'}, - ), - ); - - if (response.statusCode == 200 && response.data['data'] != null) { - final List data = response.data['data']; - final discounts = data.map((json) => DiscountEntity.fromJson(json)).toList(); - emit(HomeLoaded(discounts)); - } else { - emit(HomeError(response.data['message'] ?? 'خطا در دریافت اطلاعات')); - } - } on DioException catch (e) { - // Log Dio error - if (kDebugMode) { - print('DioException in HomeBloc: ${e.response?.data}'); - } - emit(HomeError(e.response?.data['message'] ?? 'خطای شبکه')); - } catch (e, stackTrace) { - // Log any other error - if (kDebugMode) { - print('Error in HomeBloc: $e'); - print(stackTrace); - } - emit(HomeError('خطای پیش‌بینی نشده رخ داد: ${e.toString()}')); - } + on((event, emit) async { + await _fetchDiscounts(emit, searchQuery: event.query); }); } + + Future _fetchDiscounts(Emitter emit, {String? searchQuery}) async { + emit(HomeLoading()); + try { + final token = await _tokenStorage.getAccessToken(); + if (token == null || token.isEmpty) { + emit(HomeError("خطای احراز هویت. لطفا دوباره وارد شوید.")); + return; + } + + String url = ApiConfig.getActiveDiscounts; + 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']['discounts'] ?? []; + final discounts = + data.map((json) => DiscountEntity.fromJson(json)).toList(); + emit(HomeLoaded(discounts)); + } else { + emit(HomeError(response.data['message'] ?? 'خطا در دریافت اطلاعات')); + } + } on DioException catch (e) { + if (kDebugMode) { + print('DioException in HomeBloc: ${e.response?.data}'); + } + emit(HomeError(e.response?.data['message'] ?? 'خطای شبکه')); + } catch (e, stackTrace) { + if (kDebugMode) { + print('Error in HomeBloc: $e'); + print(stackTrace); + } + emit(HomeError('خطای پیش‌بینی نشده رخ داد: ${e.toString()}')); + } + } } \ No newline at end of file diff --git a/lib/presentation/home/bloc/home_event.dart b/lib/presentation/home/bloc/home_event.dart index f788ef1..2a01034 100644 --- a/lib/presentation/home/bloc/home_event.dart +++ b/lib/presentation/home/bloc/home_event.dart @@ -2,4 +2,10 @@ part of 'home_bloc.dart'; abstract class HomeEvent {} -class FetchDiscounts extends HomeEvent {} \ No newline at end of file +class FetchDiscounts extends HomeEvent {} + +class SearchDiscounts extends HomeEvent { + final String query; + + SearchDiscounts({required this.query}); +} \ No newline at end of file diff --git a/lib/presentation/pages/add_discount_page.dart b/lib/presentation/pages/add_discount_page.dart index 8633f23..a67ba9a 100644 --- a/lib/presentation/pages/add_discount_page.dart +++ b/lib/presentation/pages/add_discount_page.dart @@ -13,19 +13,29 @@ import 'package:image_picker/image_picker.dart'; import 'package:persian_datetime_picker/persian_datetime_picker.dart'; class AddDiscountPage extends StatelessWidget { - const AddDiscountPage({super.key}); + final String? discountId; + + const AddDiscountPage({super.key, this.discountId}); @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => DiscountBloc(), - child: const _AddDiscountView(), + create: (context) { + final bloc = DiscountBloc(); + if (discountId != null) { + bloc.add(FetchDiscountDetails(discountId!)); + } + return bloc; + }, + child: _AddDiscountView(discountId: discountId), ); } } class _AddDiscountView extends StatefulWidget { - const _AddDiscountView(); + final String? discountId; + + const _AddDiscountView({this.discountId}); @override State<_AddDiscountView> createState() => _AddDiscountViewState(); @@ -37,6 +47,8 @@ class _AddDiscountViewState extends State<_AddDiscountView> { final _priceController = TextEditingController(); final _discountPriceController = TextEditingController(); + bool get _isEditMode => widget.discountId != null; + @override void dispose() { _nameController.dispose(); @@ -83,8 +95,8 @@ class _AddDiscountViewState extends State<_AddDiscountView> { ); context.read().add( - ValidityDateChanged(startDate: startDateTime, endDate: endDateTime), - ); + ValidityDateChanged(startDate: startDateTime, endDate: endDateTime), + ); } final List discountTypes = [ @@ -102,117 +114,131 @@ class _AddDiscountViewState extends State<_AddDiscountView> { Widget build(BuildContext context) { return Scaffold( appBar: _buildCustomAppBar(context), - body: BlocListener( + body: BlocConsumer( listener: (context, state) { if (state.isSuccess) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("تخفیف با موفقیت ثبت شد!"), backgroundColor: Colors.green), + SnackBar( + content: Text( + "تخفیف با موفقیت ${this._isEditMode ? 'ویرایش' : 'ثبت'} شد!"), + backgroundColor: Colors.green), ); - // میتوانید به صفحه دیگری ناوبری کنید - // Navigator.of(context).pop(); + Navigator.of(context).pop(true); } - if (state.errorMessage != null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.errorMessage!), backgroundColor: Colors.red), + if (state.errorMessage != null && !state.isLoadingDetails) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage!), backgroundColor: Colors.red), ); } + if (state.productName.isNotEmpty && _nameController.text.isEmpty) { + _nameController.text = state.productName; + _descController.text = state.description; + _priceController.text = state.price; + _discountPriceController.text = state.discountedPrice; + } }, - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "تعریف تخفیف جدید", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), - ), - const SizedBox(height: 24), - _buildSectionTitle( - title: "بارگذاری عکس از محصول", - popupTitle: "یه عکس خوب، یه فروش خوب‌تر!", - isMandatory: true, - infoText: - "عکس واضح، باکیفیت و واقعی از محصولت بذار. ترجیحا از عکس‌های اینترنتی یا تبلیغاتی استفاده نکن.", - iconPath: Assets.icons.camera, - ), - const SizedBox(height: 16), - _buildImagePickers(), - const SizedBox(height: 30), - _buildTextField( - controller: _nameController, - label: "نام محصول", - isRequired: true, - hint: "وافل شکلات فندقی", - onChanged: - (value) => context.read().add( - ProductNameChanged(value), - ), - ), - const SizedBox(height: 30), - _buildDiscountTypeDropdown(), - const SizedBox(height: 30), - _buildTextField( - controller: _descController, - label: "توضیح برای تخفیف", - hint: "مثلاً عصرونه، با ۵٪ تخفیف مهمون ما باش! ", - isRequired: true, - maxLines: 4, - maxLength: 200, - onChanged: - (value) => context.read().add( - DescriptionChanged(value), - ), - ), - const SizedBox(height: 30), - _buildDateTimePicker(), - const SizedBox(height: 30), - _buildTimeRangePicker(context), - const SizedBox(height: 30), - _buildTextField( - controller: _priceController, - label: "قیمت بدون تخفیف", - isRequired: true, - hint: "مثلاً 240000 تومان", - keyboardType: TextInputType.number, - onChanged: - (value) => - context.read().add(PriceChanged(value)), - ), - const SizedBox(height: 30), - _buildTextField( - controller: _discountPriceController, - label: "قیمت با تخفیف", - hint: "مثلاً 200000 تومان", - isRequired: true, - keyboardType: TextInputType.number, - onChanged: - (value) => context.read().add( - DiscountedPriceChanged(value), - ), - ), - const SizedBox(height: 30), - _buildNotificationRadiusSlider(), - const SizedBox(height: 30), - SizedBox( - width: double.infinity, - child: BlocBuilder( - builder: (context, state) { - return ElevatedButton( - onPressed: state.isSubmitting ? null : () { - context.read().add(SubmitDiscount()); - }, - child: state.isSubmitting - ? const CircularProgressIndicator(color: Colors.white) - : const Text("ثبت تخفیف"), - ); - }, + builder: (context, state) { + if (state.isLoadingDetails) { + return const Center(child: CircularProgressIndicator()); + } + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _isEditMode ? "ویرایش تخفیف" : "تعریف تخفیف جدید", + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 20), ), - ), - const SizedBox(height: 30), - ], - ), + const SizedBox(height: 24), + _buildSectionTitle( + title: "بارگذاری عکس از محصول", + popupTitle: "یه عکس خوب، یه فروش خوب‌تر!", + isMandatory: !_isEditMode, + infoText: + "عکس واضح، باکیفیت و واقعی از محصولت بذار. ترجیحا از عکس‌های اینترنتی یا تبلیغاتی استفاده نکن.", + iconPath: Assets.icons.camera, + ), + const SizedBox(height: 16), + _buildImagePickers(), + const SizedBox(height: 30), + _buildTextField( + controller: _nameController, + label: "نام محصول", + isRequired: true, + hint: "وافل شکلات فندقی", + onChanged: (value) => + context.read().add(ProductNameChanged(value)), + ), + const SizedBox(height: 30), + _buildDiscountTypeDropdown(state), // Pass state here + const SizedBox(height: 30), + _buildTextField( + controller: _descController, + label: "توضیح برای تخفیف", + hint: "مثلاً عصرونه، با ۵٪ تخفیف مهمون ما باش! ", + isRequired: true, + maxLines: 4, + maxLength: 200, + onChanged: (value) => + context.read().add(DescriptionChanged(value)), + ), + const SizedBox(height: 30), + _buildDateTimePicker(), + const SizedBox(height: 30), + _buildTimeRangePicker(context), + const SizedBox(height: 30), + _buildTextField( + controller: _priceController, + label: "قیمت بدون تخفیف", + isRequired: true, + hint: "مثلاً 240000 تومان", + keyboardType: TextInputType.number, + onChanged: (value) => + context.read().add(PriceChanged(value)), + ), + const SizedBox(height: 30), + _buildTextField( + controller: _discountPriceController, + label: "قیمت با تخفیف", + hint: "مثلاً 200000 تومان", + isRequired: true, + keyboardType: TextInputType.number, + onChanged: (value) => context + .read() + .add(DiscountedPriceChanged(value)), + ), + const SizedBox(height: 30), + _buildNotificationRadiusSlider(), + const SizedBox(height: 30), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: state.isSubmitting + ? null + : () { + if (_isEditMode) { + context + .read() + .add(UpdateDiscount(widget.discountId!)); + } else { + context.read().add(SubmitDiscount()); + } + }, + child: state.isSubmitting + ? const CircularProgressIndicator(color: Colors.white) + : Text(_isEditMode ? "ثبت تغییرات" : "ثبت تخفیف"), + ), + ), + const SizedBox(height: 30), + ], + ), + ); + }, ), - )); + ); } Widget _buildSectionTitle({ @@ -227,13 +253,12 @@ class _AddDiscountViewState extends State<_AddDiscountView> { children: [ if (infoText != null && iconPath != null) IconButton( - onPressed: - () => showInfoDialog( - context, - title: popupTitle ?? title, - content: infoText, - iconPath: iconPath, - ), + onPressed: () => showInfoDialog( + context, + title: popupTitle ?? title, + content: infoText, + iconPath: iconPath, + ), icon: SvgPicture.asset(Assets.icons.infoCircle, width: 17), ), Text( @@ -248,24 +273,31 @@ class _AddDiscountViewState extends State<_AddDiscountView> { Widget _buildImagePickers() { return BlocBuilder( + buildWhen: (p, c) => p.productImages != c.productImages, builder: (context, state) { + // We ensure the list has at least 2 elements for the UI, filling with null + final displayImages = List?>.from(state.productImages); + while (displayImages.length < 2) { + displayImages.add(null); + } + return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(2, (index) { - final imagePath = - state.productImages.length > index - ? state.productImages[index] - : null; + // *** CHANGE IS HERE: Read from the map structure *** + final imageMap = displayImages[index]; + final imageUrl = imageMap?['url']; + final isUrl = imageUrl?.startsWith('http') ?? false; + return GestureDetector( onTap: () async { final ImagePicker picker = ImagePicker(); - final XFile? image = await picker.pickImage( - source: ImageSource.gallery, - ); + final XFile? image = + await picker.pickImage(source: ImageSource.gallery, imageQuality: 80); if (image != null && context.mounted) { - context.read().add( - ProductImageAdded(image.path, index), - ); + context + .read() + .add(ProductImageAdded(image.path, index)); } }, child: Container( @@ -275,23 +307,23 @@ class _AddDiscountViewState extends State<_AddDiscountView> { color: AppColors.uploadElevated, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.uploadElevated), - image: - imagePath != null - ? DecorationImage( - image: FileImage(File(imagePath)), - fit: BoxFit.cover, - ) - : null, - ), - child: - imagePath == null - ? Center( - child: SvgPicture.asset( - Assets.icons.addPic, - width: 60, - ), + image: imageUrl != null + ? DecorationImage( + image: isUrl + ? NetworkImage(imageUrl) + : FileImage(File(imageUrl)) as ImageProvider, + fit: BoxFit.cover, ) - : null, + : null, + ), + child: imageUrl == null + ? Center( + child: SvgPicture.asset( + Assets.icons.addPic, + width: 60, + ), + ) + : null, ), ); }), @@ -300,9 +332,17 @@ class _AddDiscountViewState extends State<_AddDiscountView> { ); } - Widget _buildDiscountTypeDropdown() { + Widget _buildDiscountTypeDropdown(DiscountState state) { + // Create a set of available IDs for quick lookup. + final availableTypeIds = discountTypes.map((type) => type.id).toSet(); + + // Check if the current discount's type ID is in our list. If not, use null. + final String? selectedValue = availableTypeIds.contains(state.discountTypeId) + ? state.discountTypeId + : null; + return DropdownButtonFormField( - value: context.watch().state.discountTypeId, + value: selectedValue, // Use the safe value here. icon: SvgPicture.asset( Assets.icons.arrowDown, width: 24, @@ -328,7 +368,7 @@ class _AddDiscountViewState extends State<_AddDiscountView> { ); } - Widget _buildDateTimePicker() { + Widget _buildDateTimePicker() { return BlocBuilder( buildWhen: (previous, current) => previous.startDate != current.startDate || @@ -336,7 +376,6 @@ class _AddDiscountViewState extends State<_AddDiscountView> { builder: (context, state) { String displayText = "انتخاب تاریخ"; if (state.startDate != null && state.endDate != null) { - final jalaliStart = DateTimeExtensions(state.startDate!).toJalali(); final jalaliEnd = DateTimeExtensions(state.endDate!).toJalali(); @@ -361,7 +400,7 @@ class _AddDiscountViewState extends State<_AddDiscountView> { child: Text( displayText, textDirection: TextDirection.rtl, - style: const TextStyle(fontSize: 15), // Optional: for better fit + style: const TextStyle(fontSize: 15), ), ), SvgPicture.asset(Assets.icons.calendarSearch), @@ -375,10 +414,9 @@ class _AddDiscountViewState extends State<_AddDiscountView> { Widget _buildTimeRangePicker(BuildContext context) { return BlocBuilder( - buildWhen: - (previous, current) => - previous.startTime != current.startTime || - previous.endTime != current.endTime, + buildWhen: (previous, current) => + previous.startTime != current.startTime || + previous.endTime != current.endTime, builder: (context, state) { String displayText = "انتخاب بازه زمانی"; if (state.startTime != null && state.endTime != null) { @@ -387,16 +425,12 @@ class _AddDiscountViewState extends State<_AddDiscountView> { return InkWell( onTap: () async { - final TimeOfDay? startTime = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); + final TimeOfDay? startTime = + await showTimePicker(context: context, initialTime: TimeOfDay.now()); if (startTime == null) return; - final TimeOfDay? endTime = await showTimePicker( - context: context, - initialTime: startTime, - ); + final TimeOfDay? endTime = + await showTimePicker(context: context, initialTime: startTime); if (endTime == null) return; final formattedStartTime = @@ -405,11 +439,9 @@ class _AddDiscountViewState extends State<_AddDiscountView> { '${endTime.hour.toString().padLeft(2, '0')}:${endTime.minute.toString().padLeft(2, '0')}'; context.read().add( - TimeRangeChanged( - startTime: formattedStartTime, - endTime: formattedEndTime, - ), - ); + TimeRangeChanged( + startTime: formattedStartTime, endTime: formattedEndTime), + ); }, child: InputDecorator( decoration: _inputDecoration("بازه زمانی معتبر", isRequired: true), @@ -430,22 +462,18 @@ class _AddDiscountViewState extends State<_AddDiscountView> { Row( children: [ IconButton( - onPressed: - () => showInfoDialog( - context, - title: "انتخاب محدوده نمایش تخفیف", - content: - "محدوده‌ای رو مشخص کن که تخفیف‌هات فقط به کاربرانی که تو اون شعاع هستن نشون داده بشه.", - iconPath: Assets.icons.radar2, - ), + onPressed: () => showInfoDialog( + context, + title: "انتخاب محدوده نمایش تخفیف", + content: + "محدوده‌ای رو مشخص کن که تخفیف‌هات فقط به کاربرانی که تو اون شعاع هستن نشون داده بشه.", + iconPath: Assets.icons.radar2, + ), icon: SvgPicture.asset(Assets.icons.infoCircle, width: 17), ), - Text( + const Text( "شعاع ارسال اعلان تخفیف به مشتری‌ها", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), ], ), @@ -462,13 +490,11 @@ class _AddDiscountViewState extends State<_AddDiscountView> { trackShape: const RoundedRectSliderTrackShape(), trackHeight: 4.0, thumbColor: AppColors.active, - thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 12.0, - ), + thumbShape: + const RoundSliderThumbShape(enabledThumbRadius: 12.0), overlayColor: AppColors.active.withAlpha(32), - overlayShape: const RoundSliderOverlayShape( - overlayRadius: 28.0, - ), + overlayShape: + const RoundSliderOverlayShape(overlayRadius: 28.0), ), child: Slider( value: state.notificationRadius, @@ -477,24 +503,20 @@ class _AddDiscountViewState extends State<_AddDiscountView> { divisions: 100, label: '${state.notificationRadius.toInt()} متر', onChanged: (value) { - context.read().add( - NotificationRadiusChanged(value), - ); + context + .read() + .add(NotificationRadiusChanged(value)); }, ), ), - SizedBox(height: 7,), - BlocBuilder( - builder: (context, state) { - return Text( - '${state.notificationRadius.toInt()} متر', - style: const TextStyle( - fontWeight: FontWeight.normal, - fontSize: 14, - color: Colors.black, - ), - ); - }, + const SizedBox(height: 7), + Text( + '${state.notificationRadius.toInt()} متر', + style: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14, + color: Colors.black, + ), ), ], ); @@ -523,36 +545,26 @@ class _AddDiscountViewState extends State<_AddDiscountView> { maxLines: maxLines, maxLength: maxLength, keyboardType: keyboardType, - decoration: _inputDecoration( - label, - hint: hint, - isRequired: isRequired, - ).copyWith( + decoration: _inputDecoration(label, hint: hint, isRequired: isRequired) + .copyWith( counterText: '', - counter: - maxLength != null - ? Text( - '${value.text.length}/$maxLength', - style: Theme.of(context).textTheme.bodySmall, - ) - : null, + counter: maxLength != null + ? Text( + '${value.text.length}/$maxLength', + style: Theme.of(context).textTheme.bodySmall, + ) + : null, ), ); }, ); } - InputDecoration _inputDecoration( - String label, { - String? hint, - bool isRequired = false, - }) { + InputDecoration _inputDecoration(String label, + {String? hint, bool isRequired = false}) { return InputDecoration( hintText: hint, - hintStyle: TextStyle( - color: Color.fromARGB(255, 95, 95, 95), - fontSize: 14, - ), + hintStyle: const TextStyle(color: Color.fromARGB(255, 95, 95, 95), fontSize: 14), label: RichText( text: TextSpan( text: label, @@ -564,7 +576,8 @@ class _AddDiscountViewState extends State<_AddDiscountView> { ), children: [ if (isRequired) - const TextSpan(text: ' *', style: TextStyle(color: Colors.red)), + const TextSpan( + text: ' *', style: TextStyle(color: Colors.red)), ], ), ), @@ -577,9 +590,7 @@ class _AddDiscountViewState extends State<_AddDiscountView> { child: Container( decoration: BoxDecoration( color: Colors.white, - borderRadius: const BorderRadius.vertical( - bottom: Radius.circular(15), - ), + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(15)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), @@ -591,30 +602,19 @@ class _AddDiscountViewState extends State<_AddDiscountView> { child: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Column( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const SizedBox(height: 15), + SvgPicture.asset(Assets.icons.logoWithName), Row( children: [ - Padding( - padding: const EdgeInsets.only(right: 8), - child: SvgPicture.asset(Assets.icons.logoWithName), + IconButton( + onPressed: () {}, + icon: SvgPicture.asset(Assets.icons.discountShape, color: Colors.black), ), - const Spacer(), - Row( - children: [ - IconButton( - onPressed: () {}, - icon: SvgPicture.asset( - Assets.icons.discountShape, - color: Colors.black, - ), - ), - IconButton( - onPressed: () {}, - icon: SvgPicture.asset(Assets.icons.scanBarcode), - ), - ], + IconButton( + onPressed: () {}, + icon: SvgPicture.asset(Assets.icons.scanBarcode), ), ], ), @@ -625,4 +625,4 @@ class _AddDiscountViewState extends State<_AddDiscountView> { ), ); } -} +} \ No newline at end of file diff --git a/lib/presentation/pages/discount_manegment_page.dart b/lib/presentation/pages/discount_manegment_page.dart new file mode 100644 index 0000000..8d4bab3 --- /dev/null +++ b/lib/presentation/pages/discount_manegment_page.dart @@ -0,0 +1,116 @@ +import 'dart:async'; // اضافه کردن کتابخانه برای استفاده از Timer +import 'package:business_panel/presentation/home/bloc/home_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: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'; + +class DiscountManegment extends StatefulWidget { + const DiscountManegment({super.key}); + + @override + State createState() => _DiscountManegmentState(); +} + +class _DiscountManegmentState extends State { + final TextEditingController _searchController = TextEditingController(); + Timer? _debounce; + + @override + void initState() { + super.initState(); + // Fetch initial discounts when the page loads + // Note: Using addPostFrameCallback to ensure context is available + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + context.read().add(FetchDiscounts()); + } + }); + } + + @override + void dispose() { + _searchController.dispose(); + _debounce?.cancel(); // کنسل کردن تایمر برای جلوگیری از نشت حافظه + super.dispose(); + } + + void _onSearchChanged(String query) { + if (_debounce?.isActive ?? false) _debounce!.cancel(); + _debounce = Timer(const Duration(milliseconds: 500), () { + if (mounted) { + context.read().add(SearchDiscounts(query: query)); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBarSingle( + page: "رزرو ها", + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'دنبال چی می‌گردی؟', + hintStyle: TextStyle(color: Color.fromARGB(255, 157, 157, 157)), + prefixIcon: Padding( + padding: const EdgeInsets.all(12.0), + child: SvgPicture.asset( + Assets.icons.riSearch2Line, + ), + ), + fillColor: Color.fromARGB(255, 244, 244, 244), + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(50), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 0), + ), + onChanged: _onSearchChanged, // استفاده از متد جدید + ), + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state is HomeError) { + return Center(child: Text('خطا: ${state.message}')); + } + + if (state is HomeLoaded) { + if (state.discounts.isEmpty) { + return const Center(child: Text("هیچ تخفیفی با این مشخصات یافت نشد.")); + } + return RefreshIndicator( + onRefresh: () async { + context.read().add(FetchDiscounts()); + _searchController.clear(); + }, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: state.discounts.length, + itemBuilder: (context, index) { + final discount = state.discounts[index]; + return ActiveDiscountCard(discount: discount); + }, + ), + ); + } + + return const Center(child: CircularProgressIndicator()); + }, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/presentation/pages/home_page.dart b/lib/presentation/pages/home_page.dart index c537745..f3935c8 100644 --- a/lib/presentation/pages/home_page.dart +++ b/lib/presentation/pages/home_page.dart @@ -1,36 +1,23 @@ 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'; import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; +import 'package:business_panel/presentation/pages/add_discount_page.dart'; +import 'package:business_panel/presentation/widgets/custom_app_bar.dart'; +import 'package:business_panel/presentation/widgets/discount_card.dart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:slide_countdown/slide_countdown.dart'; -import 'package:business_panel/presentation/pages/add_discount_page.dart'; -class HomePage extends StatefulWidget { +class HomePage extends StatelessWidget { const HomePage({super.key}); - @override - State createState() => _HomePageState(); -} - -class _HomePageState extends State { - @override - void initState() { - super.initState(); - context.read().add(FetchDiscounts()); - } - @override Widget build(BuildContext context) { return Scaffold( - appBar: _buildCustomAppBar(context), + appBar: const CustomAppBar(), body: BlocBuilder( builder: (context, state) { - if (state is HomeLoading) { - return const Center(child: CircularProgressIndicator()); - } + // --- منطق اصلاح‌شده برای مدیریت وضعیت --- if (state is HomeError) { return Center( child: Padding( @@ -39,9 +26,10 @@ class _HomePageState extends State { ), ); } + if (state is HomeLoaded) { if (state.discounts.isEmpty) { - return _buildEmptyState(); + return _buildEmptyState(context); } return RefreshIndicator( onRefresh: () async { @@ -49,49 +37,50 @@ class _HomePageState extends State { }, child: ListView.builder( padding: const EdgeInsets.all(16), - // تعداد آیتم‌ها یکی بیشتر از تعداد تخفیف‌هاست تا دکمه هم جا شود itemCount: state.discounts.length + 1, itemBuilder: (context, index) { - // اگر ایندکس مربوط به آخرین آیتم بود، دکمه را نمایش بده if (index == state.discounts.length) { - return _buildAddDiscountButton(); + return _buildAddDiscountButton(context); } - // در غیر این صورت، کارت تخفیف را نمایش بده final discount = state.discounts[index]; - return _buildDiscountCard(discount); + return DiscountCard(discount: discount); }, ), ); } - // حالت پیش‌فرض - return _buildEmptyState(); + + // برای حالت‌های HomeInitial و HomeLoading + return const Center(child: CircularProgressIndicator()); }, ), ); } - Widget _buildEmptyState() { + Widget _buildEmptyState(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - SvgPicture.asset(Assets.images.emptyHome, height: 200), - const SizedBox(height: 20), - const Text( - "هنوز تخفیفی ثبت نکرده‌اید", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 20), + SvgPicture.asset(Assets.icons.emptyHome, height: 300), + const SizedBox(height: 35), + const Text("سلام!", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17)), + const SizedBox(height: 15), Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: _buildAddDiscountButton(), + child: Column( + children: [ + _buildAddDiscountButton(context), + const SizedBox(height: 5), + _todayState() + ], + ), ), ], ), ); } - Widget _buildAddDiscountButton() { + Widget _buildAddDiscountButton(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 16.0), child: SizedBox( @@ -105,10 +94,9 @@ class _HomePageState extends State { ), ), onPressed: () { - Navigator.of(context) - .push(MaterialPageRoute(builder: (_) => const AddDiscountPage())) - .then((_) { - // رفرش لیست بعد از بازگشت از صفحه افزودن + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const AddDiscountPage()), + ).then((_) { context.read().add(FetchDiscounts()); }); }, @@ -132,245 +120,26 @@ class _HomePageState extends State { ); } - Widget _buildDiscountCard(DiscountEntity discount) { - final remaining = discount.endDate != null - ? discount.endDate!.difference(DateTime.now()) - : const Duration(seconds: -1); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - "تخفیف ${discount.type}", - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ), - InkWell( - onTap: () { - // TODO: Implement edit functionality - }, - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - SvgPicture.asset(Assets.icons.edit, width: 20, color: AppColors.active), - const SizedBox(width: 5), - const Text("ویرایش", style: TextStyle(color: AppColors.active)), - ], - ), - ), - ), - ], - ), - const Divider(height: 1), - const SizedBox(height: 10), - Card( - color: Colors.white, - elevation: 0, - margin: const EdgeInsets.only(bottom: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(25), - side: BorderSide(color: Colors.grey.shade300, width: 1), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (discount.images.isNotEmpty && discount.images.first.isNotEmpty) - ClipRRect( - borderRadius: BorderRadius.circular(15), - child: Image.network( - discount.images.first, - width: 100, - height: 100, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => Container( - width: 100, - height: 100, - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(15), - ), - child: const Icon(Icons.image_not_supported, color: Colors.grey), - ), - ), - ) - else - Container( - width: 100, - height: 100, - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(15), - ), - child: const Icon(Icons.store, color: Colors.grey, size: 50), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - children: [ - SvgPicture.asset(Assets.icons.shop, width: 18, color: Colors.grey.shade700), - const SizedBox(width: 10), - Expanded( - child: Text( - discount.shopName, - style: const TextStyle(fontSize: 16), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - SvgPicture.asset(Assets.icons.shoppingCart, width: 18, color: Colors.grey.shade700), - const SizedBox(width: 10), - Expanded( - child: Text( - discount.name, - style: TextStyle(fontSize: 15, color: Colors.grey.shade600), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - const SizedBox(height: 12), - if (discount.endDate == null) - const Text( - 'تاریخ نامعتبر', - style: TextStyle(color: Colors.orange, fontWeight: FontWeight.bold), - ) - else if (remaining.isNegative) - const Text( - 'منقضی شده', - style: TextStyle(color: AppColors.expiryReserve, fontWeight: FontWeight.bold), - ) - else - Row( - children: [ - SvgPicture.asset(Assets.icons.timerPause, width: 18, color: Colors.grey.shade700), - const SizedBox(width: 10), - Expanded(child: _buildCountdownTimer(remaining)), - ], - ), - ], - ), - ), - ], + Widget _todayState() { + return Padding( + padding: const EdgeInsets.only(top: 16.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.active, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), ), ), - ), - ], - ); - } - - Widget _buildCountdownTimer(Duration remaining) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Localizations.override( - context: context, - locale: const Locale('en'), - child: SlideCountdown( - duration: remaining, - slideDirection: SlideDirection.up, - separator: ':', - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - color: AppColors.countdown, - ), - separatorStyle: const TextStyle( - fontSize: 15, - color: AppColors.countdown, - ), - decoration: const BoxDecoration(color: Colors.transparent), - shouldShowDays: (d) => d.inDays > 0, - shouldShowHours: (d) => true, - shouldShowMinutes: (d) => true, - ), - ), - const SizedBox(height: 4), - _buildTimerLabels(remaining), - ], - ); - } - - Widget _buildTimerLabels(Duration duration) { - const labelStyle = TextStyle(fontSize: 10, color: AppColors.selectedImg); - - List labels = []; - if (duration.inDays > 0) { - labels.add(const SizedBox(width: 30, child: Text("روز", style: labelStyle))); - } - if (duration.inHours > 0 || duration.inDays > 0) { - labels.add(const SizedBox(width: 35, child: Text("ساعت", style: labelStyle))); - } - labels.add(const SizedBox(width: 30, child: Text("دقیقه", style: labelStyle))); - labels.add(const SizedBox(width: 30, child: Text("ثانیه", style: labelStyle))); - - - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: labels.reversed.toList(), - ); - } - - PreferredSizeWidget _buildCustomAppBar(BuildContext context) { - return PreferredSize( - preferredSize: const Size.fromHeight(70.0), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.vertical( - bottom: Radius.circular(15), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SvgPicture.asset(Assets.icons.logoWithName), - Row( - children: [ - IconButton( - onPressed: () {}, - icon: SvgPicture.asset( - Assets.icons.discountShape, - color: Colors.black, - ), - ), - IconButton( - onPressed: () {}, - icon: SvgPicture.asset(Assets.icons.scanBarcode), - ), - ], - ), - ], + onPressed: () {}, + child: const Text( + "خلاصه عملکرد امروز", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.normal, + color: Colors.white, ), ), ), diff --git a/lib/presentation/pages/login_page.dart b/lib/presentation/pages/login_page.dart index 37f2e3a..1f9aa9f 100644 --- a/lib/presentation/pages/login_page.dart +++ b/lib/presentation/pages/login_page.dart @@ -17,7 +17,6 @@ class LoginPage extends StatefulWidget { class _LoginPageState extends State { final TextEditingController _phoneController = TextEditingController(); Country _selectedCountry = Country.parse('IR'); - bool _keepSignedIn = false; @override void dispose() { diff --git a/lib/presentation/pages/otp_page.dart b/lib/presentation/pages/otp_page.dart index d167308..9d2d2ad 100644 --- a/lib/presentation/pages/otp_page.dart +++ b/lib/presentation/pages/otp_page.dart @@ -13,8 +13,11 @@ import 'package:flutter_svg/svg.dart'; class OtpPage extends StatefulWidget { final String phoneNumber; final String countryCode; - const OtpPage( - {super.key, required this.phoneNumber, required this.countryCode}); + const OtpPage({ + super.key, + required this.phoneNumber, + required this.countryCode, + }); @override State createState() => _OtpPageState(); @@ -141,7 +144,7 @@ class _OtpPageState extends State { ) else const SizedBox(height: 32), - + // *** CHANGE IS HERE *** BlocConsumer( listener: (context, state) { @@ -151,26 +154,29 @@ class _OtpPageState extends State { _errorMessage = state.message; }); } - + if (state is ShopExists) { - Navigator.of(context).pushAndRemoveUntil( + Navigator.of(context).pushReplacement( MaterialPageRoute( - builder: (_) => BlocProvider( - create: (context) => HomeBloc(), - child: const HomePage(), - ), + builder: + (_) => BlocProvider( + // بلافاصله پس از ایجاد، رویداد دریافت تخفیف‌ها را ارسال می‌کند + create: + (context) => + HomeBloc()..add(FetchDiscounts()), + child: const HomePage(), + ), ), - (route) => false, ); } - // If no shop, navigate to StoreInfoPage to create one if (state is NoShop) { Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( - builder: (_) => BlocProvider( - create: (context) => StoreInfoBloc(), - child: const StoreInfoPage(), - ), + builder: + (_) => BlocProvider( + create: (context) => StoreInfoBloc(), + child: const StoreInfoPage(), + ), ), (route) => false, ); @@ -206,19 +212,20 @@ class _OtpPageState extends State { builder: (context, canResend, child) { return canResend ? TextButton( - onPressed: _resendOtp, - child: const Text( - "ارسال مجدد کد", - style: TextStyle(color: AppColors.active), - ), - ) + onPressed: _resendOtp, + child: const Text( + "ارسال مجدد کد", + style: TextStyle(color: AppColors.active), + ), + ) : ValueListenableBuilder( - valueListenable: _otpTimer.remainingSeconds, - builder: (context, seconds, child) => Text( - "${_otpTimer.formatTime()} تا دریافت مجدد", - style: const TextStyle(color: Colors.grey), - ), - ); + valueListenable: _otpTimer.remainingSeconds, + builder: + (context, seconds, child) => Text( + "${_otpTimer.formatTime()} تا دریافت مجدد", + style: const TextStyle(color: Colors.grey), + ), + ); }, ), ], @@ -256,14 +263,15 @@ class _OtpPageState extends State { enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( - color: _hasError - ? Colors.red - : (Theme.of(context) - .inputDecorationTheme - .enabledBorder - ?.borderSide - .color ?? - Colors.grey), + color: + _hasError + ? Colors.red + : (Theme.of(context) + .inputDecorationTheme + .enabledBorder + ?.borderSide + .color ?? + Colors.grey), ), ), focusedBorder: OutlineInputBorder( @@ -303,12 +311,12 @@ class _OtpPageState extends State { final otpCode = _controllers.map((c) => c.text).join(); if (otpCode.length == 5) { context.read().add( - VerifyOTPEvent( - otp: otpCode, - phoneNumber: widget.phoneNumber, - countryCode: widget.countryCode, - ), - ); + VerifyOTPEvent( + otp: otpCode, + phoneNumber: widget.phoneNumber, + countryCode: widget.countryCode, + ), + ); } } @@ -321,8 +329,12 @@ class _OtpPageState extends State { } _isOtpComplete = false; }); - context.read().add(SendOTPEvent( - phoneNumber: widget.phoneNumber, countryCode: widget.countryCode)); + context.read().add( + SendOTPEvent( + phoneNumber: widget.phoneNumber, + countryCode: widget.countryCode, + ), + ); _otpTimer.resetTimer(); } -} \ No newline at end of file +} diff --git a/lib/presentation/pages/product_creation_landing_page.dart b/lib/presentation/pages/product_creation_landing_page.dart index 4acd341..9debd37 100644 --- a/lib/presentation/pages/product_creation_landing_page.dart +++ b/lib/presentation/pages/product_creation_landing_page.dart @@ -1,6 +1,7 @@ import 'package:business_panel/core/config/app_colors.dart'; import 'package:business_panel/gen/assets.gen.dart'; import 'package:business_panel/presentation/pages/add_discount_page.dart'; +import 'package:business_panel/presentation/widgets/custom_app_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -10,7 +11,7 @@ class ProductCreationLandingPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: _buildCustomAppBar(context), + appBar: CustomAppBar(), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Column( @@ -59,56 +60,3 @@ class ProductCreationLandingPage extends StatelessWidget { ); } } - -PreferredSizeWidget _buildCustomAppBar(BuildContext context) { - return PreferredSize( - preferredSize: const Size.fromHeight(70.0), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.vertical(bottom: Radius.circular(15)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Column( - children: [ - const SizedBox(height: 15), - Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 8), - child: SvgPicture.asset(Assets.icons.logoWithName), - ), - const Spacer(), - Row( - children: [ - IconButton( - onPressed: () {}, - icon: SvgPicture.asset( - Assets.icons.discountShape, - color: Colors.black, - ), - ), - IconButton( - onPressed: () {}, - icon: SvgPicture.asset(Assets.icons.scanBarcode), - ), - ], - ), - ], - ), - ], - ), - ), - ), - ), - ); -} diff --git a/lib/presentation/pages/splash_page.dart b/lib/presentation/pages/splash_page.dart index 27c10fc..78547b7 100644 --- a/lib/presentation/pages/splash_page.dart +++ b/lib/presentation/pages/splash_page.dart @@ -33,38 +33,48 @@ class _SplashPageState extends State { MaterialPageRoute(builder: (_) => const OnboardingPage()), ); } - } else if (state is ShopExists) { - // کاربر هم توکن دارد و هم فروشگاه، به HomePage می‌رود + } else if (state is ShopExists) { Navigator.of(context).pushReplacement( MaterialPageRoute( - builder: (_) => BlocProvider( - create: (context) => HomeBloc(), - child: const HomePage(), - ), + builder: + (_) => BlocProvider( + // بلافاصله پس از ایجاد، رویداد دریافت تخفیف‌ها را ارسال می‌کند + create: (context) => HomeBloc()..add(FetchDiscounts()), + child: const HomePage(), + ), + ), + ); + } + else if (state is AuthOffline) { + // کاربر توکن دارد ولی آفلاین است، به HomePage می‌رود + // HomePage خودش خطای شبکه را مدیریت خواهد کرد + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: + (_) => BlocProvider( + create: (context) => HomeBloc(), + child: const HomePage(), + ), ), ); } else if (state is NoShop) { // کاربر توکن دارد ولی فروشگاه نساخته، به صفحه ساخت فروشگاه می‌رود Navigator.of(context).pushReplacement( MaterialPageRoute( - builder: (_) => BlocProvider( - create: (context) => StoreInfoBloc(), - child: const StoreInfoPage(), - ), + builder: + (_) => BlocProvider( + create: (context) => StoreInfoBloc(), + child: const StoreInfoPage(), + ), ), ); } else if (state is AuthFailure) { - // اگر در هر مرحله خطایی رخ داد، کاربر را به صفحه لاگین بفرست - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const OnboardingPage()), - ); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const OnboardingPage()), + ); } }, - child: const Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), - ), + child: const Scaffold(body: Center(child: CircularProgressIndicator())), ); } -} \ No newline at end of file +} diff --git a/lib/presentation/pages/store_info.dart b/lib/presentation/pages/store_info.dart index db964af..a84f70b 100644 --- a/lib/presentation/pages/store_info.dart +++ b/lib/presentation/pages/store_info.dart @@ -6,6 +6,7 @@ import 'package:business_panel/presentation/pages/store_info_display_page.dart'; import 'package:business_panel/presentation/pages/working_hours_dialog.dart'; import 'package:business_panel/presentation/store_info/bloc/store_info_bloc.dart'; import 'package:business_panel/presentation/store_info/bloc/store_info_state.dart'; +import 'package:business_panel/presentation/widgets/custom_app_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -109,7 +110,7 @@ class _StoreInfoPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: _buildCustomAppBar(context), + appBar: CustomAppBar(), body: BlocListener( listener: (context, state) { if (state.isSuccess) { @@ -617,60 +618,6 @@ class _StoreInfoPageState extends State { } } -PreferredSizeWidget _buildCustomAppBar(BuildContext context) { - return PreferredSize( - preferredSize: const Size.fromHeight(70.0), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.vertical(bottom: Radius.circular(15)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Column( - children: [ - const SizedBox(height: 15), - Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 8), - child: SvgPicture.asset(Assets.icons.logoWithName), - ), - - const Spacer(), - - Row( - children: [ - IconButton( - onPressed: () {}, - icon: SvgPicture.asset( - Assets.icons.discountShape, - color: Colors.black, - ), - ), - IconButton( - onPressed: () {}, - icon: SvgPicture.asset(Assets.icons.scanBarcode), - ), - ], - ), - ], - ), - ], - ), - ), - ), - ), - ); -} diff --git a/lib/presentation/pages/store_info_display_page.dart b/lib/presentation/pages/store_info_display_page.dart index 41eba7b..a2e911e 100644 --- a/lib/presentation/pages/store_info_display_page.dart +++ b/lib/presentation/pages/store_info_display_page.dart @@ -4,6 +4,7 @@ import 'package:business_panel/gen/assets.gen.dart'; import 'package:business_panel/presentation/pages/product_creation_landing_page.dart'; import 'package:business_panel/presentation/store_info/bloc/store_info_bloc.dart'; import 'package:business_panel/presentation/store_info/bloc/store_info_state.dart'; +import 'package:business_panel/presentation/widgets/custom_app_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -16,7 +17,7 @@ class StoreInfoDisplayPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: _buildCustomAppBar(context), + appBar: CustomAppBar(), body: BlocListener( listener: (context, state) { if (state.isSuccess) { @@ -198,62 +199,6 @@ class StoreInfoDisplayPage extends StatelessWidget { ); } - PreferredSizeWidget _buildCustomAppBar(BuildContext context) { - return PreferredSize( - preferredSize: const Size.fromHeight(70.0), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.vertical( - bottom: Radius.circular(15), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Column( - children: [ - const SizedBox(height: 15), - Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 8), - child: SvgPicture.asset(Assets.icons.logoWithName), - ), - - const Spacer(), - - Row( - children: [ - IconButton( - onPressed: () {}, - icon: SvgPicture.asset( - Assets.icons.discountShape, - color: Colors.black, - ), - ), - IconButton( - onPressed: () {}, - icon: SvgPicture.asset(Assets.icons.scanBarcode), - ), - ], - ), - ], - ), - ], - ), - ), - ), - ), - ); - } String _translateDay(String day) { const Map dayTranslations = { diff --git a/lib/presentation/widgets/active_discount_card.dart b/lib/presentation/widgets/active_discount_card.dart new file mode 100644 index 0000000..51616c5 --- /dev/null +++ b/lib/presentation/widgets/active_discount_card.dart @@ -0,0 +1,233 @@ +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'; +import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; +import 'package:business_panel/presentation/pages/add_discount_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:intl/intl.dart' show NumberFormat; +import 'package:slide_countdown/slide_countdown.dart'; + +class ActiveDiscountCard extends StatelessWidget { + final DiscountEntity discount; + + const ActiveDiscountCard({super.key, required this.discount}); + @override + Widget build(BuildContext context) { + // ... (تمام کد مربوط به _buildDiscountCard از home_page.dart به اینجا منتقل شد) + // ... (متدهای کمکی مثل _buildCountdownTimer و _buildTimerLabels هم به اینجا منتقل شدند) + final remaining = + discount.endDate != null ? discount.endDate!.difference(DateTime.now()) : const Duration(seconds: -1); + + final int discountPercentage = (discount.price > 0 && discount.price > discount.nPrice) + ? (((discount.price - discount.nPrice) / discount.price) * 100).toInt() + : 0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Text( + "تخفیف ${discount.type}", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + ], + ), + const Divider(height: 1), + const SizedBox(height: 10), + Card( + color: Colors.white, + elevation: 0, + margin: const EdgeInsets.only(bottom: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + side: BorderSide(color: Colors.grey.shade300, width: 1), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (discount.images.isNotEmpty && discount.images.first.isNotEmpty) + ClipRRect( + borderRadius: BorderRadius.circular(15), + child: Image.network( + discount.images.first, + width: 100, + height: 100, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(15), + ), + child: const Icon( + Icons.image_not_supported, + color: Colors.grey, + ), + ), + ), + ) + else + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(15), + ), + child: const Icon( + Icons.store, + color: Colors.grey, + size: 50, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.icons.shoppingCart, + width: 18, + color: Colors.grey.shade700, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + discount.name, + style: TextStyle( + fontSize: 15, + color: Colors.grey.shade600, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 12), + if (discount.endDate == null) + const Text( + 'تاریخ نامعتبر', + style: TextStyle( + color: Colors.orange, + fontWeight: FontWeight.bold, + ), + ) + else if (remaining.isNegative) + const Text( + 'منقضی شده', + style: TextStyle( + color: AppColors.expiryReserve, + fontWeight: FontWeight.bold, + ), + ) + else + Row( + children: [ + SvgPicture.asset( + Assets.icons.timerPause, + width: 18, + color: Colors.grey.shade700, + ), + const SizedBox(width: 10), + Expanded(child: _buildCountdownTimer(remaining)), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + SvgPicture.asset( + Assets.icons.scanBarcode, + width: 18, + color: Colors.grey.shade700, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + "اسکن بارکد مشتری", + style: TextStyle( + fontSize: 15, + color: AppColors.active, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + + ], + ), + ), + ), + const SizedBox(height: 4), + ], + ); + } + + Widget _buildCountdownTimer(Duration remaining) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Directionality( + textDirection: TextDirection.ltr, + child: SlideCountdown( + duration: remaining, + slideDirection: SlideDirection.up, + separator: ':', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: AppColors.countdown, + ), + separatorStyle: const TextStyle( + fontSize: 15, + color: AppColors.countdown, + ), + decoration: const BoxDecoration(color: Colors.transparent), + shouldShowDays: (d) => d.inDays > 0, + shouldShowHours: (d) => true, + shouldShowMinutes: (d) => true, + ), + ), + const SizedBox(height: 4), + _buildTimerLabels(remaining), + ], + ); + } + + Widget _buildTimerLabels(Duration duration) { + const labelStyle = TextStyle(fontSize: 9, color: AppColors.selectedImg); + List labels = []; + if (duration.inDays > 0) { + labels.add(const SizedBox(width: 30, child: Text("روز", style: labelStyle))); + } + if (duration.inHours > 0 || duration.inDays > 0) { + labels.add(const SizedBox(width: 35, child: Text("ساعت", style: labelStyle))); + } + labels.add(const SizedBox(width: 35, child: Text("دقیقه", style: labelStyle))); + labels.add(const SizedBox(width: 35, child: Text(" ثانیه", style: labelStyle))); + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: labels.reversed.toList(), + ); + } +} \ No newline at end of file diff --git a/lib/presentation/widgets/custom_app_bar.dart b/lib/presentation/widgets/custom_app_bar.dart new file mode 100644 index 0000000..d0fa6e9 --- /dev/null +++ b/lib/presentation/widgets/custom_app_bar.dart @@ -0,0 +1,111 @@ +import 'package:business_panel/core/config/app_colors.dart'; +import 'package:business_panel/gen/assets.gen.dart'; +import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; +import 'package:business_panel/presentation/pages/discount_manegment_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + const CustomAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(15)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: SafeArea( + child: Padding( + padding: EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SvgPicture.asset(Assets.icons.logoWithName), + Row( + children: [ + IconButton( + onPressed: () {}, + icon: SvgPicture.asset( + Assets.icons.discountShape, + color: Colors.black, + ), + ), + BlocBuilder( + builder: (context, state) { + final count = + state is HomeLoaded ? state.discounts.length : 0; + return Stack( + alignment: Alignment.center, + children: [ + IconButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const DiscountManegment(), + ), + ); + }, + icon: SvgPicture.asset(Assets.icons.scanBarcode), + ), + if (count > 0) + Positioned( + top: 2, + right: 6, + child: GestureDetector( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const DiscountManegment(), + ), + ); + }, + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: AppColors.selectedImg, + shape: BoxShape.circle, + ), + constraints: const BoxConstraints( + minWidth: 16, + minHeight: 16, + ), + child: Padding( + padding: const EdgeInsets.all(2.0), + child: Text( + '$count', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ), + ), + ), + ], + ); + }, + ), + ], + ), + ], + ), + ), + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(70.0); +} diff --git a/lib/presentation/widgets/custom_app_bar_single.dart b/lib/presentation/widgets/custom_app_bar_single.dart new file mode 100644 index 0000000..b4d82f3 --- /dev/null +++ b/lib/presentation/widgets/custom_app_bar_single.dart @@ -0,0 +1,55 @@ +import 'package:business_panel/core/config/app_colors.dart'; +import 'package:business_panel/gen/assets.gen.dart'; +import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; +import 'package:business_panel/presentation/pages/discount_manegment_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class CustomAppBarSingle extends StatelessWidget implements PreferredSizeWidget { + + String page = ""; + + CustomAppBarSingle({super.key,required this.page}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(15)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: SafeArea( + child: Padding( + padding: EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text("مدیریت $page",style: TextStyle(fontSize: 15)), + SizedBox(width: 15,), + InkWell( + onTap: () { + Navigator.pop(context); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset(Assets.icons.arrowLeft), + ), + ) + ], + ), + ), + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(70.0); +} \ No newline at end of file diff --git a/lib/presentation/widgets/discount_card.dart.dart b/lib/presentation/widgets/discount_card.dart.dart new file mode 100644 index 0000000..cc36b6e --- /dev/null +++ b/lib/presentation/widgets/discount_card.dart.dart @@ -0,0 +1,300 @@ +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'; +import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; +import 'package:business_panel/presentation/pages/add_discount_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:intl/intl.dart' show NumberFormat; +import 'package:slide_countdown/slide_countdown.dart'; + +class DiscountCard extends StatelessWidget { + final DiscountEntity discount; + + const DiscountCard({super.key, required this.discount}); + + @override + Widget build(BuildContext context) { + final remaining = + discount.endDate != null ? discount.endDate!.difference(DateTime.now()) : const Duration(seconds: -1); + + final int discountPercentage = (discount.price > 0 && discount.price > discount.nPrice) + ? (((discount.price - discount.nPrice) / discount.price) * 100).toInt() + : 0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + "تخفیف ${discount.type}", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + InkWell( + onTap: () { + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (_) => AddDiscountPage(discountId: discount.id), + ), + ) + .then((value) { + if (value == true) { + context.read().add(FetchDiscounts()); + } + }); + }, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + SvgPicture.asset( + Assets.icons.edit, + width: 20, + color: AppColors.active, + ), + const SizedBox(width: 5), + const Text( + "ویرایش", + style: TextStyle(color: AppColors.active), + ), + ], + ), + ), + ), + ], + ), + const Divider(height: 1), + const SizedBox(height: 10), + Card( + color: Colors.white, + elevation: 0, + margin: const EdgeInsets.only(bottom: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + side: BorderSide(color: Colors.grey.shade300, width: 1), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (discount.images.isNotEmpty && discount.images.first.isNotEmpty) + ClipRRect( + borderRadius: BorderRadius.circular(15), + child: Image.network( + discount.images.first, + width: 100, + height: 100, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(15), + ), + child: const Icon( + Icons.image_not_supported, + color: Colors.grey, + ), + ), + ), + ) + else + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(15), + ), + child: const Icon( + Icons.store, + color: Colors.grey, + size: 50, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.icons.shop, + width: 18, + color: Colors.grey.shade700, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + discount.shopName, + style: const TextStyle(fontSize: 16), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + SvgPicture.asset( + Assets.icons.shoppingCart, + width: 18, + color: Colors.grey.shade700, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + discount.name, + style: TextStyle( + fontSize: 15, + color: Colors.grey.shade600, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 12), + if (discount.endDate == null) + const Text( + 'تاریخ نامعتبر', + style: TextStyle( + color: Colors.orange, + fontWeight: FontWeight.bold, + ), + ) + else if (remaining.isNegative) + const Text( + 'منقضی شده', + style: TextStyle( + color: AppColors.expiryReserve, + fontWeight: FontWeight.bold, + ), + ) + else + Row( + children: [ + SvgPicture.asset( + Assets.icons.timerPause, + width: 18, + color: Colors.grey.shade700, + ), + const SizedBox(width: 10), + Expanded(child: _buildCountdownTimer(remaining)), + ], + ), + const SizedBox(height: 16), + ], + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + NumberFormat('#,##0').format(discount.nPrice), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.red, + ), + ), + const SizedBox(width: 8), + if (discountPercentage > 0) + Text( + '($discountPercentage%)', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.red, + ), + ), + const SizedBox(width: 4), + Text( + NumberFormat('#,##0').format(discount.price), + style: const TextStyle( + fontSize: 14, + color: Color.fromARGB(255, 124, 124, 124), + decoration: TextDecoration.lineThrough, + ), + ), + const SizedBox(width: 4), + SvgPicture.asset(Assets.icons.cardPos), + ], + ), + ), + const SizedBox(height: 4), + ], + ); + } + + Widget _buildCountdownTimer(Duration remaining) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Directionality( + textDirection: TextDirection.ltr, + child: SlideCountdown( + duration: remaining, + slideDirection: SlideDirection.up, + separator: ':', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: AppColors.countdown, + ), + separatorStyle: const TextStyle( + fontSize: 15, + color: AppColors.countdown, + ), + decoration: const BoxDecoration(color: Colors.transparent), + shouldShowDays: (d) => d.inDays > 0, + shouldShowHours: (d) => true, + shouldShowMinutes: (d) => true, + ), + ), + const SizedBox(height: 4), + _buildTimerLabels(remaining), + ], + ); + } + + Widget _buildTimerLabels(Duration duration) { + const labelStyle = TextStyle(fontSize: 9, color: AppColors.selectedImg); + List labels = []; + if (duration.inDays > 0) { + labels.add(const SizedBox(width: 30, child: Text("روز", style: labelStyle))); + } + if (duration.inHours > 0 || duration.inDays > 0) { + labels.add(const SizedBox(width: 35, child: Text("ساعت", style: labelStyle))); + } + labels.add(const SizedBox(width: 35, child: Text("دقیقه", style: labelStyle))); + labels.add(const SizedBox(width: 35, child: Text(" ثانیه", style: labelStyle))); + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: labels.reversed.toList(), + ); + } +} \ No newline at end of file