Commit fa01087d authored by DatHV's avatar DatHV
Browse files

update brand, detail..

parent c8abf95b
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import '../../../resouce/base_color.dart';
import '../../../shared/router_gage.dart';
import '../model/affiliate_brand_model.dart';
class AffiliateBrand extends StatelessWidget {
final List<AffiliateBrandModel> brands;
const AffiliateBrand({super.key, required this.brands});
List<AffiliateBrandModel> get displayBrands {
return brands.take(6).toList();
}
@override
Widget build(BuildContext context) {
if (brands.isEmpty) {
if (displayBrands.isEmpty) {
return const SizedBox.shrink();
}
final space = 8.0;
final width = (MediaQuery.of(context).size.width - space * 2 - 32)/3;
final width = (MediaQuery.of(context).size.width - space * 2 - 32) / 3;
return Column(
children: [
const SizedBox(height: 24),
......@@ -21,8 +28,15 @@ class AffiliateBrand extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Thương Hiệu Hoàn Điểm", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
Text("Xem tất cả",
style: TextStyle(color: BaseColor.primary400, fontSize: 14, fontWeight: FontWeight.w600)),
GestureDetector(
onTap: () {
Get.toNamed(affiliateBrandListScreen, arguments: {"brands": brands});
},
child: Text(
"Xem tất cả",
style: TextStyle(color: BaseColor.primary400, fontSize: 14, fontWeight: FontWeight.w600),
),
),
],
),
const SizedBox(height: 12),
......@@ -30,38 +44,37 @@ class AffiliateBrand extends StatelessWidget {
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: brands.length,
itemCount: displayBrands.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: width/(width + 30),
childAspectRatio: width / (width + 30),
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (context, index) {
final brand = brands[index];
return _buildAffiliateBrandItem(brand);
final brand = displayBrands[index];
return buildAffiliateBrandItem(brand);
},
),
],
);
}
}
Widget _buildAffiliateBrandItem(AffiliateBrandModel brand) {
Widget buildAffiliateBrandItem(AffiliateBrandModel brand) {
return LayoutBuilder(
builder: (context, constraints) {
final double imageWidth = constraints.maxWidth / 2;
return Container(
padding: const EdgeInsets.all(12),
return GestureDetector(
onTap: () {
Get.toNamed(affiliateBrandDetailScreen, arguments: {"brandId": brand.brandId});
},
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
)
],
borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5, offset: const Offset(0, 2))],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
......@@ -90,24 +103,18 @@ class AffiliateBrand extends StatelessWidget {
text: TextSpan(
style: const TextStyle(fontSize: 12),
children: [
const TextSpan(
text: "Hoàn đến: ",
style: TextStyle(color: Colors.grey),
),
const TextSpan(text: "Hoàn đến: ", style: TextStyle(color: Colors.grey)),
TextSpan(
text: "${(brand.commision ?? '').toNum()?.toString() ?? ''}%",
style: const TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold,
),
style: const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
),
],
),
),
],
),
),
);
},
);
}
}
......@@ -4,7 +4,8 @@ import '../model/affiliate_category_model.dart';
class AffiliateCategory extends StatelessWidget {
final List<AffiliateCategoryModel> categories;
const AffiliateCategory({super.key, required this.categories});
final void Function(AffiliateCategoryModel)? onTap;
const AffiliateCategory({super.key, required this.categories, this.onTap});
@override
Widget build(BuildContext context) {
......@@ -38,7 +39,11 @@ class AffiliateCategory extends StatelessWidget {
}
Widget _buildAffiliateCategoryItem(AffiliateCategoryModel category) {
return LayoutBuilder(
return GestureDetector(
onTap: () {
onTap?.call(category);
},
child: LayoutBuilder(
builder: (context, constraints) {
final double imageWidth = constraints.maxWidth / 2;
return Container(
......@@ -49,7 +54,7 @@ class AffiliateCategory extends StatelessWidget {
Image.asset('assets/images/cashback/${category.icon}.png', width: imageWidth, height: imageWidth,),
const SizedBox(height: 4),
Text(
category.name,
category.name ?? '',
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
......@@ -59,6 +64,7 @@ class AffiliateCategory extends StatelessWidget {
),
);
},
),
);
}
}
......@@ -52,7 +52,12 @@ class AffiliateProductTopSale extends StatelessWidget {
final imageUrl = product.thumnailLink ?? '';
final title = product.productName ?? '';
return Container(
return GestureDetector(
onTap: () {
print("name ${product.productLink}");
product.direcionalScreen?.begin();
},
child: Container(
width: 160,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
......@@ -107,6 +112,7 @@ class AffiliateProductTopSale extends StatelessWidget {
],
),
],
)
),
);
}
......
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import '../../../base/base_screen.dart';
import '../../../base/basic_state.dart';
import '../../resouce/base_color.dart';
import '../../widgets/back_button.dart';
import '../../widgets/image_loader.dart';
import '../../widgets/measure_size.dart';
import 'affiliate_brand_detail_tip_box.dart';
import 'affiliate_brand_detail_viewmodel.dart';
import 'package:get/get.dart';
import 'affiliate_brand_detail_tag_list.dart';
class AffiliateBrandDetailScreen extends BaseScreen {
const AffiliateBrandDetailScreen({super.key});
@override
_AffiliateBrandDetailScreenState createState() => _AffiliateBrandDetailScreenState();
}
class _AffiliateBrandDetailScreenState extends BaseState<AffiliateBrandDetailScreen> with BasicState {
late final AffiliateBrandDetailViewModel _viewModel;
double _infoHeight = 0;
@override
void initState() {
super.initState();
String? brandId;
final args = Get.arguments;
if (args is Map) {
brandId = args['brandId'];
}
if ((brandId ?? '').isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.back();
});
return;
}
_viewModel = Get.put(AffiliateBrandDetailViewModel(brandId ?? ''));
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
showAlertError(content: message);
}
};
}
@override
Widget createBody() {
return Scaffold(
backgroundColor: Colors.grey.shade100,
body: Obx(() {
double heightStepTutorial = 100;
var categoriesTag = _viewModel.brandDetailData.value?.categories ?? [];
var shoulds = _viewModel.brandDetailData.value?.contentShould ?? [];
var shouldnts = _viewModel.brandDetailData.value?.contentShouldnt ?? [];
var notes = _viewModel.brandDetailData.value?.contentNote ?? [];
var others = _viewModel.brandDetailData.value?.contentOther ?? [];
return Stack(
children: [
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderWithInfo(),
SizedBox(height: max(_infoHeight - 54, 0)),
_buildCardGuide(),
Container(
color: Colors.white,
margin: EdgeInsets.only(top: 16, bottom: 16),
padding: EdgeInsets.only(top: 16, bottom: 16),
child: SizedBox(
height: heightStepTutorial,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
const SizedBox(width: 12),
_buildStepCard(heightStepTutorial, "assets/images/banner_tutorial_refund_point_step1.png"),
const SizedBox(width: 12),
_buildStepCard(heightStepTutorial, "assets/images/banner_tutorial_refund_point_step2.png"),
const SizedBox(width: 12),
_buildStepCard(heightStepTutorial, "assets/images/banner_tutorial_refund_point_step3.png"),
const SizedBox(width: 12),
_buildStepCard(heightStepTutorial, "assets/images/banner_tutorial_refund_point_step4.png"),
const SizedBox(width: 12),
],
),
),
),
if (categoriesTag.isNotEmpty) AffiliateTagListScrollable(items: categoriesTag),
if (shoulds.isNotEmpty) AffiliateBrandDetailTipBox(type: AffiliateTipBoxType.should, tips: shoulds),
if (shouldnts.isNotEmpty)
AffiliateBrandDetailTipBox(type: AffiliateTipBoxType.shouldnt, tips: shouldnts),
if (notes.isNotEmpty) AffiliateBrandDetailTipBox(type: AffiliateTipBoxType.note, tips: notes),
if (others.isNotEmpty) AffiliateBrandDetailTipBox(type: AffiliateTipBoxType.other, tips: others),
],
),
),
SafeArea(child: Padding(padding: const EdgeInsets.only(left: 12), child: CustomBackButton())),
],
);
}),
bottomNavigationBar: _buildButton(),
);
}
Widget _buildButton() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: const BoxDecoration(
color: Colors.white,
boxShadow: [BoxShadow(color: Colors.black54, blurRadius: 8, offset: Offset(0, 4))],
),
child: SafeArea(
top: false,
child: ElevatedButton(
onPressed: () {
_viewModel.brandDetailData.value?.directionalScreen?.begin();
},
style: ElevatedButton.styleFrom(
backgroundColor: BaseColor.primary500,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
minimumSize: const Size.fromHeight(48),
),
child: const Text(
"Mua ngay",
style: TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
);
}
Widget _buildHeaderWithInfo() {
final double screenWidth = MediaQuery.of(context).size.width;
final double imageHeight = screenWidth / (16 / 9);
return Stack(
clipBehavior: Clip.none,
children: [
loadNetworkImage(
url: _viewModel.brandDetailData.value?.background,
fit: BoxFit.cover,
height: imageHeight,
width: double.infinity,
placeholderAsset: 'assets/images/bg_header_detail_brand.png',
),
Positioned(
left: 0,
right: 0,
child: MeasureSize(
onChange: (size) {
if (_infoHeight != size.height) {
setState(() {
_infoHeight = size.height;
});
}
},
child: Transform.translate(offset: Offset(0, imageHeight - 60), child: _buildCardInfo()),
),
),
],
);
}
Widget _buildCardGuide() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/ic_time_record.png', width: 36, height: 36),
const SizedBox(width: 4),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Ghi nhận sau', style: TextStyle(color: Colors.black54, fontSize: 12)),
Text(
_viewModel.brandDetailData.value?.timeConfirm ?? "",
style: TextStyle(color: Colors.black87, fontSize: 12, fontWeight: FontWeight.w700),
),
],
),
],
),
Padding(
padding: const EdgeInsets.only(left: 18, right: 18),
child: Container(
width: 1,
height: 40,
color: Colors.grey.shade300,
margin: const EdgeInsets.symmetric(horizontal: 12),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/ic_time_refund.png', width: 36, height: 36),
const SizedBox(width: 4),
Column(
children: [
const Text('Hoàn điểm sau', style: TextStyle(color: Colors.black54, fontSize: 12)),
Text(
_viewModel.brandDetailData.value?.timeCashback ?? "",
style: TextStyle(color: Colors.black87, fontSize: 12, fontWeight: FontWeight.w700),
),
],
),
],
),
],
);
}
Widget _buildCardInfo() {
final detail = _viewModel.brandDetailData.value;
if (detail == null) return SizedBox();
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 3))],
),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: loadNetworkImage(
url: detail.logo,
width: 60,
height: 60,
placeholderAsset: "assets/images/ic_logo.png",
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(detail.brandName ?? '', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
RichText(
text: TextSpan(
text: 'Hoàn đến ',
style: const TextStyle(color: Colors.black, fontSize: 13),
children: [
TextSpan(
text: detail.showCommision ?? '',
style: const TextStyle(color: Colors.deepOrangeAccent, fontWeight: FontWeight.bold),
),
],
),
),
],
),
),
const Icon(Icons.check_circle, color: Colors.green, size: 16),
const SizedBox(width: 2),
Text(
'${detail?.quantitySold?.money(CurrencyUnit.noneSpace) ?? '1'} đơn hàng',
style: TextStyle(color: Colors.green, fontSize: 13),
),
],
),
);
}
Widget _buildStepCard(double height, String imagePath) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(imagePath, fit: BoxFit.cover, width: height * 4611 / 2739, height: height),
);
}
}
import 'package:flutter/material.dart';
import 'models/affiliate_brand_detail_model.dart';
class AffiliateTagListScrollable extends StatelessWidget {
final List<AffiliateBrandCategoryModel> items;
const AffiliateTagListScrollable({super.key, required this.items});
@override
Widget build(BuildContext context) {
int lines = 2;
if (items.length > 20) {
lines = 3;
} else if (items.length < 10) {
lines = 1;
}
const double tagHeight = 36;
const double itemSpacing = 10;
final double containerHeight =
lines * tagHeight + (lines - 1) * itemSpacing;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 16, bottom: 8),
child: Text(
"Tỉ lệ hoàn điểm",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
),
Padding(
padding: EdgeInsets.only(left: 16, bottom: 0),
child: SizedBox(
height: containerHeight,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(lines, (lineIndex) {
final lineItems = items
.skip(lineIndex * (items.length ~/ lines))
.take(items.length ~/ lines)
.toList();
return Padding(
padding: EdgeInsets.only(bottom: lineIndex < lines - 1 ? itemSpacing : 0),
child: Row(
children: lineItems.map((item) {
return Container(
margin: const EdgeInsets.only(right: itemSpacing),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(8),
),
child: RichText(
text: TextSpan(
text: '${item.categoryName}: ',
style: const TextStyle(color: Colors.black87, fontSize: 14),
children: [
TextSpan(
text: item.showCommision,
style: const TextStyle(color: Colors.deepOrangeAccent, fontSize: 14),
),
],
),
),
);
}).toList(),
),
);
}),
),
),
),
),
],
);
}
}
// import 'package:flutter/material.dart';
//
// import 'models/affiliate_brand_detail_model.dart';
//
// class AffiliateTagList extends StatelessWidget {
// final List<AffiliateBrandCategoryModel> items;
//
// const AffiliateTagList({super.key, required this.items});
//
// @override
// Widget build(BuildContext context) {
// int maxLines = 2;
// if (items.length > 20) {
// maxLines = 3;
// } else if (items.length < 10) {
// maxLines = 1;
// }
//
// return Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// const Padding(
// padding: EdgeInsets.all(8.0),
// child: Text(
// "Tỉ lệ hoàn điểm",
// style: TextStyle(fontWeight: FontWeight.bold),
// ),
// ),
// Wrap(
// spacing: 8,
// runSpacing: 8,
// children: items
// .take(maxLines * 3) // optional: limit total number if needed
// .map(
// (item) => Container(
// padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
// decoration: BoxDecoration(
// color: Colors.grey.shade100,
// borderRadius: BorderRadius.circular(16),
// ),
// child: RichText(
// text: TextSpan(
// text: '${item.categoryName}: ',
// style: const TextStyle(color: Colors.black87, fontSize: 14),
// children: [
// TextSpan(
// text: item.commision ?? '',
// style: const TextStyle(color: Colors.orange, fontSize: 14),
// ),
// ],
// ),
// ),
// ),
// )
// .toList(),
// ),
// ],
// );
// }
// }
import 'package:flutter/material.dart';
enum AffiliateTipBoxType {
should,
shouldnt,
note,
other;
}
extension AffiliateTipBoxTypeExt on AffiliateTipBoxType {
String get title {
switch (this) {
case AffiliateTipBoxType.should:
return "NÊN";
case AffiliateTipBoxType.shouldnt:
return "KHÔNG NÊN";
case AffiliateTipBoxType.note:
return "Ghi chú";
case AffiliateTipBoxType.other:
return "Thông tin khác";
}
}
Color get backgroundColor {
switch (this) {
case AffiliateTipBoxType.should:
return Colors.green.shade100;
case AffiliateTipBoxType.shouldnt:
return Colors.red.shade50;
case AffiliateTipBoxType.note:
return Colors.white;
case AffiliateTipBoxType.other:
return Colors.white;
}
}
Color get textColor {
switch (this) {
case AffiliateTipBoxType.should:
return Colors.green.shade600;
case AffiliateTipBoxType.shouldnt:
return Colors.red.shade800;
case AffiliateTipBoxType.note:
return Colors.black87;
case AffiliateTipBoxType.other:
return Colors.black87;
}
}
IconData get icon {
switch (this) {
case AffiliateTipBoxType.should:
return Icons.check_circle;
case AffiliateTipBoxType.shouldnt:
return Icons.cancel;
case AffiliateTipBoxType.note:
return Icons.info;
case AffiliateTipBoxType.other:
return Icons.lightbulb;
}
}
Color get iconColor {
switch (this) {
case AffiliateTipBoxType.should:
return Colors.green;
case AffiliateTipBoxType.shouldnt:
return Colors.red;
case AffiliateTipBoxType.note:
return Colors.grey;
case AffiliateTipBoxType.other:
return Colors.blue;
}
}
}
class AffiliateBrandDetailTipBox extends StatelessWidget {
final AffiliateTipBoxType type;
final List<String> tips;
const AffiliateBrandDetailTipBox({super.key, required this.type, required this.tips});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 16), //all(16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: type.backgroundColor,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
type.title,
style: TextStyle(
color: type.textColor,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 8),
...tips.map((tip) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(type.icon, color: type.iconColor, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
tip,
style: const TextStyle(fontSize: 14, color: Colors.black87),
),
),
],
),
)),
],
),
);
}
}
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import 'models/affiliate_brand_detail_model.dart';
class AffiliateBrandDetailViewModel extends RestfulApiViewModel {
String brandId;
AffiliateBrandDetailViewModel(this.brandId);
void Function(String message)? onShowAlertError;
var isLoading = false.obs;
var brandDetailData = Rxn<AffiliateBrandDetailModel>();
@override
void onInit() {
super.onInit();
_getAffiliateBrandDetail();
}
Future<void> _getAffiliateBrandDetail() async {
showLoading();
if (isLoading.value) return;
try {
isLoading.value = true;
final response = await client.getAffiliateBrandDetail(brandId);
hideLoading();
if (response.isSuccess) {
brandDetailData.value = response.data;
} else {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
}
} catch (error) {
showLoading();
onShowAlertError?.call("Error fetching product detail: $error");
} finally {
isLoading.value = false;
}
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../affiliate/model/affiliate_brand_model.dart';
import '../affiliate/sub_widget/build_affiliate_brand.dart';
class AffiliateBrandListScreen extends StatefulWidget {
const AffiliateBrandListScreen({super.key});
@override
_AffiliateBrandListScreenState createState() => _AffiliateBrandListScreenState();
}
class _AffiliateBrandListScreenState extends State<AffiliateBrandListScreen> {
List<AffiliateBrandModel> _brands = [];
@override
void initState() {
super.initState();
final args = Get.arguments;
if (args is Map) {
_brands = args['brands'];
}
}
@override
Widget build(BuildContext context) {
final space = 8.0;
final width = (MediaQuery.of(context).size.width - space * 2 - 32) / 3;
return Scaffold(
appBar: CustomNavigationBar(title: "Thương hiệu hoàn điểm"),
backgroundColor: Colors.grey.shade100,
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: GridView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _brands.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: width / (width + 30),
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (context, index) {
final brand = _brands[index];
return buildAffiliateBrandItem(brand);
},
),
),
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../resouce/base_color.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../affiliate/affiliate_popup_brands.dart';
import '../affiliate/model/affiliate_category_model.dart';
import 'affiliate_category_grid_viewmodel.dart';
class AffiliateCategoryGridScreen extends StatefulWidget {
const AffiliateCategoryGridScreen({super.key});
@override
_AffiliateCategoryGridScreenState createState() => _AffiliateCategoryGridScreenState();
}
class _AffiliateCategoryGridScreenState extends State<AffiliateCategoryGridScreen> {
final AffiliateCategoryGridViewModel viewModel = Get.put(AffiliateCategoryGridViewModel());
List<AffiliateCategoryModel> _categories = [];
@override
void initState() {
super.initState();
final args = Get.arguments;
if (args is Map) {
_categories = args['categories'];
}
viewModel.onShowAffiliateBrandPopup = (data) {
showAffiliateBrandPopup(context, data.$1, title: data.$2);
};
}
@override
Widget build(BuildContext context) {
final space = 8.0;
final width = (MediaQuery.of(context).size.width - space * 5) / 4;
return Scaffold(
appBar: CustomNavigationBar(title: "Lĩnh vực hoàn điểm"),
backgroundColor: Colors.white,
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 4,
childAspectRatio: width / (width + 30),
children: _categories.map((category) => _buildAffiliateCategoryItem(category)).toList(),
),
),
),
);
}
Widget _buildAffiliateCategoryItem(AffiliateCategoryModel category) {
return GestureDetector(
onTap: () {
viewModel.affiliateBrandGetListBuyCategory(category);
},
child: LayoutBuilder(
builder: (context, constraints) {
final double imageWidth = constraints.maxWidth / 2;
return Container(
padding: const EdgeInsets.only(top: 12, bottom: 8, left: 4, right: 4),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Image.asset('assets/images/cashback/${category.icon}.png', width: imageWidth, height: imageWidth),
const SizedBox(height: 4),
Text(
category.name ?? '',
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, color: BaseColor.second500),
),
],
),
);
},
),
);
}
}
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import '../affiliate/model/affiliate_brand_model.dart';
import '../affiliate/model/affiliate_category_model.dart';
class AffiliateCategoryGridViewModel extends RestfulApiViewModel {
final RxBool isLoading = false.obs;
void Function((List<AffiliateBrandModel>, String) data)? onShowAffiliateBrandPopup;
affiliateBrandGetListBuyCategory(AffiliateCategoryModel category) async {
showLoading();
try {
final result = await client.affiliateBrandGetList(categoryCode: AffiliateCategoryModel.codeToJson(category.code));
hideLoading();
final data = result.data ?? [];
if (result.isSuccess && data.isNotEmpty) {
onShowAffiliateBrandPopup?.call((data, category.name ?? ''));
}
} catch (error) {
hideLoading();
print("Error fetching affiliate brands: $error");
}
}
}
\ No newline at end of file
import 'package:mypoint_flutter_app/directional/directional_action_type.dart';
import '../../../configs/constants.dart';
import '../../../directional/directional_screen.dart';
class AffiliateBrandDetailModel {
final String? commision;
final String? direction;
final String? brandId;
final String? brandName;
final String? brandUrl;
final String? timeConfirm;
final String? timeCashback;
final String? logo;
final String? background;
final List<String>? contentNote;
final List<String>? contentShould;
final List<String>? contentShouldnt;
final List<String>? contentOther;
final List<AffiliateBrandCategoryModel>? categories;
final int? quantitySold;
AffiliateBrandDetailModel({
this.commision,
this.direction,
this.brandId,
this.brandName,
this.brandUrl,
this.timeConfirm,
this.timeCashback,
this.logo,
this.background,
this.contentNote,
this.contentShould,
this.contentShouldnt,
this.contentOther,
this.categories,
this.quantitySold,
});
String get showCommision {
return '${double.tryParse(commision ?? '0')?.toStringAsFixed(1) ?? '0.0'}%';
}
String get imgBackgroundAsset {
if (background != null && background!.isNotEmpty) {
return 'assets/images/bg_header_detail_brand_${background!}.png';
}
return 'assets/images/bg_header_detail_brand_DEFAULT.png';
}
DirectionalScreen? get directionalScreen {
if (brandUrl == null || brandUrl!.isEmpty) return null;
if (direction == Constants.directionInApp) {
return DirectionalScreen.buildByName(name: DirectionalScreenName.viewDeepLinkInApp, clickActionParam: brandUrl);
}
return DirectionalScreen.buildByName(name: DirectionalScreenName.viewDeepLink, clickActionParam: brandUrl);
}
factory AffiliateBrandDetailModel.fromJson(Map<String, dynamic> json) {
return AffiliateBrandDetailModel(
commision: json['commision'],
direction: json['direction'],
brandId: json['brand_id'],
brandName: json['brand_name'],
brandUrl: json['brand_url'],
timeConfirm: json['time_confirm'],
timeCashback: json['time_cashback'],
logo: json['logo'],
background: json['background'],
contentNote: (json['content_note'] as List?)?.cast<String>(),
contentShould: (json['content_should'] as List?)?.cast<String>(),
contentShouldnt: (json['content_shouldnt'] as List?)?.cast<String>(),
contentOther: (json['content_other'] as List?)?.cast<String>(),
categories: (json['categories'] as List?)
?.map((e) => AffiliateBrandCategoryModel.fromJson(e))
.toList(),
quantitySold: json['quantity_sold'],
);
}
}
class AffiliateBrandCategoryModel {
final String? commision;
final String? categoryName;
final String? brandUrl;
AffiliateBrandCategoryModel({
this.commision,
this.categoryName,
this.brandUrl,
});
String get showCommision {
return '${double.tryParse(commision ?? '')?.toStringAsFixed(1) ?? '0.0'}%';
}
factory AffiliateBrandCategoryModel.fromJson(Map<String, dynamic> json) {
return AffiliateBrandCategoryModel(
commision: json['commision'] ?? '',
categoryName: json['category_name'] ?? '',
brandUrl: json['brand_url'] ?? '',
);
}
}
import 'package:contacts_service/contacts_service.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/screen/data_network_service/product_network_data_model.dart';
import 'package:mypoint_flutter_app/widgets/custom_empty_widget.dart';
import 'package:mypoint_flutter_app/widgets/custom_navigation_bar.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../preference/data_preference.dart';
import '../../preference/point/point_manager.dart';
import '../../resouce/base_color.dart';
import '../../widgets/alert/custom_alert_dialog.dart';
import '../../widgets/alert/data_alert_model.dart';
import '../topup/brand_select_sheet_widget.dart';
import 'data_network_service_viewmodel.dart';
class DataNetworkServiceScreen extends BaseScreen {
const DataNetworkServiceScreen({super.key});
@override
State<DataNetworkServiceScreen> createState() => _DataNetworkServiceScreenState();
}
class _DataNetworkServiceScreenState extends BaseState<DataNetworkServiceScreen> with BasicState {
final DataNetworkServiceViewModel _viewModel = Get.put(DataNetworkServiceViewModel());
late final TextEditingController _phoneController;
@override
void initState() {
super.initState();
_phoneController = TextEditingController(text: _viewModel.phoneNumber.value);
_viewModel.firstLoadNetworkData();
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
showAlertError(content: message);
}
};
_viewModel.onShowAlertRedeemSuccess = (message) {
showAlertError(content: message);
};
}
@override
Widget createBody() {
return Scaffold(
appBar: CustomNavigationBar(title: "Ưu đãi Data"),
body: Obx(() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderPhone(),
Container(height: 6, color: Colors.grey.shade200),
const Divider(height: 8),
if (_viewModel.topUpNetworkData.isEmpty) Expanded(child: EmptyWidget()),
if (_viewModel.topUpNetworkData.isNotEmpty)
Expanded(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: _viewModel.topUpNetworkData.length,
itemBuilder: (context, index) {
final data = _viewModel.topUpNetworkData.value[index];
return _buildSectionNetworkData(data);
},
),
),
const Divider(height: 1),
SafeArea(child: Padding(padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 8), child: _buildButton())),
],
);
}),
);
}
Widget _buildButton() {
return Obx(() {
final isValidInput =
(_viewModel.phoneNumber.value.trim().length >= 10) && (_viewModel.selectedProduct.value != null);
return ElevatedButton(
onPressed: isValidInput ? _redeemProductMobileCard : null,
style: ElevatedButton.styleFrom(
backgroundColor: isValidInput ? BaseColor.primary500 : Colors.grey,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
minimumSize: const Size.fromHeight(48),
),
child: const Text("Đổi ngay", style: TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.bold)),
);
});
}
_redeemProductMobileCard() {
print("redeem ${UserPointManager().point} >= ${_viewModel.payPoint}");
final isValidInput =
(_viewModel.phoneNumber.value.trim().length >= 10) && (_viewModel.selectedProduct.value != null);
if (!isValidInput) {
showAlertError(content: "Vui lòng chọn gói cước và nhập số điện thoại.");
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 gói cước 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);
}
Widget _buildSectionNetworkData(TopUpNetworkDataModel data) {
final packages = data.products ?? [];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text(
textAlign: TextAlign.start,
data.groupName,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
),
GridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: packages.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 2.8,
crossAxisSpacing: 8,
mainAxisSpacing: 12,
),
itemBuilder: (context, index) {
final item = packages[index];
final isSelected = _viewModel.selectedProduct.value?.id == item.id;
return GestureDetector(
onTap: () {
setState(() {
_viewModel.selectedProduct.value = item;
});
},
child: Container(
decoration: BoxDecoration(
border: Border.all(color: isSelected ? Colors.orange : Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
item.name ?? '',
style: const TextStyle(
color: BaseColor.primary500,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
const SizedBox(width: 2),
Text(
'/ ${item.dataDurationApply}',
style: const TextStyle(fontSize: 14, color: Colors.black87),
),
],
),
const SizedBox(height: 3),
Row(
children: [
Image.asset("assets/images/ic_point.png", width: 18, height: 18, fit: BoxFit.cover),
const SizedBox(width: 2),
Text(
item.payPoint.money(CurrencyUnit.none),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.orange),
),
],
),
],
),
),
);
},
),
],
);
}
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,
),
),
),
),
);
},
),
),
);
});
}
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 'package:mypoint_flutter_app/screen/data_network_service/product_network_data_model.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../configs/constants.dart';
import '../../preference/contact_storage_service.dart';
import '../../preference/point/point_manager.dart';
import '../voucher/models/product_brand_model.dart';
import '../voucher/models/product_model.dart';
import '../voucher/models/product_type.dart';
class DataNetworkServiceViewModel extends RestfulApiViewModel {
var histories = RxList<String>();
final RxList<ProductBrandModel> topUpBrands = <ProductBrandModel>[].obs;
final RxList<TopUpNetworkDataModel> topUpNetworkData = <TopUpNetworkDataModel>[].obs;
final Map<String, List<TopUpNetworkDataModel>> _allValue = {};
var selectedBrand = Rxn<ProductBrandModel>();
var selectedProduct = Rxn<ProductNetworkDataModel>();
var phoneNumber = ''.obs;
void Function(String message)? onShowAlertError;
void Function(String message)? onShowAlertRedeemSuccess;
int get payPoint {
return (int.tryParse(selectedProduct.value?.prices?.firstOrNull?.payPoint ?? "0") ?? 0);
}
bool get isValidBalance {
return UserPointManager().point >= payPoint;
}
@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);
}
}
firstLoadNetworkData() async {
showLoading();
await getNetworkBrands();
print("topUpBrands ${topUpBrands.length}");
await checkMobileNetwork();
hideLoading();
}
getNetworkBrands() {
client.productTopUpBrands().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 id = selectedBrand.value?.id;
final code = selectedBrand.value?.code;
if (id == null && code == null) return;
void makeSelected(List<TopUpNetworkDataModel> data) {
bool didSelect = false;
final list = data
.expand((e) => e.products ?? [])
.toList();
if (selected != null && selected.isNotEmpty) {
for (var item in list) {
final isMatch = item == int.tryParse(selected);
if (isMatch) {
selectedProduct.value = item;
didSelect = true;
}
}
}
if (!didSelect && selectedProduct.value == null) {
selectedProduct.value = list.firstOrNull;
}
}
// Dùng cache nếu có
if (_allValue.containsKey(code)) {
final cached = _allValue[code]!;
topUpNetworkData.value = cached;
makeSelected(cached);
return;
}
showLoading();
try {
final result = await client.getNetworkProducts((id ?? 0).toString());
var data = result.data ?? [];
data = data
.where((e) => e.products?.isNotEmpty == true)
.toList();
_allValue[code ?? ""] = data;
topUpNetworkData.value = data;
makeSelected(data);
hideLoading();
} catch (error) {
print("Error fetching all products: $error");
hideLoading();
}
}
redeemProductMobileCard() async {
final id = selectedProduct.value?.id.toString() ?? "";
showLoading();
try {
final response = await client.redeemProductTopUps(id, phoneNumber.value);
hideLoading();
if (!response.isSuccess) {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
return;
}
final mgs = (response.errorMessage ?? "").isEmpty ? "Chúc mừng bạn đã đổi Ưu đãi data thành công" : (response.errorMessage ?? "");
onShowAlertRedeemSuccess?.call(mgs);
} catch (error) {
hideLoading();
onShowAlertError?.call(error.toString());
return;
}
}
}
\ No newline at end of file
import 'package:json_annotation/json_annotation.dart';
import '../mobile_card/models/product_mobile_card_model.dart';
part 'product_network_data_model.g.dart';
@JsonSerializable()
class ProductNetworkDataModel {
final int? id;
@JsonKey(name: 'product_model_code')
final String? productModelCode;
final String? code;
final String? name;
@JsonKey(name: 'data_duration_apply')
final String? dataDurationApply;
@JsonKey(name: 'description')
final String? productDescription;
@JsonKey(name: 'start_time')
final String? startTime;
@JsonKey(name: 'end_time')
final String? endTime;
// @JsonKey(name: 'limit_quantity_per_transaction')
// final String? limitQuantityPerTransaction;
final List<MobileCardPriceModel>? prices;
ProductNetworkDataModel({
this.id,
this.productModelCode,
this.code,
this.name,
this.dataDurationApply,
this.productDescription,
this.startTime,
this.endTime,
// this.limitQuantityPerTransaction,
this.prices,
});
factory ProductNetworkDataModel.fromJson(Map<String, dynamic> json) =>
_$ProductNetworkDataModelFromJson(json);
Map<String, dynamic> toJson() => _$ProductNetworkDataModelToJson(this);
int get payPoint {
if (prices?.isNotEmpty != true) return 0;
final point = prices!.first.payPoint;
return int.tryParse(point ?? '0') ?? 0;
}
}
@JsonSerializable()
class TopUpNetworkDataModel {
@JsonKey(name: 'group_name')
final String groupName;
final List<ProductNetworkDataModel>? products;
TopUpNetworkDataModel({
required this.groupName,
this.products,
});
factory TopUpNetworkDataModel.fromJson(Map<String, dynamic> json) =>
_$TopUpNetworkDataModelFromJson(json);
Map<String, dynamic> toJson() => _$TopUpNetworkDataModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_network_data_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProductNetworkDataModel _$ProductNetworkDataModelFromJson(
Map<String, dynamic> json,
) => ProductNetworkDataModel(
id: (json['id'] as num?)?.toInt(),
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?,
prices:
(json['prices'] as List<dynamic>?)
?.map((e) => MobileCardPriceModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$ProductNetworkDataModelToJson(
ProductNetworkDataModel 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,
'prices': instance.prices,
};
TopUpNetworkDataModel _$TopUpNetworkDataModelFromJson(
Map<String, dynamic> json,
) => TopUpNetworkDataModel(
groupName: json['group_name'] as String,
products:
(json['products'] as List<dynamic>?)
?.map(
(e) => ProductNetworkDataModel.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
Map<String, dynamic> _$TopUpNetworkDataModelToJson(
TopUpNetworkDataModel instance,
) => <String, dynamic>{
'group_name': instance.groupName,
'products': instance.products,
};
......@@ -22,10 +22,15 @@ class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState {
final GlobalKey _infoKey = GlobalKey();
OverlayEntry? _popupEntry;
bool _isPopupShown = false;
late var _canBackButton = false;
@override
void initState() {
super.initState();
final args = Get.arguments;
if (args is Map) {
_canBackButton = args['can_back_button'] as bool;
}
_viewModel.getGames();
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
......@@ -46,7 +51,7 @@ class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState {
return Scaffold(
appBar: CustomNavigationBar(
title: "Games",
showBackButton: false,
showBackButton: _canBackButton,
backgroundImage: _headerHomeVM.headerData.background ?? "assets/images/bg_header_navi.png",
rightButtons: [
CompositedTransformTarget(
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../../widgets/custom_empty_widget.dart';
import '../../widgets/custom_navigation_bar.dart';
import 'history_point_cashback_viewmodel.dart';
import 'models/history_point_cashback_model.dart';
// TODO check api response
class HistoryPointCashBackScreen extends StatefulWidget {
const HistoryPointCashBackScreen({super.key});
@override
State<HistoryPointCashBackScreen> createState() => _HistoryPointCashBackScreenState();
}
class _HistoryPointCashBackScreenState extends State<HistoryPointCashBackScreen> {
late final HistoryPointCashBackViewModel _viewModel;
@override
void initState() {
super.initState();
_viewModel = Get.put(HistoryPointCashBackViewModel());
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
appBar: CustomNavigationBar(title: "Lịch sử hoàn điểm"),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildTab('Chờ xử lý', 0),
_buildTab('Tạm duyệt', 1),
_buildTab('Đã hoàn', 2),
_buildTab('Đã huỷ', 3),
],
),
const Divider(height: 1),
Obx(
() => Padding(
padding: const EdgeInsets.only(left: 16, top: 16),
child: Row(
children: [
Text(
"Tổng số điểm ${_viewModel.selectedTag.tag.toLowerCase()}:",
style: const TextStyle(color: Colors.black87, fontSize: 17, fontWeight: FontWeight.w600),
),
SizedBox(width: 4),
Image.asset("assets/images/ic_point.png", height: 18),
SizedBox(width: 4),
Text(
(_viewModel.pointCashBackData.value?.points ?? 0).money(CurrencyUnit.noneSpace),
style: const TextStyle(color: Colors.orange, fontSize: 18, fontWeight: FontWeight.w600),
),
],
),
),
),
if (_viewModel.orders.isEmpty)
Expanded(child: EmptyWidget(size: Size(screenWidth / 2, screenWidth / 2)))
else
Obx(
() => Expanded(
child: RefreshIndicator(
onRefresh: () async {
_viewModel.freshData(isRefresh: true);
},
child: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _viewModel.orders.length,
itemBuilder: (_, index) {
if (index >= _viewModel.orders.length) {
_viewModel.freshData(isRefresh: false);
return const Center(
child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator()),
);
}
final order = _viewModel.orders[index];
return _buildVoucherItem(order);
},
),
),
),
),
],
),
);
}
Widget _buildTab(String title, int index) {
return GestureDetector(
onTap: () => _viewModel.selectTab(index),
child: Obx(
() => Padding(
padding: const EdgeInsets.only(top: 8),
child: Column(
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: _viewModel.selectedTabIndex.value == index ? FontWeight.w600 : FontWeight.w500,
color: _viewModel.selectedTabIndex.value == index ? Colors.red : Colors.black54,
),
),
const SizedBox(height: 4),
if (_viewModel.selectedTabIndex.value == index) Container(height: 2, width: 60, color: Colors.red),
],
),
),
),
);
}
Widget _buildVoucherItem(HistoryPointCashBackOrderModel item) {
return Obx(
() => Container(
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 0),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: loadNetworkImage(
url: item.logo,
height: 32,
width: 32,
placeholderAsset: "assets/images/ic_logo.png",
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Mã đơn hàng: ${item.code}", style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 2),
Text("HSD: ${item.timeShow}", style: TextStyle(fontSize: 12, color: Colors.black54)),
],
),
),
Center(
child: Text(
_viewModel.selectedTag.tag,
style: TextStyle(color: _viewModel.selectedTag.color, fontWeight: FontWeight.w600, fontSize: 12),
),
),
],
),
const SizedBox(height: 8),
Divider(height: 1, color: Colors.grey.shade200),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Giá trị đơn hàng', style: TextStyle(color: Colors.black54)),
Text(
(item.price ?? 0).money(CurrencyUnit.VND),
style: const TextStyle(color: Colors.red, fontWeight: FontWeight.w500),
),
],
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Số lượng sản phẩm', style: TextStyle(color: Colors.black54)),
Text('${item.productQuantity}', style: const TextStyle(fontWeight: FontWeight.w500)),
],
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Điểm ${_viewModel.selectedTag.tag.toLowerCase()}', style: const TextStyle(color: Colors.black54)),
Row(
children: [
Image.asset("assets/images/ic_point.png", height: 18),
SizedBox(width: 4),
Text(
item.points.money(CurrencyUnit.noneSpace),
style: const TextStyle(color: Colors.orange, fontWeight: FontWeight.w600),
),
],
),
],
),
],
),
),
);
}
}
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/extensions/collection_extension.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import 'models/history_point_cashback_model.dart';
class HistoryPointCashBackViewModel extends RestfulApiViewModel {
late List<CashBackPointOrderStatus> tagStatus;
CashBackPointOrderStatus get selectedTag {
return tagStatus.safe(selectedTabIndex.value) ?? CashBackPointOrderStatus.pending;
}
final RxInt selectedTabIndex = 0.obs;
var pointCashBackData = Rxn<HistoryPointCashBackResponse>();
List<HistoryPointCashBackOrderModel> get orders {
return pointCashBackData.value?.orders ?? [];
}
int _page = 1;
@override
void onInit() {
super.onInit();
tagStatus = [
CashBackPointOrderStatus.pending,
CashBackPointOrderStatus.approved,
CashBackPointOrderStatus.confirmed,
CashBackPointOrderStatus.reject,
];
freshData(isRefresh: true);
}
void freshData({bool isRefresh = false}) {
if (isRefresh) {
_page = 1;
} else {
_page += 1;
}
final body = {"page": _page, "size": 20, "type": selectedTag.rawValue};
client
.historyPointCashBackRequest(body)
.then((response) {
final result = response.data;
if (isRefresh) {
pointCashBackData.value = result;
} else {
final orders = result?.orders ?? [];
pointCashBackData.value?.orders?.addAll(orders);
pointCashBackData.refresh();
}
})
.catchError((error) {
print('Error fetching products: $error');
});
}
void selectTab(int index) {
selectedTabIndex.value = index;
freshData(isRefresh: true);
}
}
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