Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Hoàng Văn Đạt
mypoint_flutter_app
Commits
c8abf95b
Commit
c8abf95b
authored
Jun 20, 2025
by
DatHV
Browse files
update screen logic
parent
fda33894
Changes
89
Hide whitespace changes
Inline
Side-by-side
lib/screen/home/header_home_viewmodel.dart
0 → 100644
View file @
c8abf95b
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
"
);
}
}
}
lib/screen/home/home_screen.dart
View file @
c8abf95b
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:
d
ata
,
dataHeader:
_headerHomeVM
.
headerD
ata
,
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
);
}
}
lib/screen/home/home_tab_viewmodel.dart
View file @
c8abf95b
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 s
ection
l
ayout
:
$error
"
);
sectionLayouts
.
value
=
await
_loadS
ection
L
ayout
HomeFromCache
(
);
}
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
lib/screen/home/models/banner_model.dart
0 → 100644
View file @
c8abf95b
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
);
}
lib/screen/home/models/banner_model.g.dart
0 → 100644
View file @
c8abf95b
// 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
,
};
lib/screen/home/models/main_section_config_model.dart
View file @
c8abf95b
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
lib/screen/home/models/my_product_model.dart
View file @
c8abf95b
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
!);
...
...
lib/screen/location_address/location_address_screen.dart
0 → 100644
View file @
c8abf95b
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
),
);
},
);
}),
),
],
),
),
);
}
}
lib/screen/location_address/location_address_viewmodel.dart
0 → 100644
View file @
c8abf95b
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
;
}
}
lib/screen/location_address/models/district_address_model.dart
0 → 100644
View file @
c8abf95b
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
,
};
}
}
lib/screen/location_address/models/province_address_model.dart
0 → 100644
View file @
c8abf95b
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
,
};
}
}
lib/screen/location_address/models/province_address_model.g.dart
0 → 100644
View file @
c8abf95b
// 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
};
lib/screen/login/login_viewmodel.dart
View file @
c8abf95b
...
...
@@ -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
)
{
...
...
lib/screen/main_tab_screen/main_tab_screen.dart
View file @
c8abf95b
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
(
...
...
lib/screen/membership/member_level_header_widget.dart
0 → 100644
View file @
c8abf95b
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
);
}
}
lib/screen/membership/membership_screen.dart
0 → 100644
View file @
c8abf95b
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
)),
),
),
);
}
}
lib/screen/membership/membership_viewmodel.dart
0 → 100644
View file @
c8abf95b
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
"
);
}
}
}
lib/screen/membership/models/accumulated_counter_model.dart
0 → 100644
View file @
c8abf95b
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
);
}
lib/screen/membership/models/accumulated_counter_model.g.dart
0 → 100644
View file @
c8abf95b
// 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
,
};
lib/screen/membership/models/membership_info_response.dart
0 → 100644
View file @
c8abf95b
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
);
}
Prev
1
2
3
4
5
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment