Commit 33ec1dde authored by DatHV's avatar DatHV
Browse files

update game, notify

parent 8bef97c9
......@@ -43,4 +43,11 @@ class APIPaths {
static const String getPreviewOrderInfo = "/order/api/v1.0/preview-order";
static const String getPreviewOrderBankAccounts = "/order/api/v1.0/payment/bank-accounts";
static const String getPreviewPaymentMethods = "/order/api/v1.0/payment/payment-methods";
static const String orderSubmitPayment = "/order/api/v1.0/submit-order";
static const String getTransactionHistoryDetail = "/order/api/v1.0/my-orders/%@";
static const String getNotificationCategories = "/dynamic-home/api/v1.0/notification_groups";
static const String getNotifications = "/notificationGetList/1.0.0";
static const String deleteNotification = "/notificationDeleteOne/1.0.0";
static const String deleteAllNotifications = "/notificationDeleteAll/1.0.0";
static const String notificationMarkAsSeen = "/notificationMarkAsSeen/1.0.0";
}
\ No newline at end of file
......@@ -15,6 +15,8 @@ import '../model/update_response_model.dart';
import '../preference/point/header_home_model.dart';
import '../screen/faqs/faqs_model.dart';
import '../screen/game/models/game_bundle_item_model.dart';
import '../screen/notification/models/category_notify_item_model.dart';
import '../screen/notification/models/notification_list_data_model.dart';
import '../screen/onboarding/model/check_phone_response_model.dart';
import '../screen/onboarding/model/onboarding_info_model.dart';
import '../screen/otp/model/create_otp_response_model.dart';
......@@ -27,8 +29,11 @@ import '../screen/shopping/model/affiliate_category_model.dart';
import '../screen/shopping/model/affiliate_product_top_sale_model.dart';
import '../screen/shopping/model/cashback_overview_model.dart';
import '../screen/splash/splash_screen_viewmodel.dart';
import '../screen/transaction/history/transaction_history_model.dart';
import '../screen/transaction/model/order_product_payment_response_model.dart';
import '../screen/transaction/model/payment_bank_account_info_model.dart';
import '../screen/transaction/model/payment_method_model.dart';
import '../screen/transaction/model/payment_method_type.dart';
import '../screen/transaction/model/preview_order_payment_model.dart';
import '../screen/voucher/models/like_product_reponse_model.dart';
import '../screen/voucher/models/product_store_model.dart';
......@@ -360,13 +365,9 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
}
Future<BaseResponseModel<PreviewOrderPaymentModel>> getPreviewOrderInfo(Json body) async {
return requestNormal(
APIPaths.getPreviewOrderInfo,
Method.POST,
body,
(data) {
return PreviewOrderPaymentModel.fromJson(data as Json);
});
return requestNormal(APIPaths.getPreviewOrderInfo, Method.POST, body, (data) {
return PreviewOrderPaymentModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<List<PaymentBankAccountInfoModel>>> getPreviewOrderBankAccounts() async {
......@@ -382,4 +383,91 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
return list.map((e) => PaymentMethodModel.fromJson(e)).toList();
});
}
Future<BaseResponseModel<List<CategoryNotifyItemModel>>> getNotificationCategories() async {
return requestNormal(APIPaths.getNotificationCategories, Method.GET, {}, (data) {
final list = data as List<dynamic>;
return list.map((e) => CategoryNotifyItemModel.fromJson(e)).toList();
});
}
Future<BaseResponseModel<NotificationListDataModel>> getNotifications(Json body) async {
return requestNormal(APIPaths.getNotifications, Method.POST, body, (data) {
return NotificationListDataModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<EmptyCodable>> deleteNotification(String id) async {
return requestNormal(APIPaths.deleteNotification, Method.POST, {"notification_id": id}, (data) {
return EmptyCodable.fromJson(data as Json);
});
}
Future<BaseResponseModel<EmptyCodable>> deleteAllNotifications() async {
String? token = DataPreference.instance.token ?? "";
final body = {"access_token": token};
return requestNormal(APIPaths.deleteAllNotifications, Method.POST, body, (data) {
return EmptyCodable.fromJson(data as Json);
});
}
Future<BaseResponseModel<EmptyCodable>> notificationMarkAsSeen() async {
String? token = DataPreference.instance.token ?? "";
final body = {"access_token": token};
return requestNormal(APIPaths.notificationMarkAsSeen, Method.POST, body, (data) {
return EmptyCodable.fromJson(data as Json);
});
}
Future<BaseResponseModel<TransactionHistoryModel>> getTransactionHistoryDetail(String id) async {
final path = APIPaths.getTransactionHistoryDetail.replaceAll("%@", id);
return requestNormal(path, Method.GET, {}, (data) {
return TransactionHistoryModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<OrderProductPaymentResponseModel>> orderSubmitPayment({
required List<ProductModel> products,
required int quantity,
required String requestId,
int? point,
int? cash,
PaymentMethodModel? method,
int? paymentTokenId,
bool? saveToken,
String? metadata,
}) async {
final items =
products.map((product) {
return {'product_id': product.id, 'product_type': product.type ?? '', 'quantity': quantity};
}).toList();
final Map<String, dynamic> params = {'request_id': requestId, 'items': items, 'flow': '21'};
// flash_sale
final firstProduct = products.first;
if (firstProduct.previewFlashSale?.isFlashSalePrice == true && firstProduct.previewFlashSale?.id != null) {
params['flash_sale_id'] = firstProduct.previewFlashSale!.id;
}
// Optional parameters
if (method != null) {
params['payment_method'] = method.code;
}
if (point != null && point != 0) {
params['pay_point'] = point;
}
if (cash != null && cash != 0) {
params['pay_cash'] = cash;
}
if (paymentTokenId != null) {
params['payment_token_id'] = paymentTokenId;
}
if (saveToken != null) {
params['save_token'] = saveToken;
}
if (metadata != null) {
params['metadata'] = metadata;
}
return requestNormal(APIPaths.orderSubmitPayment, Method.POST, params, (data) {
return OrderProductPaymentResponseModel.fromJson(data as Json);
});
}
}
......@@ -20,12 +20,17 @@ class HomeScreen extends StatelessWidget {
ElevatedButton(onPressed: () => _showMiniGame(context), child: const Text('Mini Game')),
ElevatedButton(onPressed: () => _logout(context), child: const Text('Đăng xuất')),
ElevatedButton(onPressed: () => _showSetting(context), child: const Text('Setting')),
ElevatedButton(onPressed: () => _showNotify(context), child: const Text('Notify')),
],
),
),
);
}
void _showNotify(BuildContext context) async {
Get.toNamed(notificationScreen);
}
void _showMiniGame(BuildContext context) async {
Navigator.push(
context,
......
import 'package:json_annotation/json_annotation.dart';
part 'category_notify_item_model.g.dart';
@JsonSerializable()
class CategoryNotifyItemModel {
int? id;
@JsonKey(name: 'name_vi')
String? nameVi;
@JsonKey(name: 'total_unread')
int? totalUnread;
bool? isSelected;
CategoryNotifyItemModel({
this.id,
this.nameVi,
this.totalUnread,
this.isSelected,
});
factory CategoryNotifyItemModel.fromJson(Map<String, dynamic> json) =>
_$CategoryNotifyItemModelFromJson(json);
Map<String, dynamic> toJson() => _$CategoryNotifyItemModelToJson(this);
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'category_notify_item_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CategoryNotifyItemModel _$CategoryNotifyItemModelFromJson(
Map<String, dynamic> json,
) => CategoryNotifyItemModel(
id: (json['id'] as num?)?.toInt(),
nameVi: json['name_vi'] as String?,
totalUnread: (json['total_unread'] as num?)?.toInt(),
isSelected: json['isSelected'] as bool?,
);
Map<String, dynamic> _$CategoryNotifyItemModelToJson(
CategoryNotifyItemModel instance,
) => <String, dynamic>{
'id': instance.id,
'name_vi': instance.nameVi,
'total_unread': instance.totalUnread,
'isSelected': instance.isSelected,
};
import 'package:json_annotation/json_annotation.dart';
part 'notification_item_model.g.dart';
@JsonSerializable()
class NotificationItemModel {
@JsonKey(name: 'notification_id')
final String? notificationId;
final String? title;
final String? body;
final String? type;
@JsonKey(name: 'click_action_type')
final String? clickActionType;
@JsonKey(name: 'click_action_param')
final String? clickActionParam;
@JsonKey(name: 'seen_at')
final String? seenAt;
final String? status;
@JsonKey(name: 'create_time')
final String? createTime;
@JsonKey(name: 'working_site')
final WorkingSiteModel? workingSite;
bool get hasSeen => (seenAt ?? "").isNotEmpty;
NotificationItemModel({
this.notificationId,
this.title,
this.body,
this.type,
this.clickActionType,
this.clickActionParam,
this.seenAt,
this.status,
this.createTime,
this.workingSite,
});
factory NotificationItemModel.fromJson(Map<String, dynamic> json) =>
_$NotificationItemModelFromJson(json);
Map<String, dynamic> toJson() => _$NotificationItemModelToJson(this);
}
class WorkingSiteModel {
final String? avatar;
WorkingSiteModel({
this.avatar,
});
factory WorkingSiteModel.fromJson(Map<String, dynamic> json) {
return WorkingSiteModel(
avatar: json['avatar'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'avatar': avatar,
};
}
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'notification_item_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
NotificationItemModel _$NotificationItemModelFromJson(
Map<String, dynamic> json,
) => NotificationItemModel(
notificationId: json['notification_id'] as String?,
title: json['title'] as String?,
body: json['body'] as String?,
type: json['type'] as String?,
clickActionType: json['click_action_type'] as String?,
clickActionParam: json['click_action_param'] as String?,
seenAt: json['seen_at'] as String?,
status: json['status'] as String?,
createTime: json['create_time'] as String?,
workingSite:
json['working_site'] == null
? null
: WorkingSiteModel.fromJson(
json['working_site'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$NotificationItemModelToJson(
NotificationItemModel instance,
) => <String, dynamic>{
'notification_id': instance.notificationId,
'title': instance.title,
'body': instance.body,
'type': instance.type,
'click_action_type': instance.clickActionType,
'click_action_param': instance.clickActionParam,
'seen_at': instance.seenAt,
'status': instance.status,
'create_time': instance.createTime,
'working_site': instance.workingSite,
};
import 'package:json_annotation/json_annotation.dart';
import 'notification_item_model.dart';
part 'notification_list_data_model.g.dart';
@JsonSerializable()
class NotificationListDataModel {
final String? start;
final String? limit;
final String? total;
final String? unread;
@JsonKey(name: 'list_items')
final List<NotificationItemModel>? items;
NotificationListDataModel({
this.start,
this.limit,
this.total,
this.unread,
this.items,
});
factory NotificationListDataModel.fromJson(Map<String, dynamic> json) =>
_$NotificationListDataModelFromJson(json);
Map<String, dynamic> toJson() => _$NotificationListDataModelToJson(this);
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'notification_list_data_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
NotificationListDataModel _$NotificationListDataModelFromJson(
Map<String, dynamic> json,
) => NotificationListDataModel(
start: json['start'] as String?,
limit: json['limit'] as String?,
total: json['total'] as String?,
unread: json['unread'] as String?,
items:
(json['list_items'] as List<dynamic>?)
?.map(
(e) => NotificationItemModel.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
Map<String, dynamic> _$NotificationListDataModelToJson(
NotificationListDataModel instance,
) => <String, dynamic>{
'start': instance.start,
'limit': instance.limit,
'total': instance.total,
'unread': instance.unread,
'list_items': instance.items,
};
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../resouce/base_color.dart';
import '../../widgets/alert/data_alert_model.dart';
import '../../widgets/back_button.dart';
import '../../widgets/custom_empty_widget.dart';
import '../../widgets/image_loader.dart';
import 'models/notification_item_model.dart';
import 'notification_viewmodel.dart';
class NotificationScreen extends BaseScreen {
const NotificationScreen({super.key});
@override
State<NotificationScreen> createState() => _NotificationScreenState();
}
class _NotificationScreenState extends BaseState<NotificationScreen> with BasicState {
final _scrollController = ScrollController();
final _viewModel = Get.put(NotificationViewModel());
final LayerLink _layerLink = LayerLink();
final GlobalKey _infoKey = GlobalKey();
OverlayEntry? _popupEntry;
bool _isPopupShown = false;
@override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100) {
_viewModel.fetchNotifications(refresh: false);
}
});
}
@override
Widget createBody() {
return Scaffold(
appBar: AppBar(
scrolledUnderElevation: 0,
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
title: const Text(
'Thông báo',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
),
leading: CustomBackButton(),
actions: [
CompositedTransformTarget(
link: _layerLink,
child: IconButton(
key: _infoKey,
icon: const Icon(Icons.settings, color: Colors.black),
onPressed: _toggleSetting,
),
),
],
),
body: Obx(() {
final items = _viewModel.notifications;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildNotificationCategory(),
const Divider(height: 1),
if (items.isEmpty)
const Expanded(
child: EmptyWidget(),
)
else
Expanded(
child: Container(
color: Colors.grey.shade100,
child: RefreshIndicator(
onRefresh: () async {
_viewModel.fetchNotifications(refresh: true);
},
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: items.length,
itemBuilder: (_, index) {
final item = items[index];
return _buildNotificationItem(item);
},
),
),
),
),
],
);
}),
);
}
void _toggleSetting() {
if (_isPopupShown) {
_hidePopup();
} else {
_showPopup();
}
}
void _showPopup() {
final overlay = Overlay.of(context);
final renderBox = _infoKey.currentContext?.findRenderObject() as RenderBox?;
final offset = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
final size = renderBox?.size ?? Size.zero;
final double widthSize = 270;
_popupEntry = OverlayEntry(
builder: (context) => Stack(
children: [
Positioned.fill(
child: GestureDetector(
onTap: _hidePopup,
behavior: HitTestBehavior.translucent,
child: Container(color: Colors.transparent),
),
),
Positioned(
top: offset.dy + size.height + 8,
left: MediaQuery.of(context).size.width - widthSize - 16,
child: Material(
borderRadius: BorderRadius.circular(16),
elevation: 4,
child: Container(
width: widthSize,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('Đánh dấu tất cả đã đọc', style: TextStyle(fontWeight: FontWeight.w500)),
onTap: () {
_hidePopup();
_viewModel.notificationMarkAsSeen();
},
),
const Divider(height: 1, color: Colors.grey),
ListTile(
title: const Text('Xoá tất cả', style: TextStyle(color: Colors.red, fontWeight: FontWeight.w500)),
onTap: () {
_hidePopup();
_confirmDeleteAllNotifications();
}
),
],
),
),
),
),
],
),
);
overlay.insert(_popupEntry!);
_isPopupShown = true;
}
_confirmDeleteAllNotifications() {
final dataAlert = DataAlertModel(
title: "Quên mật khẩu",
description: "Bạn cần đăng xuất khỏi tài khoản này để đặt lại mật khẩu. Bạn chắc chứ?.",
localHeaderImage: "assets/images/ic_pipi_03.png",
buttons: [AlertButton(
text: "Đồng ý",
onPressed: () {
Get.back();
_viewModel.deleteAllNotifications();
},
bgColor: BaseColor.primary500,
textColor: Colors.white,
),
AlertButton(
text: "Huỷ",
onPressed: () => Get.back(),
bgColor: Colors.white,
textColor: BaseColor.second500,
),],
);
showAlert(data: dataAlert);
}
void _hidePopup() {
_popupEntry?.remove();
_popupEntry = null;
_isPopupShown = false;
}
Widget _buildNotificationCategory() {
final categories = _viewModel.categories;
return SizedBox(
height: 60,
child: Center(
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: categories.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (_, index) {
final cat = categories[index];
final selected = cat.isSelected ?? false;
return GestureDetector(
onTap: () => _viewModel.selectCategory(index),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Stack(
clipBehavior: Clip.none,
children: [
Row(
children: [
const SizedBox(width: 4),
Text(
" ${cat.nameVi ?? ''} ",
style: TextStyle(
color: selected ? Colors.red : Colors.black87,
fontSize: 16,
fontWeight: selected ? FontWeight.bold : FontWeight.normal,
),
),
const SizedBox(width: 4),
],
),
if ((cat.totalUnread ?? 0) > 0)
Positioned(
top: -6,
right: 0,
child: Container(
width: 6,
height: 6,
decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.red),
),
),
],
),
const SizedBox(height: 4),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 2,
width: selected ? 32 : 0,
color: Colors.red,
),
],
),
);
},
),
),
);
}
Widget _buildNotificationItem(NotificationItemModel item) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: item.hasSeen ? Colors.white : Colors.pink.shade50,
borderRadius: BorderRadius.circular(12)
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: loadNetworkImage(
url: item.workingSite?.avatar ?? "",
fit: BoxFit.cover,
width: 40,
height: 40,
placeholderAsset: 'assets/images/ic_logo.png',
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.title ?? '', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
const SizedBox(height: 8),
Text(item.body ?? '', style: const TextStyle(fontSize: 14)),
const SizedBox(height: 8),
Text(item.createTime ?? '', style: const TextStyle(fontSize: 13, color: Colors.black54)),
],
),
),
],
),
],
),
);
}
}
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../preference/data_preference.dart';
import 'models/category_notify_item_model.dart';
import 'models/notification_item_model.dart';
class NotificationViewModel extends RestfulApiViewModel {
var categories = RxList<CategoryNotifyItemModel>();
var notifications = RxList<NotificationItemModel>();
final RxBool isLoading = false.obs;
CategoryNotifyItemModel? get selectedCategory =>
categories.isNotEmpty ? categories.firstWhere((item) => item.isSelected ?? false) : null;
@override
void onInit() {
super.onInit();
_fetchCategories();
}
void _fetchCategories() async {
showLoading();
client.getNotificationCategories().then((value) {
final results = value.data ?? [];
if (results.isNotEmpty) {
results[0].isSelected = true;
}
categories.value = results;
fetchNotifications(refresh: true);
});
}
void fetchNotifications({bool refresh = false}) async {
if (isLoading.value) return;
isLoading.value = true;
String? token = DataPreference.instance.token ?? "";
final body = {
"access_token": token,
"start": refresh ? 0 : notifications.length,
"limit": 10,
"noti_group_id": selectedCategory?.id ?? "",
};
client.getNotifications(body).then((value) {
isLoading.value = false;
hideLoading();
if (refresh) {
notifications.value = value.data?.items ?? [];
} else {
notifications.addAll(value.data?.items ?? []);
}
});
}
void selectCategory(int index) {
for (var i = 0; i < categories.length; i++) {
categories[i].isSelected = i == index;
}
fetchNotifications(refresh: true);
}
notificationMarkAsSeen() {
client.notificationMarkAsSeen().then((value) {
_fetchCategories();
});
}
deleteAllNotifications() {
client.deleteAllNotifications().then((value) {
_fetchCategories();
});
}
}
......@@ -5,6 +5,7 @@ import 'package:mypoint_flutter_app/resouce/base_color.dart';
import 'package:mypoint_flutter_app/screen/register_campaign/register_form_input_viewmodel.dart';
import '../../shared/router_gage.dart';
import '../../widgets/custom_app_bar.dart';
import '../voucher/models/product_model.dart';
import 'input_form_cell.dart';
import 'model/registration_form_package_model.dart';
......@@ -33,7 +34,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
_title = data?.formConfirm?.title ?? '';
_viewModel.form.value = data;
} else {
final id = args?['id'] as int?;
final id = (args?['product'] as ProductModel?)?.id;
if (id != null) {
_isConfirmScreen = false;
_viewModel.fetchRegisterFormInput(id.toString()).then((_) {
......
......@@ -6,6 +6,7 @@ import '../../base/base_response_model.dart';
import '../../configs/constants.dart';
import '../../model/update_response_model.dart';
import '../../preference/data_preference.dart';
import '../../preference/point/point_manager.dart';
import '../main_tab_screen/main_tab_screen.dart';
import '../onboarding/onboarding_screen.dart';
import 'package:url_launcher/url_launcher.dart';
......@@ -44,6 +45,7 @@ class SplashScreenViewModel extends RestfulApiViewModel {
final userProfile = value.data;
if (value.isSuccess && userProfile != null) {
await DataPreference.instance.saveUserProfile(userProfile);
await UserPointManager().fetchUserPoint();
Get.toNamed(mainScreen);
} else {
DataPreference.instance.clearLoginToken();
......
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'transaction_service.dart';
class TransactionHistoryScreen extends StatefulWidget {
const TransactionHistoryScreen({super.key});
@override
State<TransactionHistoryScreen> createState() => _TransactionHistoryScreenState();
}
class _TransactionHistoryScreenState extends State<TransactionHistoryScreen> {
final List<String> categories = [
'Tất cả',
'Viện thông',
'Mua sắm',
'Ưu đãi',
'Hóa đơn',
];
int selectedCategoryIndex = 0;
DateTime selectedMonth = DateTime.now();
bool isLoading = false;
List<Transaction> transactions = [];
final TransactionService _transactionService = TransactionService();
@override
void initState() {
super.initState();
fetchTransactions();
}
Future<void> fetchTransactions() async {
setState(() {
isLoading = true;
});
try {
final result = await _transactionService.getTransactions(
category: categories[selectedCategoryIndex],
month: selectedMonth,
);
setState(() {
transactions = result;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Không thể tải dữ liệu: $e')),
);
}
}
void selectCategory(int index) {
setState(() {
selectedCategoryIndex = index;
});
fetchTransactions();
}
void selectMonth() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: selectedMonth,
firstDate: DateTime(2020),
lastDate: DateTime(2026),
initialDatePickerMode: DatePickerMode.year,
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.light(
primary: Colors.red.shade400,
onPrimary: Colors.white,
onSurface: Colors.black,
),
),
child: child!,
);
},
);
if (picked != null) {
setState(() {
selectedMonth = picked;
});
fetchTransactions();
}
}
@override
Widget build(BuildContext context) {
final transactionCount = transactions.length;
final hasTransactions = transactionCount > 0;
// Tính tổng tiền và điểm
int totalAmount = 0;
int totalPoints = 0;
for (var transaction in transactions) {
totalAmount += transaction.amount;
totalPoints += transaction.points;
}
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
title: const Text(
'Lịch sử giao dịch',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, size: 20, color: Colors.black54),
onPressed: () => Navigator.of(context).pop(),
),
),
body: Column(
children: [
// Danh mục
Container(
height: 60,
color: Colors.white,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: categories.length,
itemBuilder: (context, index) {
final isSelected = selectedCategoryIndex == index;
return GestureDetector(
onTap: () => selectCategory(index),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: isSelected ? Colors.red.shade50 : Colors.grey.shade100,
borderRadius: BorderRadius.circular(20),
border: isSelected
? Border.all(color: Colors.red.shade100)
: null,
),
alignment: Alignment.center,
child: Text(
categories[index],
style: TextStyle(
color: isSelected ? Colors.red : Colors.grey.shade700,
fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal,
),
),
),
);
},
),
),
const SizedBox(height: 8),
// Tháng và số giao dịch
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Tháng ${DateFormat('MM/yyyy').format(selectedMonth)} (${transactionCount} giao dịch)',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
InkWell(
onTap: selectMonth,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
const Text(
'Tháng',
style: TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
const SizedBox(width: 4),
Icon(Icons.keyboard_arrow_down, size: 18, color: Colors.grey.shade700),
],
),
),
),
],
),
),
// Tổng tiền và điểm
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${totalAmount}đ',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Giao dịch bằng tiền',
style: TextStyle(
color: Colors.black54,
fontSize: 14,
),
),
Text(
'${totalAmount}đ',
style: const TextStyle(
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Giao dịch bằng điểm',
style: TextStyle(
color: Colors.black54,
fontSize: 14,
),
),
Row(
children: [
Container(
width: 18,
height: 18,
decoration: const BoxDecoration(
color: Colors.amber,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.currency_exchange,
size: 12,
color: Colors.white,
),
),
),
const SizedBox(width: 4),
Text(
'$totalPoints',
style: const TextStyle(
fontWeight: FontWeight.w500,
),
),
],
),
],
),
],
),
),
// Danh sách giao dịch hoặc trạng thái trống
Expanded(
child: isLoading
? const Center(child: CircularProgressIndicator())
: hasTransactions
? ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: transactions.length,
itemBuilder: (context, index) {
final transaction = transactions[index];
return TransactionItem(transaction: transaction);
},
)
: const EmptyTransactionState(),
),
],
),
);
}
}
class TransactionItem extends StatelessWidget {
final Transaction transaction;
const TransactionItem({
super.key,
required this.transaction,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
// Icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Stack(
children: [
Center(
child: Icon(
transaction.icon,
color: Colors.red,
size: 20,
),
),
if (transaction.status == TransactionStatus.completed)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: const Center(
child: Icon(
Icons.check,
size: 8,
color: Colors.white,
),
),
),
),
],
),
),
const SizedBox(width: 12),
// Thông tin giao dịch
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
transaction.title,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 15,
),
),
const SizedBox(height: 2),
Text(
'${DateFormat('HH:mm').format(transaction.date)} - ${DateFormat('dd/MM/yyyy').format(transaction.date)}',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 13,
),
),
],
),
),
// Điểm
Row(
children: [
Container(
width: 20,
height: 20,
decoration: const BoxDecoration(
color: Colors.amber,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.currency_exchange,
size: 12,
color: Colors.white,
),
),
),
const SizedBox(width: 4),
Text(
'${transaction.points}',
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 15,
),
),
],
),
],
),
);
}
}
class EmptyTransactionState extends StatelessWidget {
const EmptyTransactionState({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Colors.grey.shade100,
shape: BoxShape.circle,
),
child: Center(
child: Icon(
Icons.receipt_long,
size: 50,
color: Colors.grey.shade400,
),
),
),
const SizedBox(height: 16),
const Text(
'Bạn hiện chưa có giao dịch nào',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
],
),
);
}
}
\ No newline at end of file
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
enum TransactionStatus {
pending,
completed,
failed,
}
class Transaction {
final String id;
final String title;
final int amount;
final int points;
final DateTime date;
final String category;
final IconData icon;
final TransactionStatus status;
Transaction({
required this.id,
required this.title,
required this.amount,
required this.points,
required this.date,
required this.category,
required this.icon,
required this.status,
});
factory Transaction.fromJson(Map<String, dynamic> json) {
IconData getIconForCategory(String category) {
switch (category) {
case 'Viện thông':
return Icons.phone_android;
case 'Mua sắm':
return Icons.shopping_bag;
case 'Ưu đãi':
return Icons.card_giftcard;
case 'Hóa đơn':
return Icons.receipt;
default:
return Icons.receipt_long;
}
}
TransactionStatus getStatus(String statusStr) {
switch (statusStr) {
case 'completed':
return TransactionStatus.completed;
case 'pending':
return TransactionStatus.pending;
case 'failed':
return TransactionStatus.failed;
default:
return TransactionStatus.pending;
}
}
return Transaction(
id: json['id'],
title: json['title'],
amount: json['amount'],
points: json['points'],
date: DateTime.parse(json['date']),
category: json['category'],
icon: getIconForCategory(json['category']),
status: getStatus(json['status']),
);
}
}
class TransactionService {
static const String baseUrl = 'https://api.example.com';
Future<List<Transaction>> getTransactions({
required String category,
required DateTime month,
}) async {
try {
final response = await http.get(
Uri.parse(
'$baseUrl/transactions?category=${Uri.encodeComponent(category)}&month=${month.year}-${month.month}',
),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((json) => Transaction.fromJson(json)).toList();
} else {
throw Exception('Failed to load transactions: ${response.statusCode}');
}
} catch (e) {
// Trong môi trường thực tế, bạn nên xử lý lỗi một cách phù hợp
print('Error fetching transactions: $e');
// Trả về dữ liệu mẫu cho mục đích demo
if (category == 'Ưu đãi') {
return [
Transaction(
id: '1',
title: 'Thanh toán mua ưu đãi',
amount: 0,
points: 0,
date: DateTime.now(),
category: 'Ưu đãi',
icon: Icons.card_giftcard,
status: TransactionStatus.completed,
),
Transaction(
id: '2',
title: 'Thanh toán mua ưu đãi',
amount: 0,
points: 0,
date: DateTime.now(),
category: 'Ưu đãi',
icon: Icons.card_giftcard,
status: TransactionStatus.completed,
),
];
}
return [];
}
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/screen/transaction/history/transaction_history_emun.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
import 'package:mypoint_flutter_app/widgets/dashed_line.dart';
import '../../../base/base_screen.dart';
import '../../../base/basic_state.dart';
import '../../../resouce/base_color.dart';
import '../../../widgets/back_button.dart';
import '../../../widgets/image_loader.dart';
import 'transaction_history_detail_viewmodel.dart';
import 'transaction_history_model.dart';
class TransactionHistoryDetailScreen extends BaseScreen {
const TransactionHistoryDetailScreen({super.key});
@override
State<TransactionHistoryDetailScreen> createState() => _TransactionHistoryDetailScreenState();
}
class _TransactionHistoryDetailScreenState extends BaseState<TransactionHistoryDetailScreen> with BasicState {
late final TransactionHistoryDetailViewModel _viewModel;
late var canBack = true;
@override
void initState() {
super.initState();
final args = Get.arguments;
canBack = args['canBack'] as bool? ?? true;
final orderId = args['orderId'] as String? ?? '';
_viewModel = Get.put(TransactionHistoryDetailViewModel(orderID: orderId));
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
showAlertError(content: message);
}
};
}
@override
Widget createBody() {
return Scaffold(
backgroundColor: Colors.grey.shade100,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
title: const Text(
'Chi tiết giao dịch',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
),
leading: canBack ? CustomBackButton() : SizedBox.shrink(),
actions: [
IconButton(
icon: const Icon(Icons.headset_mic, size: 24, color: Colors.black),
onPressed: () {
Get.toNamed(supportScreen);
// Xử lý khi nhấn nút hỗ trợ
},
),
],
),
body: Obx(() {
final isLoading = _viewModel.isLoading.value;
final data = _viewModel.transactionData.value;
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (data == null) {
return const Center(child: Text('Không tìm thấy dữ liệu giao dịch'));
}
return Column(
children: [
Expanded(
child: IntrinsicHeight(
child: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMainInfoSection(data),
DashedLine(),
const SizedBox(height: 12),
_buildDetailInfoSection(data),
const SizedBox(height: 12),
DashedLine(),
const Divider(height: 12),
_buildProductInfoSection(data),
],
),
),
),
),
),
_buildBottomButton(data),
],
);
}),
);
}
Widget _buildMainInfoSection(TransactionHistoryModel data) {
return Center(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
child: Column(
children: [
SizedBox(
width: 60,
height: 60,
child: loadNetworkImage(
url: data.logo ?? "",
fit: BoxFit.cover,
placeholderAsset: 'assets/images/ic_logo.png',
),
),
const SizedBox(height: 8),
const Text("Thanh toán mua ưu đãi", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(
data.payCash?.isNotEmpty == true ? '${data.payCash}' : data.payPoint ?? '0đ',
style: const TextStyle(fontSize: 20, color: Colors.red, fontWeight: FontWeight.bold),
),
],
),
),
);
}
Widget _buildDetailInfoSection(TransactionHistoryModel data) {
return Container(
margin: const EdgeInsets.only(top: 8),
color: Colors.white,
child: Column(
children: [
_buildDetailRow(title: "Trạng thái", value: data.status ?? '', valueColor: _getStatusColor(data.statusT)),
_buildDetailRow(title: "Thời gian", value: data.createdAt ?? ''),
const SizedBox(height: 12),
DashedLine(),
const SizedBox(height: 12),
_buildDetailRow(
title: "Mã giao dịch",
value: data.transactionId ?? '',
trailing: SizedBox(
width: 32,
height: 32,
child: Center(
child: IconButton(
icon: const Icon(Icons.copy, size: 16, color: BaseColor.primary500),
onPressed: () {
Clipboard.setData(ClipboardData(text: data.transactionId ?? ''));
Get.snackbar('✔️', 'Đã sao chép mã giao dịch');
},
),
),
),
),
_buildDetailRow(title: "Nguồn tiền", value: data.sourceCash ?? ''),
_buildDetailRow(title: "Phí thanh toán", value: data.feesPrice ?? ''),
],
),
);
}
Widget _buildProductInfoSection(TransactionHistoryModel data) {
final productInfo = data.productInfo;
if (productInfo == null || productInfo.isEmpty) {
return const SizedBox.shrink();
}
return Container(
margin: const EdgeInsets.only(top: 8),
color: Colors.white,
child: Column(
children:
productInfo.map((info) {
return Column(
children: [
_buildDetailRow(title: info.name ?? '', value: info.value ?? ''),
// if (productInfo.last != info) _buildDivider(),
],
);
}).toList(),
),
);
}
Widget _buildDetailRow({required String title, required String value, Widget? trailing, Color? valueColor}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 150,
child: Text(
title,
style: const TextStyle(fontSize: 15, color: Colors.black87, fontWeight: FontWeight.w400),
),
),
const SizedBox(width: 12),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
value,
textAlign: TextAlign.right,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: valueColor ?? Colors.black),
),
),
if (trailing != null) ...[
// const SizedBox(height: 4),
trailing,
],
],
),
),
],
),
);
}
Widget _buildBottomButton(TransactionHistoryModel transaction) {
return SafeArea(
top: false,
minimum: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (transaction.titleRedButton != null)
ElevatedButton(
onPressed: () {
Get.until((route) => Get.currentRoute == mainScreen);
// Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: BaseColor.primary600,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Text(
transaction.titleRedButton,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 12),
if (transaction.titleClearButton != null)
TextButton(
onPressed: () {
Get.until((route) => Get.currentRoute == mainScreen);
},
style: TextButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Text(
transaction.titleClearButton ?? '',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.grey.shade700),
),
),
],
),
),
);
}
Color _getStatusColor(TransactionStatusOrder status) {
switch (status) {
case TransactionStatusOrder.success:
return Colors.green;
case TransactionStatusOrder.failed:
return Colors.red;
case TransactionStatusOrder.processing:
default:
return Colors.orange;
}
}
}
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import 'package:mypoint_flutter_app/screen/transaction/history/transaction_history_model.dart';
import '../../../base/restful_api_viewmodel.dart';
import '../../../configs/constants.dart';
class TransactionHistoryDetailViewModel extends RestfulApiViewModel {
String orderID;
TransactionHistoryDetailViewModel({required this.orderID});
var transactionData = Rxn<TransactionHistoryModel>();
final RxBool isLoading = false.obs;
void Function(String message)? onShowAlertError;
@override
void onInit() {
super.onInit();
_loadData();
}
Future<void> _loadData() async {
showLoading();
client.getTransactionHistoryDetail(orderID).then((value) {
hideLoading();
if (value.isSuccess) {
transactionData.value = value.data;
} else {
onShowAlertError?.call(value.errorMessage ?? Constants.commonError);
}
});
}
}
enum TransactionStatusOrder {
success,
failed,
processing, pending;
factory TransactionStatusOrder.fromRawValue(String? value) {
switch (value) {
case 'success':
return TransactionStatusOrder.success;
case 'failed':
return TransactionStatusOrder.failed;
case 'processing':
default:
return TransactionStatusOrder.processing;
}
}
}
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/screen/transaction/history/transaction_history_emun.dart';
import '../../../directional/directional_screen.dart';
import '../../voucher/models/product_type.dart';
part 'transaction_history_model.g.dart';
@JsonSerializable()
class TransactionHistoryModel {
final String? id;
@JsonKey(name: 'transaction_id')
final String? transactionId;
@JsonKey(name: 'item_id')
final String? itemId;
final String? name;
@JsonKey(name: 'status_code')
final String? statusCode;
final String? status;
@JsonKey(name: 'pay_point')
final String? payPoint;
@JsonKey(name: 'pay_cash')
final String? payCash;
final String? logo;
@JsonKey(name: 'pay_total')
final String? payTotal;
@JsonKey(name: 'created_at')
final String? createdAt;
@JsonKey(name: 'source_cash')
final String? sourceCash;
@JsonKey(name: 'fees_price')
final String? feesPrice;
@JsonKey(name: 'product_type')
final String? productTypeRaw;
@JsonKey(name: 'product_info')
final List<ProductInfoModel>? productInfo;
TransactionHistoryModel({
this.id,
this.transactionId,
this.itemId,
this.name,
this.statusCode,
this.status,
this.payPoint,
this.payCash,
this.logo,
this.payTotal,
this.createdAt,
this.sourceCash,
this.feesPrice,
this.productTypeRaw,
this.productInfo,
});
factory TransactionHistoryModel.fromJson(Map<String, dynamic> json) =>
_$TransactionHistoryModelFromJson(json);
Map<String, dynamic> toJson() => _$TransactionHistoryModelToJson(this);
TransactionStatusOrder get statusT =>
TransactionStatusOrder.fromRawValue(statusCode);
bool get orderCompleted =>
statusT == TransactionStatusOrder.success ||
statusT == TransactionStatusOrder.failed;
ProductType? get productType => ProductTypeExt.from(productTypeRaw) ?? ProductType.voucher;
String get titleRedButton {
switch (statusT) {
case TransactionStatusOrder.success:
switch (productType) {
case ProductType.voucher:
return 'Xem ưu đãi đã mua';
case ProductType.topupMobile:
return 'Tiếp tục nạp tiền';
case ProductType.typeCard:
return 'Xem thông tin thẻ';
case ProductType.vnTra:
return 'Xem chi tiết';
default:
return 'Về trang chủ';
}
case TransactionStatusOrder.failed:
return 'Liên hệ hỗ trợ';
default:
return 'Về trang chủ';
}
}
String? get titleClearButton => orderCompleted ? 'Về trang chủ' : null;
//
// String? get iconSupport =>
// statusT == TransactionStatusOrder.failed ? 'ic_support_transaction' : null;
// DirectionalScreen? get directionScreenRedButton {
// switch (statusT) {
// case TransactionStatusOrder.success:
// switch (productType) {
// case ProductType.voucher:
// return DirectionalScreen(clickActionType: 'productOwnVoucher', clickActionParam: itemId);
// case ProductType.topupMobile:
// return DirectionalScreen(clickActionType: 'mobileTopup', clickActionParam: itemId);
// case ProductType.typeCard:
// return DirectionalScreen(clickActionType: 'familyMedonDetailCard', clickActionParam: itemId);
// case ProductType.vnTra:
// return DirectionalScreen(clickActionType: 'detailTrafficService', clickActionParam: itemId);
// default:
// return null;
// }
// case TransactionStatusOrder.failed:
// return DirectionalScreen(clickActionType: 'customerSupport');
// default:
// return DirectionalScreen(clickActionType: 'home');
// }
// }
}
class ProductInfoModel {
final String? name;
final String? value;
ProductInfoModel({this.name, this.value});
factory ProductInfoModel.fromJson(Map<String, dynamic> json) {
return ProductInfoModel(
name: json['name'],
value: json['value'],
);
}
Map<String, dynamic> toJson() => {
'name': name,
'value': value,
};
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'transaction_history_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
TransactionHistoryModel _$TransactionHistoryModelFromJson(
Map<String, dynamic> json,
) => TransactionHistoryModel(
id: json['id'] as String?,
transactionId: json['transaction_id'] as String?,
itemId: json['item_id'] as String?,
name: json['name'] as String?,
statusCode: json['status_code'] as String?,
status: json['status'] as String?,
payPoint: json['pay_point'] as String?,
payCash: json['pay_cash'] as String?,
logo: json['logo'] as String?,
payTotal: json['pay_total'] as String?,
createdAt: json['created_at'] as String?,
sourceCash: json['source_cash'] as String?,
feesPrice: json['fees_price'] as String?,
productTypeRaw: json['product_type'] as String?,
productInfo:
(json['product_info'] as List<dynamic>?)
?.map((e) => ProductInfoModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$TransactionHistoryModelToJson(
TransactionHistoryModel instance,
) => <String, dynamic>{
'id': instance.id,
'transaction_id': instance.transactionId,
'item_id': instance.itemId,
'name': instance.name,
'status_code': instance.statusCode,
'status': instance.status,
'pay_point': instance.payPoint,
'pay_cash': instance.payCash,
'logo': instance.logo,
'pay_total': instance.payTotal,
'created_at': instance.createdAt,
'source_cash': instance.sourceCash,
'fees_price': instance.feesPrice,
'product_type': instance.productTypeRaw,
'product_info': instance.productInfo,
};
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment