Commit fa01087d authored by DatHV's avatar DatHV
Browse files

update brand, detail..

parent c8abf95b
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_mobile_card_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProductMobileCardModel _$ProductMobileCardModelFromJson(
Map<String, dynamic> json,
) => ProductMobileCardModel(
id: json['id'] as String?,
productModelCode: json['product_model_code'] as String?,
code: json['code'] as String?,
name: json['name'] as String?,
dataDurationApply: json['data_duration_apply'] as String?,
productDescription: json['description'] as String?,
startTime: json['start_time'] as String?,
endTime: json['end_time'] as String?,
limitQuantityPerTransaction:
json['limit_quantity_per_transaction'] as String?,
brandId: json['brand_id'] as String?,
brandCode: json['brand_code'] as String?,
brandName: json['brand_name'] as String?,
pointReward: json['point_reward'] as String?,
brandLogo: json['brand_logo'] as String?,
images:
(json['images'] as List<dynamic>?)
?.map((e) => MobileCardImageModel.fromJson(e as Map<String, dynamic>))
.toList(),
language:
json['language'] == null
? null
: MobileCardLanguageModel.fromJson(
json['language'] as Map<String, dynamic>,
),
prices:
(json['prices'] as List<dynamic>?)
?.map((e) => MobileCardPriceModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$ProductMobileCardModelToJson(
ProductMobileCardModel instance,
) => <String, dynamic>{
'id': instance.id,
'product_model_code': instance.productModelCode,
'code': instance.code,
'name': instance.name,
'data_duration_apply': instance.dataDurationApply,
'description': instance.productDescription,
'start_time': instance.startTime,
'end_time': instance.endTime,
'limit_quantity_per_transaction': instance.limitQuantityPerTransaction,
'brand_id': instance.brandId,
'brand_code': instance.brandCode,
'brand_name': instance.brandName,
'point_reward': instance.pointReward,
'brand_logo': instance.brandLogo,
'images': instance.images?.map((e) => e.toJson()).toList(),
'language': instance.language?.toJson(),
'prices': instance.prices?.map((e) => e.toJson()).toList(),
};
ProductMobileCardResponse _$ProductMobileCardResponseFromJson(
Map<String, dynamic> json,
) => ProductMobileCardResponse(
products:
(json['products'] as List<dynamic>?)
?.map(
(e) => ProductMobileCardModel.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
Map<String, dynamic> _$ProductMobileCardResponseToJson(
ProductMobileCardResponse instance,
) => <String, dynamic>{'products': instance.products};
MobileCardImageModel _$MobileCardImageModelFromJson(
Map<String, dynamic> json,
) => MobileCardImageModel(
id: json['id'] as String?,
caption: json['caption'] as String?,
imageUrl: json['image_url'] as String?,
);
Map<String, dynamic> _$MobileCardImageModelToJson(
MobileCardImageModel instance,
) => <String, dynamic>{
'id': instance.id,
'caption': instance.caption,
'image_url': instance.imageUrl,
};
MobileCardLanguageModel _$MobileCardLanguageModelFromJson(
Map<String, dynamic> json,
) => MobileCardLanguageModel(
content: json['content'] as String?,
termAndCondition: json['term_and_condition'] as String?,
stockRemark: json['stock_remark'] as String?,
);
Map<String, dynamic> _$MobileCardLanguageModelToJson(
MobileCardLanguageModel instance,
) => <String, dynamic>{
'content': instance.content,
'term_and_condition': instance.termAndCondition,
'stock_remark': instance.stockRemark,
};
MobileCardPriceModel _$MobileCardPriceModelFromJson(
Map<String, dynamic> json,
) => MobileCardPriceModel(
channelCode: json['channel_code'] as String?,
channelName: json['channel_name'] as String?,
payPoint: json['pay_point'] as String?,
originalPrice: json['original_price'] as String?,
poolCode: json['pool_code'] as String?,
subPoolCode: json['sub_pool_code'] as String?,
currencyCode: json['currency_code'] as String?,
);
Map<String, dynamic> _$MobileCardPriceModelToJson(
MobileCardPriceModel instance,
) => <String, dynamic>{
'channel_code': instance.channelCode,
'channel_name': instance.channelName,
'pay_point': instance.payPoint,
'original_price': instance.originalPrice,
'pool_code': instance.poolCode,
'sub_pool_code': instance.subPoolCode,
'currency_code': instance.currencyCode,
};
import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/screen/mobile_card/models/product_mobile_card_model.dart';
import '../../home/models/brand_model.dart';
part 'usable_voucher_model.g.dart';
@JsonSerializable()
class UsableVoucherModel {
@JsonKey(name: 'voucher_id')
final String? voucherID;
@JsonKey(name: 'product_item_id')
final String? voucherItemID;
@JsonKey(name: 'action_time')
final String? actionTime;
final String? code;
final String? serial;
String? name;
String? description;
@JsonKey(name: 'voucher_type_code')
final String? voucherTypeCode;
@JsonKey(name: 'voucher_type_name')
final String? voucherTypeName;
@JsonKey(name: 'voucher_value')
final String? voucherValue;
@JsonKey(name: 'content')
final String? voucherContent;
@JsonKey(name: 'term_and_condition')
final String? voucherTermAndCondition;
@JsonKey(name: 'voucher_stock_remark')
final String? voucherStockRemark;
@JsonKey(name: 'expired_time')
final String? expiredTime;
@JsonKey(name: 'status_code')
final String? statusCode;
final String? status;
@JsonKey(name: 'beneficiary_site_name')
final String? beneficiarySiteName;
final List<MobileCardPriceModel>? prices;
final List<MobileCardImageModel>? images;
final BrandModel? brand;
@JsonKey(name: 'like_id')
final String? likeId;
@JsonKey(name: 'code_secret')
String? codeSecret;
final String? password;
UsableVoucherModel({
this.voucherID,
this.voucherItemID,
this.actionTime,
this.code,
this.serial,
this.name,
this.description,
this.voucherTypeCode,
this.voucherTypeName,
this.voucherValue,
this.voucherContent,
this.voucherTermAndCondition,
this.voucherStockRemark,
this.expiredTime,
this.statusCode,
this.status,
this.beneficiarySiteName,
this.prices,
this.images,
this.brand,
this.likeId,
this.codeSecret,
this.password,
});
factory UsableVoucherModel.fromJson(Map<String, dynamic> json) =>
_$UsableVoucherModelFromJson(json);
Map<String, dynamic> toJson() => _$UsableVoucherModelToJson(this);
}
@JsonSerializable()
class RedeemProductResponseModel {
final UsableVoucherModel? item;
RedeemProductResponseModel({this.item});
factory RedeemProductResponseModel.fromJson(Map<String, dynamic> json) =>
_$RedeemProductResponseModelFromJson(json);
Map<String, dynamic> toJson() => _$RedeemProductResponseModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'usable_voucher_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UsableVoucherModel _$UsableVoucherModelFromJson(
Map<String, dynamic> json,
) => UsableVoucherModel(
voucherID: json['voucher_id'] as String?,
voucherItemID: json['product_item_id'] as String?,
actionTime: json['action_time'] as String?,
code: json['code'] as String?,
serial: json['serial'] as String?,
name: json['name'] as String?,
description: json['description'] as String?,
voucherTypeCode: json['voucher_type_code'] as String?,
voucherTypeName: json['voucher_type_name'] as String?,
voucherValue: json['voucher_value'] as String?,
voucherContent: json['content'] as String?,
voucherTermAndCondition: json['term_and_condition'] as String?,
voucherStockRemark: json['voucher_stock_remark'] as String?,
expiredTime: json['expired_time'] as String?,
statusCode: json['status_code'] as String?,
status: json['status'] as String?,
beneficiarySiteName: json['beneficiary_site_name'] as String?,
prices:
(json['prices'] as List<dynamic>?)
?.map((e) => MobileCardPriceModel.fromJson(e as Map<String, dynamic>))
.toList(),
images:
(json['images'] as List<dynamic>?)
?.map((e) => MobileCardImageModel.fromJson(e as Map<String, dynamic>))
.toList(),
brand:
json['brand'] == null
? null
: BrandModel.fromJson(json['brand'] as Map<String, dynamic>),
likeId: json['like_id'] as String?,
codeSecret: json['code_secret'] as String?,
password: json['password'] as String?,
);
Map<String, dynamic> _$UsableVoucherModelToJson(UsableVoucherModel instance) =>
<String, dynamic>{
'voucher_id': instance.voucherID,
'product_item_id': instance.voucherItemID,
'action_time': instance.actionTime,
'code': instance.code,
'serial': instance.serial,
'name': instance.name,
'description': instance.description,
'voucher_type_code': instance.voucherTypeCode,
'voucher_type_name': instance.voucherTypeName,
'voucher_value': instance.voucherValue,
'content': instance.voucherContent,
'term_and_condition': instance.voucherTermAndCondition,
'voucher_stock_remark': instance.voucherStockRemark,
'expired_time': instance.expiredTime,
'status_code': instance.statusCode,
'status': instance.status,
'beneficiary_site_name': instance.beneficiarySiteName,
'prices': instance.prices,
'images': instance.images,
'brand': instance.brand,
'like_id': instance.likeId,
'code_secret': instance.codeSecret,
'password': instance.password,
};
RedeemProductResponseModel _$RedeemProductResponseModelFromJson(
Map<String, dynamic> json,
) => RedeemProductResponseModel(
item:
json['item'] == null
? null
: UsableVoucherModel.fromJson(json['item'] as Map<String, dynamic>),
);
Map<String, dynamic> _$RedeemProductResponseModelToJson(
RedeemProductResponseModel instance,
) => <String, dynamic>{'item': instance.item};
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/resouce/base_color.dart';
import 'package:mypoint_flutter_app/screen/mobile_card/product_mobile_card_viewmodel.dart';
import 'package:mypoint_flutter_app/screen/mobile_card/usable_mobile_card_popup.dart';
import 'package:mypoint_flutter_app/widgets/custom_app_bar.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../widgets/alert/custom_alert_dialog.dart';
import '../../widgets/alert/data_alert_model.dart';
class ProductMobileCardScreen extends BaseScreen {
const ProductMobileCardScreen({super.key});
@override
State<ProductMobileCardScreen> createState() => _ProductMobileCardScreenState();
}
class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> with BasicState {
late final ProductMobileCardViewModel _viewModel;
@override
void initState() {
super.initState();
_viewModel = ProductMobileCardViewModel();
_viewModel.getProductMobileCard();
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
showAlertError(content: message);
}
};
_viewModel.onRedeemProductMobileSuccess = (data) {
if (data != null) {
showVoucherPopup(context, data);
} else {
showAlertError(content: "Đổi mã thẻ nạp thất bại, vui lòng thử lại sau!");
}
};
}
@override
Widget createBody() {
return Scaffold(
appBar: CustomAppBar.back(title: "Đổi mã thẻ nạp"),
body: Obx(() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text("Chọn nhà mạng", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
),
_buildSectionNetwork(),
const SizedBox(height: 24),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text("Mệnh giá thẻ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
),
const SizedBox(height: 12),
_buildProductItem(),
SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: _viewModel.selectedProduct == null ? null : _redeemProductMobileCard,
style: ElevatedButton.styleFrom(
backgroundColor: _viewModel.selectedProduct == null ? Colors.grey : BaseColor.primary500,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
minimumSize: const Size.fromHeight(48),
),
child: const Text(
"Xác nhận",
style: TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
),
],
);
}),
);
}
Widget _buildSectionNetwork() {
final widthCardItem = MediaQuery.of(context).size.width / 2.5;
return SizedBox(
height: widthCardItem * 9 / 16,
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
itemCount: _viewModel.mobileCardSections.value.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (_, index) {
final mobileCard = _viewModel.mobileCardSections.value[index];
final isSelected = mobileCard.brandCode == _viewModel.selectedBrandCode.value;
return GestureDetector(
onTap: () {
setState(() {
if (_viewModel.selectedBrandCode.value == mobileCard.brandCode) return;
_viewModel.selectedBrandCode.value = mobileCard.brandCode ?? "";
_viewModel.selectedProduct = null;
});
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: isSelected ? Colors.orange : Colors.grey.shade300, width: 2),
color: Colors.white,
),
alignment: Alignment.center,
child: loadNetworkImage(
url: mobileCard.brandLogo,
width: widthCardItem,
placeholderAsset: "assets/images/bg_default_169.png",
),
),
);
},
),
);
}
Widget _buildProductItem() {
return Expanded(
child: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.symmetric(horizontal: 16),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 2.4,
children:
_viewModel.products.map((product) {
final isSelected = _viewModel.selectedProduct?.id == product.id;
final amount =
int.tryParse(
(product.prices?.isNotEmpty == true) ? product.prices?.first.originalPrice ?? "0" : "0",
) ??
0;
final price =
int.tryParse((product.prices?.isNotEmpty == true) ? product.prices?.first.payPoint ?? "0" : "0") ?? 0;
return GestureDetector(
onTap: () {
setState(() {
_viewModel.selectedProduct = product;
});
},
child: Container(
decoration: BoxDecoration(
border: Border.all(color: isSelected ? Colors.orange : Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
color: isSelected ? Colors.orange.withOpacity(0.1) : Colors.white,
),
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
amount.money(CurrencyUnit.vnd),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: isSelected ? Colors.orange : Colors.black87,
),
),
const SizedBox(height: 4),
Row(
children: [
Text(
"Giá: ${price.money(CurrencyUnit.none)}",
style: TextStyle(fontSize: 14, color: isSelected ? Colors.orange : Colors.black54),
),
Image.asset("assets/images/ic_point.png", width: 16, height: 16),
],
),
],
),
),
);
}).toList(),
),
);
}
_redeemProductMobileCard() {
if (_viewModel.selectedProduct == null) return;
if (!_viewModel.isValidBalance) {
showAlertError(content: "Bạn chưa đủ điểm để đổi ưu đãi này, vui lòng tích lũy thêm điểm nhé!");
return;
}
_showAlertConfirmRedeemProduct();
}
_showAlertConfirmRedeemProduct() {
final dataAlert = DataAlertModel(
title: "Xác nhận",
description: "Bạn có muốn sử dụng ${_viewModel.payPoint.money(CurrencyUnit.point)} MyPoint để đổi lấy mã thẻ điện thoại này không?",
localHeaderImage: "assets/images/ic_pipi_02.png",
buttons: [
AlertButton(
text: "Đồng ý",
onPressed: () {
Get.back();
_viewModel.redeemProductMobileCard();
},
bgColor: BaseColor.primary500,
textColor: Colors.white,
),
AlertButton(text: "Huỷ", onPressed: () => Get.back(), bgColor: Colors.white, textColor: BaseColor.second500),
],
);
showAlert(data: dataAlert, direction: ButtonsDirection.row);
}
}
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import 'package:mypoint_flutter_app/screen/mobile_card/models/product_mobile_card_model.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../preference/point/point_manager.dart';
import 'models/usable_voucher_model.dart';
class ProductMobileCardViewModel extends RestfulApiViewModel {
void Function(String message)? onShowAlertError;
void Function(UsableVoucherModel data)? onRedeemProductMobileSuccess;
RxMap<String, List<ProductMobileCardModel>> groupedSection = RxMap<String, List<ProductMobileCardModel>>();
var mobileCardSections = RxList<ProductMobileCardModel>();
RxString selectedBrandCode = "".obs;
List<ProductMobileCardModel> get products {
return groupedSection[selectedBrandCode.value] ?? [];
}
ProductMobileCardModel? selectedProduct;
int get payPoint {
return int.tryParse(selectedProduct?.prices?.firstOrNull?.payPoint ?? "0") ?? 0;
}
bool get isValidBalance {
return UserPointManager().point >= (int.tryParse(selectedProduct?.prices?.firstOrNull?.payPoint ?? "0") ?? 0);
}
@override
onInit() {
super.onInit();
UserPointManager().fetchUserPoint();
}
redeemProductMobileCard() async {
showLoading();
try {
final response = await client.redeemMobileCard((selectedProduct?.id ?? 0).toString());
final itemId = response.data?.itemId ?? "";
hideLoading();
if (itemId.isEmpty) {
hideLoading();
print("redeemMobileCard failed: ${response.errorMessage}");
onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
return;
}
_getMobileCardCode(itemId);
} catch (error) {
hideLoading();
onShowAlertError?.call(error.toString());
return;
}
}
_getMobileCardCode(String itemId) async {
showLoading();
try {
final response = await client.getMobileCardCode(itemId);
hideLoading();
final data = response.data?.item;
if (response.isSuccess && data != null) {
onRedeemProductMobileSuccess?.call(data);
return;
}
onShowAlertError?.call(response.message ?? Constants.commonError);
} catch (error) {
hideLoading();
onShowAlertError?.call(error.toString());
return;
}
}
getProductMobileCard() async {
showLoading();
try {
final response = await client.productMobileCardGetList();
final result = response.data?.products ?? [];
final seen = <String>{};
final uniqueBrandCode = <ProductMobileCardModel>[];
for (final p in result) {
final code = p.brandCode ?? "";
if (code.isNotEmpty && seen.add(code)) {
uniqueBrandCode.add(p);
}
}
selectedBrandCode.value = uniqueBrandCode.isNotEmpty ? uniqueBrandCode.first.brandCode ?? "" : "";
mobileCardSections.value = uniqueBrandCode;
final Map<String, List<ProductMobileCardModel>> grouped = {};
for (final product in result) {
final code = product.brandCode ?? 'unknown';
if (!grouped.containsKey(code)) {
grouped[code] = [];
}
grouped[code]!.add(product);
}
groupedSection.value = grouped;
hideLoading();
if (!response.isSuccess) {
onShowAlertError?.call(response.message ?? Constants.commonError);
}
} catch (error) {
onShowAlertError?.call(error.toString());
}
}
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mypoint_flutter_app/extensions/datetime_extensions.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import 'package:mypoint_flutter_app/resouce/base_color.dart';
import 'models/usable_voucher_model.dart';
class UsableMobileCardPopup extends StatelessWidget {
final UsableVoucherModel usableVoucher;
const UsableMobileCardPopup({super.key, required this.usableVoucher});
@override
Widget build(BuildContext context) {
final String titleNonExpired = "Bạn đã đổi mã thẻ điện thoại thành công.";
final String titleSuccess = "Bạn đã đổi mã thẻ điện thoại thành công. Giá trị tới ngày %@. Vui lòng sử dụng trước ngày hết hạn.";
final expiredTime = (usableVoucher.expiredTime ?? "").toDate()?.toFormattedString() ?? "";
final String content = expiredTime.isNotEmpty
? titleSuccess.replaceAll("%@", usableVoucher.expiredTime!)
: titleNonExpired;
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Align(
alignment: Alignment.topRight,
child: GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: const Icon(Icons.close, size: 24),
),
),
Image.asset(
'assets/images/ic_pipi_02.png',
height: 200,
),
const SizedBox(height: 8),
Text(
"Thành Công",
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
content,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 14, color: Colors.black87),
),
const SizedBox(height: 16),
Container(
height: 48,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.grey.shade200,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
child: Row(
children: [
Expanded(
child: Text(
usableVoucher.codeSecret ?? '---',
style: const TextStyle(fontSize: 16),
),
),
GestureDetector(
onTap: () {
Clipboard.setData(
ClipboardData(text: usableVoucher.codeSecret ?? ''),
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Đã sao chép mã')),
);
},
child: const Icon(Icons.copy),
),
],
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
backgroundColor: BaseColor.primary500,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text("Đã hiểu", style: TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.bold)),
),
],
),
),
);
}
}
void showVoucherPopup(BuildContext context, UsableVoucherModel usableVoucher) {
showDialog(
context: context,
barrierDismissible: true,
builder: (_) => UsableMobileCardPopup(usableVoucher: usableVoucher),
);
}
......@@ -34,9 +34,10 @@ class PersonalEditViewModel extends RestfulApiViewModel {
);
birthday = profile?.workerSite?.birthday?.toDateFormat('yyyy-MM-dd');
gender = PersonalGender.from(profile.workerSite?.sex ?? "U");
var name = profile?.workerSite?.fullname ?? "";
editDataModel.value = PersonalEditDataModel(
name: DataPreference.instance.fullName,
name: name,
nickname: profile?.workerSite?.nickname,
phone: profile?.workerSite?.phoneNumber,
email: profile?.workerSite?.email,
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/directional/directional_screen.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
......@@ -64,14 +65,14 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
Widget _buildHeaderPersonal(HeaderHomeModel data) {
final width = MediaQuery.of(context).size.width;
final topPadding = MediaQuery.of(context).padding.top;
final name = DataPreference.instance.profile?.workerSite?.fullname ?? "Quý Khách";
final name = DataPreference.instance.displayName;
final level = DataPreference.instance.rankName ?? "Hạng Đồng";
final email = DataPreference.instance.profile?.workerSite?.email ?? "";
return Container(
height: width * 163 / 375,
decoration: BoxDecoration(image: DecorationImage(image: NetworkImage(data.background ?? ""), fit: BoxFit.cover)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
......@@ -125,7 +126,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
Row(
children: [
Text(
"${data.totalPointActive.toString()} điểm",
(data.totalPointActive ?? 0).money(CurrencyUnit.point),
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(width: 4),
......@@ -182,7 +183,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
{'icon': Icons.gif_box_outlined, 'title': 'Ưu đãi của tôi', 'type': 'APP_SCREEN_MY_PURCHASE_ITEMS'},
{'icon': Icons.receipt_long_outlined, 'title': 'Lịch sử giao dịch', 'sectionDivider': true, 'type': ''},
{'icon': Icons.history_outlined, 'title': 'Lịch sử điểm', 'type': ''},
{'icon': Icons.history_outlined, 'title': 'Lịch sử hoàn điểm', 'type': ''},
{'icon': Icons.history_outlined, 'title': 'Lịch sử hoàn điểm', 'type': 'APP_SCREEN_REFUND_HISTORY'},
{'icon': Icons.account_balance_wallet_outlined, 'title': 'Quản lý tài khoản/thẻ', 'type': ''},
{'icon': Icons.favorite_border, 'title': 'Yêu thích', 'type': ''},
{'icon': Icons.shopping_bag_outlined, 'title': 'Đơn mua', 'sectionDivider': true, 'type': 'APP_SCREEN_ORDER_MENU'},
......
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../voucher/models/product_brand_model.dart';
class BrandSelectSheet extends StatelessWidget {
final List<ProductBrandModel> brands;
final Function(ProductBrandModel) onSelected;
final ProductBrandModel? selectedBrand;
const BrandSelectSheet({
super.key,
required this.brands,
this.selectedBrand,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("Chọn nhà mạng",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
const SizedBox(height: 16),
GridView.builder(
shrinkWrap: true,
itemCount: brands.length,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 1.7,
),
itemBuilder: (context, index) {
final brand = brands[index];
bool isFocused = selectedBrand?.id == brand.id;
return GestureDetector(
onTap: () => onSelected(brand),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(color: isFocused ? Colors.orange : Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
child: loadNetworkImage(
url: brand.logo,
fit: BoxFit.contain,
placeholderAsset: "assets/images/bg_default_169.png",
),
),
);
},
),
],
),
);
}
}
import 'package:json_annotation/json_annotation.dart';
part 'brand_network_model.g.dart';
@JsonSerializable()
class BrandNetworkModel {
final int? id;
final int? stock;
final String? code;
final String? name;
final String? logo;
BrandNetworkModel({
this.id,
this.stock,
this.code,
this.name,
this.logo,
});
factory BrandNetworkModel.fromJson(Map<String, dynamic> json) =>
_$BrandNetworkModelFromJson(json);
Map<String, dynamic> toJson() => _$BrandNetworkModelToJson(this);
}
@JsonSerializable()
class BrandNameCheckResponse {
final String? brand;
BrandNameCheckResponse({this.brand});
factory BrandNameCheckResponse.fromJson(Map<String, dynamic> json) =>
_$BrandNameCheckResponseFromJson(json);
Map<String, dynamic> toJson() => _$BrandNameCheckResponseToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'brand_network_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
BrandNetworkModel _$BrandNetworkModelFromJson(Map<String, dynamic> json) =>
BrandNetworkModel(
id: (json['id'] as num?)?.toInt(),
stock: (json['stock'] as num?)?.toInt(),
code: json['code'] as String?,
name: json['name'] as String?,
logo: json['logo'] as String?,
);
Map<String, dynamic> _$BrandNetworkModelToJson(BrandNetworkModel instance) =>
<String, dynamic>{
'id': instance.id,
'stock': instance.stock,
'code': instance.code,
'name': instance.name,
'logo': instance.logo,
};
BrandNameCheckResponse _$BrandNameCheckResponseFromJson(
Map<String, dynamic> json,
) => BrandNameCheckResponse(brand: json['brand'] as String?);
Map<String, dynamic> _$BrandNameCheckResponseToJson(
BrandNameCheckResponse instance,
) => <String, dynamic>{'brand': instance.brand};
import 'package:contacts_service/contacts_service.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:mypoint_flutter_app/screen/topup/topup_viewmodel.dart';
import 'package:mypoint_flutter_app/widgets/custom_navigation_bar.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../preference/data_preference.dart';
import '../../resouce/base_color.dart';
import '../../shared/router_gage.dart';
import 'brand_select_sheet_widget.dart';
class PhoneTopUpScreen extends StatefulWidget {
const PhoneTopUpScreen({super.key});
@override
State<PhoneTopUpScreen> createState() => _PhoneTopUpScreenState();
}
class _PhoneTopUpScreenState extends State<PhoneTopUpScreen> {
final TopUpViewModel _viewModel = Get.put(TopUpViewModel());
late final TextEditingController _phoneController;
@override
void initState() {
super.initState();
_phoneController = TextEditingController(text: _viewModel.phoneNumber.value);
_viewModel.firstLoadTopUpData();
}
String get formattedAmount {
return NumberFormat.currency(
locale: 'vi_VN',
symbol: '',
decimalDigits: 0,
).format(_viewModel.selectedProduct.value?.amountToBePaid ?? 0);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomNavigationBar(title: "Nạp tiền điện thoại"),
body: Obx(() {
return Column(
children: [
_buildHeaderPhone(),
Container(height: 6, color: Colors.grey.shade200),
const Divider(height: 8),
_buildItemTypeProduct(),
const Divider(height: 1),
],
);
}),
bottomNavigationBar: Obx(() {
return _buildBottomAction();
}),
);
}
Widget _buildHeaderPhone() {
return Obx(() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
const Text("Số điện thoại", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextField(
controller: _phoneController,
decoration: InputDecoration(
filled: true,
fillColor: Colors.grey.shade100,
suffixIcon: InkWell(
onTap: () => pickContact(context),
child: const Icon(Icons.contacts, color: Colors.orange),
),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
),
keyboardType: TextInputType.phone,
onChanged: (value) {
_viewModel.phoneNumber.value = value;
_viewModel.checkMobileNetwork();
},
),
),
const SizedBox(width: 8),
GestureDetector(
onTap:
_viewModel.topUpBrands.value.isEmpty
? null
: () {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder:
(_) => BrandSelectSheet(
brands: _viewModel.topUpBrands.value,
selectedBrand: _viewModel.selectedBrand.value,
onSelected: (brand) {
Navigator.pop(context);
if (brand == null && brand.id != _viewModel.selectedBrand.value?.id) return;
_viewModel.selectedProduct.value = null;
_viewModel.selectedBrand.value = brand;
_viewModel.getTelcoDetail();
},
),
);
},
child: Container(
padding: const EdgeInsets.all(4),
height: 48,
width: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: loadNetworkImage(
url: _viewModel.selectedBrand.value?.logo,
fit: BoxFit.fitWidth,
placeholderAsset: "assets/images/bg_default_169.png",
),
),
),
],
),
const SizedBox(height: 16),
_buildTagHistory(),
const SizedBox(height: 8),
],
),
);
});
}
Widget _buildTagHistory() {
final histories = _viewModel.histories;
return Obx(() {
return SizedBox(
height: 36,
child: Center(
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: histories.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (_, index) {
final phone = histories[index];
final myPhone = DataPreference.instance.phone ?? '';
final isMyPhone = phone == myPhone;
final selected = phone == _viewModel.phoneNumber.value;
return GestureDetector(
onTap: () {
setState(() {
_viewModel.phoneNumber.value = phone;
_phoneController.text = phone;
_viewModel.checkMobileNetwork();
});
},
child: Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: selected ? Colors.orange.shade50 : Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: selected ? Colors.orange : Colors.grey.shade300),
),
child: Center(
child: Text(
isMyPhone ? " Số của tôi " : " $phone ",
textAlign: TextAlign.center,
style: TextStyle(
color: selected ? Colors.orange : Colors.black54,
fontSize: 16,
fontWeight: selected ? FontWeight.bold : FontWeight.normal,
),
),
),
),
);
},
),
),
);
});
}
Widget _buildItemTypeProduct() {
return Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("Mệnh giá", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(height: 12),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 3,
children:
_viewModel.products.value.map((product) {
final isSelected = product.id == _viewModel.selectedProduct.value?.id;
final preview = product.previewCampaign;
return GestureDetector(
onTap: () => setState(() => _viewModel.selectedProduct.value = product),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: isSelected ? Colors.orange : Colors.grey.shade300),
color: isSelected ? Colors.orange.withOpacity(0.1) : Colors.white,
),
child: Stack(
children: [
// Gift icon
if (preview?.hasGift == true)
Positioned(
top: 8,
left: 0,
child: Image.asset('assets/images/ic_mark_give_voucher.png', height: 16),
),
// Point icon
if ((preview?.rewardPoint ?? 0) > 0)
Positioned(
top: 0,
right: 8,
child: Image.asset('assets/images/ic_mark_give_point.png', width: 24),
),
// Text center
Center(
child: Text(
"${NumberFormat.currency(locale: 'vi_VN', symbol: '', decimalDigits: 0).format(product.amountToBePaid ?? 0)}đ",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: isSelected ? Colors.orange : Colors.black87,
),
),
),
],
),
),
);
}).toList(),
),
],
),
),
);
}
Widget _buildBottomAction() {
final product = _viewModel.selectedProduct.value;
final preview = product?.previewCampaign;
final rewardPoint = preview?.rewardPoint ?? 0;
final hasGift = preview?.hasGift == true;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
decoration: const BoxDecoration(
color: Colors.white,
boxShadow: [BoxShadow(color: Colors.black54, blurRadius: 8, offset: Offset(0, 4))],
),
child: SafeArea(
top: false,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: Colors.white,
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text("Tổng: ", style: TextStyle(color: Colors.grey.shade600, fontSize: 16)),
Text(
"$formattedAmountđ",
style: const TextStyle(color: BaseColor.primary500, fontWeight: FontWeight.bold, fontSize: 20),
),
],
),
const SizedBox(height: 4),
Row(
children: [
if (rewardPoint > 0)
Row(
children: [
Text(
"+",
style: const TextStyle(color: Colors.orange, fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(width: 2),
Image.asset('assets/images/ic_point.png', width: 16, height: 16, fit: BoxFit.cover),
const SizedBox(width: 2),
Text(
NumberFormat.decimalPattern('vi_VN').format(rewardPoint),
style: const TextStyle(color: Colors.orange, fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
if (rewardPoint > 0 && hasGift) const SizedBox(width: 12),
if (hasGift)
Row(
children: [
Text(
"+",
style: const TextStyle(color: Colors.red, fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(width: 2),
Image.asset('assets/images/ic_gift_red.png', width: 16, height: 16, fit: BoxFit.cover),
],
),
],
),
],
),
const Spacer(),
ElevatedButton(
onPressed: () {
Get.toNamed(
transactionDetailScreen,
arguments: {"product": product, "quantity": 1, "targetPhoneNumber": _viewModel.phoneNumber.value},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: BaseColor.primary500,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Text(
"Nạp ngay",
style: TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
],
),
),
),
);
}
Future<void> pickContact(BuildContext context) async {
try {
// Gọi sẽ tự động hiện dialog yêu cầu quyền (nếu cần)
final Contact? contact = await ContactsService.openDeviceContactPicker();
if (contact != null && contact.phones != null && contact.phones!.isNotEmpty) {
String phone = contact.phones!.first.value ?? '';
phone = phone.replaceAll(RegExp(r'[\s\-\(\)]'), '');
_phoneController.text = phone;
_viewModel.phoneNumber.value = phone;
_viewModel.checkMobileNetwork();
} else {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("Không tìm thấy số điện thoại hợp lệ")));
}
} catch (e) {
print("❌ Lỗi khi truy cập danh bạ: $e");
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Không thể truy cập danh bạ")));
}
}
}
import 'package:get/get.dart';
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/preference/data_preference.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../preference/contact_storage_service.dart';
import '../voucher/models/product_brand_model.dart';
import '../voucher/models/product_model.dart';
import '../voucher/models/product_type.dart';
class TopUpViewModel extends RestfulApiViewModel {
var histories = RxList<String>();
final RxList<ProductBrandModel> topUpBrands = <ProductBrandModel>[].obs;
final RxList<ProductModel> products = <ProductModel>[].obs;
var selectedBrand = Rxn<ProductBrandModel>();
var selectedProduct = Rxn<ProductModel>();
final Map<String, List<ProductModel>> _allValue = {};
var phoneNumber = ''.obs;
@override
void onInit() {
super.onInit();
final myPhone = DataPreference.instance.phone ?? '';
phoneNumber.value = myPhone;
ContactStorageService().getUsedContacts().then((value) {
if (value.isNotEmpty) {
histories.value = value;
} else {
histories.value = [myPhone];
}
});
if (!histories.contains(myPhone)) {
histories.value.insert(0, myPhone);
ContactStorageService().saveUsedContact(myPhone);
}
}
firstLoadTopUpData() async {
showLoading();
await getTopUpBrands();
await checkMobileNetwork();
hideLoading();
}
getTopUpBrands() {
client.getTopUpBrands(ProductType.topupMobile).then((response) {
topUpBrands.value = response.data ?? [];
}).catchError((error) {
print('Error fetching brands topup: $error');
});
}
checkMobileNetwork() {
client.checkMobileNetwork(phoneNumber.value).then((response) {
final brandCode = response.data?.brand ?? '';
final brand = topUpBrands.isNotEmpty
? topUpBrands.firstWhere(
(brand) => brand.code == brandCode,
orElse: () => topUpBrands.first,
)
: null;
selectedBrand.value = brand;
getTelcoDetail();
}).catchError((error) {
final first = topUpBrands.value.firstOrNull;
if (first != null) {
selectedBrand.value = first;
}
getTelcoDetail();
print('Error checking mobile network: $error');
});
}
Future<void> getTelcoDetail({String? selected}) async {
final code = selectedBrand.value?.code;
final id = selectedBrand.value?.id;
if (code == null || id == null) return;
void makeSelected(List<ProductModel> list) {
bool didSelect = false;
if (selected != null && selected.isNotEmpty) {
for (var item in list) {
final isMatch = item.id == int.tryParse(selected);
if (isMatch) {
selectedProduct.value = item;
didSelect = true;
}
}
}
// Nếu chưa có item nào được chọn → mặc định chọn 100k
if (!didSelect && selectedProduct.value == null) {
final item100k = list.isNotEmpty
? list.firstWhere(
(e) => e.amountToBePaid == 100000,
orElse: () => list.first,
) : null;
selectedProduct.value = item100k;
}
}
// Dùng cache nếu có
if (_allValue.containsKey(code)) {
final cached = _allValue[code]!;
products.value = cached;
makeSelected(cached);
return;
}
showLoading();
final body = {
"type": ProductType.topupMobile.value,
"size": 200,
"index": 0,
"brand_id": selectedBrand.value?.id ?? 0,
};
try {
final result = await client.getProducts(body);
final data = result.data ?? [];
_allValue[code] = data;
products.value = result.data ?? [];
makeSelected(data);
hideLoading();
} catch (error) {
print("Error fetching all products: $error");
hideLoading();
}
}
}
\ No newline at end of file
......@@ -25,6 +25,7 @@ class _TransactionDetailScreenState extends BaseState<TransactionDetailScreen> w
final currencyFormatter = NumberFormat.currency(locale: 'vi_VN', symbol: 'đ', decimalDigits: 0);
ProductModel? _product;
int _quantity = 1;
String? _targetPhoneNumber;
bool _isPaymentMethodsExpanded = true;
bool shouldDisablePaymentMethods = false;
......@@ -35,6 +36,7 @@ class _TransactionDetailScreenState extends BaseState<TransactionDetailScreen> w
if (args is Map) {
_product = args['product'];
_quantity = args['quantity'] ?? 1;
_targetPhoneNumber = args['targetPhoneNumber'];
}
if (_product == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
......@@ -42,7 +44,9 @@ class _TransactionDetailScreenState extends BaseState<TransactionDetailScreen> w
});
return;
}
_viewModel = Get.put(TransactionDetailViewModel(product: _product!, quantity: _quantity));
_viewModel = Get.put(
TransactionDetailViewModel(product: _product!, quantity: _quantity, targetPhoneNumber: _targetPhoneNumber),
);
_viewModel.refreshData();
_viewModel.onShowAlertError = (message) {
......
......@@ -6,6 +6,7 @@ import 'package:mypoint_flutter_app/shared/router_gage.dart';
import 'package:uuid/uuid.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../configs/constants.dart';
import '../../preference/contact_storage_service.dart';
import '../../preference/data_preference.dart';
import '../voucher/models/product_model.dart';
import '../webview/payment_web_view_screen.dart';
......@@ -22,6 +23,7 @@ class TransactionDetailViewModel extends RestfulApiViewModel {
final RxBool isLoading = false.obs;
final ProductModel product;
final int quantity;
final String? targetPhoneNumber;
final RxBool usePoints = true.obs;
var selectedPaymentMethodIndex = -1.obs;
void Function(String message)? onShowAlertError;
......@@ -29,13 +31,12 @@ class TransactionDetailViewModel extends RestfulApiViewModel {
int get finalTotal {
final totalPrice = previewData.value?.totalPrice ?? 0;
final pointValue = previewData.value?.pointData?.point ?? 0;
final finalTotal = usePoints.value && previewData.value?.pointData?.status == 1
? totalPrice - pointValue
: totalPrice;
final finalTotal =
usePoints.value && previewData.value?.pointData?.status == 1 ? totalPrice - pointValue : totalPrice;
return finalTotal;
}
TransactionDetailViewModel({required this.product, required this.quantity});
TransactionDetailViewModel({required this.product, required this.quantity, this.targetPhoneNumber});
@override
void onInit() {
......@@ -75,12 +76,16 @@ class TransactionDetailViewModel extends RestfulApiViewModel {
paymentTokenId: selectedBankAccount?.id,
saveToken: saveToken,
metadata: "",
targetPhoneNumber: targetPhoneNumber,
)
.then((value) {
hideLoading();
if (value.isSuccess) {
final data = value.data;
if ((data?.paymentUrl ?? "").isNotEmpty) {
if ((targetPhoneNumber ?? "").isNotEmpty) {
ContactStorageService().saveUsedContact(targetPhoneNumber ?? "");
}
Get.toNamed(
paymentWebViewScreen,
arguments: PaymentWebViewInput(
......@@ -88,16 +93,20 @@ class TransactionDetailViewModel extends RestfulApiViewModel {
isContract: false,
orderId: data?.id ?? "",
showAlertBack: true,
callback: (result) {},
callback: (result) {
if (result == PaymentProcess.success) {
print("PaymentProcess.success");
print(data?.id ?? "");
Get.offNamed(
transactionHistoryDetailScreen,
arguments: {"orderId": data?.id ?? "", "canBack": true},
);
}
},
)
);
} else if ((data?.redeemId ?? "").isNotEmpty && (data?.id ?? "").isNotEmpty) {
Get.offNamed(transactionHistoryDetailScreen,
arguments: {
"orderId": data?.id ?? "",
"canBack": true,
}
);
Get.offNamed(transactionHistoryDetailScreen, arguments: {"orderId": data?.id ?? "", "canBack": true});
} else {
onShowAlertError?.call(value.errorMessage ?? Constants.commonError);
}
......@@ -115,6 +124,7 @@ class TransactionDetailViewModel extends RestfulApiViewModel {
"quantity": quantity,
"price": product.amountToBePaid ?? 0,
"access_token": token,
"target_phone_number": targetPhoneNumber ?? "",
};
if (product.previewFlashSale?.isFlashSalePrice == true && product.previewFlashSale?.id != null) {
body["flash_sale_id"] = product.previewFlashSale!.id;
......
......@@ -57,10 +57,7 @@ class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with Basi
});
return;
}
_viewModel = Get.put(VoucherDetailViewModel(
productId: productId,
customerProductId: customerProductId,
));
_viewModel = Get.put(VoucherDetailViewModel(productId: productId, customerProductId: customerProductId));
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
showAlertError(content: message);
......@@ -367,7 +364,7 @@ class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with Basi
child: ElevatedButton(
onPressed: () {
if (_viewModel.product.value == null) return;
Get.to(() => VoucherCodeCardScreen(product: _viewModel.product.value!,));
Get.to(() => VoucherCodeCardScreen(product: _viewModel.product.value!));
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
......@@ -442,9 +439,9 @@ class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with Basi
},
),
),
const SizedBox(width: 24),
],
),
const SizedBox(width: 36),
Expanded(
child: SizedBox(
height: 48,
......
......@@ -25,3 +25,29 @@ class ProductBrandModel {
factory ProductBrandModel.fromJson(Map<String, dynamic> json) => _$ProductBrandModelFromJson(json);
Map<String, dynamic> toJson() => _$ProductBrandModelToJson(this);
}
class PrefixMobileCarrier {
static const String mobiFoneCode = "MOBIFONE";
static const String viettelCode = "VIETTEL";
static const String vinaphoneCode = "VINAPHONE";
static const String vietNamMobile = "VNM";
static List<String> viettelPrefixNumbers() => [
"086", "096", "097", "098",
"032", "033", "034", "035", "036", "037", "038", "039",
];
static List<String> vinaPhonePrefixNumbers() => [
"088", "091", "094",
"083", "084", "085", "081", "082",
];
static List<String> mobiPhonePrefixNumbers() => [
"089", "090", "093",
"070", "079", "077", "076", "078",
];
static List<String> vietNamMobilePrefixNumbers() => [
"092", "056", "058",
];
}
......@@ -15,6 +15,7 @@ import 'media_type.dart';
import 'my_product_status_type.dart';
part 'product_model.g.dart';
@JsonSerializable()
class ProductModel {
final int? id;
......@@ -28,6 +29,8 @@ class ProductModel {
@JsonKey(name: 'voucher_properties')
final ProductPropertiesModel? properties;
final List<ProductMediaItem>? media;
@JsonKey(name: 'preview_campaign')
final ProductPreviewCampaignModel? previewCampaign;
@JsonKey(name: 'preview_campaign_flash_sale')
final PreviewFlashSale? previewFlashSale;
@JsonKey(name: 'customer_product_info')
......@@ -49,6 +52,7 @@ class ProductModel {
this.brand,
this.properties,
this.media,
this.previewCampaign,
this.previewFlashSale,
this.customerInfoModel,
this.item,
......@@ -109,7 +113,9 @@ class ProductModel {
}
double get progress {
if (previewFlashSale?.fsQuantityTotal != null && previewFlashSale?.fsQuantitySold != null && previewFlashSale!.fsQuantityTotal! > 0) {
if (previewFlashSale?.fsQuantityTotal != null &&
previewFlashSale?.fsQuantitySold != null &&
previewFlashSale!.fsQuantityTotal! > 0) {
return previewFlashSale!.fsQuantitySold! / previewFlashSale!.fsQuantityTotal!;
}
return 0.0;
......@@ -139,4 +145,20 @@ class ProductModel {
factory ProductModel.fromJson(Map<String, dynamic> json) => _$ProductModelFromJson(json);
Map<String, dynamic> toJson() => _$ProductModelToJson(this);
}
\ No newline at end of file
}
@JsonSerializable()
class ProductPreviewCampaignModel {
@JsonKey(name: 'reward_point')
int? rewardPoint;
@JsonKey(name: 'has_gift')
bool? hasGift;
ProductPreviewCampaignModel({
this.rewardPoint,
this.hasGift,
});
factory ProductPreviewCampaignModel.fromJson(Map<String, dynamic> json) => _$ProductPreviewCampaignModelFromJson(json);
Map<String, dynamic> toJson() => _$ProductPreviewCampaignModelToJson(this);
}
......@@ -34,6 +34,12 @@ ProductModel _$ProductModelFromJson(Map<String, dynamic> json) => ProductModel(
(json['media'] as List<dynamic>?)
?.map((e) => ProductMediaItem.fromJson(e as Map<String, dynamic>))
.toList(),
previewCampaign:
json['preview_campaign'] == null
? null
: ProductPreviewCampaignModel.fromJson(
json['preview_campaign'] as Map<String, dynamic>,
),
previewFlashSale:
json['preview_campaign_flash_sale'] == null
? null
......@@ -67,6 +73,7 @@ Map<String, dynamic> _$ProductModelToJson(ProductModel instance) =>
'brand': instance.brand,
'voucher_properties': instance.properties,
'media': instance.media,
'preview_campaign': instance.previewCampaign,
'preview_campaign_flash_sale': instance.previewFlashSale,
'customer_product_info': instance.customerInfoModel,
'product_item': instance.item,
......@@ -74,3 +81,17 @@ Map<String, dynamic> _$ProductModelToJson(ProductModel instance) =>
'require_form_regis': instance.requireFormRegis,
'type': instance.type,
};
ProductPreviewCampaignModel _$ProductPreviewCampaignModelFromJson(
Map<String, dynamic> json,
) => ProductPreviewCampaignModel(
rewardPoint: (json['reward_point'] as num?)?.toInt(),
hasGift: json['has_gift'] as bool?,
);
Map<String, dynamic> _$ProductPreviewCampaignModelToJson(
ProductPreviewCampaignModel instance,
) => <String, dynamic>{
'reward_point': instance.rewardPoint,
'has_gift': instance.hasGift,
};
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import '../../../directional/directional_action_type.dart';
import '../../../directional/directional_screen.dart';
import '../../../resouce/base_color.dart';
import '../../../shared/router_gage.dart';
class VoucherActionMenu extends StatelessWidget {
const VoucherActionMenu({super.key});
......@@ -14,10 +19,10 @@ class VoucherActionMenu extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: const [
_ActionItem(icon: "assets/images/ic_topup.png", label: 'Nạp tiền\ndiện thoại'),
_ActionItem(icon: "assets/images/ic_card_code.png", label: 'Đổi mã\nthẻ nạp'),
_ActionItem(icon: "assets/images/ic_sim_service.png", label: 'Gói cước\nnhà mạng'),
_ActionItem(icon: "assets/images/ic_topup_data.png", label: 'Ưu đãi\nData'),
_ActionItem(icon: "assets/images/ic_topup.png", label: 'Nạp tiền\ndiện thoại', type: DirectionalScreenName.topup,),
_ActionItem(icon: "assets/images/ic_card_code.png", label: 'Đổi mã\nthẻ nạp', type: DirectionalScreenName.productMobileCard,),
_ActionItem(icon: "assets/images/ic_sim_service.png", label: 'Gói cước\nnhà mạng', type: DirectionalScreenName.carrierPackage,),
_ActionItem(icon: "assets/images/ic_topup_data.png", label: 'Ưu đãi\nData', type: DirectionalScreenName.simService,),
],
),
);
......@@ -27,34 +32,45 @@ class VoucherActionMenu extends StatelessWidget {
class _ActionItem extends StatelessWidget {
final String icon;
final String label;
final DirectionalScreenName type;
const _ActionItem({required this.icon, required this.label});
const _ActionItem({required this.icon, required this.label, required this.type});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final itemWidth = screenWidth / 4;
return SizedBox(
width: itemWidth,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
icon,
fit: BoxFit.cover,
width: 40,
height: 40,
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
return GestureDetector(
onTap: () {
final param = type == DirectionalScreenName.carrierPackage ? "https://mypoint.uudaigoicuoc.com/" : null;
DirectionalScreen? screen = DirectionalScreen.build(
clickActionType: type.rawValue,
clickActionParam: param,
);
screen?.begin();
},
child: SizedBox(
width: itemWidth,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
icon,
fit: BoxFit.cover,
width: 40,
height: 40,
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
......
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