add product
This commit is contained in:
parent
0c53bfdbbb
commit
9ea1edf9b8
|
|
@ -0,0 +1,38 @@
|
||||||
|
class ApiConfig {
|
||||||
|
// Private constructor to prevent instantiation
|
||||||
|
ApiConfig._();
|
||||||
|
|
||||||
|
// Base URL for the API
|
||||||
|
static const String baseUrl = 'https://fartak.liara.run';
|
||||||
|
|
||||||
|
// ========== Auth Endpoints ==========
|
||||||
|
/// Endpoint to send OTP code to the user.
|
||||||
|
/// Method: POST
|
||||||
|
/// Body: {'Phone': phoneNumber, 'Code': countryCode}
|
||||||
|
static const String sendOtp = '$baseUrl/login/sendcode';
|
||||||
|
|
||||||
|
/// Endpoint to verify the OTP code.
|
||||||
|
/// Method: POST
|
||||||
|
/// Body: {'Phone': phoneNumber, 'Code': countryCode, 'OTP': otp}
|
||||||
|
static const String verifyOtp = '$baseUrl/login/getcode';
|
||||||
|
|
||||||
|
/// Endpoint to check if the user has a registered shop.
|
||||||
|
/// Method: GET
|
||||||
|
/// Headers: {'Authorization': 'Bearer <token>'}
|
||||||
|
static const String checkShopStatus = '$baseUrl/shop/get';
|
||||||
|
|
||||||
|
// ========== Store Endpoints ==========
|
||||||
|
/// Endpoint to add a new store.
|
||||||
|
/// Method: POST
|
||||||
|
/// Body: FormData
|
||||||
|
/// Headers: {'Authorization': 'Bearer <token>'}
|
||||||
|
static const String addStore = '$baseUrl/shop/add';
|
||||||
|
|
||||||
|
// ========== Discount Endpoints ==========
|
||||||
|
/// Endpoint to add a new discount.
|
||||||
|
/// Method: POST
|
||||||
|
/// Body: FormData
|
||||||
|
/// Headers: {'Authorization': 'Bearer <token>'}
|
||||||
|
static const String addDiscount = '$baseUrl/discount/add';
|
||||||
|
static const String getDiscounts = '$baseUrl/discount/get';
|
||||||
|
}
|
||||||
|
|
@ -17,4 +17,5 @@ class AppColors {
|
||||||
static const Color countdownBorderRserve = Color.fromARGB(255, 186, 222, 251);
|
static const Color countdownBorderRserve = Color.fromARGB(255, 186, 222, 251);
|
||||||
static const Color expiryReserve = Color.fromARGB(255, 183, 28, 28);
|
static const Color expiryReserve = Color.fromARGB(255, 183, 28, 28);
|
||||||
static const Color uploadElevated = Color.fromARGB(255, 233, 245, 254);
|
static const Color uploadElevated = Color.fromARGB(255, 233, 245, 254);
|
||||||
|
static const Color secTitle = Color.fromARGB(255, 95, 95, 95);
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
class DiscountEntity {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String shopName;
|
||||||
|
final List<String> images;
|
||||||
|
final String type;
|
||||||
|
final String description;
|
||||||
|
final double price;
|
||||||
|
final double nPrice;
|
||||||
|
final DateTime? endDate;
|
||||||
|
|
||||||
|
DiscountEntity({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.shopName,
|
||||||
|
required this.images,
|
||||||
|
required this.type,
|
||||||
|
required this.description,
|
||||||
|
required this.price,
|
||||||
|
required this.nPrice,
|
||||||
|
required this.endDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory DiscountEntity.fromJson(Map<String, dynamic> json) {
|
||||||
|
// A helper function to safely extract lists
|
||||||
|
List<String> _parseImages(dynamic imageList) {
|
||||||
|
if (imageList is List) {
|
||||||
|
return imageList.map((img) => (img['Url'] ?? '') as String).toList();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return DiscountEntity(
|
||||||
|
id: json['_id'] ?? '',
|
||||||
|
name: json['Name'] ?? 'بدون نام',
|
||||||
|
// Safely access nested properties
|
||||||
|
shopName: (json['Shop'] != null ? json['Shop']['Name'] : 'فروشگاه') ?? 'فروشگاه',
|
||||||
|
images: _parseImages(json['Images']),
|
||||||
|
type: (json['Type'] != null ? json['Type']['Description'] : 'عمومی') ?? 'عمومی',
|
||||||
|
description: json['Description'] ?? '',
|
||||||
|
price: (json['Price'] as num? ?? 0).toDouble(),
|
||||||
|
nPrice: (json['NPrice'] as num? ?? 0).toDouble(),
|
||||||
|
// Handle potential null or invalid date
|
||||||
|
endDate: json['EndDate'] != null ? DateTime.tryParse(json['EndDate']) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
class DiscountTypeEntity {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
DiscountTypeEntity({required this.id, required this.name});
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:business_panel/core/config/app_colors.dart';
|
import 'package:business_panel/core/config/app_colors.dart';
|
||||||
import 'package:business_panel/presentation/auth/bloc/auth_bloc.dart';
|
import 'package:business_panel/presentation/auth/bloc/auth_bloc.dart';
|
||||||
import 'package:business_panel/presentation/pages/onboarding_page.dart';
|
import 'package:business_panel/presentation/pages/onboarding_page.dart';
|
||||||
|
import 'package:business_panel/presentation/pages/splash_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
|
@ -99,8 +100,9 @@ class MyApp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
home: const OnboardingPage(),
|
home: const SplashPage(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:business_panel/core/config/api_config.dart'; // <-- این خط را اضافه کنید
|
||||||
import 'package:business_panel/core/services/token_storage_service.dart';
|
import 'package:business_panel/core/services/token_storage_service.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:business_panel/core/utils/logging_interceptor.dart';
|
import 'package:business_panel/core/utils/logging_interceptor.dart';
|
||||||
|
|
@ -6,40 +7,74 @@ part 'auth_event.dart';
|
||||||
part 'auth_state.dart';
|
part 'auth_state.dart';
|
||||||
|
|
||||||
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||||
|
|
||||||
late final Dio _dio;
|
late final Dio _dio;
|
||||||
final TokenStorageService _tokenStorage = TokenStorageService();
|
final TokenStorageService _tokenStorage = TokenStorageService();
|
||||||
|
|
||||||
AuthBloc() : super(AuthInitial()) {
|
AuthBloc() : super(AuthInitial()) {
|
||||||
_dio = Dio();
|
_dio = Dio();
|
||||||
_dio.interceptors.add(LoggingInterceptor());
|
_dio.interceptors.add(LoggingInterceptor());
|
||||||
|
|
||||||
on<SendOTPEvent>((event, emit) async {
|
on<CheckAuthStatus>((event, emit) async {
|
||||||
emit(AuthLoading());
|
emit(AuthLoading());
|
||||||
|
final token = await _tokenStorage.getAccessToken();
|
||||||
|
emit(AuthChecked(token != null && token.isNotEmpty));
|
||||||
|
});
|
||||||
|
|
||||||
|
on<CheckShopStatus>((event, emit) async {
|
||||||
emit(AuthLoading());
|
emit(AuthLoading());
|
||||||
try {
|
try {
|
||||||
|
final token = await _tokenStorage.getAccessToken();
|
||||||
|
if (token == null || token.isEmpty) {
|
||||||
|
emit(AuthFailure("Token not found. Please log in again."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the ApiConfig class
|
||||||
|
await _dio.get(
|
||||||
|
ApiConfig.checkShopStatus, // <-- تغییر در این خط
|
||||||
|
options: Options(
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(ShopExists());
|
||||||
|
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.response?.statusCode == 404) {
|
||||||
|
emit(NoShop());
|
||||||
|
} else {
|
||||||
|
emit(AuthFailure(e.response?.data?['message'] ?? 'An error occurred while checking shop status.'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
emit(AuthFailure('An unexpected error occurred: ${e.toString()}'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
on<SendOTPEvent>((event, emit) async {
|
||||||
|
emit(AuthLoading());
|
||||||
|
try {
|
||||||
|
// Use the ApiConfig class
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
'https://fartak.liara.run/login/sendcode',
|
ApiConfig.sendOtp, // <-- تغییر در این خط
|
||||||
data: {
|
data: {'Phone': event.phoneNumber, 'Code': event.countryCode},
|
||||||
'Phone': event.phoneNumber,
|
|
||||||
'Code': event.countryCode,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
emit(AuthCodeSentSuccess());
|
emit(AuthCodeSentSuccess());
|
||||||
} else {
|
} else {
|
||||||
emit(AuthFailure(response.data['message'] ?? 'خطایی رخ داد'));
|
emit(AuthFailure(response.data['message'] ?? 'An error occurred.'));
|
||||||
}
|
}
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
emit(AuthFailure(e.response?.data['message'] ?? 'خطای شبکه'));
|
emit(AuthFailure(e.response?.data['message'] ?? 'Network error.'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
on<VerifyOTPEvent>((event, emit) async {
|
on<VerifyOTPEvent>((event, emit) async {
|
||||||
emit(AuthLoading());
|
emit(AuthLoading());
|
||||||
try {
|
try {
|
||||||
|
// Use the ApiConfig class
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
'https://fartak.liara.run/login/getcode',
|
ApiConfig.verifyOtp, // <-- تغییر در این خط
|
||||||
data: {
|
data: {
|
||||||
'Phone': event.phoneNumber,
|
'Phone': event.phoneNumber,
|
||||||
'Code': event.countryCode,
|
'Code': event.countryCode,
|
||||||
|
|
@ -48,7 +83,6 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200 && response.data['data']['accessToken'] != null) {
|
if (response.statusCode == 200 && response.data['data']['accessToken'] != null) {
|
||||||
|
|
||||||
final accessToken = response.data['data']['accessToken'];
|
final accessToken = response.data['data']['accessToken'];
|
||||||
final refreshToken = response.data['data']['refreshToken'];
|
final refreshToken = response.data['data']['refreshToken'];
|
||||||
|
|
||||||
|
|
@ -57,24 +91,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||||
refreshToken: refreshToken,
|
refreshToken: refreshToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
emit(AuthVerified());
|
add(CheckShopStatus());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
emit(AuthFailure(response.data['message'] ?? 'کد تایید صحیح نمیباشد.'));
|
emit(AuthFailure(response.data['message'] ?? 'The verification code is incorrect.'));
|
||||||
}
|
}
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
emit(AuthFailure(e.response?.data['message'] ?? 'خطای شبکه'));
|
emit(AuthFailure(e.response?.data['message'] ?? 'Network error.'));
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
on<SaveUserInfoEvent>((event, emit) async {
|
|
||||||
emit(AuthLoading());
|
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
|
||||||
|
|
||||||
if (event.name.trim().isEmpty) {
|
|
||||||
emit(AuthFailure('لطفاً نام خود را وارد کنید.'));
|
|
||||||
} else {
|
|
||||||
emit(UserInfoSaved());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ part of 'auth_bloc.dart';
|
||||||
|
|
||||||
abstract class AuthEvent {}
|
abstract class AuthEvent {}
|
||||||
|
|
||||||
|
class CheckAuthStatus extends AuthEvent {}
|
||||||
|
|
||||||
|
class CheckShopStatus extends AuthEvent {}
|
||||||
|
|
||||||
class SendOTPEvent extends AuthEvent {
|
class SendOTPEvent extends AuthEvent {
|
||||||
final String phoneNumber;
|
final String phoneNumber;
|
||||||
final String countryCode;
|
final String countryCode;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
part of 'auth_bloc.dart';
|
part of 'auth_bloc.dart';
|
||||||
|
|
||||||
abstract class AuthState {}
|
abstract class AuthState {}
|
||||||
|
|
@ -7,6 +6,18 @@ class AuthInitial extends AuthState {}
|
||||||
|
|
||||||
class AuthLoading extends AuthState {}
|
class AuthLoading extends AuthState {}
|
||||||
|
|
||||||
|
// ADDED: State to indicate the result of the token check
|
||||||
|
class AuthChecked extends AuthState {
|
||||||
|
final bool hasToken;
|
||||||
|
AuthChecked(this.hasToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADDED: State for when the user has a shop
|
||||||
|
class ShopExists extends AuthState {}
|
||||||
|
|
||||||
|
// ADDED: State for when the user is logged in but has no shop
|
||||||
|
class NoShop extends AuthState {}
|
||||||
|
|
||||||
class AuthCodeSentSuccess extends AuthState {}
|
class AuthCodeSentSuccess extends AuthState {}
|
||||||
|
|
||||||
class AuthVerified extends AuthState {}
|
class AuthVerified extends AuthState {}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
|
import 'package:business_panel/core/config/api_config.dart';
|
||||||
|
import 'package:business_panel/core/services/token_storage_service.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'discount_event.dart';
|
import 'discount_event.dart';
|
||||||
import 'discount_state.dart';
|
import 'discount_state.dart';
|
||||||
|
|
||||||
class DiscountBloc extends Bloc<DiscountEvent, DiscountState> {
|
class DiscountBloc extends Bloc<DiscountEvent, DiscountState> {
|
||||||
DiscountBloc() : super(const DiscountState()) {
|
DiscountBloc() : super(const DiscountState()) {
|
||||||
|
|
||||||
|
final Dio _dio = Dio();
|
||||||
|
final TokenStorageService _tokenStorage = TokenStorageService();
|
||||||
|
|
||||||
on<ProductImageAdded>((event, emit) {
|
on<ProductImageAdded>((event, emit) {
|
||||||
List<String> updatedImages = List.from(state.productImages);
|
List<String> updatedImages = List.from(state.productImages);
|
||||||
if (updatedImages.length > event.index) {
|
if (updatedImages.length > event.index) {
|
||||||
|
|
@ -19,7 +27,7 @@ class DiscountBloc extends Bloc<DiscountEvent, DiscountState> {
|
||||||
});
|
});
|
||||||
|
|
||||||
on<DiscountTypeChanged>((event, emit) {
|
on<DiscountTypeChanged>((event, emit) {
|
||||||
emit(state.copyWith(discountType: event.type));
|
emit(state.copyWith(discountTypeId: event.typeId));
|
||||||
});
|
});
|
||||||
|
|
||||||
on<DescriptionChanged>((event, emit) {
|
on<DescriptionChanged>((event, emit) {
|
||||||
|
|
@ -45,5 +53,59 @@ class DiscountBloc extends Bloc<DiscountEvent, DiscountState> {
|
||||||
on<NotificationRadiusChanged>((event, emit) {
|
on<NotificationRadiusChanged>((event, emit) {
|
||||||
emit(state.copyWith(notificationRadius: event.radius));
|
emit(state.copyWith(notificationRadius: event.radius));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
on<SubmitDiscount>((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;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MultipartFile> imageFiles = [];
|
||||||
|
for (var imagePath in state.productImages) {
|
||||||
|
imageFiles.add(await MultipartFile.fromFile(
|
||||||
|
imagePath,
|
||||||
|
filename: imagePath.split('/').last,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = {
|
||||||
|
'Name' : state.productName,
|
||||||
|
'Images': imageFiles,
|
||||||
|
'Type': state.discountTypeId,
|
||||||
|
'Description': state.description,
|
||||||
|
'Price': double.tryParse(state.price),
|
||||||
|
'NPrice': double.tryParse(state.discountedPrice),
|
||||||
|
'Start': state.startDate != null ? DateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(state.startDate!) : null,
|
||||||
|
'End': state.endDate != null ? DateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(state.endDate!) : null,
|
||||||
|
'StartTime': state.startTime,
|
||||||
|
'EndTime': state.endTime,
|
||||||
|
'Radius': state.notificationRadius,
|
||||||
|
};
|
||||||
|
|
||||||
|
final formData = FormData.fromMap(data);
|
||||||
|
|
||||||
|
await _dio.post(
|
||||||
|
ApiConfig.addDiscount,
|
||||||
|
data: formData,
|
||||||
|
options: Options(
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(state.copyWith(isSubmitting: false, isSuccess: true));
|
||||||
|
|
||||||
|
} on DioException catch (e) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
isSubmitting: false,
|
||||||
|
errorMessage: e.response?.data['message'] ?? 'خطای سرور.',
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(isSubmitting: false, errorMessage: 'خطای ناشناخته: ${e.toString()}'));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,8 +13,8 @@ class ProductNameChanged extends DiscountEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
class DiscountTypeChanged extends DiscountEvent {
|
class DiscountTypeChanged extends DiscountEvent {
|
||||||
final String type;
|
final String typeId;
|
||||||
DiscountTypeChanged(this.type);
|
DiscountTypeChanged(this.typeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DescriptionChanged extends DiscountEvent {
|
class DescriptionChanged extends DiscountEvent {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import 'package:equatable/equatable.dart';
|
||||||
class DiscountState extends Equatable {
|
class DiscountState extends Equatable {
|
||||||
final List<String> productImages;
|
final List<String> productImages;
|
||||||
final String productName;
|
final String productName;
|
||||||
final String? discountType;
|
final String? discountTypeId;
|
||||||
final String description;
|
final String description;
|
||||||
final DateTime? startDate;
|
final DateTime? startDate;
|
||||||
final DateTime? endDate;
|
final DateTime? endDate;
|
||||||
|
|
@ -19,7 +19,7 @@ class DiscountState extends Equatable {
|
||||||
const DiscountState({
|
const DiscountState({
|
||||||
this.productImages = const [],
|
this.productImages = const [],
|
||||||
this.productName = '',
|
this.productName = '',
|
||||||
this.discountType,
|
this.discountTypeId,
|
||||||
this.description = '',
|
this.description = '',
|
||||||
this.startDate,
|
this.startDate,
|
||||||
this.endDate,
|
this.endDate,
|
||||||
|
|
@ -36,7 +36,7 @@ class DiscountState extends Equatable {
|
||||||
DiscountState copyWith({
|
DiscountState copyWith({
|
||||||
List<String>? productImages,
|
List<String>? productImages,
|
||||||
String? productName,
|
String? productName,
|
||||||
String? discountType,
|
String? discountTypeId,
|
||||||
String? description,
|
String? description,
|
||||||
DateTime? startDate,
|
DateTime? startDate,
|
||||||
DateTime? endDate,
|
DateTime? endDate,
|
||||||
|
|
@ -52,7 +52,7 @@ class DiscountState extends Equatable {
|
||||||
return DiscountState(
|
return DiscountState(
|
||||||
productImages: productImages ?? this.productImages,
|
productImages: productImages ?? this.productImages,
|
||||||
productName: productName ?? this.productName,
|
productName: productName ?? this.productName,
|
||||||
discountType: discountType ?? this.discountType,
|
discountTypeId: discountTypeId ?? this.discountTypeId,
|
||||||
description: description ?? this.description,
|
description: description ?? this.description,
|
||||||
startDate: startDate ?? this.startDate,
|
startDate: startDate ?? this.startDate,
|
||||||
endDate: endDate ?? this.endDate,
|
endDate: endDate ?? this.endDate,
|
||||||
|
|
@ -71,7 +71,7 @@ class DiscountState extends Equatable {
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
productImages,
|
productImages,
|
||||||
productName,
|
productName,
|
||||||
discountType,
|
discountTypeId,
|
||||||
description,
|
description,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:business_panel/core/config/api_config.dart';
|
||||||
|
import 'package:business_panel/core/services/token_storage_service.dart';
|
||||||
|
import 'package:business_panel/domain/entities/discount_entity.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
part 'home_event.dart';
|
||||||
|
part 'home_state.dart';
|
||||||
|
|
||||||
|
class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||||
|
final Dio _dio = Dio();
|
||||||
|
final TokenStorageService _tokenStorage = TokenStorageService();
|
||||||
|
|
||||||
|
HomeBloc() : super(HomeInitial()) {
|
||||||
|
on<FetchDiscounts>((event, emit) async {
|
||||||
|
emit(HomeLoading());
|
||||||
|
try {
|
||||||
|
final token = await _tokenStorage.getAccessToken();
|
||||||
|
if (token == null || token.isEmpty) {
|
||||||
|
emit(HomeError("خطای احراز هویت. لطفا دوباره وارد شوید."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _dio.get(
|
||||||
|
ApiConfig.getDiscounts,
|
||||||
|
options: Options(
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 && response.data['data'] != null) {
|
||||||
|
final List<dynamic> data = response.data['data'];
|
||||||
|
final discounts = data.map((json) => DiscountEntity.fromJson(json)).toList();
|
||||||
|
emit(HomeLoaded(discounts));
|
||||||
|
} else {
|
||||||
|
emit(HomeError(response.data['message'] ?? 'خطا در دریافت اطلاعات'));
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
// Log Dio error
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('DioException in HomeBloc: ${e.response?.data}');
|
||||||
|
}
|
||||||
|
emit(HomeError(e.response?.data['message'] ?? 'خطای شبکه'));
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
// Log any other error
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('Error in HomeBloc: $e');
|
||||||
|
print(stackTrace);
|
||||||
|
}
|
||||||
|
emit(HomeError('خطای پیشبینی نشده رخ داد: ${e.toString()}'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
part of 'home_bloc.dart';
|
||||||
|
|
||||||
|
abstract class HomeEvent {}
|
||||||
|
|
||||||
|
class FetchDiscounts extends HomeEvent {}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
part of 'home_bloc.dart';
|
||||||
|
|
||||||
|
abstract class HomeState {}
|
||||||
|
|
||||||
|
class HomeInitial extends HomeState {}
|
||||||
|
|
||||||
|
class HomeLoading extends HomeState {}
|
||||||
|
|
||||||
|
class HomeLoaded extends HomeState {
|
||||||
|
final List<DiscountEntity> discounts;
|
||||||
|
|
||||||
|
HomeLoaded(this.discounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeError extends HomeState {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
HomeError(this.message);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:business_panel/core/config/app_colors.dart';
|
import 'package:business_panel/core/config/app_colors.dart';
|
||||||
|
import 'package:business_panel/domain/entities/discount_type_entity.dart';
|
||||||
import 'package:business_panel/gen/assets.gen.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_bloc.dart';
|
||||||
import 'package:business_panel/presentation/discount/bloc/discount_event.dart';
|
import 'package:business_panel/presentation/discount/bloc/discount_event.dart';
|
||||||
|
|
@ -86,11 +87,37 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final List<DiscountTypeEntity> discountTypes = [
|
||||||
|
DiscountTypeEntity(id: "dda9aef3-367e-48b3-ba35-8e7744d99659", name: "ساعت خوش"),
|
||||||
|
DiscountTypeEntity(id: "57577fac-7d06-4b2e-a577-7d2ce98fee58", name: "رفیق بازی"),
|
||||||
|
DiscountTypeEntity(id: "06156635-048b-4ed9-b5d5-2f89824435e1", name: "محصول جانبی رایگان"),
|
||||||
|
DiscountTypeEntity(id: "16e7d1e9-29c4-4320-9869-3c986cc20734", name: "کالای مکمل"),
|
||||||
|
DiscountTypeEntity(id: "fb600fbb-bab4-4e63-a25b-d8ffdacb7c09", name: "پلکانی"),
|
||||||
|
DiscountTypeEntity(id: "488ef29e-415d-4362-b984-509faabac058", name: "دعوت نامه طلایی"),
|
||||||
|
DiscountTypeEntity(id: "e03e5823-27d8-4f45-bd6c-f7c11822ec7a", name: "بازگشت وجه"),
|
||||||
|
DiscountTypeEntity(id: "bb0eea57-b630-4373-baff-72df72921e67", name: "سایر"),
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: _buildCustomAppBar(context),
|
appBar: _buildCustomAppBar(context),
|
||||||
body: SingleChildScrollView(
|
body: BlocListener<DiscountBloc, DiscountState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.isSuccess) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text("تخفیف با موفقیت ثبت شد!"), backgroundColor: Colors.green),
|
||||||
|
);
|
||||||
|
// میتوانید به صفحه دیگری ناوبری کنید
|
||||||
|
// Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
if (state.errorMessage != null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(state.errorMessage!), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -168,18 +195,24 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: BlocBuilder<DiscountBloc, DiscountState>(
|
||||||
onPressed: () {
|
builder: (context, state) {
|
||||||
// TODO: Implement submit logic
|
return ElevatedButton(
|
||||||
|
onPressed: state.isSubmitting ? null : () {
|
||||||
|
context.read<DiscountBloc>().add(SubmitDiscount());
|
||||||
|
},
|
||||||
|
child: state.isSubmitting
|
||||||
|
? const CircularProgressIndicator(color: Colors.white)
|
||||||
|
: const Text("ثبت تخفیف"),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: const Text("ثبت تخفیف"),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSectionTitle({
|
Widget _buildSectionTitle({
|
||||||
|
|
@ -268,25 +301,15 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDiscountTypeDropdown() {
|
Widget _buildDiscountTypeDropdown() {
|
||||||
final List<String> discountTypes = [
|
|
||||||
"ساعت خوش",
|
|
||||||
"رفیق بازی",
|
|
||||||
"محصول جانبی رایگان",
|
|
||||||
"کالای مکمل",
|
|
||||||
"پلکانی",
|
|
||||||
"دعوتنامه طلایی",
|
|
||||||
"بازگشت وجه",
|
|
||||||
"سایر",
|
|
||||||
];
|
|
||||||
|
|
||||||
return DropdownButtonFormField<String>(
|
return DropdownButtonFormField<String>(
|
||||||
|
value: context.watch<DiscountBloc>().state.discountTypeId,
|
||||||
icon: SvgPicture.asset(
|
icon: SvgPicture.asset(
|
||||||
Assets.icons.arrowDown,
|
Assets.icons.arrowDown,
|
||||||
width: 24,
|
width: 24,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
),
|
),
|
||||||
menuMaxHeight: 400,
|
menuMaxHeight: 400,
|
||||||
hint: Text("ساعت خوش"),
|
hint: const Text("نوع تخفیف را انتخاب کنید"),
|
||||||
decoration: _inputDecoration("نوع تخفیف", isRequired: true).copyWith(
|
decoration: _inputDecoration("نوع تخفیف", isRequired: true).copyWith(
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
vertical: 14,
|
vertical: 14,
|
||||||
|
|
@ -294,10 +317,9 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
items:
|
items: discountTypes.map((type) {
|
||||||
discountTypes
|
return DropdownMenuItem(value: type.id, child: Text(type.name));
|
||||||
.map((type) => DropdownMenuItem(value: type, child: Text(type)))
|
}).toList(),
|
||||||
.toList(),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
context.read<DiscountBloc>().add(DiscountTypeChanged(value));
|
context.read<DiscountBloc>().add(DiscountTypeChanged(value));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,380 @@
|
||||||
|
import 'package:business_panel/core/config/app_colors.dart';
|
||||||
|
import 'package:business_panel/domain/entities/discount_entity.dart';
|
||||||
|
import 'package:business_panel/gen/assets.gen.dart';
|
||||||
|
import 'package:business_panel/presentation/home/bloc/home_bloc.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:slide_countdown/slide_countdown.dart';
|
||||||
|
import 'package:business_panel/presentation/pages/add_discount_page.dart';
|
||||||
|
|
||||||
|
class HomePage extends StatefulWidget {
|
||||||
|
const HomePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HomePage> createState() => _HomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePageState extends State<HomePage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
context.read<HomeBloc>().add(FetchDiscounts());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: _buildCustomAppBar(context),
|
||||||
|
body: BlocBuilder<HomeBloc, HomeState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state is HomeLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (state is HomeError) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Text('خطا: ${state.message}', textAlign: TextAlign.center),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (state is HomeLoaded) {
|
||||||
|
if (state.discounts.isEmpty) {
|
||||||
|
return _buildEmptyState();
|
||||||
|
}
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
context.read<HomeBloc>().add(FetchDiscounts());
|
||||||
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
// تعداد آیتمها یکی بیشتر از تعداد تخفیفهاست تا دکمه هم جا شود
|
||||||
|
itemCount: state.discounts.length + 1,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
// اگر ایندکس مربوط به آخرین آیتم بود، دکمه را نمایش بده
|
||||||
|
if (index == state.discounts.length) {
|
||||||
|
return _buildAddDiscountButton();
|
||||||
|
}
|
||||||
|
// در غیر این صورت، کارت تخفیف را نمایش بده
|
||||||
|
final discount = state.discounts[index];
|
||||||
|
return _buildDiscountCard(discount);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// حالت پیشفرض
|
||||||
|
return _buildEmptyState();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyState() {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(Assets.images.emptyHome, height: 200),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text(
|
||||||
|
"هنوز تخفیفی ثبت نکردهاید",
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
|
child: _buildAddDiscountButton(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAddDiscountButton() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.confirm,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(40),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context)
|
||||||
|
.push(MaterialPageRoute(builder: (_) => const AddDiscountPage()))
|
||||||
|
.then((_) {
|
||||||
|
// رفرش لیست بعد از بازگشت از صفحه افزودن
|
||||||
|
context.read<HomeBloc>().add(FetchDiscounts());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.add, color: Colors.white),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
"تعریف تخفیف جدید",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDiscountCard(DiscountEntity discount) {
|
||||||
|
final remaining = discount.endDate != null
|
||||||
|
? discount.endDate!.difference(DateTime.now())
|
||||||
|
: const Duration(seconds: -1);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Text(
|
||||||
|
"تخفیف ${discount.type}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// TODO: Implement edit functionality
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(Assets.icons.edit, width: 20, color: AppColors.active),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
const Text("ویرایش", style: TextStyle(color: AppColors.active)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Card(
|
||||||
|
color: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
side: BorderSide(color: Colors.grey.shade300, width: 1),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (discount.images.isNotEmpty && discount.images.first.isNotEmpty)
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
child: Image.network(
|
||||||
|
discount.images.first,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) => Container(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.image_not_supported, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Container(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.store, color: Colors.grey, size: 50),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(Assets.icons.shop, width: 18, color: Colors.grey.shade700),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
discount.shopName,
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(Assets.icons.shoppingCart, width: 18, color: Colors.grey.shade700),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
discount.name,
|
||||||
|
style: TextStyle(fontSize: 15, color: Colors.grey.shade600),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
if (discount.endDate == null)
|
||||||
|
const Text(
|
||||||
|
'تاریخ نامعتبر',
|
||||||
|
style: TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
|
||||||
|
)
|
||||||
|
else if (remaining.isNegative)
|
||||||
|
const Text(
|
||||||
|
'منقضی شده',
|
||||||
|
style: TextStyle(color: AppColors.expiryReserve, fontWeight: FontWeight.bold),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(Assets.icons.timerPause, width: 18, color: Colors.grey.shade700),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(child: _buildCountdownTimer(remaining)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCountdownTimer(Duration remaining) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Localizations.override(
|
||||||
|
context: context,
|
||||||
|
locale: const Locale('en'),
|
||||||
|
child: SlideCountdown(
|
||||||
|
duration: remaining,
|
||||||
|
slideDirection: SlideDirection.up,
|
||||||
|
separator: ':',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.countdown,
|
||||||
|
),
|
||||||
|
separatorStyle: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: AppColors.countdown,
|
||||||
|
),
|
||||||
|
decoration: const BoxDecoration(color: Colors.transparent),
|
||||||
|
shouldShowDays: (d) => d.inDays > 0,
|
||||||
|
shouldShowHours: (d) => true,
|
||||||
|
shouldShowMinutes: (d) => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
_buildTimerLabels(remaining),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimerLabels(Duration duration) {
|
||||||
|
const labelStyle = TextStyle(fontSize: 10, color: AppColors.selectedImg);
|
||||||
|
|
||||||
|
List<Widget> labels = [];
|
||||||
|
if (duration.inDays > 0) {
|
||||||
|
labels.add(const SizedBox(width: 30, child: Text("روز", style: labelStyle)));
|
||||||
|
}
|
||||||
|
if (duration.inHours > 0 || duration.inDays > 0) {
|
||||||
|
labels.add(const SizedBox(width: 35, child: Text("ساعت", style: labelStyle)));
|
||||||
|
}
|
||||||
|
labels.add(const SizedBox(width: 30, child: Text("دقیقه", style: labelStyle)));
|
||||||
|
labels.add(const SizedBox(width: 30, child: Text("ثانیه", style: labelStyle)));
|
||||||
|
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: labels.reversed.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PreferredSizeWidget _buildCustomAppBar(BuildContext context) {
|
||||||
|
return PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(70.0),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
bottom: Radius.circular(15),
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.08),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(Assets.icons.logoWithName),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: SvgPicture.asset(
|
||||||
|
Assets.icons.discountShape,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: SvgPicture.asset(Assets.icons.scanBarcode),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -107,17 +107,17 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
// Row(
|
||||||
children: [
|
// children: [
|
||||||
Checkbox(
|
// Checkbox(
|
||||||
value: _keepSignedIn,
|
// value: _keepSignedIn,
|
||||||
onChanged: (value) =>
|
// onChanged: (value) =>
|
||||||
setState(() => _keepSignedIn = value ?? false),
|
// setState(() => _keepSignedIn = value ?? false),
|
||||||
activeColor: AppColors.primary,
|
// activeColor: AppColors.primary,
|
||||||
),
|
// ),
|
||||||
Text("مرا به خاطر بسپار", style: textTheme.bodyMedium),
|
// Text("مرا به خاطر بسپار", style: textTheme.bodyMedium),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
BlocConsumer<AuthBloc, AuthState>(
|
BlocConsumer<AuthBloc, AuthState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import 'package:business_panel/core/config/app_colors.dart';
|
import 'package:business_panel/core/config/app_colors.dart';
|
||||||
import 'package:business_panel/gen/assets.gen.dart';
|
import 'package:business_panel/gen/assets.gen.dart';
|
||||||
import 'package:business_panel/presentation/auth/bloc/auth_bloc.dart';
|
import 'package:business_panel/presentation/auth/bloc/auth_bloc.dart';
|
||||||
|
import 'package:business_panel/presentation/home/bloc/home_bloc.dart';
|
||||||
|
import 'package:business_panel/presentation/pages/home_page.dart';
|
||||||
import 'package:business_panel/presentation/pages/store_info.dart';
|
import 'package:business_panel/presentation/pages/store_info.dart';
|
||||||
import 'package:business_panel/presentation/store_info/bloc/store_info_bloc.dart';
|
import 'package:business_panel/presentation/store_info/bloc/store_info_bloc.dart';
|
||||||
import 'package:business_panel/presentation/utils/otp_timer_helper.dart';
|
import 'package:business_panel/presentation/utils/otp_timer_helper.dart';
|
||||||
|
|
@ -139,6 +141,8 @@ class _OtpPageState extends State<OtpPage> {
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// *** CHANGE IS HERE ***
|
||||||
BlocConsumer<AuthBloc, AuthState>(
|
BlocConsumer<AuthBloc, AuthState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state is AuthFailure) {
|
if (state is AuthFailure) {
|
||||||
|
|
@ -147,7 +151,20 @@ class _OtpPageState extends State<OtpPage> {
|
||||||
_errorMessage = state.message;
|
_errorMessage = state.message;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (state is AuthVerified) {
|
|
||||||
|
if (state is ShopExists) {
|
||||||
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => BlocProvider(
|
||||||
|
create: (context) => HomeBloc(),
|
||||||
|
child: const HomePage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// If no shop, navigate to StoreInfoPage to create one
|
||||||
|
if (state is NoShop) {
|
||||||
Navigator.of(context).pushAndRemoveUntil(
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => BlocProvider(
|
builder: (_) => BlocProvider(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import 'package:business_panel/presentation/auth/bloc/auth_bloc.dart';
|
||||||
|
import 'package:business_panel/presentation/home/bloc/home_bloc.dart';
|
||||||
|
import 'package:business_panel/presentation/pages/home_page.dart';
|
||||||
|
import 'package:business_panel/presentation/pages/onboarding_page.dart';
|
||||||
|
import 'package:business_panel/presentation/pages/store_info.dart';
|
||||||
|
import 'package:business_panel/presentation/store_info/bloc/store_info_bloc.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class SplashPage extends StatefulWidget {
|
||||||
|
const SplashPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SplashPage> createState() => _SplashPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SplashPageState extends State<SplashPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
context.read<AuthBloc>().add(CheckAuthStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocListener<AuthBloc, AuthState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state is AuthChecked) {
|
||||||
|
if (state.hasToken) {
|
||||||
|
context.read<AuthBloc>().add(CheckShopStatus());
|
||||||
|
} else {
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(builder: (_) => const OnboardingPage()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (state is ShopExists) {
|
||||||
|
// کاربر هم توکن دارد و هم فروشگاه، به HomePage میرود
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => BlocProvider(
|
||||||
|
create: (context) => HomeBloc(),
|
||||||
|
child: const HomePage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (state is NoShop) {
|
||||||
|
// کاربر توکن دارد ولی فروشگاه نساخته، به صفحه ساخت فروشگاه میرود
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => BlocProvider(
|
||||||
|
create: (context) => StoreInfoBloc(),
|
||||||
|
child: const StoreInfoPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (state is AuthFailure) {
|
||||||
|
// اگر در هر مرحله خطایی رخ داد، کاربر را به صفحه لاگین بفرست
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(builder: (_) => const OnboardingPage()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:business_panel/core/config/api_config.dart';
|
||||||
import 'package:business_panel/core/services/token_storage_service.dart';
|
import 'package:business_panel/core/services/token_storage_service.dart';
|
||||||
import 'package:business_panel/presentation/store_info/bloc/store_info_state.dart';
|
import 'package:business_panel/presentation/store_info/bloc/store_info_state.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
@ -111,6 +112,7 @@ class StoreInfoBloc extends Bloc<StoreInfoEvent, StoreInfoState> {
|
||||||
'Address': state.address,
|
'Address': state.address,
|
||||||
'Property': jsonEncode(state.features),
|
'Property': jsonEncode(state.features),
|
||||||
'ShopNumber': state.plaque,
|
'ShopNumber': state.plaque,
|
||||||
|
'Phone': state.contactPhone,
|
||||||
'PostalCode': state.postalCode,
|
'PostalCode': state.postalCode,
|
||||||
'BusinessLicense': state.licenseNumber,
|
'BusinessLicense': state.licenseNumber,
|
||||||
'Coordinates': jsonEncode({
|
'Coordinates': jsonEncode({
|
||||||
|
|
@ -130,7 +132,7 @@ class StoreInfoBloc extends Bloc<StoreInfoEvent, StoreInfoState> {
|
||||||
final formData = FormData.fromMap(data);
|
final formData = FormData.fromMap(data);
|
||||||
|
|
||||||
await _dio.post(
|
await _dio.post(
|
||||||
'https://fartak.liara.run/shop/add',
|
ApiConfig.addStore,
|
||||||
data: formData,
|
data: formData,
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: {'Authorization': 'Bearer $token'},
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
|
@ -140,6 +142,7 @@ class StoreInfoBloc extends Bloc<StoreInfoEvent, StoreInfoState> {
|
||||||
emit(state.copyWith(isSubmitting: false, isSuccess: true));
|
emit(state.copyWith(isSubmitting: false, isSuccess: true));
|
||||||
|
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
|
print(e.response?.data['message']);
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
errorMessage: e.response?.data['message'] ?? 'خطایی در ارتباط با سرور رخ داد.'));
|
errorMessage: e.response?.data['message'] ?? 'خطایی در ارتباط با سرور رخ داد.'));
|
||||||
|
|
|
||||||
16
pubspec.lock
16
pubspec.lock
|
|
@ -893,6 +893,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
pausable_timer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pausable_timer
|
||||||
|
sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0+3"
|
||||||
persian_datetime_picker:
|
persian_datetime_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -1066,6 +1074,14 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
slide_countdown:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: slide_countdown
|
||||||
|
sha256: "363914f96389502467d4dc9c0f26e88f93df3d8e37de2d5ff05b16d981fe973d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ dependencies:
|
||||||
geolocator: ^14.0.2
|
geolocator: ^14.0.2
|
||||||
dio: ^5.8.0+1
|
dio: ^5.8.0+1
|
||||||
flutter_secure_storage: ^9.2.4
|
flutter_secure_storage: ^9.2.4
|
||||||
|
slide_countdown: ^2.0.2
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue