diff --git a/assets/icons/addPic.svg b/assets/icons/addPic.svg new file mode 100644 index 0000000..45e58cd --- /dev/null +++ b/assets/icons/addPic.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/calendar-search.svg b/assets/icons/calendar-search.svg new file mode 100644 index 0000000..bc23c10 --- /dev/null +++ b/assets/icons/calendar-search.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/call-calling.svg b/assets/icons/call-calling.svg new file mode 100644 index 0000000..4f40fd1 --- /dev/null +++ b/assets/icons/call-calling.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/document-text.svg b/assets/icons/document-text.svg new file mode 100644 index 0000000..d35499c --- /dev/null +++ b/assets/icons/document-text.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/info-circle.svg b/assets/icons/info-circle.svg new file mode 100644 index 0000000..c828515 --- /dev/null +++ b/assets/icons/info-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/radar-2.svg b/assets/icons/radar-2.svg new file mode 100644 index 0000000..b3c46e3 --- /dev/null +++ b/assets/icons/radar-2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/emptyShop.svg b/assets/images/emptyShop.svg new file mode 100644 index 0000000..966302c --- /dev/null +++ b/assets/images/emptyShop.svg @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/shoppAdded.png b/assets/images/shoppAdded.png new file mode 100644 index 0000000..f9b0e74 Binary files /dev/null and b/assets/images/shoppAdded.png differ diff --git a/lib/core/services/token_storage_service.dart b/lib/core/services/token_storage_service.dart new file mode 100644 index 0000000..5eba95d --- /dev/null +++ b/lib/core/services/token_storage_service.dart @@ -0,0 +1,29 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class TokenStorageService { + final _storage = const FlutterSecureStorage(); + + static const _accessTokenKey = 'access_token'; + static const _refreshTokenKey = 'refresh_token'; + + Future saveTokens({ + required String accessToken, + required String refreshToken, + }) async { + await _storage.write(key: _accessTokenKey, value: accessToken); + await _storage.write(key: _refreshTokenKey, value: refreshToken); + } + + Future getAccessToken() async { + return await _storage.read(key: _accessTokenKey); + } + + Future getRefreshToken() async { + return await _storage.read(key: _refreshTokenKey); + } + + Future deleteAllTokens() async { + await _storage.delete(key: _accessTokenKey); + await _storage.delete(key: _refreshTokenKey); + } +} \ No newline at end of file diff --git a/lib/core/utils/logging_interceptor.dart b/lib/core/utils/logging_interceptor.dart new file mode 100644 index 0000000..2cf0f4a --- /dev/null +++ b/lib/core/utils/logging_interceptor.dart @@ -0,0 +1,42 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + +class LoggingInterceptor extends Interceptor { + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + if (kDebugMode) { + print('--- API REQUEST ---'); + print('METHOD: ${options.method}'); + print('URL: ${options.uri}'); + print('HEADERS: ${options.headers}'); + print('BODY: ${options.data}'); + print('-------------------'); + } + super.onRequest(options, handler); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + if (kDebugMode) { + print('--- API RESPONSE ---'); + print('STATUS_CODE: ${response.statusCode}'); + print('URL: ${response.requestOptions.uri}'); + print('DATA: ${response.data}'); + print('--------------------'); + } + super.onResponse(response, handler); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + if (kDebugMode) { + print('--- API ERROR ---'); + print('STATUS_CODE: ${err.response?.statusCode}'); + print('URL: ${err.requestOptions.uri}'); + print('ERROR: ${err.error}'); + print('RESPONSE_DATA: ${err.response?.data}'); + print('-----------------'); + } + super.onError(err, handler); + } +} \ No newline at end of file diff --git a/lib/domain/entities/category_entity.dart b/lib/domain/entities/category_entity.dart new file mode 100644 index 0000000..64f57b3 --- /dev/null +++ b/lib/domain/entities/category_entity.dart @@ -0,0 +1,7 @@ +class CategoryEntity { + final String id; + final String name; + final String emoji; + + CategoryEntity({required this.id, required this.name, this.emoji = ''}); +} \ No newline at end of file diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 59bb631..90d88bc 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -66,6 +66,9 @@ class $AssetsIconsGen { /// File path: assets/icons/addImg.svg String get addImg => 'assets/icons/addImg.svg'; + /// File path: assets/icons/addPic.svg + String get addPic => 'assets/icons/addPic.svg'; + /// File path: assets/icons/appbar2.svg String get appbar2 => 'assets/icons/appbar2.svg'; @@ -87,6 +90,12 @@ class $AssetsIconsGen { /// File path: assets/icons/backArrow.svg String get backArrow => 'assets/icons/backArrow.svg'; + /// File path: assets/icons/calendar-search.svg + String get calendarSearch => 'assets/icons/calendar-search.svg'; + + /// File path: assets/icons/call-calling.svg + String get callCalling => 'assets/icons/call-calling.svg'; + /// File path: assets/icons/camera.svg String get camera => 'assets/icons/camera.svg'; @@ -111,6 +120,9 @@ class $AssetsIconsGen { /// File path: assets/icons/discount-shape.svg String get discountShape => 'assets/icons/discount-shape.svg'; + /// File path: assets/icons/document-text.svg + String get documentText => 'assets/icons/document-text.svg'; + /// File path: assets/icons/edit-02.svg String get edit02 => 'assets/icons/edit-02.svg'; @@ -129,6 +141,9 @@ class $AssetsIconsGen { /// File path: assets/icons/global-search.svg String get globalSearch => 'assets/icons/global-search.svg'; + /// File path: assets/icons/info-circle.svg + String get infoCircle => 'assets/icons/info-circle.svg'; + /// File path: assets/icons/kafsh.svg String get kafsh => 'assets/icons/kafsh.svg'; @@ -147,6 +162,9 @@ class $AssetsIconsGen { /// File path: assets/icons/pooshak.svg String get pooshak => 'assets/icons/pooshak.svg'; + /// File path: assets/icons/radar-2.svg + String get radar2 => 'assets/icons/radar-2.svg'; + /// File path: assets/icons/receipt-disscount.svg String get receiptDisscount => 'assets/icons/receipt-disscount.svg'; @@ -227,6 +245,7 @@ class $AssetsIconsGen { tshirt, vector, addImg, + addPic, appbar2, arayesh, arrowDown, @@ -234,6 +253,8 @@ class $AssetsIconsGen { arrowUp, back, backArrow, + calendarSearch, + callCalling, camera, cardPos, cinama, @@ -242,18 +263,21 @@ class $AssetsIconsGen { coffeeshop, digital, discountShape, + documentText, edit02, edit, error, fastfood, galleryAdd, globalSearch, + infoCircle, kafsh, location, logo, map, notification, pooshak, + radar2, receiptDisscount, resturan, routing, @@ -307,6 +331,13 @@ class $AssetsImagesGen { /// File path: assets/images/empty home.svg String get emptyHome => 'assets/images/empty home.svg'; + /// File path: assets/images/emptyShop.svg + String get emptyShop => 'assets/images/emptyShop.svg'; + + /// File path: assets/images/shoppAdded.png + AssetGenImage get shoppAdded => + const AssetGenImage('assets/images/shoppAdded.png'); + /// File path: assets/images/userinfo.png AssetGenImage get userinfo => const AssetGenImage('assets/images/userinfo.png'); @@ -320,6 +351,8 @@ class $AssetsImagesGen { rectangle3, rectangle4, emptyHome, + emptyShop, + shoppAdded, userinfo, ]; } diff --git a/lib/presentation/auth/bloc/auth_bloc.dart b/lib/presentation/auth/bloc/auth_bloc.dart index 7ddc22f..2e68cb0 100644 --- a/lib/presentation/auth/bloc/auth_bloc.dart +++ b/lib/presentation/auth/bloc/auth_bloc.dart @@ -1,31 +1,72 @@ - - import 'package:bloc/bloc.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'; part 'auth_event.dart'; part 'auth_state.dart'; class AuthBloc extends Bloc { + + late final Dio _dio; + final TokenStorageService _tokenStorage = TokenStorageService(); AuthBloc() : super(AuthInitial()) { + _dio = Dio(); + _dio.interceptors.add(LoggingInterceptor()); + on((event, emit) async { emit(AuthLoading()); - await Future.delayed(const Duration(seconds: 1)); - if (event.phoneNumber.isNotEmpty) { - emit(AuthCodeSentSuccess()); - } else { - emit(AuthFailure('شماره موبایل معتبر نیست.')); + emit(AuthLoading()); + try { + final response = await _dio.post( + 'https://fartak.liara.run/login/sendcode', + data: { + 'Phone': event.phoneNumber, + 'Code': event.countryCode, + }, + ); + + if (response.statusCode == 200) { + emit(AuthCodeSentSuccess()); + } else { + emit(AuthFailure(response.data['message'] ?? 'خطایی رخ داد')); + } + } on DioException catch (e) { + emit(AuthFailure(e.response?.data['message'] ?? 'خطای شبکه')); } }); on((event, emit) async { emit(AuthLoading()); - await Future.delayed(const Duration(seconds: 1)); - if (event.otp == '12345') { - emit(AuthVerified()); - } else { - emit(AuthFailure('کد تایید صحیح نمی‌باشد.')); + try { + final response = await _dio.post( + 'https://fartak.liara.run/login/getcode', + data: { + 'Phone': event.phoneNumber, + 'Code': event.countryCode, + 'OTP': event.otp, + }, + ); + + if (response.statusCode == 200 && response.data['data']['accessToken'] != null) { + + final accessToken = response.data['data']['accessToken']; + final refreshToken = response.data['data']['refreshToken']; + + await _tokenStorage.saveTokens( + accessToken: accessToken, + refreshToken: refreshToken, + ); + + emit(AuthVerified()); + } else { + emit(AuthFailure(response.data['message'] ?? 'کد تایید صحیح نمی‌باشد.')); + } + } on DioException catch (e) { + emit(AuthFailure(e.response?.data['message'] ?? 'خطای شبکه')); } }); + on((event, emit) async { emit(AuthLoading()); await Future.delayed(const Duration(milliseconds: 500)); @@ -37,4 +78,4 @@ class AuthBloc extends Bloc { } }); } -} +} \ No newline at end of file diff --git a/lib/presentation/auth/bloc/auth_event.dart b/lib/presentation/auth/bloc/auth_event.dart index adf1b2b..a21dcc0 100644 --- a/lib/presentation/auth/bloc/auth_event.dart +++ b/lib/presentation/auth/bloc/auth_event.dart @@ -1,18 +1,20 @@ - part of 'auth_bloc.dart'; abstract class AuthEvent {} class SendOTPEvent extends AuthEvent { final String phoneNumber; + final String countryCode; - SendOTPEvent({required this.phoneNumber}); + SendOTPEvent({required this.phoneNumber, required this.countryCode}); } class VerifyOTPEvent extends AuthEvent { final String otp; + final String phoneNumber; + final String countryCode; - VerifyOTPEvent({required this.otp}); + VerifyOTPEvent({required this.otp, required this.phoneNumber, required this.countryCode}); } class SaveUserInfoEvent extends AuthEvent { @@ -20,4 +22,4 @@ class SaveUserInfoEvent extends AuthEvent { final String gender; SaveUserInfoEvent({required this.name, required this.gender}); -} +} \ No newline at end of file diff --git a/lib/presentation/discount/bloc/discount_bloc.dart b/lib/presentation/discount/bloc/discount_bloc.dart new file mode 100644 index 0000000..b040851 --- /dev/null +++ b/lib/presentation/discount/bloc/discount_bloc.dart @@ -0,0 +1,49 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'discount_event.dart'; +import 'discount_state.dart'; + +class DiscountBloc extends Bloc { + DiscountBloc() : super(const DiscountState()) { + on((event, emit) { + List updatedImages = List.from(state.productImages); + if (updatedImages.length > event.index) { + updatedImages[event.index] = event.imagePath; + } else { + updatedImages.add(event.imagePath); + } + emit(state.copyWith(productImages: updatedImages)); + }); + + on((event, emit) { + emit(state.copyWith(productName: event.name)); + }); + + on((event, emit) { + emit(state.copyWith(discountType: event.type)); + }); + + on((event, emit) { + emit(state.copyWith(description: event.description)); + }); + + on((event, emit) { + emit(state.copyWith(startDate: event.startDate, endDate: event.endDate)); + }); + + on((event, emit) { + emit(state.copyWith(startTime: event.startTime, endTime: event.endTime)); + }); + + on((event, emit) { + emit(state.copyWith(price: event.price)); + }); + + on((event, emit) { + emit(state.copyWith(discountedPrice: event.price)); + }); + + on((event, emit) { + emit(state.copyWith(notificationRadius: event.radius)); + }); + } +} \ No newline at end of file diff --git a/lib/presentation/discount/bloc/discount_event.dart b/lib/presentation/discount/bloc/discount_event.dart new file mode 100644 index 0000000..1ff9850 --- /dev/null +++ b/lib/presentation/discount/bloc/discount_event.dart @@ -0,0 +1,52 @@ + +abstract class DiscountEvent {} + +class ProductImageAdded extends DiscountEvent { + final String imagePath; + final int index; + ProductImageAdded(this.imagePath, this.index); +} + +class ProductNameChanged extends DiscountEvent { + final String name; + ProductNameChanged(this.name); +} + +class DiscountTypeChanged extends DiscountEvent { + final String type; + DiscountTypeChanged(this.type); +} + +class DescriptionChanged extends DiscountEvent { + final String description; + DescriptionChanged(this.description); +} + +class ValidityDateChanged extends DiscountEvent { + final DateTime startDate; + final DateTime endDate; + ValidityDateChanged({required this.startDate, required this.endDate}); +} + +class TimeRangeChanged extends DiscountEvent { + final String startTime; + final String endTime; + TimeRangeChanged({required this.startTime, required this.endTime}); +} + +class PriceChanged extends DiscountEvent { + final String price; + PriceChanged(this.price); +} + +class DiscountedPriceChanged extends DiscountEvent { + final String price; + DiscountedPriceChanged(this.price); +} + +class NotificationRadiusChanged extends DiscountEvent { + final double radius; + NotificationRadiusChanged(this.radius); +} + +class SubmitDiscount extends DiscountEvent {} \ No newline at end of file diff --git a/lib/presentation/discount/bloc/discount_state.dart b/lib/presentation/discount/bloc/discount_state.dart new file mode 100644 index 0000000..9c48637 --- /dev/null +++ b/lib/presentation/discount/bloc/discount_state.dart @@ -0,0 +1,87 @@ +import 'package:equatable/equatable.dart'; + +class DiscountState extends Equatable { + final List productImages; + final String productName; + final String? discountType; + final String description; + final DateTime? startDate; + final DateTime? endDate; + final String? startTime; + final String? endTime; + final String price; + final String discountedPrice; + final double notificationRadius; + final bool isSubmitting; + final bool isSuccess; + final String? errorMessage; + + const DiscountState({ + this.productImages = const [], + this.productName = '', + this.discountType, + this.description = '', + this.startDate, + this.endDate, + this.startTime, + this.endTime, + this.price = '', + this.discountedPrice = '', + this.notificationRadius = 0.0, + this.isSubmitting = false, + this.isSuccess = false, + this.errorMessage, + }); + + DiscountState copyWith({ + List? productImages, + String? productName, + String? discountType, + String? description, + DateTime? startDate, + DateTime? endDate, + String? startTime, + String? endTime, + String? price, + String? discountedPrice, + double? notificationRadius, + bool? isSubmitting, + bool? isSuccess, + String? errorMessage, + }) { + return DiscountState( + productImages: productImages ?? this.productImages, + productName: productName ?? this.productName, + discountType: discountType ?? this.discountType, + description: description ?? this.description, + startDate: startDate ?? this.startDate, + endDate: endDate ?? this.endDate, + startTime: startTime ?? this.startTime, + endTime: endTime ?? this.endTime, + price: price ?? this.price, + discountedPrice: discountedPrice ?? this.discountedPrice, + notificationRadius: notificationRadius ?? this.notificationRadius, + isSubmitting: isSubmitting ?? this.isSubmitting, + isSuccess: isSuccess ?? this.isSuccess, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [ + productImages, + productName, + discountType, + description, + startDate, + endDate, + startTime, + endTime, + price, + discountedPrice, + notificationRadius, + isSubmitting, + isSuccess, + errorMessage, + ]; +} \ No newline at end of file diff --git a/lib/presentation/pages/add_discount_page.dart b/lib/presentation/pages/add_discount_page.dart new file mode 100644 index 0000000..0191eee --- /dev/null +++ b/lib/presentation/pages/add_discount_page.dart @@ -0,0 +1,606 @@ +import 'dart:io'; +import 'package:business_panel/core/config/app_colors.dart'; +import 'package:business_panel/gen/assets.gen.dart'; +import 'package:business_panel/presentation/discount/bloc/discount_bloc.dart'; +import 'package:business_panel/presentation/discount/bloc/discount_event.dart'; +import 'package:business_panel/presentation/discount/bloc/discount_state.dart'; +import 'package:business_panel/presentation/widgets/info_popup.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:persian_datetime_picker/persian_datetime_picker.dart'; + +class AddDiscountPage extends StatelessWidget { + const AddDiscountPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => DiscountBloc(), + child: const _AddDiscountView(), + ); + } +} + +class _AddDiscountView extends StatefulWidget { + const _AddDiscountView(); + + @override + State<_AddDiscountView> createState() => _AddDiscountViewState(); +} + +class _AddDiscountViewState extends State<_AddDiscountView> { + final _nameController = TextEditingController(); + final _descController = TextEditingController(); + final _priceController = TextEditingController(); + final _discountPriceController = TextEditingController(); + + @override + void dispose() { + _nameController.dispose(); + _descController.dispose(); + _priceController.dispose(); + _discountPriceController.dispose(); + super.dispose(); + } + + Future _pickValidityDates(BuildContext context) async { + Jalali? startDate = await showPersianDatePicker( + context: context, + initialDate: Jalali.now(), + firstDate: Jalali.now(), + lastDate: Jalali(1500), + ); + 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(1500), + ); + if (endDate == null || !context.mounted) return; + + TimeOfDay? endTime = await showTimePicker( + context: context, + initialTime: startTime, + ); + if (endTime == null || !context.mounted) return; + + 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( + ValidityDateChanged(startDate: startDateTime, endDate: endDateTime), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: _buildCustomAppBar(context), + body: 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: ElevatedButton( + onPressed: () { + // TODO: Implement submit logic + }, + child: const Text("ثبت تخفیف"), + ), + ), + const SizedBox(height: 30), + ], + ), + ), + ); + } + + Widget _buildSectionTitle({ + required String title, + String? popupTitle, + bool isMandatory = false, + String? infoText, + String? iconPath, + }) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (infoText != null && iconPath != null) + IconButton( + onPressed: + () => showInfoDialog( + context, + title: popupTitle ?? title, + content: infoText, + iconPath: iconPath, + ), + icon: SvgPicture.asset(Assets.icons.infoCircle, width: 17), + ), + Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + ), + if (isMandatory) + const Text(' *', style: TextStyle(color: Colors.red, fontSize: 17)), + ], + ); + } + + Widget _buildImagePickers() { + return BlocBuilder( + builder: (context, state) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(2, (index) { + final imagePath = + state.productImages.length > index + ? state.productImages[index] + : null; + return GestureDetector( + onTap: () async { + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage( + source: ImageSource.gallery, + ); + if (image != null && context.mounted) { + context.read().add( + ProductImageAdded(image.path, index), + ); + } + }, + child: Container( + width: 125, + height: 125, + decoration: BoxDecoration( + 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, + ), + ) + : null, + ), + ); + }), + ); + }, + ); + } + + Widget _buildDiscountTypeDropdown() { + final List discountTypes = [ + "ساعت خوش", + "رفیق بازی", + "محصول جانبی رایگان", + "کالای مکمل", + "پلکانی", + "دعوتنامه طلایی", + "بازگشت وجه", + "سایر", + ]; + + return DropdownButtonFormField( + icon: SvgPicture.asset( + Assets.icons.arrowDown, + width: 24, + color: Colors.black, + ), + menuMaxHeight: 400, + hint: Text("ساعت خوش"), + decoration: _inputDecoration("نوع تخفیف", isRequired: true).copyWith( + contentPadding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 20, + ), + ), + borderRadius: BorderRadius.circular(12.0), + items: + discountTypes + .map((type) => DropdownMenuItem(value: type, child: Text(type))) + .toList(), + onChanged: (value) { + if (value != null) { + context.read().add(DiscountTypeChanged(value)); + } + }, + ); + } + + Widget _buildDateTimePicker() { + return BlocBuilder( + buildWhen: (previous, current) => + previous.startDate != current.startDate || + previous.endDate != current.endDate, + builder: (context, state) { + String displayText = "انتخاب تاریخ"; + if (state.startDate != null && state.endDate != null) { + + final jalaliStart = DateTimeExtensions(state.startDate!).toJalali(); + final jalaliEnd = DateTimeExtensions(state.endDate!).toJalali(); + + final startFormatted = + '${jalaliStart.year}/${jalaliStart.month.toString().padLeft(2, '0')}/${jalaliStart.day.toString().padLeft(2, '0')} - ${state.startDate!.hour.toString().padLeft(2, '0')}:${state.startDate!.minute.toString().padLeft(2, '0')}'; + final endFormatted = + '${jalaliEnd.year}/${jalaliEnd.month.toString().padLeft(2, '0')}/${jalaliEnd.day.toString().padLeft(2, '0')} - ${state.endDate!.hour.toString().padLeft(2, '0')}:${state.endDate!.minute.toString().padLeft(2, '0')}'; + + displayText = 'از $startFormatted\nتا $endFormatted'; + } + return InkWell( + onTap: () => _pickValidityDates(context), + child: InputDecorator( + decoration: _inputDecoration( + "تاریخ اعتبار تخفیف", + isRequired: true, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + displayText, + textDirection: TextDirection.rtl, + style: const TextStyle(fontSize: 15), // Optional: for better fit + ), + ), + SvgPicture.asset(Assets.icons.calendarSearch), + ], + ), + ), + ); + }, + ); + } + + Widget _buildTimeRangePicker(BuildContext context) { + return BlocBuilder( + buildWhen: + (previous, current) => + previous.startTime != current.startTime || + previous.endTime != current.endTime, + builder: (context, state) { + String displayText = "انتخاب بازه زمانی"; + if (state.startTime != null && state.endTime != null) { + displayText = 'از ساعت ${state.startTime} تا ${state.endTime}'; + } + + return InkWell( + onTap: () async { + final TimeOfDay? startTime = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (startTime == null) return; + + final TimeOfDay? endTime = await showTimePicker( + context: context, + initialTime: startTime, + ); + if (endTime == null) return; + + final formattedStartTime = + '${startTime.hour.toString().padLeft(2, '0')}:${startTime.minute.toString().padLeft(2, '0')}'; + final formattedEndTime = + '${endTime.hour.toString().padLeft(2, '0')}:${endTime.minute.toString().padLeft(2, '0')}'; + + context.read().add( + TimeRangeChanged( + startTime: formattedStartTime, + endTime: formattedEndTime, + ), + ); + }, + child: InputDecorator( + decoration: _inputDecoration("بازه زمانی معتبر", isRequired: true), + child: Text(displayText), + ), + ); + }, + ); + } + + Widget _buildNotificationRadiusSlider() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + onPressed: + () => showInfoDialog( + context, + title: "انتخاب محدوده نمایش تخفیف", + content: + "محدوده‌ای رو مشخص کن که تخفیف‌هات فقط به کاربرانی که تو اون شعاع هستن نشون داده بشه.", + iconPath: Assets.icons.radar2, + ), + icon: SvgPicture.asset(Assets.icons.infoCircle, width: 17), + ), + Text( + "شعاع ارسال اعلان تخفیف به مشتری‌ها", + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ], + ), + ], + ), + BlocBuilder( + builder: (context, state) { + return Column( + children: [ + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: AppColors.active, + inactiveTrackColor: Colors.grey.shade300, + trackShape: const RoundedRectSliderTrackShape(), + trackHeight: 4.0, + thumbColor: AppColors.active, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 12.0, + ), + overlayColor: AppColors.active.withAlpha(32), + overlayShape: const RoundSliderOverlayShape( + overlayRadius: 28.0, + ), + ), + child: Slider( + value: state.notificationRadius, + min: 0, + max: 1000, + divisions: 100, + label: '${state.notificationRadius.toInt()} متر', + onChanged: (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, + ), + ); + }, + ), + ], + ); + }, + ), + ], + ); + } + + Widget _buildTextField({ + required String label, + String? hint, + bool isRequired = false, + int? maxLines, + int? maxLength, + TextInputType? keyboardType, + required TextEditingController controller, + ValueChanged? onChanged, + }) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) { + return TextFormField( + controller: controller, + onChanged: onChanged, + maxLines: maxLines, + maxLength: maxLength, + keyboardType: keyboardType, + decoration: _inputDecoration( + label, + hint: hint, + isRequired: isRequired, + ).copyWith( + counterText: '', + counter: + maxLength != null + ? Text( + '${value.text.length}/$maxLength', + style: Theme.of(context).textTheme.bodySmall, + ) + : null, + ), + ); + }, + ); + } + + InputDecoration _inputDecoration( + String label, { + String? hint, + bool isRequired = false, + }) { + return InputDecoration( + hintText: hint, + hintStyle: TextStyle( + color: Color.fromARGB(255, 95, 95, 95), + fontSize: 14, + ), + label: RichText( + text: TextSpan( + text: label, + style: const TextStyle( + color: Colors.black, + fontFamily: 'Dana', + fontSize: 18, + fontWeight: FontWeight.bold, + ), + children: [ + if (isRequired) + const TextSpan(text: ' *', style: TextStyle(color: Colors.red)), + ], + ), + ), + ); + } + + 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/login_page.dart b/lib/presentation/pages/login_page.dart index 172fa33..bf9e26e 100644 --- a/lib/presentation/pages/login_page.dart +++ b/lib/presentation/pages/login_page.dart @@ -42,10 +42,18 @@ class _LoginPageState extends State { Column( children: [ Center( - child: SvgPicture.asset(Assets.icons.logo,height: 160,), + child: SvgPicture.asset( + Assets.icons.logo, + height: 160, + ), ), - SizedBox(height: 15,), - Text("پنل فروشگاهی شما",style: TextStyle(color: AppColors.active),) + SizedBox( + height: 15, + ), + Text( + "پنل فروشگاهی شما", + style: TextStyle(color: AppColors.active), + ) ], ), SizedBox(height: 48), @@ -103,9 +111,8 @@ class _LoginPageState extends State { children: [ Checkbox( value: _keepSignedIn, - onChanged: - (value) => - setState(() => _keepSignedIn = value ?? false), + onChanged: (value) => + setState(() => _keepSignedIn = value ?? false), activeColor: AppColors.primary, ), Text("مرا به خاطر بسپار", style: textTheme.bodyMedium), @@ -120,14 +127,20 @@ class _LoginPageState extends State { content: Text(state.message), backgroundColor: Colors.red, ), - ); + ); } if (state is AuthCodeSentSuccess) { - final fullPhoneNumber = "0${_phoneController.text}"; + final fullPhoneNumber = _phoneController.text; Navigator.push( context, MaterialPageRoute( - builder: (_) => OtpPage(phoneNumber: fullPhoneNumber), + builder: (_) => BlocProvider.value( + value: context.read(), + child: OtpPage( + phoneNumber: fullPhoneNumber, + countryCode: _selectedCountry.phoneCode, + ), + ), ), ); } @@ -165,7 +178,10 @@ class _LoginPageState extends State { SizedBox( width: double.infinity, child: OutlinedButton.icon( - icon: SvgPicture.asset(Assets.icons.googleSvg,width: 24,), + icon: SvgPicture.asset( + Assets.icons.googleSvg, + width: 24, + ), label: const Text( "ورود با حساب گوگل", style: TextStyle(color: Colors.black), @@ -204,7 +220,10 @@ class _LoginPageState extends State { void _sendOtp() { context.read().add( - SendOTPEvent(phoneNumber: _phoneController.text), - ); + SendOTPEvent( + phoneNumber: _phoneController.text, + countryCode: _selectedCountry.phoneCode, + ), + ); } -} +} \ No newline at end of file diff --git a/lib/presentation/pages/otp_page.dart b/lib/presentation/pages/otp_page.dart index be85468..aa19afa 100644 --- a/lib/presentation/pages/otp_page.dart +++ b/lib/presentation/pages/otp_page.dart @@ -10,7 +10,9 @@ import 'package:flutter_svg/svg.dart'; class OtpPage extends StatefulWidget { final String phoneNumber; - const OtpPage({super.key, required this.phoneNumber}); + final String countryCode; + const OtpPage( + {super.key, required this.phoneNumber, required this.countryCode}); @override State createState() => _OtpPageState(); @@ -90,7 +92,7 @@ class _OtpPageState extends State { style: TextStyle(fontSize: 15), ), TextSpan( - text: widget.phoneNumber, + text: "0${widget.phoneNumber}", style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 15, @@ -137,7 +139,6 @@ class _OtpPageState extends State { ) else const SizedBox(height: 32), - BlocConsumer( listener: (context, state) { if (state is AuthFailure) { @@ -149,11 +150,10 @@ class _OtpPageState extends State { if (state is AuthVerified) { Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( - builder: - (_) => BlocProvider( - create: (context) => StoreInfoBloc(), - child: const StoreInfoPage(), - ), + builder: (_) => BlocProvider( + create: (context) => StoreInfoBloc(), + child: const StoreInfoPage(), + ), ), (route) => false, ); @@ -189,20 +189,19 @@ 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), + ), + ); }, ), ], @@ -240,15 +239,14 @@ 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( @@ -287,7 +285,13 @@ class _OtpPageState extends State { void _verifyOtp() { final otpCode = _controllers.map((c) => c.text).join(); if (otpCode.length == 5) { - context.read().add(VerifyOTPEvent(otp: otpCode)); + context.read().add( + VerifyOTPEvent( + otp: otpCode, + phoneNumber: widget.phoneNumber, + countryCode: widget.countryCode, + ), + ); } } @@ -300,7 +304,8 @@ class _OtpPageState extends State { } _isOtpComplete = false; }); - context.read().add(SendOTPEvent(phoneNumber: widget.phoneNumber)); + 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 new file mode 100644 index 0000000..4acd341 --- /dev/null +++ b/lib/presentation/pages/product_creation_landing_page.dart @@ -0,0 +1,114 @@ +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:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ProductCreationLandingPage extends StatelessWidget { + const ProductCreationLandingPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: _buildCustomAppBar(context), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SvgPicture.asset(Assets.images.emptyShop, height: 350), + const SizedBox(height: 60), + const Text( + "فروشگاه با موفقیت ثبت شد!", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + "حالا وقتشه اولین تخفیف رو اضافه کنی.", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 25), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const AddDiscountPage()), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.confirm, + padding: const EdgeInsets.symmetric(vertical: 14), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add), + SizedBox(width: 5,), + const Text( + "تعریف تخفیف جدید", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.normal), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +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.dart b/lib/presentation/pages/store_info.dart index 3b8bbc3..db964af 100644 --- a/lib/presentation/pages/store_info.dart +++ b/lib/presentation/pages/store_info.dart @@ -1,6 +1,8 @@ import 'dart:io'; import 'package:business_panel/core/config/app_colors.dart'; +import 'package:business_panel/domain/entities/category_entity.dart'; import 'package:business_panel/gen/assets.gen.dart'; +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'; @@ -9,13 +11,53 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:business_panel/presentation/pages/osm_map_picker_page.dart'; // ایمپورت صفحه جدید +import 'package:business_panel/presentation/pages/osm_map_picker_page.dart'; import 'package:latlong2/latlong.dart'; -import 'package:persian_datetime_picker/persian_datetime_picker.dart'; -class StoreInfoPage extends StatelessWidget { +class StoreInfoPage extends StatefulWidget { const StoreInfoPage({super.key}); + @override + State createState() => _StoreInfoPageState(); +} + +class _StoreInfoPageState extends State { + final _nameController = TextEditingController(); + final _provinceController = TextEditingController(); + final _cityController = TextEditingController(); + final _addressController = TextEditingController(); + final _plaqueController = TextEditingController(); + final _postalCodeController = TextEditingController(); + final _phoneController = TextEditingController(); + final _licenseController = TextEditingController(); + + @override + void initState() { + super.initState(); + final bloc = context.read(); + _nameController.text = bloc.state.storeName; + _provinceController.text = bloc.state.province; + _cityController.text = bloc.state.city; + _addressController.text = bloc.state.address; + _plaqueController.text = bloc.state.plaque; + _postalCodeController.text = bloc.state.postalCode; + _phoneController.text = bloc.state.contactPhone ?? ''; + _licenseController.text = bloc.state.licenseNumber ?? ''; + } + + @override + void dispose() { + _nameController.dispose(); + _provinceController.dispose(); + _cityController.dispose(); + _addressController.dispose(); + _plaqueController.dispose(); + _postalCodeController.dispose(); + _phoneController.dispose(); + _licenseController.dispose(); + super.dispose(); + } + Future _pickImage(BuildContext context) async { try { final ImagePicker picker = ImagePicker(); @@ -34,7 +76,7 @@ class StoreInfoPage extends StatelessWidget { } } - Future _showWorkingHoursDialog(BuildContext context) async { + Future _showWorkingHoursDialog(BuildContext context) async { final result = await showDialog>( context: context, builder: (context) => const WorkingHoursDialog(), @@ -51,64 +93,46 @@ class StoreInfoPage extends StatelessWidget { } } - // 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, - // ), - // ); - // } + final List activityTypes = [ + CategoryEntity(id: "6803b940-3e19-48cd-9190-28d9f25421ff", name: "فست فود", emoji: "🍔🍕"), + CategoryEntity(id: "71e371f8-a47a-4a58-aee6-4ed0f26bf29b", name: "پوشاک", emoji: "👚👔"), + CategoryEntity(id: "42acff41-1165-4e62-89b9-58db7329ec3a", name: "تریا", emoji: "🍨🍹"), + CategoryEntity(id: "e33dd7f9-5b20-4273-8eea-59da6ca5f206", name: "لوازم دیجیتال", emoji: "📱📷"), + CategoryEntity(id: "b5881239-bfd5-4c27-967a-187316a7e0b7", name: "رستوران", emoji: "🍣🍢"), + CategoryEntity(id: "b73a868a-a2d2-4d96-8fd4-615327ed9629", name: "کافی شاپ", emoji: "☕🍰"), + CategoryEntity(id: "2f38918c-5566-4aec-a0a9-2c7c48b1e878", name: "کیف و کفش", emoji: "👜👞"), + CategoryEntity(id: "52c51010-3a63-4264-a350-e011c889f3dd", name: "سینما", emoji: "🎭🎟️"), + CategoryEntity(id: "34185954-f79f-4b9e-8eb2-1702679c40a0", name: "لوازم آرایشی", emoji: "💄💅️"), + CategoryEntity(id: "e4517b0c-aacf-4758-94bd-85f45062980f", name: "طلا و زیورآلات", emoji: "💍💎"), + ]; @override Widget build(BuildContext context) { return Scaffold( appBar: _buildCustomAppBar(context), - body: SingleChildScrollView( - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + body: BlocListener( + listener: (context, state) { + if (state.isSuccess) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => BlocProvider.value( + value: BlocProvider.of(context), + child: const StoreInfoDisplayPage(), + ), + ), + ); + } + if (state.errorMessage != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.errorMessage!), backgroundColor: Colors.red), + ); + } + }, + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ const SizedBox(height: 16), const Row( children: [ @@ -134,7 +158,12 @@ class StoreInfoPage extends StatelessWidget { shape: BoxShape.circle, boxShadow: [ BoxShadow( - color: const Color.fromARGB(255, 224, 224, 224).withOpacity(0.5), + color: const Color.fromARGB( + 255, + 224, + 224, + 224, + ).withOpacity(0.5), spreadRadius: 1, blurRadius: 40, offset: const Offset(0, 10), @@ -159,9 +188,7 @@ class StoreInfoPage extends StatelessWidget { child: CircleAvatar( radius: 20, backgroundColor: Colors.white, - child: SvgPicture.asset( - Assets.icons.edit02 - ), + child: SvgPicture.asset(Assets.icons.edit02), ), ), ), @@ -180,6 +207,11 @@ class StoreInfoPage extends StatelessWidget { label: "نام فروشگاه", isRequired: true, hint: "مثلاً کافه ایرونی", + controller: _nameController, + onChanged: + (value) => context.read().add( + StoreNameChanged(value), + ), ), const SizedBox(height: 30), _buildActivityTypeDropdown(context), @@ -187,24 +219,65 @@ class StoreInfoPage extends StatelessWidget { Row( children: [ Expanded( - child: _buildTextField(label: "استان", hint: "اصفهان"), + child: _buildTextField( + label: "استان", + hint: "اصفهان", + controller: _provinceController, + onChanged: + (value) => context.read().add( + ProvinceChanged(value), + ), + ), ), const SizedBox(width: 16), - Expanded(child: _buildTextField(label: "شهر", hint: "اصفهان")), + Expanded( + child: _buildTextField( + controller: _cityController, + label: "شهر", + hint: "اصفهان", + onChanged: + (value) => context.read().add( + CityChanged(value), + ), + ), + ), ], ), const SizedBox(height: 30), _buildTextField( + controller: _addressController, label: "جزئیات آدرس", maxLines: 3, hint: "خیابان، محله، ساختمان و ....", + onChanged: + (value) => + context.read().add(AddressChanged(value)), ), const SizedBox(height: 30), + _buildFeaturesSection(), + const SizedBox(height: 50), Row( children: [ - Expanded(child: _buildTextField(label: "پلاک")), + Expanded( + child: _buildTextField( + controller: _plaqueController, + label: "پلاک", + onChanged: + (value) => context.read().add( + PlaqueChanged(value), + ), + ), + ), const SizedBox(width: 16), - Expanded(child: _buildTextField(label: "کد پستی")), + Expanded( + child: _buildTextField( + controller: _postalCodeController, + label: "کد پستی", + onChanged: (value) => context + .read() + .add(PostalCodeChanged(value)), + ), + ), ], ), const SizedBox(height: 30), @@ -257,9 +330,12 @@ class StoreInfoPage extends StatelessWidget { ), const SizedBox(height: 30), _buildTextField( + controller: _phoneController, label: "تلفن تماس", keyboardType: TextInputType.phone, hint: "شماره تماس ثابت یا موبایل فروشگاه", + onChanged: (value) => + context.read().add(ContactPhoneChanged(value)), ), const SizedBox(height: 30), @@ -267,23 +343,36 @@ class StoreInfoPage extends StatelessWidget { const SizedBox(height: 30), _buildTextField( + controller: _licenseController, label: "شماره جواز کسب", hint: "شناسه صنفی 12 رقمی یکتا", + onChanged: (value) => + context.read().add(LicenseNumberChanged(value)), ), const SizedBox(height: 44), SizedBox( - width: double.infinity, + width: double.infinity, child: ElevatedButton( - onPressed: () {}, + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => BlocProvider.value( + value: BlocProvider.of(context), + child: const StoreInfoDisplayPage(), + ), + ), + ); + }, child: const Text("تایید و ادامه"), ), ), - const SizedBox(height: 34), - ], + const SizedBox(height: 34), + ], ), ), - ); + )); } + Widget _buildSectionTitle() { return const Text( @@ -297,12 +386,19 @@ class StoreInfoPage extends StatelessWidget { bool isRequired = false, String? hint, int maxLines = 1, + int? maxLength, TextInputType? keyboardType, + TextEditingController? controller, + ValueChanged? onChanged, }) { return TextFormField( + controller: controller, + onChanged: onChanged, maxLines: maxLines, - keyboardType: keyboardType, + maxLength: maxLength, + keyboardType: keyboardType, decoration: InputDecoration( + counterText: "", hintText: hint, hintStyle: const TextStyle(fontSize: 15, color: Colors.grey), label: RichText( @@ -327,6 +423,80 @@ class StoreInfoPage extends StatelessWidget { ); } + Widget _buildFeaturesSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "ویژگی‌های فروشگاه", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text( + "حداکثر ۳ ویژگی برای معرفی بهتر فروشگاه خود اضافه کنید.", + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + const SizedBox(height: 16), + BlocBuilder( + buildWhen: (previous, current) => previous.features != current.features, + builder: (context, state) { + return Column( + children: [ + ...state.features.asMap().entries.map((entry) { + final index = entry.key; + final controller = TextEditingController(text: entry.value) + ..selection = TextSelection.fromPosition( + TextPosition(offset: entry.value.length), + ); + + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Row( + children: [ + Expanded( + child: _buildTextField( + controller: controller, + label: "ویژگی ${index + 1}", + hint: "مثلا: دارای ویوی شهر", + maxLength: 60, + onChanged: (value) { + context + .read() + .add(StoreFeatureUpdated(index, value)); + }, + ), + ), + IconButton( + icon: const Icon(Icons.remove_circle_outline, color: Colors.red), + onPressed: () { + context + .read() + .add(StoreFeatureRemoved(index)); + }, + ), + ], + ), + ); + }).toList(), + if (state.features.length < 3) + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + icon: const Icon(Icons.add), + label: const Text("افزودن ویژگی جدید"), + onPressed: () { + context.read().add(StoreFeatureAdded()); + }, + ), + ), + ], + ); + }, + ), + ], + ); + } + Widget _buildWorkingHoursPicker(BuildContext context) { return InkWell( onTap: () => _showWorkingHoursDialog(context), @@ -342,26 +512,41 @@ class StoreInfoPage extends StatelessWidget { fontWeight: FontWeight.bold, ), children: [ - TextSpan(text: ' *', style: TextStyle(color: Colors.red, fontSize: 16)), + TextSpan( + text: ' *', + style: TextStyle(color: Colors.red, fontSize: 16), + ), ], ), ), - contentPadding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12), + contentPadding: const EdgeInsets.symmetric( + vertical: 18, + horizontal: 12, + ), ), child: BlocBuilder( buildWhen: (p, c) => p.workingDays != c.workingDays, builder: (context, state) { - final hasData = state.workingDays.isNotEmpty && state.startTime != null; + final hasData = + state.workingDays.isNotEmpty && state.startTime != null; const Map dayTranslations = { - 'Saturday': 'شنبه', 'Sunday': 'یکشنبه', 'Monday': 'دوشنبه', - 'Tuesday': 'سه‌شنبه', 'Wednesday': 'چهارشنبه', 'Thursday': 'پنج‌شنبه', 'Friday': 'جمعه' + 'Saturday': 'شنبه', + 'Sunday': 'یکشنبه', + 'Monday': 'دوشنبه', + 'Tuesday': 'سه‌شنبه', + 'Wednesday': 'چهارشنبه', + 'Thursday': 'پنج‌شنبه', + 'Friday': 'جمعه', }; - final displayDays = state.workingDays.map((day) => dayTranslations[day] ?? '').join('، '); + final displayDays = state.workingDays + .map((day) => dayTranslations[day] ?? '') + .join('، '); - String displayText = hasData - ? "$displayDays\nاز ساعت ${state.startTime} تا ${state.endTime}" - : "انتخاب روز و ساعت کاری"; + String displayText = + hasData + ? "$displayDays\nاز ساعت ${state.startTime} تا ${state.endTime}" + : "انتخاب روز و ساعت کاری"; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -385,25 +570,16 @@ class StoreInfoPage extends StatelessWidget { ); } - Widget _buildActivityTypeDropdown(BuildContext context) { - final List activityTypes = [ - "🍔🍕 فست فود", - "👚👔 پوشاک", - "🍨🍹 تریا", - "📱📷 لوازم دیجیتال", - "🍣🍢 رستوران", - "☕🍰 کافی شاپ", - "👜👞 کیف و کفش", - "🎭🎟️ سینما", - "💄💅️ لوازم آرایشی", - "💍💎 طلا و زیورآلات", - ]; - return Theme( data: Theme.of(context).copyWith(canvasColor: const Color(0xFFF6F6F6)), child: DropdownButtonFormField( - icon: SvgPicture.asset(Assets.icons.arrowDown, width: 24,color: Colors.black,), + value: context.watch().state.activityTypeId, + icon: SvgPicture.asset( + Assets.icons.arrowDown, + width: 24, + color: Colors.black, + ), decoration: InputDecoration( label: RichText( text: const TextSpan( @@ -425,10 +601,12 @@ class StoreInfoPage extends StatelessWidget { ), borderRadius: BorderRadius.circular(12.0), isExpanded: true, - items: - activityTypes.map((String value) { - return DropdownMenuItem(value: value, child: Text(value)); - }).toList(), + items: activityTypes.map((CategoryEntity category) { + return DropdownMenuItem( + value: category.id, + child: Text("${category.emoji} ${category.name}"), + ); + }).toList(), onChanged: (value) { if (value != null) { context.read().add(ActivityTypeChanged(value)); @@ -493,3 +671,54 @@ PreferredSizeWidget _buildCustomAppBar(BuildContext context) { ), ); } + + + +// 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 diff --git a/lib/presentation/pages/store_info_display_page.dart b/lib/presentation/pages/store_info_display_page.dart new file mode 100644 index 0000000..41eba7b --- /dev/null +++ b/lib/presentation/pages/store_info_display_page.dart @@ -0,0 +1,270 @@ +import 'dart:io'; +import 'package:business_panel/core/config/app_colors.dart'; +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:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:latlong2/latlong.dart'; + +class StoreInfoDisplayPage extends StatelessWidget { + const StoreInfoDisplayPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: _buildCustomAppBar(context), + body: BlocListener( + listener: (context, state) { + if (state.isSuccess) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (_) => const ProductCreationLandingPage(), + ), + (route) => false, + ); + } + if (state.errorMessage != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage!), + backgroundColor: Colors.red, + ), + ); + } + }, + child: BlocBuilder( + builder: (context, state) { + return SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 10), + _buildStoreLogo(state), + const SizedBox(height: 24), + Text( + state.storeName, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + ), + ), + const SizedBox(height: 12), + if (state.activityType.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(15), + ), + child: Text( + state.activityType.split(' ').sublist(1).join(' '), + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(height: 8), + TextButton.icon( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: SvgPicture.asset( + Assets.icons.edit, + width: 20, + color: AppColors.button, + ), + label: const Text( + "ویرایش", + style: TextStyle(color: AppColors.button, fontSize: 16), + ), + ), + if (state.workingDays.isNotEmpty) + _buildInfoRow( + icon: Assets.icons.clock, + text: + "${state.workingDays.map((day) => _translateDay(day)).join('، ')}\nاز ساعت ${state.startTime} تا ${state.endTime}", + ), + if (state.contactPhone != null && + state.contactPhone!.isNotEmpty) + _buildInfoRow( + icon: Assets.icons.callCalling, + text: state.contactPhone!, + ), + if (state.licenseNumber != null && + state.licenseNumber!.isNotEmpty) + _buildInfoRow( + icon: Assets.icons.documentText, + text: "شماره جواز کسب ${state.licenseNumber!}", + ), + if (state.address.isNotEmpty) + _buildInfoRow( + icon: Assets.icons.location, + text: state.address, + ), + const SizedBox(height: 24), + if (state.latitude != null && state.longitude != null) + _buildMapView(state.latitude!, state.longitude!), + const SizedBox(height: 40), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: state.isSubmitting ? null : () { + context.read().add(SubmitStoreInfo()); + }, + child: state.isSubmitting + ? const CircularProgressIndicator(color: Colors.white) + : const Text("ثبت"), + ), + ), + const SizedBox(height: 24), + ], + ), + ); + }, + ), + )); + } + + Widget _buildStoreLogo(StoreInfoState state) { + return CircleAvatar( + radius: 65, + backgroundColor: AppColors.uploadElevated, + backgroundImage: + state.logoPath != null ? FileImage(File(state.logoPath!)) : null, + child: null, + ); + } + + Widget _buildInfoRow({required String icon, required String text}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Row( + children: [ + SvgPicture.asset( + icon, + width: 24, + color: Color.fromARGB(255, 161, 160, 160), + ), + const SizedBox(width: 16), + Expanded( + child: Text( + text, + style: const TextStyle(fontSize: 14, height: 1.5), + textAlign: TextAlign.right, + ), + ), + ], + ), + ); + } + + Widget _buildMapView(double latitude, double longitude) { + return SizedBox( + height: 200, + child: ClipRRect( + borderRadius: BorderRadius.circular(15), + child: FlutterMap( + options: MapOptions( + initialCenter: LatLng(latitude, longitude), + initialZoom: 15.0, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + ), + MarkerLayer( + markers: [ + Marker( + width: 80.0, + height: 80.0, + point: LatLng(latitude, longitude), + child: const Icon( + Icons.location_on, + color: Colors.red, + size: 40.0, + ), + ), + ], + ), + ], + ), + ), + ); + } + + 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 = { + 'Saturday': 'شنبه', + 'Sunday': 'یکشنبه', + 'Monday': 'دوشنبه', + 'Tuesday': 'سه‌شنبه', + 'Wednesday': 'چهارشنبه', + 'Thursday': 'پنج‌شنبه', + 'Friday': 'جمعه', + }; + return dayTranslations[day] ?? day; + } +} diff --git a/lib/presentation/pages/working_hours_dialog.dart b/lib/presentation/pages/working_hours_dialog.dart index 213dbf9..6dc849c 100644 --- a/lib/presentation/pages/working_hours_dialog.dart +++ b/lib/presentation/pages/working_hours_dialog.dart @@ -32,7 +32,6 @@ class _WorkingHoursDialogState extends State { children: [ const Text("روزهای فعالیت را انتخاب کنید:"), const SizedBox(height: 8), - // ویجت برای نمایش و انتخاب روزها Wrap( spacing: 4.0, runSpacing: 8.0, @@ -51,7 +50,6 @@ class _WorkingHoursDialogState extends State { const Divider(height: 32), const Text("بازه ساعت کاری را مشخص کنید:"), const SizedBox(height: 16), - // انتخابگرهای ساعت شروع و پایان Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ @@ -73,7 +71,6 @@ class _WorkingHoursDialogState extends State { ), ElevatedButton( onPressed: () { - // فقط در صورتی که تمام مقادیر انتخاب شده باشند، بازگشت انجام شود if (_startTime != null && _endTime != null) { final selectedDays = _days.entries .where((entry) => entry.value['isSelected']) @@ -81,12 +78,10 @@ class _WorkingHoursDialogState extends State { .toList(); if (selectedDays.isNotEmpty) { - // فرمت کردن ساعت به رشته "HH:mm" final format = NumberFormat("00"); final startTimeString = "${format.format(_startTime!.hour)}:${format.format(_startTime!.minute)}"; final endTimeString = "${format.format(_endTime!.hour)}:${format.format(_endTime!.minute)}"; - // بازگرداندن نتیجه به صفحه قبل Navigator.of(context).pop({ 'days': selectedDays, 'startTime': startTimeString, diff --git a/lib/presentation/store_info/bloc/store_info_bloc.dart b/lib/presentation/store_info/bloc/store_info_bloc.dart index b725181..9c499d5 100644 --- a/lib/presentation/store_info/bloc/store_info_bloc.dart +++ b/lib/presentation/store_info/bloc/store_info_bloc.dart @@ -1,8 +1,16 @@ +import 'dart:convert'; + import 'package:bloc/bloc.dart'; +import 'package:business_panel/core/services/token_storage_service.dart'; import 'package:business_panel/presentation/store_info/bloc/store_info_state.dart'; +import 'package:dio/dio.dart'; + part 'store_info_event.dart'; class StoreInfoBloc extends Bloc { + final Dio _dio = Dio(); + final TokenStorageService _tokenStorage = TokenStorageService(); + StoreInfoBloc() : super(StoreInfoState()) { on((event, emit) { emit(state.copyWith(logoPath: event.imagePath)); @@ -13,26 +21,42 @@ class StoreInfoBloc extends Bloc { }); on((event, emit) { - emit(state.copyWith(activityType: event.activityType)); + emit(state.copyWith(activityTypeId: event.activityTypeId)); }); - - on((event, emit) async { - emit(state.copyWith(isSubmitting: true)); - await Future.delayed(const Duration(seconds: 2)); - - if (state.storeName.isEmpty || state.activityType.isEmpty) { - emit(state.copyWith(isSubmitting: false, errorMessage: "فیلدهای ستاره‌دار اجباری هستند.")); - } else { - emit(state.copyWith(isSubmitting: false, isSuccess: true)); - } + on((event, emit) { + emit(state.copyWith(province: event.province)); + }); + + on((event, emit) { + emit(state.copyWith(city: event.city)); + }); + + on((event, emit) { + emit(state.copyWith(address: event.address)); + }); + + on((event, emit) { + emit(state.copyWith(plaque: event.plaque)); + }); + + on((event, emit) { + emit(state.copyWith(postalCode: event.postalCode)); + }); + + on((event, emit) { + emit(state.copyWith(contactPhone: event.phone)); + }); + + on((event, emit) { + emit(state.copyWith(licenseNumber: event.license)); }); on((event, emit) { emit(state.copyWith(latitude: event.latitude, longitude: event.longitude)); }); - on((event, emit) { + on((event, emit) { emit(state.copyWith( workingDays: event.days, startTime: event.startTime, @@ -40,6 +64,89 @@ class StoreInfoBloc extends Bloc { )); }); + on((event, emit) { + if (state.features.length < 3) { + final updatedFeatures = List.from(state.features)..add(''); + emit(state.copyWith(features: updatedFeatures)); + } + }); + + on((event, emit) { + final updatedFeatures = List.from(state.features)..removeAt(event.index); + emit(state.copyWith(features: updatedFeatures)); + }); + + on((event, emit) { + final updatedFeatures = List.from(state.features); + if (event.index < updatedFeatures.length) { + updatedFeatures[event.index] = event.value; + emit(state.copyWith(features: updatedFeatures)); + } + }); + + on((event, emit) 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 schedule = state.workingDays.map((day) { + return { + 'Day': day, + 'StartTime': state.startTime, + 'EndTime': state.endTime, + 'Status': true, + }; + }).toList(); + + final Map data = { + 'Name': state.storeName, + 'Category': state.activityTypeId, + 'Province': state.province, + 'City': state.city, + 'Address': state.address, + 'Property': jsonEncode(state.features), + 'ShopNumber': state.plaque, + 'PostalCode': state.postalCode, + 'BusinessLicense': state.licenseNumber, + 'Coordinates': jsonEncode({ + 'longitude': state.longitude?.toString(), + 'latitude': state.latitude?.toString(), + }), + 'Schedule': jsonEncode(schedule), + }; + + if (state.logoPath != null && state.logoPath!.isNotEmpty) { + data['Logo'] = await MultipartFile.fromFile( + state.logoPath!, + filename: state.logoPath!.split('/').last, + ); + } + + final formData = FormData.fromMap(data); + + await _dio.post( + 'https://fartak.liara.run/shop/add', + 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) { + print('یک خطای پیش‌بینی نشده رخ داد: ${e.toString()}'); + emit(state.copyWith(isSubmitting: false, errorMessage: 'یک خطای پیش‌بینی نشده رخ داد: ${e.toString()}')); + } + }); } - } \ No newline at end of file diff --git a/lib/presentation/store_info/bloc/store_info_event.dart b/lib/presentation/store_info/bloc/store_info_event.dart index 46c2d34..7d0bac3 100644 --- a/lib/presentation/store_info/bloc/store_info_event.dart +++ b/lib/presentation/store_info/bloc/store_info_event.dart @@ -13,10 +13,11 @@ class StoreNameChanged extends StoreInfoEvent { } class ActivityTypeChanged extends StoreInfoEvent { - final String activityType; - ActivityTypeChanged(this.activityType); + final String activityTypeId; + ActivityTypeChanged(this.activityTypeId); } + class ProvinceChanged extends StoreInfoEvent { final String province; ProvinceChanged(this.province); @@ -71,5 +72,18 @@ class WorkingScheduleChanged extends StoreInfoEvent { }); } +class StoreFeatureAdded extends StoreInfoEvent {} + +class StoreFeatureRemoved extends StoreInfoEvent { + final int index; + StoreFeatureRemoved(this.index); +} + +class StoreFeatureUpdated extends StoreInfoEvent { + final int index; + final String value; + StoreFeatureUpdated(this.index, this.value); +} + class SubmitStoreInfo extends StoreInfoEvent {} \ No newline at end of file diff --git a/lib/presentation/store_info/bloc/store_info_state.dart b/lib/presentation/store_info/bloc/store_info_state.dart index abc790f..fc82a1d 100644 --- a/lib/presentation/store_info/bloc/store_info_state.dart +++ b/lib/presentation/store_info/bloc/store_info_state.dart @@ -1,5 +1,3 @@ -import 'package:equatable/equatable.dart'; - class StoreInfoState { final String? logoPath; final String storeName; @@ -19,6 +17,8 @@ class StoreInfoState { final List workingDays; final String? startTime; final String? endTime; + final List features; + final String? activityTypeId; StoreInfoState({ this.logoPath, @@ -36,9 +36,11 @@ class StoreInfoState { this.longitude, this.contactPhone, this.licenseNumber, - this.workingDays = const [], // مقدار اولیه یک لیست خالی است + this.workingDays = const [], this.startTime, this.endTime, + this.features = const [], + this.activityTypeId, }); StoreInfoState copyWith({ @@ -60,6 +62,8 @@ class StoreInfoState { List? workingDays, String? startTime, String? endTime, + List? features, + String? activityTypeId, }) { return StoreInfoState( logoPath: logoPath ?? this.logoPath, @@ -80,28 +84,30 @@ class StoreInfoState { workingDays: workingDays ?? this.workingDays, startTime: startTime ?? this.startTime, endTime: endTime ?? this.endTime, + features: features ?? this.features, + activityTypeId: activityTypeId ?? this.activityTypeId, ); } - @override List get props => [ - logoPath, - storeName, - activityType, - province, - city, - address, - plaque, - postalCode, - isSubmitting, - isSuccess, - errorMessage, - latitude, - longitude, - contactPhone, - licenseNumber, - workingDays, - startTime, - endTime, - ]; -} + logoPath, + storeName, + activityType, + province, + city, + address, + plaque, + postalCode, + isSubmitting, + isSuccess, + errorMessage, + latitude, + longitude, + contactPhone, + licenseNumber, + workingDays, + startTime, + endTime, + features, + ]; +} \ No newline at end of file diff --git a/lib/presentation/widgets/info_popup.dart b/lib/presentation/widgets/info_popup.dart new file mode 100644 index 0000000..eb56ec9 --- /dev/null +++ b/lib/presentation/widgets/info_popup.dart @@ -0,0 +1,89 @@ +import 'package:business_panel/core/config/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +Future showInfoDialog( + BuildContext context, { + required String title, + required String content, + required String iconPath, +}) async { + await showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + elevation: 10, + backgroundColor: Colors.white, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.topCenter, + children: [ + Padding( + padding: const EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Text( + content, + style: const TextStyle(color: AppColors.hint, fontSize: 16, height: 1.6), + textAlign: TextAlign.start, + ), + const SizedBox(height: 24), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: const BorderSide(color: AppColors.border), + ), + padding: const EdgeInsets.symmetric( + horizontal: 90, vertical: 10), + ), + onPressed: () async { + Navigator.of(context).pop(); + }, + child: const Text( + "متوجه شدم", + style: TextStyle(color: Colors.white), + ), + ), + ], + ), + ), + Positioned( + top: -40, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: CircleAvatar( + backgroundColor: Colors.white, + radius: 40, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: SvgPicture.asset(iconPath, height: 60, width: 60), + ), + ), + ), + ), + ], + ), + ); + }, + ); +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index e68dfa6..2983bed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -241,6 +241,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + dio: + dependency: "direct main" + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" equatable: dependency: "direct main" description: @@ -379,6 +395,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.28" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_svg: dependency: "direct main" description: @@ -617,10 +681,10 @@ packages: dependency: transitive description: name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.6.7" json_annotation: dependency: transitive description: @@ -781,6 +845,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" path_provider_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bc0fae8..4d313ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,8 @@ dependencies: equatable: ^2.0.7 intl: ^0.19.0 geolocator: ^14.0.2 + dio: ^5.8.0+1 + flutter_secure_storage: ^9.2.4 dev_dependencies: