From 0c53bfdbbba7b7d272a1c20369169df73cdb6cf4 Mon Sep 17 00:00:00 2001 From: mohamadmahdi jebeli Date: Wed, 9 Jul 2025 10:15:18 +0330 Subject: [PATCH] store added api --- assets/icons/addPic.svg | 3 + assets/icons/calendar-search.svg | 11 + assets/icons/call-calling.svg | 5 + assets/icons/document-text.svg | 6 + assets/icons/info-circle.svg | 5 + assets/icons/radar-2.svg | 4 + assets/images/emptyShop.svg | 271 ++++++++ assets/images/shoppAdded.png | Bin 0 -> 51173 bytes lib/core/services/token_storage_service.dart | 29 + lib/core/utils/logging_interceptor.dart | 42 ++ lib/domain/entities/category_entity.dart | 7 + lib/gen/assets.gen.dart | 33 + lib/presentation/auth/bloc/auth_bloc.dart | 67 +- lib/presentation/auth/bloc/auth_event.dart | 10 +- .../discount/bloc/discount_bloc.dart | 49 ++ .../discount/bloc/discount_event.dart | 52 ++ .../discount/bloc/discount_state.dart | 87 +++ lib/presentation/pages/add_discount_page.dart | 606 ++++++++++++++++++ lib/presentation/pages/login_page.dart | 45 +- lib/presentation/pages/otp_page.dart | 71 +- .../pages/product_creation_landing_page.dart | 114 ++++ lib/presentation/pages/store_info.dart | 427 +++++++++--- .../pages/store_info_display_page.dart | 270 ++++++++ .../pages/working_hours_dialog.dart | 5 - .../store_info/bloc/store_info_bloc.dart | 133 +++- .../store_info/bloc/store_info_event.dart | 18 +- .../store_info/bloc/store_info_state.dart | 54 +- lib/presentation/widgets/info_popup.dart | 89 +++ pubspec.lock | 92 ++- pubspec.yaml | 2 + 30 files changed, 2399 insertions(+), 208 deletions(-) create mode 100644 assets/icons/addPic.svg create mode 100644 assets/icons/calendar-search.svg create mode 100644 assets/icons/call-calling.svg create mode 100644 assets/icons/document-text.svg create mode 100644 assets/icons/info-circle.svg create mode 100644 assets/icons/radar-2.svg create mode 100644 assets/images/emptyShop.svg create mode 100644 assets/images/shoppAdded.png create mode 100644 lib/core/services/token_storage_service.dart create mode 100644 lib/core/utils/logging_interceptor.dart create mode 100644 lib/domain/entities/category_entity.dart create mode 100644 lib/presentation/discount/bloc/discount_bloc.dart create mode 100644 lib/presentation/discount/bloc/discount_event.dart create mode 100644 lib/presentation/discount/bloc/discount_state.dart create mode 100644 lib/presentation/pages/add_discount_page.dart create mode 100644 lib/presentation/pages/product_creation_landing_page.dart create mode 100644 lib/presentation/pages/store_info_display_page.dart create mode 100644 lib/presentation/widgets/info_popup.dart 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 0000000000000000000000000000000000000000..f9b0e74457dab2ddd546407bc139da1c9e450ab0 GIT binary patch literal 51173 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
d+V3!NN+0J3AzcM3U{X!8c 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 eFdW)CV!r_r!_<*F8HXtV0000 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: