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ạ")));
}
}
}
This diff is collapsed.
......@@ -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) {
......
......@@ -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,
......
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