diff --git a/android/app/build.gradle b/android/app/build.gradle index 1e76f31..e0740a6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -45,7 +45,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.baseproject" - minSdkVersion flutter.minSdkVersion + minSdkVersion 28 targetSdkVersion 36 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/lib/core/common/custom_interceptor.dart b/lib/core/common/custom_interceptor.dart index 973372d..b65c549 100644 --- a/lib/core/common/custom_interceptor.dart +++ b/lib/core/common/custom_interceptor.dart @@ -26,6 +26,14 @@ class CustomInterceptor extends InterceptorsWrapper { // }); } + // Xóa các field null trong query và body + options.queryParameters.removeWhere((String key, dynamic value) => value == null); + + final dynamic data = options.data; + if (data is Map) { + options.data = _removeNullFields(data); + } + return super.onRequest(options, handler); } @@ -87,8 +95,8 @@ class CustomInterceptor extends InterceptorsWrapper { } } - final dynamic errorData = err.response?.data; - //&& err.response?.statusCode == 400 + // final dynamic errorData = err.response?.data; + // && err.response?.statusCode == 400 // if (errorData != null && errorData["responseException"] != null) { // final dynamic temp = errorData["responseException"]["exceptionMessage"]; // try { @@ -104,4 +112,26 @@ class CustomInterceptor extends InterceptorsWrapper { return super.onError(err, handler); } + + Map _removeNullFields(Map source) { + final Map result = {}; + source.forEach((String key, dynamic value) { + final dynamic cleanedValue = _cleanValue(value); + if (cleanedValue != null) { + result[key] = cleanedValue; + } + }); + return result; + } + + dynamic _cleanValue(dynamic value) { + if (value is Map) { + return _removeNullFields(value); + } + if (value is List) { + final List cleanedList = value.map(_cleanValue).where((dynamic e) => e != null).toList(); + return cleanedList; + } + return value; + } } diff --git a/lib/core/common/injection.config.dart b/lib/core/common/injection.config.dart index e0bb3c7..2bd534b 100644 --- a/lib/core/common/injection.config.dart +++ b/lib/core/common/injection.config.dart @@ -8,12 +8,15 @@ import 'package:get_it/get_it.dart' as _i1; import 'package:injectable/injectable.dart' as _i2; -import '../../features/presentation/account/bloc/login_bloc.dart' as _i7; -import '../../features/presentation/app/bloc/user_bloc.dart' as _i3; -import '../../features/repositories/hra_repository.dart' as _i6; -import '../../features/usecases/index.dart' as _i4; +import '../../features/presentation/account/bloc/login_bloc.dart' as _i8; +import '../../features/presentation/app/bloc/user_bloc.dart' as _i5; +import '../../features/presentation/order/bloc/order_detail_bloc.dart' as _i9; +import '../../features/presentation/order/bloc/order_list_bloc.dart' as _i10; +import '../../features/repositories/hra_repository.dart' as _i4; +import '../../features/usecases/index.dart' as _i6; +import '../../features/usecases/order/order_use_cases.dart' as _i3; import '../../features/usecases/user/user_use_cases.dart' - as _i5; // ignore_for_file: unnecessary_lambdas + as _i7; // ignore_for_file: unnecessary_lambdas // ignore_for_file: lines_longer_than_80_chars /// initializes the registration of provided dependencies inside of [GetIt] @@ -27,9 +30,15 @@ _i1.GetIt $initGetIt( environment, environmentFilter, ); - gh.factory<_i3.UserBloc>(() => _i3.UserBloc(get<_i4.UserUseCases>())); - gh.lazySingleton<_i5.UserUseCases>( - () => _i5.UserUseCases(get<_i6.HraRepository>())); - gh.factory<_i7.LoginBloc>(() => _i7.LoginBloc(get<_i5.UserUseCases>())); + gh.lazySingleton<_i3.OrderUseCases>( + () => _i3.OrderUseCases(get<_i4.HraRepository>())); + gh.factory<_i5.UserBloc>(() => _i5.UserBloc(get<_i6.UserUseCases>())); + gh.lazySingleton<_i7.UserUseCases>( + () => _i7.UserUseCases(get<_i4.HraRepository>())); + gh.factory<_i8.LoginBloc>(() => _i8.LoginBloc(get<_i7.UserUseCases>())); + gh.factory<_i9.OrderDetailBloc>( + () => _i9.OrderDetailBloc(get<_i3.OrderUseCases>())); + gh.factory<_i10.OrderListBloc>( + () => _i10.OrderListBloc(get<_i3.OrderUseCases>())); return get; } diff --git a/lib/core/common/injection.dart b/lib/core/common/injection.dart index 0b5d716..d28af6b 100644 --- a/lib/core/common/injection.dart +++ b/lib/core/common/injection.dart @@ -3,6 +3,7 @@ import 'package:baseproject/core/components/alice.dart'; import 'package:baseproject/core/constants/index.dart'; import 'package:baseproject/features/presentation/app/view/app.dart'; import 'package:baseproject/features/repositories/hra_repository.dart'; +import 'package:baseproject/features/usecases/order/order_use_cases.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; diff --git a/lib/features/presentation/account/login_screen.dart b/lib/features/presentation/account/login_screen.dart index 0caca4d..9138744 100644 --- a/lib/features/presentation/account/login_screen.dart +++ b/lib/features/presentation/account/login_screen.dart @@ -54,7 +54,7 @@ class _LoginScreenState extends State { child: FormBuilder( initialValue: kDebugMode ? { - 'userName': 'quylx', + 'userName': 'hocsinh001', 'password': 'BearCMS0011002848238master', } : {}, diff --git a/lib/features/presentation/home/view/home.dart b/lib/features/presentation/home/view/home.dart index 620ba81..8894dbb 100644 --- a/lib/features/presentation/home/view/home.dart +++ b/lib/features/presentation/home/view/home.dart @@ -27,6 +27,14 @@ class _HomeState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text("Chào ${userInfo.fullName ?? ''}"), + ConstantWidget.heightSpace16, + ConstantWidget.buildPrimaryButton( + onPressed: () { + gotoMyOrders(context); + }, + text: 'Khóa học đã mua', + ), + ConstantWidget.heightSpace16, ConstantWidget.buildPrimaryButton( onPressed: () { BlocProvider.of(context).logout(); diff --git a/lib/features/presentation/order/bloc/order_detail_bloc.dart b/lib/features/presentation/order/bloc/order_detail_bloc.dart new file mode 100644 index 0000000..cba2d0c --- /dev/null +++ b/lib/features/presentation/order/bloc/order_detail_bloc.dart @@ -0,0 +1,68 @@ +import 'package:baseproject/core/common/index.dart'; +import 'package:baseproject/features/repositories/hra_repository_models.dart'; +import 'package:baseproject/features/usecases/order/order_use_cases.dart'; + +class OrderDetailViewModel { + const OrderDetailViewModel({ + this.isLoading = false, + this.order, + }); + + final bool isLoading; + final OrderDto? order; + + OrderDetailViewModel copyWith({ + bool? isLoading, + OrderDto? order, + }) { + return OrderDetailViewModel( + isLoading: isLoading ?? this.isLoading, + order: order ?? this.order, + ); + } +} + +class OrderDetailBloc extends BaseCubit> { + OrderDetailBloc(this._orderUseCases) + : super( + InitState( + const OrderDetailViewModel(), + ), + ); + + final OrderUseCases _orderUseCases; + + Future loadDetail(int id) async { + final currentModel = state.model; + + emit( + LoadingState( + currentModel.copyWith(isLoading: true), + ), + ); + + final result = await _orderUseCases.getOrderDetail(id); + + result.fold( + (error) { + showErrorMessage(error); + emit( + LoadedState( + currentModel.copyWith(isLoading: false), + ), + ); + }, + (order) { + emit( + LoadedState( + currentModel.copyWith( + isLoading: false, + order: order, + ), + ), + ); + }, + ); + } +} + diff --git a/lib/features/presentation/order/bloc/order_list_bloc.dart b/lib/features/presentation/order/bloc/order_list_bloc.dart new file mode 100644 index 0000000..52c9780 --- /dev/null +++ b/lib/features/presentation/order/bloc/order_list_bloc.dart @@ -0,0 +1,123 @@ +import 'package:baseproject/core/common/index.dart'; +import 'package:baseproject/features/repositories/hra_repository_models.dart'; +import 'package:baseproject/features/usecases/order/order_use_cases.dart'; + +class OrderListViewModel { + const OrderListViewModel({ + this.isLoading = false, + this.orders = const [], + this.totalRows = 0, + this.pageIndex = 1, + this.pageSize = 10, + }); + + final bool isLoading; + final List orders; + final int totalRows; + final int pageIndex; + final int pageSize; + + OrderListViewModel copyWith({ + bool? isLoading, + List? orders, + int? totalRows, + int? pageIndex, + int? pageSize, + }) { + return OrderListViewModel( + isLoading: isLoading ?? this.isLoading, + orders: orders ?? this.orders, + totalRows: totalRows ?? this.totalRows, + pageIndex: pageIndex ?? this.pageIndex, + pageSize: pageSize ?? this.pageSize, + ); + } +} + +class OrderListBloc extends BaseCubit> { + OrderListBloc(this._orderUseCases) + : super( + InitState( + const OrderListViewModel(), + ), + ); + + final OrderUseCases _orderUseCases; + + Future loadFirstPage() async { + final currentModel = state.model; + + emit( + LoadingState( + currentModel.copyWith(isLoading: true, pageIndex: 1), + ), + ); + + final result = await _orderUseCases.getMyOrders( + pageIndex: 1, + pageSize: currentModel.pageSize, + ); + + result.fold( + (error) { + emit( + LoadedState( + currentModel.copyWith( + isLoading: false, + ), + ), + ); + }, + (data) { + emit( + LoadedState( + currentModel.copyWith( + isLoading: false, + pageIndex: 1, + totalRows: data.totalRows ?? 0, + orders: data.data ?? [], + ), + ), + ); + }, + ); + } + + Future> loadMore() async { + final currentModel = state.model; + final nextPage = currentModel.pageIndex + 1; + + final result = await _orderUseCases.getMyOrders( + pageIndex: nextPage, + pageSize: currentModel.pageSize, + ); + + return result.fold( + (error) => [], + (data) { + final List newOrders = data.data ?? []; + final List updatedOrders = [ + ...currentModel.orders, + ...newOrders, + ]; + + emit( + LoadedState( + currentModel.copyWith( + pageIndex: nextPage, + totalRows: data.totalRows ?? currentModel.totalRows, + orders: updatedOrders, + ), + ), + ); + + return newOrders; + }, + ); + } + + Future refreshList() async { + await loadFirstPage(); + } +} + diff --git a/lib/features/presentation/order/view/order_detail_screen.dart b/lib/features/presentation/order/view/order_detail_screen.dart new file mode 100644 index 0000000..adc6e99 --- /dev/null +++ b/lib/features/presentation/order/view/order_detail_screen.dart @@ -0,0 +1,363 @@ +import 'package:baseproject/core/common/index.dart'; +import 'package:baseproject/core/components/index.dart'; +import 'package:baseproject/core/language/app_localizations.dart'; +import 'package:baseproject/features/presentation/order/bloc/order_detail_bloc.dart'; +import 'package:baseproject/features/usecases/order/order_use_cases.dart'; +import 'package:baseproject/features/repositories/hra_repository_enums.dart' + as enums; +import 'package:baseproject/features/repositories/hra_repository_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class OrderDetailScreen extends StatefulWidget { + const OrderDetailScreen({ + Key? key, + required this.orderId, + }) : super(key: key); + + final int orderId; + + @override + State createState() => _OrderDetailScreenState(); +} + +class _OrderDetailScreenState extends State { + late final OrderDetailBloc _bloc = OrderDetailBloc( + getItSuper(), + ); + + @override + void initState() { + super.initState(); + _bloc.loadDetail(widget.orderId); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => _bloc, + child: Scaffold( + appBar: AppBar( + title: const Text('Chi tiết đơn hàng'), + ), + body: SafeArea( + child: BlocBuilder>( + builder: (context, state) { + final vm = state.model; + final bool isLoading = + state is LoadingState || vm.isLoading; + + if (isLoading && vm.order == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (vm.order == null) { + return const Center( + child: Text('Không tìm thấy đơn hàng'), + ); + } + + return _buildDetailContent(context, vm.order!); + }, + ), + ), + ), + ); + } + + Widget _buildDetailContent(BuildContext context, OrderDto order) { + final appLoc = AppLocalizations.of(context)!; + final DateTime? date = order.paidDate ?? order.createdDate; + final String dateText = + appLoc.displayDateTime(date, isFullTime: true, isDateOfMonth: true); + + final String studentName = order.fullName ?? ''; + final String phone = order.phone ?? ''; + final String address = order.address ?? ''; + + final _OrderStatusUI statusUI = _getStatusUI(order.status); + + final String amountText = order.totalAmount != null + ? appLoc.displayNumber(order.totalAmount) + : ''; + + final List items = order.items ?? []; + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildInfoRow( + context, + leftLabel: 'Ngày đặt', + leftValue: dateText, + rightLabel: 'Người nhận', + rightValue: studentName, + ), + const SizedBox(height: 8), + _buildInfoRow( + context, + leftLabel: 'Số điện thoại', + leftValue: phone, + rightLabel: 'Địa chỉ giao hàng', + rightValue: address, + ), + const SizedBox(height: 8), + _buildStatusRow(context, statusUI), + const SizedBox(height: 16), + ...items.map((e) => _buildItem(context, e)).toList(), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + gradient: const LinearGradient( + colors: [ + Color(0xFFFFF8E1), + Color(0xFFFFECB3), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Tổng tiền', + style: TextStyle( + fontSize: 14, + ), + ), + const SizedBox(height: 4), + if (amountText.isNotEmpty) + Text.rich( + TextSpan( + text: amountText, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + children: const [ + TextSpan( + text: ' đ', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.normal, + color: Colors.red, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildInfoRow( + BuildContext context, { + required String leftLabel, + required String leftValue, + required String rightLabel, + required String rightValue, + }) { + return Row( + children: [ + Expanded( + child: _InfoBox( + label: leftLabel, + value: leftValue, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _InfoBox( + label: rightLabel, + value: rightValue, + ), + ), + ], + ); + } + + Widget _buildStatusRow(BuildContext context, _OrderStatusUI statusUI) { + return Row( + children: [ + Expanded( + child: _InfoBox( + label: 'Trạng thái', + valueWidget: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: statusUI.backgroundColor), + ), + child: Text( + statusUI.label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: statusUI.backgroundColor, + ), + ), + ), + ), + ), + const SizedBox(width: 8), + const Expanded(child: SizedBox()), + ], + ); + } + + Widget _buildItem(BuildContext context, OrderItemDto item) { + final appLoc = AppLocalizations.of(context)!; + final String name = item.product?.name ?? 'Khóa học'; + final String image = item.product?.image ?? ''; + final int quantity = item.quantity ?? 1; + final double? price = item.totalPrice ?? item.salePrice ?? item.unitPrice; + final String priceText = + price != null ? appLoc.displayNumber(price) : ''; + + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomImage( + imageUrl: image, + width: 56, + height: 56, + fit: BoxFit.cover, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + if (priceText.isNotEmpty) + Text.rich( + TextSpan( + text: '$quantity x ', + style: const TextStyle( + fontSize: 13, + ), + children: [ + TextSpan( + text: priceText, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + const TextSpan( + text: ' đ', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.normal, + color: Colors.red, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + _OrderStatusUI _getStatusUI(enums.OrderStatusEnum? status) { + if (status == enums.OrderStatusEnum.value_4 || + status == enums.OrderStatusEnum.value_5) { + return const _OrderStatusUI( + label: 'Thành công', + backgroundColor: Colors.green, + ); + } + + return const _OrderStatusUI( + label: 'Chờ liên hệ', + backgroundColor: Colors.blue, + ); + } +} + +class _InfoBox extends StatelessWidget { + const _InfoBox({ + Key? key, + required this.label, + this.value, + this.valueWidget, + }) : super(key: key); + + final String label; + final String? value; + final Widget? valueWidget; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFF1976D2)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 12, + color: Colors.black54, + ), + ), + const SizedBox(height: 4), + valueWidget ?? + Text( + value ?? '', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } +} + +class _OrderStatusUI { + const _OrderStatusUI({ + required this.label, + required this.backgroundColor, + }); + + final String label; + final Color backgroundColor; +} + diff --git a/lib/features/presentation/order/view/order_list_screen.dart b/lib/features/presentation/order/view/order_list_screen.dart new file mode 100644 index 0000000..eb44980 --- /dev/null +++ b/lib/features/presentation/order/view/order_list_screen.dart @@ -0,0 +1,219 @@ +import 'package:baseproject/core/common/index.dart'; +import 'package:baseproject/core/components/index.dart'; +import 'package:baseproject/core/language/app_localizations.dart'; +import 'package:baseproject/features/presentation/order/bloc/order_list_bloc.dart'; +import 'package:baseproject/features/presentation/order/view/order_detail_screen.dart'; +import 'package:baseproject/features/usecases/order/order_use_cases.dart'; +import 'package:baseproject/features/repositories/hra_repository_models.dart'; +import 'package:baseproject/features/repositories/hra_repository_enums.dart' as enums; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class OrderListScreen extends StatefulWidget { + const OrderListScreen({Key? key}) : super(key: key); + + @override + State createState() => _OrderListScreenState(); +} + +class _OrderListScreenState extends State { + late final OrderListBloc _bloc = OrderListBloc( + getItSuper(), + ); + + @override + void initState() { + super.initState(); + _bloc.loadFirstPage(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => _bloc, + child: Scaffold( + appBar: AppBar( + title: const Text('Khóa học đã mua'), + ), + body: SafeArea( + child: BlocBuilder>( + builder: (context, state) { + final vm = state.model; + final bool isLoading = state is LoadingState || vm.isLoading; + + return Column( + children: [ + if (isLoading && vm.orders.isEmpty) const LinearProgressIndicator(), + Expanded( + child: CustomListView( + totalItem: vm.totalRows, + items: vm.orders, + onRefresh: _bloc.refreshList, + onLoading: () async { + final List newItems = await _bloc.loadMore(); + return newItems; + }, + itemBuilder: (BuildContext context, int index) { + if (index >= vm.orders.length) { + return const SizedBox.shrink(); + } + final OrderDto order = vm.orders[index]; + return _buildOrderItem(context, order); + }, + separatorWidget: const Divider(height: 1), + padding: const EdgeInsets.all(16), + ), + ), + ], + ); + }, + ), + ), + ), + ); + } + + Widget _buildOrderItem(BuildContext context, OrderDto order) { + final appLoc = AppLocalizations.of(context)!; + final DateTime? date = order.paidDate ?? order.createdDate; + final String dateText = appLoc.displayDateTime(date, isFullTime: true, isDateOfMonth: true); + + final String studentName = order.fullName ?? ''; + final String phone = order.phone ?? ''; + + final _OrderStatusUI statusUI = _getStatusUI(order.status); + + return Card( + margin: const EdgeInsets.only(bottom: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFFE3F2FD), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + dateText, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF1976D2), + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: statusUI.backgroundColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + statusUI.label, + style: const TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + '$studentName • $phone', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Divider( + height: 1, + color: Colors.grey.shade300, + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Tổng tiền:', + style: TextStyle(fontSize: 13), + ), + if (order.totalAmount != null) + Text( + AppLocalizations.of(context)!.displayCurrency(order.totalAmount ?? 0), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + ], + ), + InkWell( + onTap: () { + if (order.id != null) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => OrderDetailScreen( + orderId: order.id!, + ), + ), + ); + } + }, + child: Text( + 'Chi tiết →', + style: TextStyle( + fontSize: 13, + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + _OrderStatusUI _getStatusUI(enums.OrderStatusEnum? status) { + if (status == enums.OrderStatusEnum.value_4 || status == enums.OrderStatusEnum.value_5) { + return const _OrderStatusUI( + label: 'Thành công', + backgroundColor: Colors.green, + ); + } + + return const _OrderStatusUI( + label: 'Chờ liên hệ', + backgroundColor: Colors.blue, + ); + } +} + +class _OrderStatusUI { + const _OrderStatusUI({ + required this.label, + required this.backgroundColor, + }); + + final String label; + final Color backgroundColor; +} diff --git a/lib/features/presentation/zoom/view/zoom_join_screen.dart b/lib/features/presentation/zoom/view/zoom_join_screen.dart new file mode 100644 index 0000000..5fe2d1c --- /dev/null +++ b/lib/features/presentation/zoom/view/zoom_join_screen.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; + +class ZoomJoinScreen extends StatelessWidget { + const ZoomJoinScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} +// import 'package:baseproject/core/components/constants_widget.dart'; +// import 'package:baseproject/core/theme/size.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; + +// class ZoomJoinScreen extends StatefulWidget { +// const ZoomJoinScreen({Key? key}) : super(key: key); + +// @override +// State createState() => _ZoomJoinScreenState(); +// } + +// class _ZoomJoinScreenState extends State { +// final TextEditingController _sessionNameController = TextEditingController(); +// final TextEditingController _userNameController = TextEditingController(text: 'Guest'); +// final TextEditingController _tokenController = TextEditingController(); +// final TextEditingController _passwordController = TextEditingController(); + +// final ZoomVideoSdk _zoom = ZoomVideoSdk(); +// bool _isInitializing = false; +// bool _isInitialized = false; +// bool _isJoining = false; + +// @override +// void initState() { +// super.initState(); +// _initZoomSdk(); +// } + +// @override +// void dispose() { +// _sessionNameController.dispose(); +// _userNameController.dispose(); +// _tokenController.dispose(); +// _passwordController.dispose(); +// super.dispose(); +// } + +// Future _initZoomSdk() async { +// if (_isInitializing || _isInitialized) return; +// setState(() { +// _isInitializing = true; +// }); + +// try { +// final initConfig = InitConfig( +// domain: 'zoom.us', +// enableLog: true, +// ); +// await _zoom.initSdk(initConfig); +// setState(() { +// _isInitialized = true; +// }); +// } catch (e) { +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text('Khởi tạo Zoom SDK thất bại: $e'), +// ), +// ); +// } finally { +// if (mounted) { +// setState(() { +// _isInitializing = false; +// }); +// } +// } +// } + +// Future _onJoin() async { +// if (!_isInitialized) { +// await _initZoomSdk(); +// if (!_isInitialized) return; +// } + +// final String sessionName = _sessionNameController.text.trim(); +// final String userName = _userNameController.text.trim(); +// final String token = _tokenController.text.trim(); +// final String password = _passwordController.text.trim(); + +// if (sessionName.isEmpty || token.isEmpty) { +// ScaffoldMessenger.of(context).showSnackBar( +// const SnackBar( +// content: Text('Vui lòng nhập đủ Session name và Token'), +// ), +// ); +// return; +// } + +// setState(() { +// _isJoining = true; +// }); + +// try { +// final Map audioOptions = { +// 'connect': true, +// 'mute': false, +// }; + +// final Map videoOptions = { +// 'localVideoOn': true, +// }; + +// final JoinSessionConfig joinSession = JoinSessionConfig( +// sessionName: sessionName, +// sessionPassword: password.isEmpty ? null : password, +// token: token, +// userName: userName.isEmpty ? 'Guest' : userName, +// audioOptions: audioOptions, +// videoOptions: videoOptions, +// sessionIdleTimeoutMins: 40, +// ); + +// await _zoom.joinSession(joinSession); +// } catch (e) { +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text('Không thể join Zoom: $e'), +// ), +// ); +// } finally { +// if (mounted) { +// setState(() { +// _isJoining = false; +// }); +// } +// } +// } + +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// appBar: AppBar( +// title: const Text('Tham gia Zoom (Video SDK)'), +// ), +// body: SafeArea( +// child: Padding( +// padding: const EdgeInsets.all(kPaddingDefault), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.stretch, +// children: [ +// ConstantWidget.textBodyDefault( +// 'Nhập thông tin phiên Zoom Video SDK (session) được backend cấp: Session name, Token, mật khẩu (nếu có).', +// textAlign: TextAlign.left, +// ), +// ConstantWidget.heightSpace16, +// TextField( +// controller: _sessionNameController, +// decoration: const InputDecoration( +// labelText: 'Session name', +// border: OutlineInputBorder(), +// ), +// ), +// ConstantWidget.heightSpace16, +// TextField( +// controller: _userNameController, +// decoration: const InputDecoration( +// labelText: 'Tên hiển thị', +// border: OutlineInputBorder(), +// ), +// ), +// ConstantWidget.heightSpace16, +// TextField( +// controller: _tokenController, +// decoration: const InputDecoration( +// labelText: 'SDK JWT Token', +// hintText: 'Token từ server Zoom/Backend', +// border: OutlineInputBorder(), +// ), +// ), +// ConstantWidget.heightSpace16, +// TextField( +// controller: _passwordController, +// decoration: const InputDecoration( +// labelText: 'Mật khẩu (nếu có)', +// border: OutlineInputBorder(), +// ), +// ), +// ConstantWidget.heightSpace24, +// SizedBox( +// height: 48, +// child: ConstantWidget.buildPrimaryButton( +// onPressed: (_isInitializing || _isJoining) ? null : _onJoin, +// text: _isJoining ? 'Đang join...' : 'Tham gia', +// ), +// ), +// ], +// ), +// ), +// ), +// ); +// } +// } diff --git a/lib/features/route/route_const.dart b/lib/features/route/route_const.dart index 8e80f52..928bdee 100644 --- a/lib/features/route/route_const.dart +++ b/lib/features/route/route_const.dart @@ -1,3 +1,4 @@ const String appInitRouteName = '/app_init'; const String loginRouteName = '/login'; -const String homeApp = '/home_app'; \ No newline at end of file +const String homeApp = '/home_app'; +const String myOrderRouteName = '/my_orders'; diff --git a/lib/features/route/route_generator.dart b/lib/features/route/route_generator.dart index 0ee9be3..0d51945 100644 --- a/lib/features/route/route_generator.dart +++ b/lib/features/route/route_generator.dart @@ -1,6 +1,7 @@ import 'package:baseproject/features/presentation/account/login_screen.dart'; import 'package:baseproject/features/presentation/app/view/init_screen.dart'; import 'package:baseproject/features/presentation/home/view/home.dart'; +import 'package:baseproject/features/presentation/order/view/order_list_screen.dart'; import 'package:baseproject/features/route/route_const.dart'; import 'package:flutter/material.dart'; @@ -15,6 +16,8 @@ class RouteGenerator { return MaterialPageRoute(settings: setting, builder: (_) => const Home()); case loginRouteName: return MaterialPageRoute(settings: setting, builder: (_) => const LoginScreen()); + case myOrderRouteName: + return MaterialPageRoute(settings: setting, builder: (_) => const OrderListScreen()); default: return null; } diff --git a/lib/features/route/route_goto.dart b/lib/features/route/route_goto.dart index 0ceeeaf..f00fb06 100644 --- a/lib/features/route/route_goto.dart +++ b/lib/features/route/route_goto.dart @@ -12,3 +12,7 @@ void gotoHome(BuildContext context) { Navigator.pushReplacementNamed(context, homeApp); } } + +void gotoMyOrders(BuildContext context) { + Navigator.pushNamed(context, myOrderRouteName); +} diff --git a/lib/features/usecases/index.dart b/lib/features/usecases/index.dart index 1a0721b..a4c7b70 100644 --- a/lib/features/usecases/index.dart +++ b/lib/features/usecases/index.dart @@ -1 +1,2 @@ export 'user/user_use_cases.dart'; +export 'order/order_use_cases.dart'; diff --git a/lib/features/usecases/order/order_use_cases.dart b/lib/features/usecases/order/order_use_cases.dart new file mode 100644 index 0000000..5bbade5 --- /dev/null +++ b/lib/features/usecases/order/order_use_cases.dart @@ -0,0 +1,54 @@ +import 'package:baseproject/core/common/index.dart'; +import 'package:baseproject/features/repositories/hra_repository.dart'; +import 'package:baseproject/features/repositories/hra_repository_models.dart'; +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +@lazySingleton +class OrderUseCases { + OrderUseCases(this._hraRepository); + + final HraRepository _hraRepository; + + Future> getMyOrders({ + required int pageIndex, + required int pageSize, + }) async { + try { + final query = OrderGetListQuery( + pageIndex: pageIndex, + pageSize: pageSize, + ); + + final result = await _hraRepository.orderListMy(query); + + if (result.data == null || result.success != true) { + return Left( + result.message ?? 'Không lấy được danh sách khóa học đã mua', + ); + } + + return Right(result.data!); + } catch (ex) { + showErrorMessage(ex.toString()); + return Left(ex.toString()); + } + } + + Future> getOrderDetail(int id) async { + try { + final OrderDtoApiResponse result = await _hraRepository.orderId(id); + + if (result.data == null || result.success != true) { + return Left( + result.message ?? 'Không lấy được thông tin đơn hàng', + ); + } + + return Right(result.data!); + } catch (ex) { + return Left(ex.toString()); + } + } +} + diff --git a/pubspec.yaml b/pubspec.yaml index b8ceee8..47a2eea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: flutter_staggered_grid_view: ^0.7.0 pull_to_refresh: ^2.0.0 dartz: ^0.10.1 + # flutter_zoom_videosdk: ^2.3.0 dependency_overrides: watcher: ^1.1.0