Trang order
This commit is contained in:
parent
3e8bf01018
commit
c1813273b4
@ -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
|
||||
|
||||
@ -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<String, dynamic>) {
|
||||
options.data = _removeNullFields(data);
|
||||
}
|
||||
|
||||
return super.onRequest(options, handler);
|
||||
}
|
||||
|
||||
@ -87,7 +95,7 @@ class CustomInterceptor extends InterceptorsWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
final dynamic errorData = err.response?.data;
|
||||
// final dynamic errorData = err.response?.data;
|
||||
// && err.response?.statusCode == 400
|
||||
// if (errorData != null && errorData["responseException"] != null) {
|
||||
// final dynamic temp = errorData["responseException"]["exceptionMessage"];
|
||||
@ -104,4 +112,26 @@ class CustomInterceptor extends InterceptorsWrapper {
|
||||
|
||||
return super.onError(err, handler);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _removeNullFields(Map<String, dynamic> source) {
|
||||
final Map<String, dynamic> result = <String, dynamic>{};
|
||||
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<String, dynamic>) {
|
||||
return _removeNullFields(value);
|
||||
}
|
||||
if (value is List) {
|
||||
final List<dynamic> cleanedList = value.map(_cleanValue).where((dynamic e) => e != null).toList();
|
||||
return cleanedList;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -54,7 +54,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
child: FormBuilder(
|
||||
initialValue: kDebugMode
|
||||
? {
|
||||
'userName': 'quylx',
|
||||
'userName': 'hocsinh001',
|
||||
'password': 'BearCMS0011002848238master',
|
||||
}
|
||||
: {},
|
||||
|
||||
@ -27,6 +27,14 @@ class _HomeState extends State<Home> {
|
||||
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<UserBloc>(context).logout();
|
||||
|
||||
68
lib/features/presentation/order/bloc/order_detail_bloc.dart
Normal file
68
lib/features/presentation/order/bloc/order_detail_bloc.dart
Normal file
@ -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<BaseStateBloc<OrderDetailViewModel>> {
|
||||
OrderDetailBloc(this._orderUseCases)
|
||||
: super(
|
||||
InitState<OrderDetailViewModel>(
|
||||
const OrderDetailViewModel(),
|
||||
),
|
||||
);
|
||||
|
||||
final OrderUseCases _orderUseCases;
|
||||
|
||||
Future<void> loadDetail(int id) async {
|
||||
final currentModel = state.model;
|
||||
|
||||
emit(
|
||||
LoadingState<OrderDetailViewModel>(
|
||||
currentModel.copyWith(isLoading: true),
|
||||
),
|
||||
);
|
||||
|
||||
final result = await _orderUseCases.getOrderDetail(id);
|
||||
|
||||
result.fold(
|
||||
(error) {
|
||||
showErrorMessage(error);
|
||||
emit(
|
||||
LoadedState<OrderDetailViewModel>(
|
||||
currentModel.copyWith(isLoading: false),
|
||||
),
|
||||
);
|
||||
},
|
||||
(order) {
|
||||
emit(
|
||||
LoadedState<OrderDetailViewModel>(
|
||||
currentModel.copyWith(
|
||||
isLoading: false,
|
||||
order: order,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
123
lib/features/presentation/order/bloc/order_list_bloc.dart
Normal file
123
lib/features/presentation/order/bloc/order_list_bloc.dart
Normal file
@ -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 <OrderDto>[],
|
||||
this.totalRows = 0,
|
||||
this.pageIndex = 1,
|
||||
this.pageSize = 10,
|
||||
});
|
||||
|
||||
final bool isLoading;
|
||||
final List<OrderDto> orders;
|
||||
final int totalRows;
|
||||
final int pageIndex;
|
||||
final int pageSize;
|
||||
|
||||
OrderListViewModel copyWith({
|
||||
bool? isLoading,
|
||||
List<OrderDto>? 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<BaseStateBloc<OrderListViewModel>> {
|
||||
OrderListBloc(this._orderUseCases)
|
||||
: super(
|
||||
InitState<OrderListViewModel>(
|
||||
const OrderListViewModel(),
|
||||
),
|
||||
);
|
||||
|
||||
final OrderUseCases _orderUseCases;
|
||||
|
||||
Future<void> loadFirstPage() async {
|
||||
final currentModel = state.model;
|
||||
|
||||
emit(
|
||||
LoadingState<OrderListViewModel>(
|
||||
currentModel.copyWith(isLoading: true, pageIndex: 1),
|
||||
),
|
||||
);
|
||||
|
||||
final result = await _orderUseCases.getMyOrders(
|
||||
pageIndex: 1,
|
||||
pageSize: currentModel.pageSize,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(error) {
|
||||
emit(
|
||||
LoadedState<OrderListViewModel>(
|
||||
currentModel.copyWith(
|
||||
isLoading: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
(data) {
|
||||
emit(
|
||||
LoadedState<OrderListViewModel>(
|
||||
currentModel.copyWith(
|
||||
isLoading: false,
|
||||
pageIndex: 1,
|
||||
totalRows: data.totalRows ?? 0,
|
||||
orders: data.data ?? <OrderDto>[],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<OrderDto>> 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) => <OrderDto>[],
|
||||
(data) {
|
||||
final List<OrderDto> newOrders = data.data ?? <OrderDto>[];
|
||||
final List<OrderDto> updatedOrders = <OrderDto>[
|
||||
...currentModel.orders,
|
||||
...newOrders,
|
||||
];
|
||||
|
||||
emit(
|
||||
LoadedState<OrderListViewModel>(
|
||||
currentModel.copyWith(
|
||||
pageIndex: nextPage,
|
||||
totalRows: data.totalRows ?? currentModel.totalRows,
|
||||
orders: updatedOrders,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return newOrders;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> refreshList() async {
|
||||
await loadFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
363
lib/features/presentation/order/view/order_detail_screen.dart
Normal file
363
lib/features/presentation/order/view/order_detail_screen.dart
Normal file
@ -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<OrderDetailScreen> createState() => _OrderDetailScreenState();
|
||||
}
|
||||
|
||||
class _OrderDetailScreenState extends State<OrderDetailScreen> {
|
||||
late final OrderDetailBloc _bloc = OrderDetailBloc(
|
||||
getItSuper<OrderUseCases>(),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc.loadDetail(widget.orderId);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<OrderDetailBloc>(
|
||||
create: (_) => _bloc,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Chi tiết đơn hàng'),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderDetailBloc,
|
||||
BaseStateBloc<OrderDetailViewModel>>(
|
||||
builder: (context, state) {
|
||||
final vm = state.model;
|
||||
final bool isLoading =
|
||||
state is LoadingState<OrderDetailViewModel> || 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<OrderItemDto> items = order.items ?? <OrderItemDto>[];
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
_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>[
|
||||
Color(0xFFFFF8E1),
|
||||
Color(0xFFFFECB3),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
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>[
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
CustomImage(
|
||||
imageUrl: image,
|
||||
width: 56,
|
||||
height: 56,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
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>[
|
||||
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: <Widget>[
|
||||
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;
|
||||
}
|
||||
|
||||
219
lib/features/presentation/order/view/order_list_screen.dart
Normal file
219
lib/features/presentation/order/view/order_list_screen.dart
Normal file
@ -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<OrderListScreen> createState() => _OrderListScreenState();
|
||||
}
|
||||
|
||||
class _OrderListScreenState extends State<OrderListScreen> {
|
||||
late final OrderListBloc _bloc = OrderListBloc(
|
||||
getItSuper<OrderUseCases>(),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc.loadFirstPage();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<OrderListBloc>(
|
||||
create: (_) => _bloc,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Khóa học đã mua'),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<OrderListBloc, BaseStateBloc<OrderListViewModel>>(
|
||||
builder: (context, state) {
|
||||
final vm = state.model;
|
||||
final bool isLoading = state is LoadingState<OrderListViewModel> || vm.isLoading;
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (isLoading && vm.orders.isEmpty) const LinearProgressIndicator(),
|
||||
Expanded(
|
||||
child: CustomListView<OrderDto>(
|
||||
totalItem: vm.totalRows,
|
||||
items: vm.orders,
|
||||
onRefresh: _bloc.refreshList,
|
||||
onLoading: () async {
|
||||
final List<OrderDto> 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: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
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<void>(
|
||||
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;
|
||||
}
|
||||
202
lib/features/presentation/zoom/view/zoom_join_screen.dart
Normal file
202
lib/features/presentation/zoom/view/zoom_join_screen.dart
Normal file
@ -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<ZoomJoinScreen> createState() => _ZoomJoinScreenState();
|
||||
// }
|
||||
|
||||
// class _ZoomJoinScreenState extends State<ZoomJoinScreen> {
|
||||
// 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<void> _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<void> _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<String, bool> audioOptions = {
|
||||
// 'connect': true,
|
||||
// 'mute': false,
|
||||
// };
|
||||
|
||||
// final Map<String, bool> 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: <Widget>[
|
||||
// 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',
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
@ -1,3 +1,4 @@
|
||||
const String appInitRouteName = '/app_init';
|
||||
const String loginRouteName = '/login';
|
||||
const String homeApp = '/home_app';
|
||||
const String myOrderRouteName = '/my_orders';
|
||||
|
||||
@ -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<void>(settings: setting, builder: (_) => const Home());
|
||||
case loginRouteName:
|
||||
return MaterialPageRoute<void>(settings: setting, builder: (_) => const LoginScreen());
|
||||
case myOrderRouteName:
|
||||
return MaterialPageRoute<void>(settings: setting, builder: (_) => const OrderListScreen());
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -12,3 +12,7 @@ void gotoHome(BuildContext context) {
|
||||
Navigator.pushReplacementNamed(context, homeApp);
|
||||
}
|
||||
}
|
||||
|
||||
void gotoMyOrders(BuildContext context) {
|
||||
Navigator.pushNamed(context, myOrderRouteName);
|
||||
}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export 'user/user_use_cases.dart';
|
||||
export 'order/order_use_cases.dart';
|
||||
|
||||
54
lib/features/usecases/order/order_use_cases.dart
Normal file
54
lib/features/usecases/order/order_use_cases.dart
Normal file
@ -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<Either<String, OrderDtoFilterResult>> 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<String, OrderDtoFilterResult>(
|
||||
result.message ?? 'Không lấy được danh sách khóa học đã mua',
|
||||
);
|
||||
}
|
||||
|
||||
return Right<String, OrderDtoFilterResult>(result.data!);
|
||||
} catch (ex) {
|
||||
showErrorMessage(ex.toString());
|
||||
return Left<String, OrderDtoFilterResult>(ex.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<Either<String, OrderDto>> getOrderDetail(int id) async {
|
||||
try {
|
||||
final OrderDtoApiResponse result = await _hraRepository.orderId(id);
|
||||
|
||||
if (result.data == null || result.success != true) {
|
||||
return Left<String, OrderDto>(
|
||||
result.message ?? 'Không lấy được thông tin đơn hàng',
|
||||
);
|
||||
}
|
||||
|
||||
return Right<String, OrderDto>(result.data!);
|
||||
} catch (ex) {
|
||||
return Left<String, OrderDto>(ex.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user