Trang order

This commit is contained in:
minhhieu2312 2026-03-02 16:57:34 +07:00
parent 3e8bf01018
commit c1813273b4
17 changed files with 1101 additions and 14 deletions

View File

@ -45,7 +45,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.baseproject" applicationId "com.example.baseproject"
minSdkVersion flutter.minSdkVersion minSdkVersion 28
targetSdkVersion 36 targetSdkVersion 36
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName

View File

@ -26,6 +26,14 @@ class CustomInterceptor extends InterceptorsWrapper {
// }); // });
} }
// Xóa các field null trong query 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); 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 // && err.response?.statusCode == 400
// if (errorData != null && errorData["responseException"] != null) { // if (errorData != null && errorData["responseException"] != null) {
// final dynamic temp = errorData["responseException"]["exceptionMessage"]; // final dynamic temp = errorData["responseException"]["exceptionMessage"];
@ -104,4 +112,26 @@ class CustomInterceptor extends InterceptorsWrapper {
return super.onError(err, handler); 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;
}
} }

View File

@ -8,12 +8,15 @@
import 'package:get_it/get_it.dart' as _i1; import 'package:get_it/get_it.dart' as _i1;
import 'package:injectable/injectable.dart' as _i2; import 'package:injectable/injectable.dart' as _i2;
import '../../features/presentation/account/bloc/login_bloc.dart' as _i7; import '../../features/presentation/account/bloc/login_bloc.dart' as _i8;
import '../../features/presentation/app/bloc/user_bloc.dart' as _i3; import '../../features/presentation/app/bloc/user_bloc.dart' as _i5;
import '../../features/repositories/hra_repository.dart' as _i6; import '../../features/presentation/order/bloc/order_detail_bloc.dart' as _i9;
import '../../features/usecases/index.dart' as _i4; 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' 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 // ignore_for_file: lines_longer_than_80_chars
/// initializes the registration of provided dependencies inside of [GetIt] /// initializes the registration of provided dependencies inside of [GetIt]
@ -27,9 +30,15 @@ _i1.GetIt $initGetIt(
environment, environment,
environmentFilter, environmentFilter,
); );
gh.factory<_i3.UserBloc>(() => _i3.UserBloc(get<_i4.UserUseCases>())); gh.lazySingleton<_i3.OrderUseCases>(
gh.lazySingleton<_i5.UserUseCases>( () => _i3.OrderUseCases(get<_i4.HraRepository>()));
() => _i5.UserUseCases(get<_i6.HraRepository>())); gh.factory<_i5.UserBloc>(() => _i5.UserBloc(get<_i6.UserUseCases>()));
gh.factory<_i7.LoginBloc>(() => _i7.LoginBloc(get<_i5.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; return get;
} }

View File

@ -3,6 +3,7 @@ import 'package:baseproject/core/components/alice.dart';
import 'package:baseproject/core/constants/index.dart'; import 'package:baseproject/core/constants/index.dart';
import 'package:baseproject/features/presentation/app/view/app.dart'; import 'package:baseproject/features/presentation/app/view/app.dart';
import 'package:baseproject/features/repositories/hra_repository.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:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';

View File

@ -54,7 +54,7 @@ class _LoginScreenState extends State<LoginScreen> {
child: FormBuilder( child: FormBuilder(
initialValue: kDebugMode initialValue: kDebugMode
? { ? {
'userName': 'quylx', 'userName': 'hocsinh001',
'password': 'BearCMS0011002848238master', 'password': 'BearCMS0011002848238master',
} }
: {}, : {},

View File

@ -27,6 +27,14 @@ class _HomeState extends State<Home> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text("Chào ${userInfo.fullName ?? ''}"), Text("Chào ${userInfo.fullName ?? ''}"),
ConstantWidget.heightSpace16,
ConstantWidget.buildPrimaryButton(
onPressed: () {
gotoMyOrders(context);
},
text: 'Khóa học đã mua',
),
ConstantWidget.heightSpace16,
ConstantWidget.buildPrimaryButton( ConstantWidget.buildPrimaryButton(
onPressed: () { onPressed: () {
BlocProvider.of<UserBloc>(context).logout(); BlocProvider.of<UserBloc>(context).logout();

View 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,
),
),
);
},
);
}
}

View 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();
}
}

View 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;
}

View 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;
}

View 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',
// ),
// ),
// ],
// ),
// ),
// ),
// );
// }
// }

View File

@ -1,3 +1,4 @@
const String appInitRouteName = '/app_init'; const String appInitRouteName = '/app_init';
const String loginRouteName = '/login'; const String loginRouteName = '/login';
const String homeApp = '/home_app'; const String homeApp = '/home_app';
const String myOrderRouteName = '/my_orders';

View File

@ -1,6 +1,7 @@
import 'package:baseproject/features/presentation/account/login_screen.dart'; import 'package:baseproject/features/presentation/account/login_screen.dart';
import 'package:baseproject/features/presentation/app/view/init_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/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:baseproject/features/route/route_const.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -15,6 +16,8 @@ class RouteGenerator {
return MaterialPageRoute<void>(settings: setting, builder: (_) => const Home()); return MaterialPageRoute<void>(settings: setting, builder: (_) => const Home());
case loginRouteName: case loginRouteName:
return MaterialPageRoute<void>(settings: setting, builder: (_) => const LoginScreen()); return MaterialPageRoute<void>(settings: setting, builder: (_) => const LoginScreen());
case myOrderRouteName:
return MaterialPageRoute<void>(settings: setting, builder: (_) => const OrderListScreen());
default: default:
return null; return null;
} }

View File

@ -12,3 +12,7 @@ void gotoHome(BuildContext context) {
Navigator.pushReplacementNamed(context, homeApp); Navigator.pushReplacementNamed(context, homeApp);
} }
} }
void gotoMyOrders(BuildContext context) {
Navigator.pushNamed(context, myOrderRouteName);
}

View File

@ -1 +1,2 @@
export 'user/user_use_cases.dart'; export 'user/user_use_cases.dart';
export 'order/order_use_cases.dart';

View 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());
}
}
}

View File

@ -58,6 +58,7 @@ dependencies:
flutter_staggered_grid_view: ^0.7.0 flutter_staggered_grid_view: ^0.7.0
pull_to_refresh: ^2.0.0 pull_to_refresh: ^2.0.0
dartz: ^0.10.1 dartz: ^0.10.1
# flutter_zoom_videosdk: ^2.3.0
dependency_overrides: dependency_overrides:
watcher: ^1.1.0 watcher: ^1.1.0