Commit c8abf95b authored by DatHV's avatar DatHV
Browse files

update screen logic

parent fda33894
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../preference/point/header_home_model.dart';
import 'models/notification_unread_model.dart';
class HeaderHomeViewModel extends RestfulApiViewModel {
final Rx<HeaderHomeModel?> _headerHomeData = Rx<HeaderHomeModel?>(null);
var notificationUnreadData = Rxn<NotificationUnreadData>();
HeaderHomeModel get headerData {
return _headerHomeData.value ??
HeaderHomeModel(
greeting: 'Xin chào!',
totalVoucher: 0,
totalPointActive: 0,
background:
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
);
}
Future<void> freshData() async {
await _getDynamicHeaderHome();
await _getNotificationUnread();
}
Future<void> _getDynamicHeaderHome() async {
try {
final result = await client.getDynamicHeaderHome();
_headerHomeData.value = result.data;
} catch (error) {
print("Error fetching getDynamicHeaderHome: $error");
}
}
Future<void> _getNotificationUnread() async {
try {
final result = await client.getNotificationUnread();
notificationUnreadData.value = result.data;
} catch (error) {
print("Error fetching hot products: $error");
}
}
}
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:game_miniapp/game_miniapp.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
......@@ -8,15 +6,21 @@ import 'package:mypoint_flutter_app/screen/home/custom_widget/header_home.dart';
import 'package:mypoint_flutter_app/screen/home/custom_widget/product_grid_widget.dart';
import 'package:mypoint_flutter_app/screen/home/pipi_detail_screen.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
import '../../preference/point/header_home_model.dart';
import '../shopping/sub_widget/build_affiliate_brand.dart';
import '../voucher/sub_widget/voucher_section_title.dart';
import 'custom_widget/achievement_carousel_widget.dart';
import 'custom_widget/affiliate_brand_grid_widget.dart';
import 'custom_widget/banner_carousel_widget.dart';
import 'custom_widget/brand_grid_widget.dart';
import 'custom_widget/flash_sale_carousel_widget.dart';
import 'custom_widget/hover_view.dart';
import 'custom_widget/main_service_grid_widget.dart';
import 'custom_widget/my_product_carousel_widget.dart';
import 'custom_widget/news_carousel_widget.dart';
import 'header_home_viewmodel.dart';
import 'home_tab_viewmodel.dart';
import 'models/achievement_model.dart';
import 'models/main_service_model.dart';
import 'models/header_section_type.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
......@@ -27,28 +31,157 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> {
final HomeTabViewModel _viewModel = Get.put(HomeTabViewModel());
final _headerHomeVM = Get.find<HeaderHomeViewModel>();
bool _showHover = true;
@override
void initState() {
super.initState();
_viewModel.getSectionLayoutHome();
}
Widget _buildSliverHeader(double heightHeader) {
return Obx(() {
final data = _viewModel.headerHomeData.value;
if (data == null) return SliverToBoxAdapter(child: SizedBox.shrink());
final notifyUnreadData = _headerHomeVM.notificationUnreadData.value;
return SliverToBoxAdapter(
child: HomeGreetingHeader(
dataHeader: data,
dataHeader: _headerHomeVM.headerData,
heightContent: heightHeader,
notificationUnreadData: notifyUnreadData,
),
);
});
}
List<Widget> _buildSectionContent() {
final List<Widget> sections = [];
for (var section in _viewModel.sectionLayouts.value) {
switch (section.headerSectionType) {
case HeaderSectionType.banner:
if (_viewModel.banners.isNotEmpty) {
sections.add(
BannerCarousel(
banners: _viewModel.banners,
sectionConfig: _viewModel.getMainSectionConfigModel(HeaderSectionType.banner),
onTap: (item) {
item.directionalScreen?.begin();
},
),
);
}
break;
case HeaderSectionType.topButton:
if (_viewModel.services.isNotEmpty) {
sections.add(
MainServiceGrid(
services: _viewModel.services,
sectionConfig: _viewModel.getMainSectionConfigModel(HeaderSectionType.topButton),
onTap: (item) {
item.directionalScreen?.begin();
},
),
);
}
break;
case HeaderSectionType.campaign:
if (_viewModel.achievements.isNotEmpty) {
sections.add(
AchievementCarousel(
items: _viewModel.achievements,
sectionConfig: _viewModel.getMainSectionConfigModel(HeaderSectionType.campaign),
onTap: (item) {
item.directionScreen?.begin();
},
),
);
}
break;
case HeaderSectionType.product:
if (_viewModel.products.isNotEmpty) {
sections.add(
ProductGrid(
products: _viewModel.products,
sectionConfig: _viewModel.getMainSectionConfigModel(HeaderSectionType.product),
onTap: (product) {
Get.toNamed(voucherDetailScreen, arguments: product.id);
},
),
);
}
break;
case HeaderSectionType.news:
if (_viewModel.news.isNotEmpty) {
sections.add(
NewsCarouselWidget(
items: _viewModel.news,
sectionConfig: _viewModel.getMainSectionConfigModel(HeaderSectionType.news),
onTap: (item) async {
Get.toNamed(campaignDetailScreen, arguments: {"id": item.pageId});
},
),
);
}
break;
case HeaderSectionType.myProduct:
if (_viewModel.myProducts.isNotEmpty) {
sections.add(
MyProductCarouselWidget(
items: _viewModel.myProducts,
sectionConfig: _viewModel.getMainSectionConfigModel(HeaderSectionType.myProduct),
onTap: (item) async {
Get.toNamed(voucherDetailScreen, arguments: {"customerProductId": item.id});
},
),
);
}
break;
case HeaderSectionType.flashSale:
final products = _viewModel.flashSaleData?.value?.products ?? [];
if (products.isNotEmpty) {
sections.add(
FlashSaleCarouselWidget(
products: products,
sectionConfig: _viewModel.getMainSectionConfigModel(HeaderSectionType.flashSale),
onTap: (product) {
Get.toNamed(voucherDetailScreen, arguments: product.id);
},
),
);
}
break;
case HeaderSectionType.brand:
if (_viewModel.brands.isNotEmpty) {
sections.add(
BrandGridWidget(
brands: _viewModel.brands,
sectionConfig: _viewModel.getMainSectionConfigModel(HeaderSectionType.brand),
onTap: (item) {
// Get.toNamed(affiliateDetailScreen, arguments: item.brandId);
},
),
);
}
break;
case HeaderSectionType.pointPartner:
if (_viewModel.affiliates.isNotEmpty) {
sections.add(
AffiliateBrandGridWidget(
affiliateBrands: _viewModel.affiliates,
sectionConfig: _viewModel.getMainSectionConfigModel(HeaderSectionType.pointPartner),
onTap: (item) {
// Get.toNamed(affiliateDetailScreen, arguments: item.brandId);
},
),
);
}
break;
default:
break;
}
}
return sections;
}
@override
Widget build(BuildContext context) {
final paddingBottom = MediaQuery.of(context).padding.bottom + 20;
......@@ -59,83 +192,14 @@ class _HomeScreenState extends State<HomeScreen> {
children: [
NestedScrollView(
physics: AlwaysScrollableScrollPhysics(),
headerSliverBuilder:
(_, _) => [
_buildSliverHeader(heightHeader),
// SliverToBoxAdapter(
// child: Obx(() {
// if (_viewModel.headerHomeData.value == null) return SizedBox.shrink();
// return HomeGreetingHeader(
// dataHeader: _viewModel.headerHomeData.value!,
// heightContent: heightHeader);
// }),
// ),
],
headerSliverBuilder: (_, _) => [_buildSliverHeader(heightHeader)],
body: RefreshIndicator(
onRefresh: _onRefresh,
child: Obx(() {
return ListView(
padding: EdgeInsets.only(bottom: paddingBottom),
physics: AlwaysScrollableScrollPhysics(),
children: [
BannerCarousel(
imageUrls: [
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/1B67CFBF96BD24929EB10F1853A47651/1740708747',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/29B40B1E04EBEC8A1C1F9D13C0194A27/1735194572',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/C29872C4F95B280B880DE45BC07E7DE4/1693906872',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
],
),
if (_viewModel.services.value.isNotEmpty)
MainServiceGrid(
services: _viewModel.services.value,
onTap: (item) {
item.directionalScreen?.begin();
},
),
if (_viewModel.achievements.value.isNotEmpty)
HeaderSectionTitle(
title: 'Sự kiện MyPoint',
onViewAll: () {
Get.toNamed(achievementListScreen);
},
),
if (_viewModel.achievements.value.isNotEmpty)
AchievementCarousel(
items: _viewModel.achievements.value,
onTap: (item) {
item.directionScreen?.begin();
},
),
if (_viewModel.products.value.isNotEmpty)
ProductGrid(
products: _viewModel.products.value,
onTap: (product) {
Get.toNamed(voucherDetailScreen, arguments: product.id);
},
),
if (_viewModel.news.value.isNotEmpty)
HeaderSectionTitle(
title: 'MyPoint có gì hot?',
onViewAll: () {
Get.toNamed(newsListScreen);
},
),
if (_viewModel.news.value.isNotEmpty)
NewsCarouselWidget(
items: _viewModel.news.value,
onTap: (item) async {
Get.toNamed(campaignDetailScreen, arguments: {"id": item.pageId});
},
),
// ElevatedButton(onPressed: () => _showMiniGame(context), child: const Text('Mini Game')),
// ElevatedButton(onPressed: () => _logout(context), child: const Text('Đăng xuất')),
// ElevatedButton(onPressed: () => _showSetting(context), child: const Text('Setting')),
// ElevatedButton(onPressed: () => _showNotify(context), child: const Text('Notify')),
],
children: _buildSectionContent(),
);
}),
),
......@@ -176,6 +240,8 @@ class _HomeScreenState extends State<HomeScreen> {
Future<void> _onRefresh() async {
print("onRefresh");
await _viewModel.getSectionLayoutHome();
await _viewModel.loadDataPiPiHome();
await _headerHomeVM.freshData();
}
void _showMiniGame(BuildContext context) async {
......@@ -218,8 +284,4 @@ class _HomeScreenState extends State<HomeScreen> {
Get.offAllNamed(onboardingScreen);
}
}
void _showSetting(BuildContext context) async {
Get.toNamed(settingScreen);
}
}
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../preference/point/header_home_model.dart';
import '../faqs/faqs_model.dart';
import '../shopping/model/affiliate_brand_model.dart';
import '../voucher/models/product_model.dart';
import '../voucher/models/product_type.dart';
import 'models/achievement_model.dart';
import 'models/banner_model.dart';
import 'models/brand_model.dart';
import 'models/flash_sale_model.dart';
import 'models/header_section_type.dart';
import 'models/hover_data_model.dart';
import 'models/main_section_config_model.dart';
import 'models/main_service_model.dart';
import 'models/notification_unread_model.dart';
import 'models/my_product_model.dart';
class HomeTabViewModel extends RestfulApiViewModel {
final RxList<ProductModel> products = <ProductModel>[].obs;
final RxList<PageItemModel> news = <PageItemModel>[].obs;
final RxList<MainServiceModel> services = <MainServiceModel>[].obs;
final RxList<AchievementModel> achievements = <AchievementModel>[].obs;
final RxList<BannerModel> banners = <BannerModel>[].obs;
final RxList<BrandModel> brands = <BrandModel>[].obs;
final RxList<AffiliateBrandModel> affiliates = <AffiliateBrandModel>[].obs;
final RxList<MyProductModel> myProducts = <MyProductModel>[].obs;
final RxList<MainSectionConfigModel> sectionLayouts = <MainSectionConfigModel>[].obs;
var flashSaleData = Rxn<FlashSaleModel>();
var hoverData = Rxn<HoverDataModel>();
var notificationUnreadData = Rxn<NotificationUnreadData>();
var headerHomeData = Rxn<HeaderHomeModel>();
List<MainSectionConfigModel> sectionLayouts = [];
@override
void onInit() {
super.onInit();
getDynamicHeaderHome();
// getSectionLayoutHome();
// getHotProducts();
// fetchFAQItems();
// loadMainServicesFromAsset();
// loadMainAchievementsFromAsset();
// loadDataPiPiHome();
// getNotificationUnread();
getSectionLayoutHome();
loadDataPiPiHome();
}
MainSectionConfigModel? getMainSectionConfigModel(HeaderSectionType type) {
return sectionLayouts.firstWhereOrNull((section) => section.headerSectionType == type);
}
Future<void> getSectionLayoutHome() async {
showLoading();
try {
final response = await client.getSectionLayoutHome();
if (response.data != null) {
sectionLayouts = response.data ?? [];
}
sectionLayouts.value = response.data ?? [];
} catch (error) {
print("Error fetching section layout: $error");
sectionLayouts.value = await _loadSectionLayoutHomeFromCache();
} finally {
hideLoading();
}
}
Future<void> getHotProducts() async {
final body = {
"type": ProductType.voucher.value,
"size": 10,
"index": 0,
"catalog_code": "HOT",
};
try {
final result = await client.getProducts(body);
products.value = result.data ?? [];
} catch (error) {
print("Error fetching hot products: $error");
if (sectionLayouts.value.isEmpty) {
sectionLayouts.value = await _loadSectionLayoutHomeFromCache();
}
for (final section in sectionLayouts.value) {
await _processSection(section);
}
}
}
......@@ -75,43 +67,81 @@ class HomeTabViewModel extends RestfulApiViewModel {
}
}
Future<void> getDynamicHeaderHome() async {
try {
final result = await client.getDynamicHeaderHome();
headerHomeData.value = result.data;
} catch (error) {
print("Error fetching getDynamicHeaderHome: $error");
}
Future<List<MainSectionConfigModel>> _loadSectionLayoutHomeFromCache() async {
final jsonStr = await rootBundle.loadString('assets/data/main_layout_section_home.json');
final List<dynamic> jsonList = json.decode(jsonStr);
return jsonList.map((e) => MainSectionConfigModel.fromJson(e)).toList() ?? [];
}
Future<void> getNotificationUnread() async {
try {
final result = await client.getNotificationUnread();
notificationUnreadData.value = result.data;
} catch (error) {
print("Error fetching hot products: $error");
Future<void> _processSection(MainSectionConfigModel section) async {
final path = section.apiList ?? "";
switch (section.headerSectionType) {
case HeaderSectionType.topButton:
final res = await client.fetchList<MainServiceModel>(
path,
(json) => MainServiceModel.fromJson(json as Map<String, dynamic>),
);
services.value = res.data ?? [];
break;
case HeaderSectionType.banner:
final res = await client.fetchList<BannerModel>(
path,
(json) => BannerModel.fromJson(json as Map<String, dynamic>),
);
banners.value = res.data ?? [];
break;
case HeaderSectionType.campaign:
final res = await client.fetchList<AchievementModel>(
path,
(json) => AchievementModel.fromJson(json as Map<String, dynamic>),
);
achievements.value = res.data ?? [];
break;
case HeaderSectionType.product:
final res = await client.fetchList<ProductModel>(
path,
(json) => ProductModel.fromJson(json as Map<String, dynamic>),
);
products.value = res.data ?? [];
break;
case HeaderSectionType.news:
final res = await client.fetchList<PageItemModel>(
path,
(json) => PageItemModel.fromJson(json as Map<String, dynamic>),
);
news.value = res.data ?? [];
break;
case HeaderSectionType.flashSale:
final res = await client.fetchObject<FlashSaleModel>(
path,
(json) => FlashSaleModel.fromJson(json as Map<String, dynamic>),
);
flashSaleData.value = res.data;
break;
case HeaderSectionType.brand:
final res = await client.fetchList<BrandModel>(
path,
(json) => BrandModel.fromJson(json as Map<String, dynamic>),
);
brands.value = res.data ?? [];
break;
case HeaderSectionType.pointPartner:
final res = await client.fetchList<AffiliateBrandModel>(
path,
(json) => AffiliateBrandModel.fromJson(json as Map<String, dynamic>),
);
affiliates.value = (res.data ?? []).take(6).toList();
break;
case HeaderSectionType.myProduct:
final res = await client.fetchList<MyProductModel>(
path,
(json) => MyProductModel.fromJson(json as Map<String, dynamic>),
);
myProducts.value = res.data ?? [];
break;
default:
print("Unknown section type: ${section.headerSectionType}");
break;
}
}
Future<void> fetchFAQItems() async {
showLoading();
client.websiteFolderGetPageList({"folder_uri": "TIN-TUC", "limit": 20}).then((value) {
hideLoading();
news.value = value.data?.items ?? [];
});
}
Future<void> loadMainServicesFromAsset() async {
final jsonStr = await rootBundle.loadString('assets/data/main_services.json');
final json = jsonDecode(jsonStr);
final List list = json['data'];
services.value = list.map((e) => MainServiceModel.fromJson(e)).toList();
}
Future<void> loadMainAchievementsFromAsset() async {
final jsonStr = await rootBundle.loadString('assets/data/main_achievements.json');
final json = jsonDecode(jsonStr);
final List list = json['data'];
achievements.value = list.map((e) => AchievementModel.fromJson(e)).toList();
}
}
\ No newline at end of file
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/directional/directional_screen.dart';
part 'banner_model.g.dart';
@JsonSerializable()
class BannerModel {
@JsonKey(name: 'banner_id')
final String? bannerID;
@JsonKey(name: 'item_title')
final String? itemTitle;
@JsonKey(name: 'item_image')
final String? itemImage;
@JsonKey(name: 'click_action')
final String? clickAction;
@JsonKey(name: 'click_action_params')
final String? clickActionParams;
@JsonKey(name: 'slider_speed')
final int? sliderSpeed;
const BannerModel({
this.bannerID,
this.itemTitle,
this.itemImage,
this.clickAction,
this.clickActionParams,
this.sliderSpeed,
});
DirectionalScreen? get directionalScreen {
return DirectionalScreen.build(
clickActionType: clickAction,
clickActionParam: clickActionParams,
);
}
factory BannerModel.fromJson(Map<String, dynamic> json) => _$BannerModelFromJson(json);
Map<String, dynamic> toJson() => _$BannerModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'banner_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
BannerModel _$BannerModelFromJson(Map<String, dynamic> json) => BannerModel(
bannerID: json['banner_id'] as String?,
itemTitle: json['item_title'] as String?,
itemImage: json['item_image'] as String?,
clickAction: json['click_action'] as String?,
clickActionParams: json['click_action_params'] as String?,
sliderSpeed: (json['slider_speed'] as num?)?.toInt(),
);
Map<String, dynamic> _$BannerModelToJson(BannerModel instance) =>
<String, dynamic>{
'banner_id': instance.bannerID,
'item_title': instance.itemTitle,
'item_image': instance.itemImage,
'click_action': instance.clickAction,
'click_action_params': instance.clickActionParams,
'slider_speed': instance.sliderSpeed,
};
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/directional/directional_screen.dart';
import '../../flash_sale/preview_flash_sale_model.dart';
import 'header_section_type.dart';
......@@ -45,11 +45,14 @@ class MainHeaderConfigRightButton {
final String? clickActionType;
final String? clickActionParam;
MainHeaderConfigRightButton({
this.text,
this.clickActionType,
this.clickActionParam,
});
MainHeaderConfigRightButton({this.text, this.clickActionType, this.clickActionParam});
DirectionalScreen? get directionalScreen {
return DirectionalScreen.build(
clickActionType: clickActionType,
clickActionParam: clickActionParam,
);
}
factory MainHeaderConfigRightButton.fromJson(Map<String, dynamic> json) {
return MainHeaderConfigRightButton(
......@@ -60,10 +63,6 @@ class MainHeaderConfigRightButton {
}
Map<String, dynamic> toJson() {
return {
'text': text,
'click_action_type': clickActionType,
'click_action_param': clickActionParam,
};
return {'text': text, 'click_action_type': clickActionType, 'click_action_param': clickActionParam};
}
}
\ No newline at end of file
import 'package:mypoint_flutter_app/extensions/datetime_extensions.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import '../../voucher/models/my_product_status_type.dart';
class MyProductModel {
......@@ -55,6 +58,11 @@ class MyProductModel {
return DateTime.tryParse(expireTime!);
}
String get expire {
final ex = expireTime ?? "";
return ex.toDate()?.toFormattedString() ?? "";
}
String get deadline {
if (expireDate == null) return '';
final formatted = _formatDate(expireDate!);
......
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../widgets/custom_app_bar.dart';
import 'location_address_viewmodel.dart';
enum LocationAddressType {
province,
district;
static LocationAddressType? fromString(String? value) {
if (value == null) return null;
return values.cast<LocationAddressType?>().firstWhere(
(e) => e?.key == value,
orElse: () => null,
);
}
String get key {
switch (this) {
case LocationAddressType.province:
return 'province';
case LocationAddressType.district:
return 'district';
}
}
}
class LocationAddressScreen extends StatefulWidget {
const LocationAddressScreen({super.key});
@override
State<LocationAddressScreen> createState() => _LocationAddressScreenState();
}
class _LocationAddressScreenState extends State<LocationAddressScreen> {
late final LocationAddressViewModel viewModel;
final ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
final args = Get.arguments ?? {};
var typeRaw = args['type'] as String?;
var selectedCode = args['selectedCode'] as String? ?? '';
var type = LocationAddressType.fromString(typeRaw) ?? LocationAddressType.province;
viewModel = Get.put(LocationAddressViewModel(
type: type,
provinceCode: args['provinceCode'] ?? '',
));
viewModel.selectedCode.value = selectedCode;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar.back(title: "Location Address"),
// backgroundColor: Colors.transparent,
body: SafeArea(
child: Column(
children: [
Container(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
child: TextField(
decoration: const InputDecoration(
hintText: "Tìm kiếm",
prefixIcon: Icon(Icons.search_outlined),
border: InputBorder.none,
),
onChanged: (value) {
viewModel.search(value);
},
),
),
const Divider(height: 1),
Expanded(
child: Obx(() {
final items = viewModel.displayItems;
// Scroll đến vị trí selected sau khi hiển thị list
WidgetsBinding.instance.addPostFrameCallback((_) {
final index = items.indexWhere((e) => e.code == viewModel.selectedCode.value);
if (index != -1 && scrollController.hasClients) {
scrollController.animateTo(
index * 48.0, // chiều cao mỗi item
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
});
return ListView.separated(
controller: scrollController,
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final item = items[index];
final isSelected = viewModel.selectedCode.value == item.code;
return ListTile(
title: Text(item.name ?? '', style: TextStyle(color: isSelected ? Colors.blue : Colors.black87)),
trailing: isSelected ? const Icon(Icons.check, color: Colors.blue) : null,
onTap: () => viewModel.select(item),
);
},
);
}),
),
],
),
),
);
}
}
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
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 'location_address_screen.dart';
import 'models/district_address_model.dart';
import 'models/province_address_model.dart';
class AddressBaseModel {
final String? code;
final String? name;
AddressBaseModel({this.code, this.name});
}
class LocationAddressViewModel extends RestfulApiViewModel {
List<AddressBaseModel> _allItems = [];
final RxList<AddressBaseModel> displayItems = <AddressBaseModel>[].obs;
final RxString selectedCode = ''.obs;
LocationAddressType type = LocationAddressType.province;
String provinceCode = '';
LocationAddressViewModel({this.type = LocationAddressType.province, this.provinceCode = ''});
@override
void onInit() {
super.onInit();
if (type == LocationAddressType.province) {
client.locationProvinceGetList().then((value) {
final data = value.data?.items ?? [];
_loadFromProvince(data);
});
} else {
client.locationDistrictGetList(provinceCode).then((value) {
final data = value.data?.items ?? [];
_loadFromDistrict(data);
});
}
}
void _loadFromProvince(List<ProvinceAddressModel> provinces) {
_allItems = provinces.map((e) => AddressBaseModel(code: e.cityCode, name: e.cityName)).toList();
displayItems.value = _allItems;
}
void _loadFromDistrict(List<DistrictAddressModel> districts) {
_allItems = districts.map((e) => AddressBaseModel(code: e.districtCode, name: e.districtName)).toList();
displayItems.value = _allItems;
}
void search(String query) {
if (query.isEmpty) {
displayItems.value = _allItems;
return;
}
final lowerQuery = _removeDiacritics(query).toLowerCase();
final filteredItems = _allItems.where((item) {
final name = item.name ?? '';
final normalized = _removeDiacritics(name).toLowerCase();
return normalized.contains(lowerQuery);
}).toList();
displayItems.value = filteredItems;
}
void select(AddressBaseModel item) {
print(" Selected code: ${item.code}");
selectedCode.value = item.code ?? '';
displayItems.refresh();
Get.back(result: item);
}
String _removeDiacritics(String str) {
const withDiacritics = 'àáảãạâầấẩẫậăằắẳẵặèéẻẽẹêềếểễệìíỉĩịòóỏõọôồốổỗộơờớởỡợùúủũụưừứửữựỳýỷỹỵđ'
'ÀÁẢÃẠÂẦẤẨẪẬĂẰẮẲẴẶÈÉẺẼẸÊỀẾỂỄỆÌÍỈĨỊÒÓỎÕỌÔỒỐỔỖỘƠỜỚỞỠỢÙÚỦŨỤƯỪỨỬỮỰỲÝỶỸỴĐ';
const withoutDiacritics = 'aaaaaaaaaaaaaaaaaeeeeeeeeeeeiiiiiooooooooooooooooouuuuuuuuuuuyyyyyd'
'AAAAAAAAAAAAAAAAAEEEEEEEEEEEIIIIIOOOOOOOOOOOOOOOOOUUUUUUUUUUUYYYYYD';
for (int i = 0; i < withDiacritics.length; i++) {
str = str.replaceAll(withDiacritics[i], withoutDiacritics[i]);
}
return str;
}
}
class DistrictAddressResponse {
final List<DistrictAddressModel>? items;
DistrictAddressResponse({this.items});
factory DistrictAddressResponse.fromJson(Map<String, dynamic> json) {
return DistrictAddressResponse(
items: (json['list_items'] as List<dynamic>?)?.map((item) => DistrictAddressModel.fromJson(item)).toList(),
);
}
Map<String, dynamic> toJson() {
return {'list_items': items?.map((item) => item.toJson()).toList()};
}
}
class DistrictAddressModel {
final String? countryCode2;
final String? cityCode;
final String? districtCode;
final String? districtType;
final String? districtName;
final String? districtLatitude;
final String? districtLongitude;
DistrictAddressModel({
this.countryCode2,
this.cityCode,
this.districtCode,
this.districtType,
this.districtName,
this.districtLatitude,
this.districtLongitude,
});
factory DistrictAddressModel.fromJson(Map<String, dynamic> json) {
return DistrictAddressModel(
countryCode2: json['country_code2'] as String?,
cityCode: json['city_code'] as String?,
districtCode: json['district_code'] as String?,
districtType: json['district_type'] as String?,
districtName: json['district_name'] as String?,
districtLatitude: json['district_latitude'] as String?,
districtLongitude: json['district_longitude'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'country_code2': countryCode2,
'city_code': cityCode,
'district_code': districtCode,
'district_type': districtType,
'district_name': districtName,
'district_latitude': districtLatitude,
'district_longitude': districtLongitude,
};
}
}
import 'package:json_annotation/json_annotation.dart';
part 'province_address_model.g.dart';
@JsonSerializable()
class ProvinceAddressResponse {
@JsonKey(name: 'list_items')
final List<ProvinceAddressModel>? items;
ProvinceAddressResponse({this.items});
factory ProvinceAddressResponse.fromJson(Map<String, dynamic> json) =>
_$ProvinceAddressResponseFromJson(json);
Map<String, dynamic> toJson() => _$ProvinceAddressResponseToJson(this);
}
class ProvinceAddressModel {
final String? countryCode2;
final String? cityCode;
final String? cityType;
final String? cityName;
final String? cityLatitude;
final String? cityLongitude;
ProvinceAddressModel({
this.countryCode2,
this.cityCode,
this.cityType,
this.cityName,
this.cityLatitude,
this.cityLongitude,
});
factory ProvinceAddressModel.fromJson(Map<String, dynamic> json) {
return ProvinceAddressModel(
countryCode2: json['country_code2'] as String?,
cityCode: json['city_code'] as String?,
cityType: json['city_type'] as String?,
cityName: json['city_name'] as String?,
cityLatitude: json['city_latitude'] as String?,
cityLongitude: json['city_longitude'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'country_code2': countryCode2,
'city_code': cityCode,
'city_type': cityType,
'city_name': cityName,
'city_latitude': cityLatitude,
'city_longitude': cityLongitude,
};
}
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'province_address_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProvinceAddressResponse _$ProvinceAddressResponseFromJson(
Map<String, dynamic> json,
) => ProvinceAddressResponse(
items:
(json['list_items'] as List<dynamic>?)
?.map((e) => ProvinceAddressModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$ProvinceAddressResponseToJson(
ProvinceAddressResponse instance,
) => <String, dynamic>{'list_items': instance.items};
......@@ -82,7 +82,12 @@ class LoginViewModel extends RestfulApiViewModel {
}
void onChangePhonePressed() {
Get.back();
if (Get.key.currentState?.canPop() == true) {
Get.back();
} else {
DataPreference.instance.clearData();
Get.offAllNamed(onboardingScreen);
}
}
void onForgotPassPressed(String phone) {
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../resouce/base_color.dart';
import '../game/game_tab_screen.dart';
import '../home/header_home_viewmodel.dart';
import '../home/home_screen.dart';
import '../personal/personal_screen.dart';
import '../shopping/affiliate_tab_screen.dart';
......@@ -24,6 +26,13 @@ class _MainTabScreenState extends State<MainTabScreen> {
PersonalScreen(),
];
@override
void initState() {
super.initState();
final viewModel = Get.put(HeaderHomeViewModel());
viewModel.freshData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
......
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import '../../preference/data_preference.dart';
import '../../resouce/base_color.dart';
import '../../widgets/image_loader.dart';
import '../../widgets/measure_size.dart';
import 'models/membership_level_model.dart';
class MemberLevelHeaderWidget extends StatelessWidget {
final MembershipLevelModel? level;
const MemberLevelHeaderWidget({super.key, this.level});
@override
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.of(context).size.width;
final double imageHeight = screenWidth / (1125 / 702);
return Stack(
clipBehavior: Clip.none,
children: [
loadNetworkImage(
url: level?.images?.firstOrNull?.imageUrl ?? "",
fit: BoxFit.cover,
height: imageHeight,
width: double.infinity,
placeholderAsset: 'assets/images/bg_header_membership.png',
),
Positioned(left: 16, right: 16, bottom: -36, child: _buildCardHeader()),
],
);
}
Widget _buildCardHeader() {
final name = DataPreference.instance.fullName;
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2)),
child: ClipOval(child: Image.asset("assets/images/bg_default_11.png")),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name.toUpperCase(),
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)),
child: Text(
(level?.levelName ?? "").capitalizeWords(),
style: TextStyle(color: BaseColor.primary800, fontSize: 14, fontWeight: FontWeight.bold),
),
),
],
),
],
),
const SizedBox(height: 16),
// Progress bar
_buildCardInfo(),
],
);
}
Widget _buildCardInfo() {
final int point = double.tryParse(level?.accumulatedCounter?.couterPointValue ?? "0")?.toInt() ?? 0;
final int pointMax = double.tryParse(level?.upgradePointThreshold ?? "0")?.toInt() ?? 1;
final int spending = double.tryParse(level?.accumulatedCounter?.couterGmvValue ?? "0")?.toInt() ?? 0;
final int spendingMax = double.tryParse(level?.upgradeGmvThreshold ?? "0")?.toInt() ?? 1;
return Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
print("GestureDetector");
},
behavior: HitTestBehavior.opaque,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("Điểm xét hạng", style: TextStyle(fontSize: 13, color: Colors.black54)),
const SizedBox(height: 4),
Text(
"${formatNumber(point)}/${formatNumber(pointMax)}",
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
const SizedBox(height: 4),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: pointMax == 0 ? 0 : point / pointMax,
backgroundColor: Colors.grey.shade200,
color: Colors.orangeAccent,
minHeight: 6,
),
),
],
),
),
),
const SizedBox(width: 16),
Container(width: 1, height: 48, color: Colors.grey.shade300),
const SizedBox(width: 16),
// Chi tiêu
Expanded(
child: GestureDetector(
onTap: () {
print("GestureDetector");
},
behavior: HitTestBehavior.opaque,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("Chi tiêu", style: TextStyle(fontSize: 13, color: Colors.black54)),
const SizedBox(height: 4),
Text(
"${formatNumber(spending)}/${formatNumber(spendingMax)}",
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
const SizedBox(height: 4),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: spendingMax == 0 ? 0 : spending / spendingMax,
backgroundColor: Colors.grey.shade200,
color: Colors.orangeAccent,
minHeight: 6,
),
),
],
),
),
),
],
),
);
}
String formatNumber(int value) {
return NumberFormat.decimalPattern('vi_VN').format(value);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../shared/router_gage.dart';
import '../../widgets/back_button.dart';
import 'member_level_header_widget.dart';
import 'membership_viewmodel.dart';
import 'models/membership_level_term_and_condition_model.dart';
class MembershipScreen extends BaseScreen {
const MembershipScreen({super.key});
@override
_MembershipScreenState createState() => _MembershipScreenState();
}
class _MembershipScreenState extends BaseState<MembershipScreen> with BasicState {
late final MembershipViewModel _viewModel;
@override
void initState() {
super.initState();
_viewModel = Get.put(MembershipViewModel());
}
@override
Widget createBody() {
return Scaffold(
backgroundColor: Colors.grey.shade100,
body: Obx(() {
return Stack(
children: [
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MemberLevelHeaderWidget(
level: _viewModel.selectedLevel,
),
const SizedBox(height: 40),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
"Hạng thành viên sẽ được cập nhật sau ${_viewModel.selectedLevel?.levelEndAtDate}",
style: TextStyle(color: Colors.black54, fontSize: 13),
),
),
_buildTagLevels(),
const SizedBox(height: 16),
if ((_viewModel.conditions ?? []).isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(children: _viewModel.conditions!.map((e) => _buildLevelItem(e)).toList()),
),
],
),
),
_buildTopBar(),
],
);
}),
);
}
Widget _buildLevelItem(MembershipLevelTermAndConditionModel item) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_buildIconCondition(item.icon ?? 'assets/images/bg_default_11.png'),
const SizedBox(width: 8),
Text(item.title ?? '', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
],
),
const SizedBox(height: 8),
HtmlWidget(item.content ?? ""),
],
),
);
}
Widget _buildIconCondition(String icon) {
final bool isHttpIcon = (icon.startsWith('http://') || icon.startsWith('https://'));
if (isHttpIcon) {
return Container(
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
child: loadNetworkImage(
url: icon,
width: 24,
height: 24,
fit: BoxFit.cover,
placeholderAsset: 'assets/images/bg_default_11.png',
),
);
} else {
return Container(
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
child: Image.asset('assets/images/$icon.png', width: 24, height: 24, fit: BoxFit.cover),
);
}
}
Widget _buildTagLevels() {
final levels = _viewModel.membershipInfo?.value?.levels;
if (levels == null || levels.isEmpty) {
return const SizedBox.shrink();
}
return SizedBox(
height: 40,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: levels.length,
itemBuilder: (context, index) {
final level = levels[index];
final isSelected = _viewModel.selectedTab.value == index;
return GestureDetector(
onTap: () {
setState(() {
_viewModel.selectedTab.value = index;
});
},
child: Container(
margin: const EdgeInsets.only(right: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
level.description ?? '',
style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? Colors.red : Colors.black54,
fontSize: 16,
),
),
if (isSelected)
Container(margin: const EdgeInsets.only(top: 4), height: 3, width: 30, color: Colors.red),
],
),
),
);
},
),
);
}
Widget _buildTopBar() {
final top = MediaQuery.of(context).padding.top + 8;
return Positioned(
top: top,
left: 0,
right: 0,
child: SizedBox(
height: 40,
child: Stack(
alignment: Alignment.center,
children: [
Positioned(left: 8, child: CustomBackButton()),
Center(
child: Text(
"Hạng thành viên",
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white),
),
),
Positioned(right: 8, child: _buildInfoButton()),
],
),
),
);
}
Widget _buildInfoButton() {
return Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(top: 8, right: 8),
child: GestureDetector(
onTap: () {
final pageId = _viewModel.membershipInfo.value?.membershipRule ?? "";
if (pageId.isNotEmpty) {
Get.toNamed(campaignDetailScreen, arguments: {"id": pageId});
}
},
child: SizedBox(width: 40, height: 40, child: Icon(Icons.info_outline, color: Colors.white, size: 24)),
),
),
);
}
}
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:get/get.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/membership_info_response.dart';
import 'models/membership_level_model.dart';
import 'models/membership_level_term_and_condition_model.dart';
class MembershipViewModel extends RestfulApiViewModel {
var isLoading = false.obs;
var membershipInfo = Rxn<MembershipInfoResponse>();
var selectedTab = 0.obs;
MembershipLevelModel? selectedLevel;
List<MembershipLevelModel>? get levels {
return membershipInfo.value?.levels;
}
List<MembershipLevelTermAndConditionModel>? get conditions {
if (levels == null || levels!.isEmpty) {
return null;
}
return levels?.safe(selectedTab.value)?.conditions;
}
@override
onInit() {
super.onInit();
// getMembershipLevelInfo();
loadMembershipInfoFromAssets();
}
_makeSelectedLevel() {
if (levels == null || levels!.isEmpty) {
selectedLevel = null;
return;
}
selectedLevel = levels!.firstWhere(
(e) => e.levelStartAtDate?.isNotEmpty == true,
orElse: () => levels!.first,
);
}
loadMembershipInfoFromAssets() async {
final jsonStr = await rootBundle.loadString('assets/data/membership_info.json');
final jsonMap = jsonDecode(jsonStr);
final result = MembershipInfoResponse.fromJson(jsonMap['data']);
membershipInfo.value = result;
_makeSelectedLevel();
}
getMembershipLevelInfo() async {
showLoading();
try {
final response = await client.getMembershipLevelInfo();
membershipInfo.value = response.data;
hideLoading();
} catch (e) {
hideLoading();
print("Error fetching membership level info: $e");
}
}
}
import 'package:json_annotation/json_annotation.dart';
part 'accumulated_counter_model.g.dart';
@JsonSerializable()
class AccumulatedCounter {
@JsonKey(name: 'what_to_count_code')
final String? whatToCountCode;
@JsonKey(name: 'what_to_count_name')
final String? whatToCountName;
@JsonKey(name: 'counter_value')
final String? counterValue;
@JsonKey(name: 'counter_point_value')
final String? couterPointValue;
@JsonKey(name: 'counter_gmv_value')
final String? couterGmvValue;
AccumulatedCounter({
this.whatToCountCode,
this.whatToCountName,
this.counterValue,
this.couterPointValue,
this.couterGmvValue,
});
String get counterValueDisplay {
final amount = int.tryParse(counterValue ?? '') ?? 0;
return amount.toString().replaceAllMapped(RegExp(r'\B(?=(\d{3})+(?!\d))'), (match) => ',');
}
factory AccumulatedCounter.fromJson(Map<String, dynamic> json) =>
_$AccumulatedCounterFromJson(json);
Map<String, dynamic> toJson() => _$AccumulatedCounterToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'accumulated_counter_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AccumulatedCounter _$AccumulatedCounterFromJson(Map<String, dynamic> json) =>
AccumulatedCounter(
whatToCountCode: json['what_to_count_code'] as String?,
whatToCountName: json['what_to_count_name'] as String?,
counterValue: json['counter_value'] as String?,
couterPointValue: json['counter_point_value'] as String?,
couterGmvValue: json['counter_gmv_value'] as String?,
);
Map<String, dynamic> _$AccumulatedCounterToJson(AccumulatedCounter instance) =>
<String, dynamic>{
'what_to_count_code': instance.whatToCountCode,
'what_to_count_name': instance.whatToCountName,
'counter_value': instance.counterValue,
'counter_point_value': instance.couterPointValue,
'counter_gmv_value': instance.couterGmvValue,
};
import 'package:json_annotation/json_annotation.dart';
import 'membership_level_model.dart';
part 'membership_info_response.g.dart';
@JsonSerializable()
class MembershipInfoResponse {
final List<MembershipLevelModel>? levels;
@JsonKey(name: 'membership_rule')
final String? membershipRule;
MembershipInfoResponse({
this.levels,
this.membershipRule,
});
factory MembershipInfoResponse.fromJson(Map<String, dynamic> json) =>
_$MembershipInfoResponseFromJson(json);
Map<String, dynamic> toJson() => _$MembershipInfoResponseToJson(this);
}
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