Commit 8bef97c9 authored by DatHV's avatar DatHV
Browse files

update payment detail

parent 7777aa65
enum PaymentMethodType {
card,
wallet,
transfer,
internalCard;
factory PaymentMethodType.fromString(String value) {
switch (value) {
case 'CARD':
return PaymentMethodType.card;
case 'WALLET':
return PaymentMethodType.wallet;
case 'VA_QRCODE':
return PaymentMethodType.transfer;
case 'INTERNATIONAL_CARD':
return PaymentMethodType.internalCard;
default:
throw ArgumentError('Unknown PaymentMethodType: $value');
}
}
String get methodBillEVN {
switch (this) {
case PaymentMethodType.card:
return 'DomesticATM';
case PaymentMethodType.wallet:
return 'VitapayWallet';
case PaymentMethodType.transfer:
return 'VA_QRCODE';
case PaymentMethodType.internalCard:
return 'INTERNATIONAL_CARD';
}
}
}
import 'dart:core';
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/screen/transaction/model/preview_order_payment_point_data_model.dart';
import 'package:mypoint_flutter_app/screen/transaction/model/preview_order_product_info_model.dart';
part 'preview_order_payment_model.g.dart';
@JsonSerializable()
class PreviewOrderPaymentModel {
@JsonKey(name: 'total_cash_coupon')
final int? totalCashCoupon;
@JsonKey(name: 'fees_price')
final int? feesPrice;
@JsonKey(name: 'total_cash')
final int? totalCash;
@JsonKey(name: 'fee_note')
final String? feeNote;
@JsonKey(name: 'point_data')
final PreviewOrderPaymentPointDataModel? pointData;
@JsonKey(name: 'product_info')
final List<PreviewOrderProductInfoModel>? productInfo;
@JsonKey(name: 'total_price')
final int? totalPrice;
@JsonKey(name: 'discount_coupon_value')
final int? discountCouponValue;
@JsonKey(name: 'payment_method')
final String? paymentMethod;
@JsonKey(name: 'product_type')
final String? productType;
PreviewOrderPaymentModel({
this.totalCashCoupon,
this.feesPrice,
this.totalCash,
this.feeNote,
this.pointData,
this.productInfo,
this.totalPrice,
this.discountCouponValue,
this.paymentMethod,
this.productType,
});
factory PreviewOrderPaymentModel.fromJson(Map<String, dynamic> json) =>
_$PreviewOrderPaymentModelFromJson(json);
Map<String, dynamic> toJson() => _$PreviewOrderPaymentModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'preview_order_payment_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PreviewOrderPaymentModel _$PreviewOrderPaymentModelFromJson(
Map<String, dynamic> json,
) => PreviewOrderPaymentModel(
totalCashCoupon: (json['total_cash_coupon'] as num?)?.toInt(),
feesPrice: (json['fees_price'] as num?)?.toInt(),
totalCash: (json['total_cash'] as num?)?.toInt(),
feeNote: json['fee_note'] as String?,
pointData:
json['point_data'] == null
? null
: PreviewOrderPaymentPointDataModel.fromJson(
json['point_data'] as Map<String, dynamic>,
),
productInfo:
(json['product_info'] as List<dynamic>?)
?.map(
(e) => PreviewOrderProductInfoModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
totalPrice: (json['total_price'] as num?)?.toInt(),
discountCouponValue: (json['discount_coupon_value'] as num?)?.toInt(),
paymentMethod: json['payment_method'] as String?,
productType: json['product_type'] as String?,
);
Map<String, dynamic> _$PreviewOrderPaymentModelToJson(
PreviewOrderPaymentModel instance,
) => <String, dynamic>{
'total_cash_coupon': instance.totalCashCoupon,
'fees_price': instance.feesPrice,
'total_cash': instance.totalCash,
'fee_note': instance.feeNote,
'point_data': instance.pointData,
'product_info': instance.productInfo,
'total_price': instance.totalPrice,
'discount_coupon_value': instance.discountCouponValue,
'payment_method': instance.paymentMethod,
'product_type': instance.productType,
};
import 'package:json_annotation/json_annotation.dart';
part 'preview_order_payment_point_data_model.g.dart';
@JsonSerializable()
class PreviewOrderPaymentPointDataModel {
final int? status;
final int? point;
@JsonKey(name: 'text_display')
final String? textDisplay;
PreviewOrderPaymentPointDataModel({
this.status,
this.point,
this.textDisplay,
});
factory PreviewOrderPaymentPointDataModel.fromJson(Map<String, dynamic> json) =>
_$PreviewOrderPaymentPointDataModelFromJson(json);
Map<String, dynamic> toJson() => _$PreviewOrderPaymentPointDataModelToJson(this);
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'preview_order_payment_point_data_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PreviewOrderPaymentPointDataModel _$PreviewOrderPaymentPointDataModelFromJson(
Map<String, dynamic> json,
) => PreviewOrderPaymentPointDataModel(
status: (json['status'] as num?)?.toInt(),
point: (json['point'] as num?)?.toInt(),
textDisplay: json['text_display'] as String?,
);
Map<String, dynamic> _$PreviewOrderPaymentPointDataModelToJson(
PreviewOrderPaymentPointDataModel instance,
) => <String, dynamic>{
'status': instance.status,
'point': instance.point,
'text_display': instance.textDisplay,
};
import 'package:json_annotation/json_annotation.dart';
part 'preview_order_product_info_model.g.dart';
@JsonSerializable()
class PreviewOrderProductInfoModel {
final String? name;
final String? value;
PreviewOrderProductInfoModel({
this.name,
this.value,
});
factory PreviewOrderProductInfoModel.fromJson(Map<String, dynamic> json) =>
_$PreviewOrderProductInfoModelFromJson(json);
Map<String, dynamic> toJson() => _$PreviewOrderProductInfoModelToJson(this);
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'preview_order_product_info_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PreviewOrderProductInfoModel _$PreviewOrderProductInfoModelFromJson(
Map<String, dynamic> json,
) => PreviewOrderProductInfoModel(
name: json['name'] as String?,
value: json['value'] as String?,
);
Map<String, dynamic> _$PreviewOrderProductInfoModelToJson(
PreviewOrderProductInfoModel instance,
) => <String, dynamic>{'name': instance.name, 'value': instance.value};
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:intl/intl.dart';
import '../../widgets/custom_app_bar.dart';
import '../../widgets/dashed_line.dart';
import 'model/payment_bank_account_info_model.dart';
import 'model/payment_method_model.dart';
import 'model/preview_order_payment_model.dart';
import 'model/preview_order_payment_point_data_model.dart';
import 'transaction_detail_viewmodel.dart';
class TransactionDetailScreen extends StatefulWidget {
const TransactionDetailScreen({super.key});
@override
State<TransactionDetailScreen> createState() => _TransactionDetailScreenState();
}
class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
final TransactionDetailViewModel viewModel = Get.put(TransactionDetailViewModel());
final currencyFormatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
bool usePoints = true;
int selectedPaymentMethodIndex = -1;
bool isPaymentMethodsExpanded = true;
@override
void initState() {
super.initState();
viewModel.refreshData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: CustomAppBar.back(title: "Thông tin thanh toán"),
body: Obx(() {
if (viewModel.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final previewData = viewModel.previewData.value;
if (previewData == null) {
return const Center(child: Text('Không có dữ liệu'));
}
final totalPrice = previewData.totalPrice ?? 0;
final pointValue = previewData.pointData?.point ?? 0;
final finalTotal = usePoints && previewData.pointData?.status == 1
? totalPrice - pointValue
: totalPrice;
return Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader('Chi tiết giao dịch'),
_buildProductInfoSection(previewData),
if (previewData.pointData != null && previewData.pointData!.status == 1)
_buildPointToggleSection(previewData.pointData!),
_buildTotalSection(totalPrice, usePoints ? pointValue : 0, finalTotal),
_buildSavedCardsSection(),
_buildPaymentMethodsSection(),
const SizedBox(height: 100), // Khoảng trống để không bị che bởi bottom bar
],
),
),
),
// Bottom bar với tổng thanh toán và nút tiếp tục
_buildBottomBar(finalTotal),
],
);
}),
);
}
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
);
}
Widget _buildProductInfoSection(PreviewOrderPaymentModel data) {
final productInfo = data.productInfo ?? [];
return Container(
color: Colors.white,
child: Column(
children: productInfo.map((item) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item.name ?? '',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade700,
),
),
Text(
item.value ?? '',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
);
}).toList(),
),
);
}
Widget _buildPointToggleSection(PreviewOrderPaymentPointDataModel pointData) {
return Container(
color: Colors.white,
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Center(
child: Image.asset('assets/images/ic_point.png', width: 30, height: 30),
),
const SizedBox(width: 12),
Text(
pointData.textDisplay ?? '',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
CupertinoSwitch(
value: usePoints,
activeColor: Colors.green,
onChanged: (value) {
setState(() {
usePoints = value;
});
},
),
],
),
);
}
Widget _buildTotalSection(int totalPrice, int pointsUsed, int finalTotal) {
return Container(
color: Colors.white,
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
children: [
_buildTotalRow('Tổng số tiền', totalPrice, false),
if (pointsUsed > 0)
_buildTotalRow('Sử dụng điểm', -pointsUsed, false),
_buildTotalRow('Tổng tạm tính', finalTotal, true),
],
),
);
}
Widget _buildTotalRow(String label, int amount, bool isHighlighted) {
final formattedAmount = currencyFormatter.format(amount);
final displayAmount = amount < 0 ? formattedAmount : formattedAmount;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 16,
color: isHighlighted ? Colors.black87 : Colors.grey.shade700,
),
),
Text(
displayAmount,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: isHighlighted && amount == 0 ? Colors.red : Colors.black87,
),
),
],
),
);
}
Widget _buildSavedCardsSection() {
return Obx(() {
final bankAccounts = viewModel.paymentBankAccounts;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader('Tài khoản/Thẻ đã lưu'),
Container(
color: Colors.white,
child: bankAccounts.isEmpty
? _buildNoSavedCardsItem()
: Column(
children: List.generate(
bankAccounts.length,
(index) => _buildSavedCardItem(bankAccounts[index], index),
),
),
),
],
);
});
}
Widget _buildNoSavedCardsItem() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(4),
),
child: const Center(
child: Icon(
Icons.credit_card,
color: Colors.grey,
),
),
),
const SizedBox(width: 12),
const Text(
'Không có thẻ đã lưu',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildSavedCardItem(PaymentBankAccountInfoModel account, int index) {
final isSelected = selectedPaymentMethodIndex == -1000 - index;
return InkWell(
onTap: () {
setState(() {
selectedPaymentMethodIndex = -1000 - index; // tránh đụng index method
});
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: isSelected ? Colors.red : Colors.grey.shade300,
width: 1.5,
),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
if (account.bankLogo != null)
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
imageUrl: account.bankLogo!,
width: 32,
height: 32,
placeholder: (context, url) => Container(
width: 32,
height: 32,
color: Colors.grey.shade200,
),
errorWidget: (context, url, error) => Container(
width: 32,
height: 32,
color: Colors.grey.shade200,
child: const Icon(Icons.error, size: 16),
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
account.bankName ?? '',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
if (account.cardNumber != null)
Text(
'**** ${account.cardNumber!.substring(account.cardNumber!.length - 4)}',
style: TextStyle(color: Colors.grey.shade600),
),
],
),
],
),
),
);
}
Widget _buildPaymentMethodsSection() {
return Obx(() {
final methods = viewModel.paymentMethods;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Phương thức thanh toán khác',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
IconButton(
icon: Icon(
isPaymentMethodsExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
color: Colors.grey,
),
onPressed: () {
setState(() {
isPaymentMethodsExpanded = !isPaymentMethodsExpanded;
});
},
),
],
),
),
if (isPaymentMethodsExpanded)
Container(
color: Colors.white,
child: Column(
children: List.generate(
methods.length,
(index) => _buildPaymentMethodItem(methods[index], index),
),
),
),
],
);
});
}
Widget _buildPaymentMethodItem(PaymentMethodModel method, int index) {
final isSelected = selectedPaymentMethodIndex == index;
return InkWell(
onTap: () {
setState(() {
selectedPaymentMethodIndex = index;
});
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
decoration: BoxDecoration(
border: index < viewModel.paymentMethods.length - 1
? Border(bottom: BorderSide(color: Colors.grey.shade200, width: 0.5))
: null,
),
child: Row(
children: [
Radio<int>(
value: index,
groupValue: selectedPaymentMethodIndex,
onChanged: (value) {
setState(() {
selectedPaymentMethodIndex = value!;
});
},
activeColor: Colors.red,
),
if (method.logo != null && method.logo!.isNotEmpty)
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
imageUrl: method.logo!,
width: 32,
height: 32,
placeholder: (context, url) => Container(
width: 32,
height: 32,
color: Colors.grey.shade200,
),
errorWidget: (context, url, error) => Container(
width: 32,
height: 32,
color: Colors.grey.shade200,
child: const Icon(Icons.error, size: 16),
),
),
),
const SizedBox(width: 12),
Text(
method.name ?? '',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
Widget _buildBottomBar(int finalTotal) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -5),
),
],
),
child: Column(
children: [
if ((viewModel.previewData.value?.feeNote ?? "").isNotEmpty)
Text(
viewModel.previewData.value?.feeNote ?? '',
style: TextStyle(
fontSize: 12,
color: Colors.orange.shade700,
),
),
const SizedBox(height: 12),
Container(
height: 1,
color: Colors.grey.shade200,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Tổng thanh toán',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
currencyFormatter.format(finalTotal),
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
],
),
),
ElevatedButton(
onPressed: selectedPaymentMethodIndex >= 0 ? () {
// Xử lý khi nhấn nút tiếp tục
final selectedMethod = viewModel.paymentMethods[selectedPaymentMethodIndex];
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Đang xử lý thanh toán với ${selectedMethod.name}...')),
);
} : null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
disabledBackgroundColor: Colors.red.withOpacity(0.6),
disabledForegroundColor: Colors.white.withOpacity(0.8),
),
child: const Text(
'Tiếp tục',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 44),
],
),
);
}
}
\ No newline at end of file
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../preference/data_preference.dart';
import 'model/payment_bank_account_info_model.dart';
import 'model/payment_method_model.dart';
import 'model/preview_order_payment_model.dart';
class TransactionDetailViewModel extends RestfulApiViewModel {
var previewData = Rxn<PreviewOrderPaymentModel>();
var paymentMethods = RxList<PaymentMethodModel>();
var paymentBankAccounts = RxList<PaymentBankAccountInfoModel>();
final RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
refreshData();
}
Future<void> refreshData() async {
isLoading.value = true;
await Future.wait([
_getPreviewOrderPayment(),
_getPaymentMethods(),
_getPaymentBankAccounts(),
]);
isLoading.value = false;
}
Future<void> _getPreviewOrderPayment() async {
String? token = DataPreference.instance.token ?? "";
try {
final body = {
"product_id": 13796,
"quantity": 1,
"access_token": token,
"price": 100000,
};
final response = await client.getPreviewOrderInfo(body);
previewData.value = response.data;
} catch (error) {
print("Error fetching preview order payment: $error");
}
}
Future<void> _getPaymentMethods() async {
try {
final response = await client.getPreviewPaymentMethods();
paymentMethods.value = response.data ?? [];
} catch (error) {
print("Error fetching payment methods: $error");
}
}
Future<void> _getPaymentBankAccounts() async {
try {
final response = await client.getPreviewOrderBankAccounts();
paymentBankAccounts.value = response.data ?? [];
} catch (error) {
print("Error fetching payment bank accounts: $error");
}
}
}
\ No newline at end of file
......@@ -6,6 +6,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../../base/base_screen.dart';
import '../../../base/basic_state.dart';
import '../../../resouce/base_color.dart';
import '../../../shared/router_gage.dart';
import '../../../widgets/back_button.dart';
import '../../../widgets/custom_empty_widget.dart';
import '../../../widgets/custom_point_text_tag.dart';
......@@ -378,6 +379,7 @@ class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with Basi
height: 48,
child: ElevatedButton(
onPressed: () {
Get.toNamed(registerFormInputScreen, arguments: {"id": 13484});
// TODO: Handle đổi ưu đãi
},
style: ElevatedButton.styleFrom(
......
......@@ -2,6 +2,7 @@ import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../../base/restful_api_viewmodel.dart';
import '../../../configs/constants.dart';
import '../../../shared/router_gage.dart';
import '../models/product_model.dart';
import '../models/product_store_model.dart';
......@@ -85,7 +86,8 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
if (!value.isSuccess) {
onShowAlertError?.call(value.errorMessage ?? Constants.commonError);
} else {
onShowAlertError?.call("Verify Order Product Success -> Go To Payment Detail");
Get.toNamed(transactionDetailScreen);
// onShowAlertError?.call("Verify Order Product Success -> Go To Payment Detail");
}
});
}
......
......@@ -3,8 +3,10 @@ import '../screen/game/game_cards/game_card_screen.dart';
import '../screen/login/login_screen.dart';
import '../screen/main_tab_screen/main_tab_screen.dart';
import '../screen/onboarding/onboarding_screen.dart';
import '../screen/register_campaign/register_form_input_screen.dart';
import '../screen/setting/setting_screen.dart';
import '../screen/splash/splash_screen.dart';
import '../screen/transaction/transaction_detail_screen.dart';
import '../screen/voucher/detail/voucher_detail_screen.dart';
import '../screen/voucher/voucher_list/voucher_list_screen.dart';
......@@ -16,6 +18,8 @@ const settingScreen = '/setting';
const vouchersScreen = '/vouchers';
const voucherDetailScreen = '/voucherDetail';
const gameCardScreen = '/gameCardScreen';
const registerFormInputScreen = '/registerFormInputScreen';
const transactionDetailScreen = '/transactionDetailScreen';
class RouterPage {
static List<GetPage> pages() {
......@@ -34,6 +38,8 @@ class RouterPage {
GetPage(name: vouchersScreen, page: () => VoucherListScreen(),),
GetPage(name: voucherDetailScreen, page: () => VoucherDetailScreen(),),
GetPage(name: gameCardScreen, page: () => GameCardScreen(),),
GetPage(name: registerFormInputScreen, page: () => RegisterFormInputScreen(),),
GetPage(name: transactionDetailScreen, page: () => TransactionDetailScreen(),),
];
}
}
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