diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d59eecf..6a292df 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,8 @@ + + '} static String editDiscount(String id) => '$baseUrl/discount/edit/$id'; static String deleteDiscount(String id) => '$baseUrl/discount/delete/$id'; + + // ========== Order Endpoints ========== + /// Endpoint to add a new order. + /// Method: POST + /// Body: {'Discount': discountId, 'User': userId} + /// Headers: {'Authorization': 'Bearer '} + static const String addOrder = '$baseUrl/order/add'; } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index e0ffce9..656da74 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:business_panel/core/config/app_colors.dart'; import 'package:business_panel/presentation/auth/bloc/auth_bloc.dart'; import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; +import 'package:business_panel/presentation/order/bloc/order_bloc.dart'; import 'package:business_panel/presentation/pages/splash_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -31,6 +32,7 @@ class MyApp extends StatelessWidget { BlocProvider( create: (context) => HomeBloc()..add(FetchDiscounts()), ), + BlocProvider(create: (context) => OrderBloc()), ], child: MaterialApp( title: 'Proxibuy', diff --git a/lib/presentation/discount/bloc/discount_bloc.dart b/lib/presentation/discount/bloc/discount_bloc.dart index 3134069..345d67c 100644 --- a/lib/presentation/discount/bloc/discount_bloc.dart +++ b/lib/presentation/discount/bloc/discount_bloc.dart @@ -1,4 +1,3 @@ -// lib/presentation/discount/bloc/discount_bloc.dart import 'dart:developer'; import 'package:business_panel/core/config/api_config.dart'; @@ -6,10 +5,8 @@ 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(); @@ -156,6 +153,10 @@ class DiscountBloc extends Bloc { discountId: event.discountId, ); }); + + on((event, emit) { + emit(state.copyWith(clearErrorMessage: true)); + }); } Future _submitDiscountForm( @@ -224,7 +225,7 @@ class DiscountBloc extends Bloc { ); emit(state.copyWith(isSubmitting: false, isSuccess: true)); - } on DioException catch (e) { + }on DioException catch (e) { emit( state.copyWith( isSubmitting: false, diff --git a/lib/presentation/discount/bloc/discount_event.dart b/lib/presentation/discount/bloc/discount_event.dart index fa28db6..2d5df61 100644 --- a/lib/presentation/discount/bloc/discount_event.dart +++ b/lib/presentation/discount/bloc/discount_event.dart @@ -58,8 +58,9 @@ class NotificationRadiusChanged extends DiscountEvent { 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 +} + +class ClearErrorMessage extends DiscountEvent {} // <-- این رویداد جدید را اضافه کنید diff --git a/lib/presentation/discount/bloc/discount_state.dart b/lib/presentation/discount/bloc/discount_state.dart index 769ad64..15d2b16 100644 --- a/lib/presentation/discount/bloc/discount_state.dart +++ b/lib/presentation/discount/bloc/discount_state.dart @@ -1,10 +1,7 @@ -// lib/presentation/discount/bloc/discount_state.dart - import 'package:equatable/equatable.dart'; class DiscountState extends Equatable { final String? discountId; - // *** CHANGE IS HERE: Storing image ID and URL together *** final List> productImages; final String productName; final String? discountTypeId; @@ -57,6 +54,7 @@ class DiscountState extends Equatable { bool? isLoadingDetails, bool? isSuccess, String? errorMessage, + bool? clearErrorMessage, }) { return DiscountState( discountId: discountId ?? this.discountId, @@ -74,7 +72,7 @@ class DiscountState extends Equatable { isSubmitting: isSubmitting ?? this.isSubmitting, isLoadingDetails: isLoadingDetails ?? this.isLoadingDetails, isSuccess: isSuccess ?? this.isSuccess, - errorMessage: errorMessage ?? this.errorMessage, + errorMessage: (clearErrorMessage == true) ? null : errorMessage ?? this.errorMessage, ); } diff --git a/lib/presentation/order/bloc/order_bloc.dart b/lib/presentation/order/bloc/order_bloc.dart new file mode 100644 index 0000000..3f2796d --- /dev/null +++ b/lib/presentation/order/bloc/order_bloc.dart @@ -0,0 +1,46 @@ +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:dio/dio.dart'; + +part 'order_event.dart'; +part 'order_state.dart'; + +class OrderBloc extends Bloc { + final Dio _dio = Dio(); + final TokenStorageService _tokenStorage = TokenStorageService(); + + OrderBloc() : super(OrderInitial()) { + on((event, emit) async { + emit(OrderSubmissionInProgress()); + try { + final token = await _tokenStorage.getAccessToken(); + if (token == null) { + emit(OrderSubmissionFailure("خطای احراز هویت.")); + return; + } + + final response = await _dio.post( + ApiConfig.addOrder, + data: { + 'Discount': event.discountId, + 'User': event.userId, + }, + options: Options( + headers: {'Authorization': 'Bearer $token'}, + ), + ); + + if (response.statusCode == 200) { + emit(OrderSubmissionSuccess(response.data['message'] ?? "سفارش با موفقیت ثبت شد.")); + } else { + emit(OrderSubmissionFailure(response.data['message'] ?? 'خطا در ثبت سفارش.')); + } + } on DioException catch (e) { + emit(OrderSubmissionFailure(e.response?.data['message'] ?? 'خطای شبکه.')); + } catch (e) { + emit(OrderSubmissionFailure('خطای پیش‌بینی نشده: ${e.toString()}')); + } + }); + } +} \ No newline at end of file diff --git a/lib/presentation/order/bloc/order_event.dart b/lib/presentation/order/bloc/order_event.dart new file mode 100644 index 0000000..be182dc --- /dev/null +++ b/lib/presentation/order/bloc/order_event.dart @@ -0,0 +1,10 @@ +part of 'order_bloc.dart'; + +abstract class OrderEvent {} + +class SubmitOrder extends OrderEvent { + final String discountId; + final String userId; + + SubmitOrder({required this.discountId, required this.userId}); +} \ No newline at end of file diff --git a/lib/presentation/order/bloc/order_state.dart b/lib/presentation/order/bloc/order_state.dart new file mode 100644 index 0000000..756fc8e --- /dev/null +++ b/lib/presentation/order/bloc/order_state.dart @@ -0,0 +1,17 @@ +part of 'order_bloc.dart'; + +abstract class OrderState {} + +class OrderInitial extends OrderState {} + +class OrderSubmissionInProgress extends OrderState {} + +class OrderSubmissionSuccess extends OrderState { + final String message; + OrderSubmissionSuccess(this.message); +} + +class OrderSubmissionFailure extends OrderState { + final String error; + OrderSubmissionFailure(this.error); +} \ 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 351d1f5..dca7d8b 100644 --- a/lib/presentation/pages/add_discount_page.dart +++ b/lib/presentation/pages/add_discount_page.dart @@ -10,11 +10,39 @@ import 'package:business_panel/presentation/pages/home_page.dart'; import 'package:business_panel/presentation/widgets/custom_app_bar.dart'; import 'package:business_panel/presentation/widgets/info_popup.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; import 'package:persian_datetime_picker/persian_datetime_picker.dart'; +class ThousandsSeparatorInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + if (newValue.text.isEmpty) { + return newValue.copyWith(text: ''); + } + + final String newText = newValue.text.replaceAll(',', ''); + final number = int.tryParse(newText); + + if (number == null) { + return oldValue; + } + + final formatter = NumberFormat('#,##0'); + final String newString = formatter.format(number); + + return TextEditingValue( + text: newString, + selection: TextSelection.collapsed(offset: newString.length), + ); + } +} + + class AddDiscountPage extends StatelessWidget { final String? discountId; @@ -49,9 +77,18 @@ class _AddDiscountViewState extends State<_AddDiscountView> { final _descController = TextEditingController(); final _priceController = TextEditingController(); final _discountPriceController = TextEditingController(); + final _priceFormatter = NumberFormat('#,##0'); + bool get _isEditMode => widget.discountId != null; + @override + void initState() { + super.initState(); + _priceController.addListener(() => setState(() {})); + _discountPriceController.addListener(() => setState(() {})); + } + @override void dispose() { _nameController.dispose(); @@ -126,7 +163,6 @@ class _AddDiscountViewState extends State<_AddDiscountView> { "تخفیف با موفقیت ${_isEditMode ? 'ویرایش' : 'ثبت'} شد!"), backgroundColor: Colors.green), ); - // HIGHLIGHT: مسیریابی به صفحه‌ی اصلی و حذف تمام صفحات قبلی Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (_) => BlocProvider( @@ -142,12 +178,15 @@ class _AddDiscountViewState extends State<_AddDiscountView> { SnackBar( content: Text(state.errorMessage!), backgroundColor: Colors.red), ); + context.read().add(ClearErrorMessage()); } if (state.productName.isNotEmpty && _nameController.text.isEmpty) { _nameController.text = state.productName; _descController.text = state.description; - _priceController.text = state.price; - _discountPriceController.text = state.discountedPrice; + final price = int.tryParse(state.price.replaceAll(',', '')) ?? 0; + _priceController.text = _priceFormatter.format(price); + final discountedPrice = int.tryParse(state.discountedPrice.replaceAll(',', '')) ?? 0; + _discountPriceController.text = _priceFormatter.format(discountedPrice); } }, builder: (context, state) { @@ -185,7 +224,7 @@ class _AddDiscountViewState extends State<_AddDiscountView> { context.read().add(ProductNameChanged(value)), ), const SizedBox(height: 30), - _buildDiscountTypeDropdown(state), // Pass state here + _buildDiscountTypeDropdown(state), const SizedBox(height: 30), _buildTextField( controller: _descController, @@ -202,25 +241,19 @@ class _AddDiscountViewState extends State<_AddDiscountView> { const SizedBox(height: 30), _buildTimeRangePicker(context), const SizedBox(height: 30), - _buildTextField( + + _buildPriceField( controller: _priceController, - label: "قیمت بدون تخفیف", - isRequired: true, - hint: "مثلاً 240000 تومان", - keyboardType: TextInputType.number, - onChanged: (value) => - context.read().add(PriceChanged(value)), + label: "قیمت بدون تخفیف (تومان)", + hint: "مثلاً 240,000", + onChanged: (value) => context.read().add(PriceChanged(value)), ), const SizedBox(height: 30), - _buildTextField( + _buildPriceField( controller: _discountPriceController, - label: "قیمت با تخفیف", - hint: "مثلاً 200000 تومان", - isRequired: true, - keyboardType: TextInputType.number, - onChanged: (value) => context - .read() - .add(DiscountedPriceChanged(value)), + label: "قیمت با تخفیف (تومان)", + hint: "مثلاً 200,000", + onChanged: (value) => context.read().add(DiscountedPriceChanged(value)), ), const SizedBox(height: 30), _buildNotificationRadiusSlider(), @@ -253,6 +286,30 @@ class _AddDiscountViewState extends State<_AddDiscountView> { ); } + Widget _buildPriceField({ + required TextEditingController controller, + required String label, + required String hint, + required ValueChanged onChanged, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: controller, + onChanged: (value) => onChanged(value.replaceAll(',', '')), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ThousandsSeparatorInputFormatter(), + ], + decoration: _inputDecoration(label, hint: hint, isRequired: true), + ), + + ], + ); + } + Widget _buildSectionTitle({ required String title, String? popupTitle, @@ -287,7 +344,6 @@ class _AddDiscountViewState extends State<_AddDiscountView> { 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); @@ -296,7 +352,6 @@ class _AddDiscountViewState extends State<_AddDiscountView> { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(2, (index) { - // *** CHANGE IS HERE: Read from the map structure *** final imageMap = displayImages[index]; final imageUrl = imageMap?['url']; final isUrl = imageUrl?.startsWith('http') ?? false; @@ -345,16 +400,14 @@ class _AddDiscountViewState extends State<_AddDiscountView> { } 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: selectedValue, // Use the safe value here. + value: selectedValue, icon: SvgPicture.asset( Assets.icons.arrowDown, width: 24, @@ -411,7 +464,6 @@ class _AddDiscountViewState extends State<_AddDiscountView> { Expanded( child: Text( displayText, - textDirection: TextDirection.rtl, style: const TextStyle(fontSize: 15), ), ), diff --git a/lib/presentation/pages/barcode_scanner_page.dart b/lib/presentation/pages/barcode_scanner_page.dart new file mode 100644 index 0000000..78543ae --- /dev/null +++ b/lib/presentation/pages/barcode_scanner_page.dart @@ -0,0 +1,124 @@ +import 'package:audioplayers/audioplayers.dart'; +import 'package:business_panel/presentation/order/bloc/order_bloc.dart'; +import 'package:business_panel/presentation/widgets/success_popup.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:jwt_decoder/jwt_decoder.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:vibration/vibration.dart'; + +class BarcodeScannerPage extends StatefulWidget { + final String discountId; + const BarcodeScannerPage({super.key, required this.discountId}); + + @override + State createState() => _BarcodeScannerPageState(); +} + +class _BarcodeScannerPageState extends State { + final MobileScannerController _scannerController = MobileScannerController(); + final AudioPlayer _audioPlayer = AudioPlayer(); + bool _isProcessing = false; + + void _handleBarcode(BarcodeCapture capture) { + if (_isProcessing) return; + setState(() => _isProcessing = true); + + final String? rawValue = capture.barcodes.first.rawValue; + if (rawValue == null) { + _showError("بارکد نامعتبر است."); + return; + } + + try { + final Map decodedToken = JwtDecoder.decode(rawValue); + + final String? userId = decodedToken['userID']; + final String? discountIdFromToken = decodedToken['discountID']; + + if (userId == null || discountIdFromToken == null) { + _showError("اطلاعات لازم در بارکد یافت نشد."); + return; + } + + if (discountIdFromToken != widget.discountId) { + _showError("این بارکد برای این تخفیف معتبر نیست."); + return; + } + + context.read().add( + SubmitOrder(discountId: discountIdFromToken, userId: userId), + ); + + } catch (e) { + _showError("فرمت بارکد صحیح نیست."); + } + } + + void _showError(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message), backgroundColor: Colors.red), + ); + setState(() => _isProcessing = false); + } + + @override + void dispose() { + _scannerController.dispose(); + _audioPlayer.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("اسکن بارکد مشتری")), + body: BlocListener( + listener: (context, state) async { + if (state is OrderSubmissionSuccess) { + 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(); + } else if (state is OrderSubmissionFailure) { + _showError(state.error); + } + }, + child: Stack( + children: [ + MobileScanner( + controller: _scannerController, + onDetect: _handleBarcode, + ), + Center( + child: Container( + width: 250, + height: 250, + decoration: BoxDecoration( + border: Border.all(color: Colors.green, width: 4), + borderRadius: BorderRadius.circular(12), + ), + ), + ), + BlocBuilder( + builder: (context, state) { + if (state is OrderSubmissionInProgress) { + return Container( + color: Colors.black.withOpacity(0.5), + child: const Center( + child: CircularProgressIndicator(color: Colors.white), + ), + ); + } + return const SizedBox.shrink(); + }, + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/presentation/pages/store_info.dart b/lib/presentation/pages/store_info.dart index a84f70b..2e4a561 100644 --- a/lib/presentation/pages/store_info.dart +++ b/lib/presentation/pages/store_info.dart @@ -8,7 +8,7 @@ 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/services.dart'; // **ADD THIS IMPORT** import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; @@ -127,6 +127,7 @@ class _StoreInfoPageState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(state.errorMessage!), backgroundColor: Colors.red), ); + context.read().add(ClearStoreInfoError()); } }, child: SingleChildScrollView( @@ -181,6 +182,7 @@ class _StoreInfoPageState extends State { child: Stack( alignment: Alignment.center, children: [ + if(state.logoPath == null) SvgPicture.asset(Assets.icons.addImg), Align( alignment: Alignment.bottomRight, child: CircleAvatar( @@ -248,6 +250,7 @@ class _StoreInfoPageState extends State { _buildTextField( controller: _addressController, label: "جزئیات آدرس", + isRequired: true, maxLines: 3, hint: "خیابان، محله، ساختمان و ....", onChanged: @@ -263,6 +266,8 @@ class _StoreInfoPageState extends State { child: _buildTextField( controller: _plaqueController, label: "پلاک", + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (value) => context.read().add( PlaqueChanged(value), @@ -274,6 +279,8 @@ class _StoreInfoPageState extends State { child: _buildTextField( controller: _postalCodeController, label: "کد پستی", + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (value) => context .read() .add(PostalCodeChanged(value)), @@ -305,7 +312,7 @@ class _StoreInfoPageState extends State { height: 23, ), label: const Text( - "انتخاب آدرس فروشگاه روی نقشه", + "انتخاب آدرس فروشگاه روی نقشه *", style: TextStyle(color: AppColors.button), ), ), @@ -333,7 +340,9 @@ class _StoreInfoPageState extends State { _buildTextField( controller: _phoneController, label: "تلفن تماس", + isRequired: true, keyboardType: TextInputType.phone, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], hint: "شماره تماس ثابت یا موبایل فروشگاه", onChanged: (value) => context.read().add(ContactPhoneChanged(value)), @@ -346,6 +355,8 @@ class _StoreInfoPageState extends State { _buildTextField( controller: _licenseController, label: "شماره جواز کسب", + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], hint: "شناسه صنفی 12 رقمی یکتا", onChanged: (value) => context.read().add(LicenseNumberChanged(value)), @@ -353,18 +364,32 @@ class _StoreInfoPageState extends State { const SizedBox(height: 44), SizedBox( width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => BlocProvider.value( - value: BlocProvider.of(context), - child: const StoreInfoDisplayPage(), - ), + child: BlocBuilder( + builder: (context, state) { + return ElevatedButton( + onPressed: state.isFormValid ? () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => BlocProvider.value( + value: BlocProvider.of(context), + child: const StoreInfoDisplayPage(), + ), + ), + ); + } : () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("لطفاً تمام فیلدهای ستاره‌دار را پر کنید."), + backgroundColor: Colors.red, + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: state.isFormValid ? AppColors.button : Colors.grey, ), + child: const Text("تایید و ادامه"), ); }, - child: const Text("تایید و ادامه"), ), ), const SizedBox(height: 34), @@ -391,6 +416,7 @@ class _StoreInfoPageState extends State { TextInputType? keyboardType, TextEditingController? controller, ValueChanged? onChanged, + List? inputFormatters, }) { return TextFormField( controller: controller, @@ -398,6 +424,7 @@ class _StoreInfoPageState extends State { maxLines: maxLines, maxLength: maxLength, keyboardType: keyboardType, + inputFormatters: inputFormatters, decoration: InputDecoration( counterText: "", hintText: hint, @@ -577,7 +604,7 @@ class _StoreInfoPageState extends State { child: DropdownButtonFormField( value: context.watch().state.activityTypeId, icon: SvgPicture.asset( - Assets.icons.arrowDown, + Assets.icons.arrowDown, width: 24, color: Colors.black, ), @@ -616,56 +643,4 @@ class _StoreInfoPageState extends State { ), ); } -} - - - - -// Future _pickWorkingHours(BuildContext context) async { -// // ۱. انتخاب تاریخ شروع -// Jalali? startDate = await showPersianDatePicker( -// context: context, -// initialDate: Jalali.now(), -// firstDate: Jalali(1400), -// lastDate: Jalali(1405), -// ); -// if (startDate == null || !context.mounted) return; - -// // ۲. انتخاب ساعت شروع -// TimeOfDay? startTime = await showTimePicker( -// context: context, -// initialTime: TimeOfDay.now(), -// ); -// if (startTime == null || !context.mounted) return; - -// // ۳. انتخاب تاریخ پایان -// Jalali? endDate = await showPersianDatePicker( -// context: context, -// initialDate: startDate, // شروع از تاریخ انتخابی قبلی -// firstDate: startDate, // تاریخ پایان نمی‌تواند قبل از شروع باشد -// lastDate: Jalali(1405), -// ); -// if (endDate == null || !context.mounted) return; - -// // ۴. انتخاب ساعت پایان -// TimeOfDay? endTime = await showTimePicker( -// context: context, -// initialTime: startTime, -// ); -// if (endTime == null || !context.mounted) return; - -// // ۵. تبدیل به آبجکت DateTime و ارسال به BLoC -// final DateTime startDateTime = startDate.toDateTime().add( -// Duration(hours: startTime.hour, minutes: startTime.minute), -// ); -// final DateTime endDateTime = endDate.toDateTime().add( -// Duration(hours: endTime.hour, minutes: endTime.minute), -// ); - -// context.read().add( -// WorkingHoursChanged( -// startDateTime: startDateTime, -// endDateTime: endDateTime, -// ), -// ); -// } \ No newline at end of file +} \ No newline at end of file diff --git a/lib/presentation/store_info/bloc/store_info_bloc.dart b/lib/presentation/store_info/bloc/store_info_bloc.dart index 050fe3e..4811519 100644 --- a/lib/presentation/store_info/bloc/store_info_bloc.dart +++ b/lib/presentation/store_info/bloc/store_info_bloc.dart @@ -13,6 +13,10 @@ class StoreInfoBloc extends Bloc { final TokenStorageService _tokenStorage = TokenStorageService(); StoreInfoBloc() : super(StoreInfoState()) { + on((event, emit) { + emit(state.copyWith(clearErrorMessage: true)); + }); + on((event, emit) { emit(state.copyWith(logoPath: event.imagePath)); }); diff --git a/lib/presentation/store_info/bloc/store_info_event.dart b/lib/presentation/store_info/bloc/store_info_event.dart index 7d0bac3..2a3f10e 100644 --- a/lib/presentation/store_info/bloc/store_info_event.dart +++ b/lib/presentation/store_info/bloc/store_info_event.dart @@ -1,7 +1,9 @@ + part of 'store_info_bloc.dart'; abstract class StoreInfoEvent {} +class ClearStoreInfoError extends StoreInfoEvent {} class StoreLogoChanged extends StoreInfoEvent { final String imagePath; StoreLogoChanged(this.imagePath); diff --git a/lib/presentation/store_info/bloc/store_info_state.dart b/lib/presentation/store_info/bloc/store_info_state.dart index fc82a1d..ca4a40c 100644 --- a/lib/presentation/store_info/bloc/store_info_state.dart +++ b/lib/presentation/store_info/bloc/store_info_state.dart @@ -43,6 +43,21 @@ class StoreInfoState { this.activityTypeId, }); + bool get isFormValid => + storeName.isNotEmpty && + activityTypeId != null && + activityTypeId!.isNotEmpty && + address.isNotEmpty && + contactPhone != null && + contactPhone!.isNotEmpty && + workingDays.isNotEmpty && + startTime != null && + startTime!.isNotEmpty && + endTime != null && + endTime!.isNotEmpty && + latitude != null && + longitude != null; + StoreInfoState copyWith({ String? logoPath, String? storeName, @@ -64,6 +79,7 @@ class StoreInfoState { String? endTime, List? features, String? activityTypeId, + bool? clearErrorMessage, }) { return StoreInfoState( logoPath: logoPath ?? this.logoPath, @@ -76,7 +92,7 @@ class StoreInfoState { postalCode: postalCode ?? this.postalCode, isSubmitting: isSubmitting ?? this.isSubmitting, isSuccess: isSuccess ?? this.isSuccess, - errorMessage: errorMessage ?? this.errorMessage, + errorMessage: (clearErrorMessage == true) ? null : errorMessage ?? this.errorMessage, latitude: latitude ?? this.latitude, longitude: longitude ?? this.longitude, contactPhone: contactPhone ?? this.contactPhone, diff --git a/lib/presentation/widgets/active_discount_card.dart b/lib/presentation/widgets/active_discount_card.dart index 51616c5..32ad109 100644 --- a/lib/presentation/widgets/active_discount_card.dart +++ b/lib/presentation/widgets/active_discount_card.dart @@ -1,8 +1,8 @@ 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/order/bloc/order_bloc.dart'; +import 'package:business_panel/presentation/pages/barcode_scanner_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; @@ -15,8 +15,6 @@ class ActiveDiscountCard extends StatelessWidget { 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); @@ -150,30 +148,42 @@ class ActiveDiscountCard extends StatelessWidget { ], ), 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, + InkWell( // <-- ویجت را در InkWell قرار دهید + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => BlocProvider.value( + value: context.read(), + child: BarcodeScannerPage(discountId: discount.id), ), - overflow: TextOverflow.ellipsis, ), - ), - ], + ); + }, + child: 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, + fontWeight: FontWeight.normal, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), ), ], ), ), - ], ), ), diff --git a/lib/presentation/widgets/discount_card.dart b/lib/presentation/widgets/discount_card.dart index cc36b6e..68ee0bf 100644 --- a/lib/presentation/widgets/discount_card.dart +++ b/lib/presentation/widgets/discount_card.dart @@ -214,7 +214,7 @@ class DiscountCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - NumberFormat('#,##0').format(discount.nPrice), + "${NumberFormat('#,##0').format(discount.nPrice)} تومان", style: const TextStyle( fontSize: 16, fontWeight: FontWeight.normal, diff --git a/lib/presentation/widgets/success_popup.dart b/lib/presentation/widgets/success_popup.dart new file mode 100644 index 0000000..70924fa --- /dev/null +++ b/lib/presentation/widgets/success_popup.dart @@ -0,0 +1,43 @@ +import 'package:business_panel/core/config/app_colors.dart'; +import 'package:business_panel/gen/assets.gen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +Future showSuccessDialog( + BuildContext context, { + required String message, +}) async { + await showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(Assets.icons.tickCircle, height: 80, color: AppColors.confirm), + const SizedBox(height: 24), + Text( + "موفقیت‌آمیز!", + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Text( + message, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16, color: Colors.black54), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("فهمیدم"), + ), + ], + ), + ), + ); + }, + ); +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 0dd74af..52647f4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.12.0" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: e653f162ddfcec1da2040ba2d8553fff1662b5c2a5c636f4c21a3b11bee497de + url: "https://pub.dev" + source: hosted + version: "6.5.0" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 + url: "https://pub.dev" + source: hosted + version: "4.2.1" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" + url: "https://pub.dev" + source: hosted + version: "7.1.1" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7" + url: "https://pub.dev" + source: hosted + version: "4.2.1" bloc: dependency: "direct main" description: @@ -273,6 +329,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" + url: "https://pub.dev" + source: hosted + version: "11.5.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" dio: dependency: "direct main" description: @@ -797,6 +869,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" + jwt_decoder: + dependency: "direct main" + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" latlong2: dependency: "direct main" description: @@ -901,6 +981,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95" + url: "https://pub.dev" + source: hosted + version: "7.0.1" nested: dependency: transitive description: @@ -1234,6 +1322,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + url: "https://pub.dev" + source: hosted + version: "3.3.1" term_glyph: dependency: transitive description: @@ -1330,6 +1426,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vibration: + dependency: "direct main" + description: + name: vibration + sha256: "804ee8f9628f31ee71fbe6137a2bc6206a64e101ec22cd9dd6d3a7dc0272591b" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + vibration_platform_interface: + dependency: transitive + description: + name: vibration_platform_interface + sha256: "03e9deaa4df48a1a6212e281bfee5f610d62e9247929dd2f26f4efd4fa5e225c" + url: "https://pub.dev" + source: hosted + version: "0.1.0" vm_service: dependency: transitive description: @@ -1378,6 +1490,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" wkt_parser: dependency: transitive description: @@ -1412,4 +1532,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.7.2 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index ed506de..19f86e2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,10 @@ dependencies: firebase_auth: ^5.7.0 cloud_firestore: ^5.6.12 firebase_storage: ^12.4.10 + mobile_scanner: ^7.0.1 + vibration: ^3.1.3 + audioplayers: ^6.5.0 + jwt_decoder: ^2.0.1 dev_dependencies: @@ -83,6 +87,7 @@ flutter: assets: - assets/images/ - assets/icons/ + - assets/sounds/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images