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
cc202df4
Commit
cc202df4
authored
Aug 25, 2025
by
DatHV
Browse files
update change id, popup common manager
parent
417358c5
Changes
43
Hide whitespace changes
Inline
Side-by-side
lib/screen/mobile_card/models/product_mobile_card_model.dart
View file @
cc202df4
import
'package:json_annotation/json_annotation.dart'
;
import
'package:mypoint_flutter_app/extensions/datetime_extensions.dart'
;
import
'package:mypoint_flutter_app/extensions/string_extension.dart'
;
part
'product_mobile_card_model.g.dart'
;
@JsonSerializable
(
explicitToJson:
true
)
...
...
@@ -56,6 +58,11 @@ class ProductMobileCardModel {
_$ProductMobileCardModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$ProductMobileCardModelToJson
(
this
);
String
get
expire
{
final
ex
=
endTime
??
""
;
return
ex
.
toDate
()?.
toFormattedString
()
??
""
;
}
}
@JsonSerializable
()
...
...
lib/screen/mobile_card/product_mobile_card_screen.dart
View file @
cc202df4
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:get/get_core/src/get_main.dart'
;
import
'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'
;
import
'package:mypoint_flutter_app/extensions/num_extension.dart'
;
import
'package:mypoint_flutter_app/screen/mobile_card/product_mobile_card_viewmodel.dart'
;
import
'package:mypoint_flutter_app/screen/mobile_card/usable_mobile_card_popup.dart'
;
import
'package:mypoint_flutter_app/widgets/custom_app_bar.dart'
;
import
'package:mypoint_flutter_app/widgets/image_loader.dart'
;
import
'../../base/base_screen.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../resources/base_color.dart'
;
import
'../../widgets/alert/custom_alert_dialog.dart'
;
import
'../../widgets/alert/data_alert_model.dart'
;
import
'../../widgets/custom_navigation_bar.dart'
;
class
ProductMobileCardScreen
extends
BaseScreen
{
const
ProductMobileCardScreen
({
super
.
key
});
...
...
@@ -45,7 +43,7 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w
@override
Widget
createBody
()
{
return
Scaffold
(
appBar:
Custom
AppBar
.
back
(
title:
"Đổi mã thẻ nạp"
),
appBar:
Custom
NavigationBar
(
title:
"Đổi mã thẻ nạp"
),
body:
Obx
(()
{
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
...
...
lib/screen/order_menu/order_menu_screen.dart
View file @
cc202df4
...
...
@@ -15,7 +15,7 @@ class OrderMenuScreen extends StatelessWidget {
OrderMenuScreen
({
super
.
key
});
final
List
<
_OrderMenuItem
>
items
=
[
_OrderMenuItem
(
title:
'Thẻ nạp của tôi'
,
icon:
Icons
.
credit_card
,
type:
''
),
_OrderMenuItem
(
title:
'Thẻ nạp của tôi'
,
icon:
Icons
.
credit_card
,
type:
'
VIEW_MY_MOBILE_CARD
'
),
_OrderMenuItem
(
title:
'Sổ sức khỏe điện tử'
,
icon:
Icons
.
medical_services_outlined
,
type:
''
),
_OrderMenuItem
(
title:
'Dịch vụ giao thông'
,
icon:
Icons
.
traffic_outlined
,
type:
'APP_SCREEN_MY_VNTRA_PACKAGE'
),
];
...
...
lib/screen/personal/personal_screen.dart
View file @
cc202df4
...
...
@@ -5,12 +5,14 @@ import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
import
'../../base/base_screen.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../directional/directional_action_type.dart'
;
import
'../../preference/package_info.dart'
;
import
'../../preference/point/header_home_model.dart'
;
import
'../../resources/base_color.dart'
;
import
'../../shared/router_gage.dart'
;
import
'../../widgets/alert/data_alert_model.dart'
;
import
'../home/header_home_viewmodel.dart'
;
import
'../popup_manager/popup_runner_helper.dart'
;
class
PersonalScreen
extends
BaseScreen
{
const
PersonalScreen
({
super
.
key
});
...
...
@@ -19,7 +21,7 @@ class PersonalScreen extends BaseScreen {
State
<
PersonalScreen
>
createState
()
=>
_PersonalScreenState
();
}
class
_PersonalScreenState
extends
BaseState
<
PersonalScreen
>
with
BasicState
{
class
_PersonalScreenState
extends
BaseState
<
PersonalScreen
>
with
BasicState
,
PopupOnInit
{
final
_headerHomeVM
=
Get
.
find
<
HeaderHomeViewModel
>();
String
?
_version
,
_buildNumber
;
...
...
@@ -28,6 +30,18 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
super
.
initState
();
_loadAppInfo
();
_headerHomeVM
.
freshData
();
// WidgetsBinding.instance.addPostFrameCallback((_) async {
// if (!mounted) return;
// await PopupManagerViewModel.instance.ensureLoaded();
// final popup = PopupManagerViewModel.instance.getForScreen(DirectionalScreenName.personal.rawValue);
// final id = popup?.id ?? '';
// if (id.isEmpty || popup == null) return;
// await showPopupManagerScreen(
// context,
// modelPopup: popup,
// );
// });
runPopupCheck
(
DirectionalScreenName
.
personal
);
}
Future
<
void
>
_loadAppInfo
()
async
{
...
...
@@ -189,7 +203,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
{
'icon'
:
Icons
.
receipt_long_outlined
,
'title'
:
'Lịch sử giao dịch'
,
'sectionDivider'
:
true
,
'type'
:
'APP_SCREEN_TRANSACTION_HISTORIES'
},
{
'icon'
:
Icons
.
history_outlined
,
'title'
:
'Lịch sử điểm'
,
'type'
:
'APP_SCREEN_SURVERY_APP'
},
{
'icon'
:
Icons
.
history_outlined
,
'title'
:
'Lịch sử hoàn điểm'
,
'type'
:
'APP_SCREEN_REFUND_HISTORY'
},
{
'icon'
:
Icons
.
account_balance_wallet_outlined
,
'title'
:
'Quản lý tài khoản/thẻ'
,
'type'
:
'
APP_SCREEN_ELECTRIC_BILL
'
},
{
'icon'
:
Icons
.
account_balance_wallet_outlined
,
'title'
:
'Quản lý tài khoản/thẻ'
,
'type'
:
'
BANK_ACCOUNT_MANAGER
'
},
{
'icon'
:
Icons
.
favorite_border
,
'title'
:
'Yêu thích'
,
'type'
:
'APP_SCREEN_CATEGORY_TAB_FAVORITE'
},
{
'icon'
:
Icons
.
shopping_bag_outlined
,
'title'
:
'Đơn mua'
,
'sectionDivider'
:
true
,
'type'
:
'APP_SCREEN_ORDER_MENU'
},
{
'icon'
:
Icons
.
info_outline
,
'title'
:
'Giới thiệu MyPoint'
,
'sectionDivider'
:
true
,
'type'
:
'VIEW_WEBSITE_PAGE'
},
...
...
@@ -293,7 +307,8 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
});
final
phone
=
DataPreference
.
instance
.
phoneNumberUsedForLoginScreen
;
final
displayName
=
DataPreference
.
instance
.
displayName
;
if
(
phone
!=
null
&&
!
found
)
{
print
(
"Safe back to login screen with phone:
$phone
, displayName:
$displayName
, found:
$found
"
);
if
(
phone
!=
null
&&
found
)
{
Get
.
offAllNamed
(
loginScreen
,
arguments:
{
"phone"
:
phone
,
'fullName'
:
displayName
});
}
else
{
DataPreference
.
instance
.
clearData
();
...
...
lib/screen/popup_manager/popup_manager_model.dart
View file @
cc202df4
import
'package:json_annotation/json_annotation.dart'
;
import
'package:mypoint_flutter_app/directional/directional_screen.dart'
;
part
'popup_manager_model.g.dart'
;
@JsonSerializable
()
...
...
@@ -81,6 +82,13 @@ class PopupManagerModel {
this
.
requestId
,
});
DirectionalScreen
?
get
directional
{
return
DirectionalScreen
.
build
(
clickActionType:
clickActionType
,
clickActionParam:
clickActionParam
,
);
}
factory
PopupManagerModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$PopupManagerModelFromJson
(
json
);
...
...
lib/screen/popup_manager/popup_manager_
popup
.dart
→
lib/screen/popup_manager/popup_manager_
screen
.dart
View file @
cc202df4
import
'dart:async'
;
import
'package:flutter/material.dart'
;
import
'package:mypoint_flutter_app/screen/popup_manager/popup_manager_model.dart'
;
/// Giao diện điều hướng để bạn nối với router của app
typedef
PopupNavigator
=
void
Function
({
required
String
name
,
required
String
identifier
,
String
?
title
,
String
?
body
,
});
import
'package:mypoint_flutter_app/screen/popup_manager/popup_manager_viewmodel.dart'
;
/// Logger tuỳ bạn hook vào hệ thống hiện có (Firebase, MoEngage, …)
void
logPopupShowing
(
{
required
String
popupId
,
required
String
requestId
})
{
...
...
@@ -22,56 +15,42 @@ void logPopupClick({required String popupId}) {
debugPrint
(
'[Popup] click: popup_id=
$popupId
'
);
}
/// ==== API hiển thị popup (gọi giống hàm Swift) ====
Future
<
void
>
showPopup
(
BuildContext
context
,
{
required
PopupManagerModel
modelPopup
,
required
PopupNavigator
onNavigate
,
VoidCallback
?
onDismissed
,
// thay cho NotificationCenter
})
async
{
Future
<
void
>
showPopupManagerScreen
(
BuildContext
context
,
{
required
PopupManagerModel
modelPopup
,
VoidCallback
?
onDismissed
,
})
async
{
int
timeCountDown
=
int
.
tryParse
(
modelPopup
.
timeCountDown
??
'1000000'
)
??
1000000
;
final
popupId
=
modelPopup
.
id
??
''
;
final
requestId
=
modelPopup
.
requestId
??
''
;
logPopupShowing
(
popupId:
popupId
,
requestId:
requestId
);
await
PopupManagerViewModel
.
instance
.
markShownOnce
(
popupId
);
await
showGeneralDialog
(
context:
context
,
barrierDismissible:
false
,
// giống SwiftEntryKit, user không chạm ra ngoài để tắt
barrierDismissible:
false
,
barrierLabel:
'popup'
,
transitionDuration:
const
Duration
(
milliseconds:
220
),
pageBuilder:
(
_
,
__
,
___
)
{
return
_BasePopupView
(
model:
modelPopup
,
initialCountdown:
timeCountDown
,
onNavigate:
onNavigate
,
onDismissed:
()
{
onDismissed
?.
call
();
},
);
},
transitionBuilder:
(
_
,
anim
,
__
,
child
)
{
return
Transform
.
scale
(
scale:
0.96
+
(
0.04
*
anim
.
value
),
child:
Opacity
(
opacity:
anim
.
value
,
child:
child
),
);
return
Transform
.
scale
(
scale:
0.96
+
(
0.04
*
anim
.
value
),
child:
Opacity
(
opacity:
anim
.
value
,
child:
child
));
},
);
}
/// ==== Widget nền của popup (tương đương BasePopupView + SwiftEntryKit display) ====
class
_BasePopupView
extends
StatefulWidget
{
final
PopupManagerModel
model
;
final
int
initialCountdown
;
final
PopupNavigator
onNavigate
;
final
VoidCallback
onDismissed
;
const
_BasePopupView
({
required
this
.
model
,
required
this
.
initialCountdown
,
required
this
.
onNavigate
,
required
this
.
onDismissed
,
});
const
_BasePopupView
({
required
this
.
model
,
required
this
.
initialCountdown
,
required
this
.
onDismissed
});
@override
State
<
_BasePopupView
>
createState
()
=>
_BasePopupViewState
();
...
...
@@ -80,7 +59,7 @@ class _BasePopupView extends StatefulWidget {
class
_BasePopupViewState
extends
State
<
_BasePopupView
>
{
Timer
?
_timer
;
late
int
_countdown
;
double
?
_imageAspectRatio
;
// width / height
double
?
_imageAspectRatio
;
bool
_imageLoaded
=
false
;
@override
...
...
@@ -113,35 +92,16 @@ class _BasePopupViewState extends State<_BasePopupView> {
if
(
mounted
)
Navigator
.
of
(
context
).
pop
();
}
void
_onImageTap
()
{
final
model
=
widget
.
model
;
if
((
model
.
clickActionType
??
''
).
isEmpty
)
return
;
_timer
?.
cancel
();
logPopupClick
(
popupId:
model
.
id
??
''
);
// Điều hướng tương đương DirectionalScreen.begin(...)
widget
.
onNavigate
(
name:
model
.
clickActionType
!,
identifier:
model
.
clickActionParam
??
''
,
title:
model
.
popupTitleTemplate
??
''
,
body:
model
.
popupBodyTemplate
??
''
,
void
_onContentTap
()
{
logPopupClick
(
popupId:
widget
.
model
.
id
??
''
);
print
(
'Popup clicked:
${widget.model.directional?.clickActionType ?? ''}
-
${widget.model.directional?.clickActionParam ?? ''}
'
,
);
_dismiss
();
}
void
_onCancelTap
()
async
{
final
model
=
widget
.
model
;
if
((
model
.
clickActionType
??
''
)
==
'VIEW_GIFT'
)
{
// Show "GiftMessageView" dạng bottom sheet
await
showModalBottomSheet
(
context:
context
,
backgroundColor:
Colors
.
transparent
,
builder:
(
_
)
=>
_GiftMessageSheet
(
model:
model
,
onNavigate:
widget
.
onNavigate
),
);
}
_timer
?.
cancel
();
_dismiss
();
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
widget
.
model
.
directional
?.
begin
();
});
}
@override
...
...
@@ -151,18 +111,10 @@ class _BasePopupViewState extends State<_BasePopupView> {
final
body
=
(
model
.
popupBodyTemplate
??
''
).
trim
();
final
hasTitle
=
title
.
isNotEmpty
;
final
hasBody
=
body
.
isNotEmpty
;
// Tính phần height phụ theo Swift (55 + 50, trừ bớt khi ẩn)
int
extra
=
55
+
50
;
if
(!
hasTitle
)
extra
-=
55
;
if
(!
hasBody
)
extra
-=
50
;
final
media
=
MediaQuery
.
of
(
context
);
final
screenW
=
media
.
size
.
width
;
final
maxPopupHeight
=
media
.
size
.
height
-
200
;
// Card radius phụ thuộc điều kiện
final
imageCornerRadius
=
(!
hasTitle
&&
!
hasBody
)
?
12.0
:
12.0
;
// ảnh trên cùng vẫn bo 12
final
imageCornerRadius
=
12.0
;
// ảnh trên cùng vẫn bo 12
final
subHeaderRadius
=
(
hasBody
&&
!
hasTitle
)
?
12.0
:
0.0
;
return
Material
(
...
...
@@ -172,7 +124,6 @@ class _BasePopupViewState extends State<_BasePopupView> {
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
20
),
child:
LayoutBuilder
(
builder:
(
context
,
constraints
)
{
// Nếu đã biết tỷ lệ ảnh: tính height theo công thức Swift
double
imageHeight
=
0
;
if
(
_imageAspectRatio
!=
null
&&
_imageAspectRatio
!
>
0
)
{
// image.size.height*(screenW - 40)/image.size.width
...
...
@@ -182,79 +133,59 @@ class _BasePopupViewState extends State<_BasePopupView> {
final
content
=
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
// Nút đóng
Align
(
alignment:
Alignment
.
topRight
,
child:
IconButton
(
icon:
const
Icon
(
Icons
.
close
,
color:
Colors
.
white
),
onPressed:
_onCancelTap
,
),
child:
IconButton
(
icon:
const
Icon
(
Icons
.
close
,
color:
Colors
.
white
),
onPressed:
_dismiss
),
),
ConstrainedBox
(
constraints:
BoxConstraints
(
maxHeight:
maxPopupHeight
,
minWidth:
double
.
infinity
,
),
child:
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
12
),
child:
Material
(
color:
Colors
.
white
,
child:
SingleChildScrollView
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
stretch
,
mainAxisSize:
MainAxisSize
.
min
,
children:
[
// Ảnh
_buildImage
(
url:
model
.
imageURL
,
heightHint:
imageHeight
>
0
?
imageHeight
:
null
,
cornerRadius:
imageCornerRadius
,
),
// Header
if
(
hasTitle
)
Padding
(
padding:
const
EdgeInsets
.
fromLTRB
(
16
,
16
,
16
,
0
),
child:
Text
(
title
,
style:
const
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
w700
,
constraints:
BoxConstraints
(
maxHeight:
maxPopupHeight
,
minWidth:
double
.
infinity
),
child:
GestureDetector
(
onTap:
_onContentTap
,
child:
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
12
),
child:
Material
(
color:
Colors
.
white
,
child:
SingleChildScrollView
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
stretch
,
mainAxisSize:
MainAxisSize
.
min
,
children:
[
// Ảnh
_buildImage
(
url:
model
.
imageURL
,
heightHint:
imageHeight
>
0
?
imageHeight
:
null
,
cornerRadius:
imageCornerRadius
,
),
// Header
if
(
hasTitle
)
Padding
(
padding:
const
EdgeInsets
.
fromLTRB
(
16
,
16
,
16
,
0
),
child:
Text
(
title
,
style:
const
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
w700
),
),
),
),
// SubHeader
if
(
hasBody
)
Container
(
decoration:
BoxDecoration
(
color:
const
Color
(
0xFFF6F7F9
),
borderRadius:
BorderRadius
.
vertical
(
bottom:
Radius
.
circular
(
subHeaderRadius
),
// SubHeader
if
(
hasBody
)
Container
(
decoration:
BoxDecoration
(
color:
const
Color
(
0xFFF6F7F9
),
borderRadius:
BorderRadius
.
vertical
(
bottom:
Radius
.
circular
(
subHeaderRadius
)),
),
padding:
const
EdgeInsets
.
fromLTRB
(
16
,
10
,
16
,
16
),
child:
Text
(
body
,
style:
const
TextStyle
(
fontSize:
15
,
height:
1.4
)),
),
// Dòng trạng thái đếm ngược (tuỳ chọn)
Padding
(
padding:
const
EdgeInsets
.
fromLTRB
(
16
,
10
,
16
,
16
),
child:
Text
(
body
,
style:
const
TextStyle
(
fontSize:
15
,
height:
1.4
),
),
),
// Dòng trạng thái đếm ngược (tuỳ chọn)
Padding
(
padding:
const
EdgeInsets
.
fromLTRB
(
16
,
10
,
16
,
16
),
child:
Text
(
_countdown
>
0
?
'
$_countdown
seconds dismiss popup'
:
'Closing…'
,
style:
TextStyle
(
fontSize:
12
,
color:
Colors
.
grey
.
shade600
,
_countdown
>
0
?
'
$_countdown
seconds dismiss popup'
:
'Closing…'
,
style:
TextStyle
(
fontSize:
12
,
color:
Colors
.
grey
.
shade600
),
textAlign:
TextAlign
.
right
,
),
textAlign:
TextAlign
.
right
,
),
)
,
]
,
]
,
)
,
),
),
),
...
...
@@ -262,7 +193,6 @@ class _BasePopupViewState extends State<_BasePopupView> {
),
],
);
return
content
;
},
),
...
...
@@ -271,14 +201,9 @@ class _BasePopupViewState extends State<_BasePopupView> {
);
}
Widget
_buildImage
({
required
String
?
url
,
double
?
heightHint
,
required
double
cornerRadius
,
})
{
Widget
_buildImage
({
required
String
?
url
,
double
?
heightHint
,
required
double
cornerRadius
})
{
final
imageUrl
=
(
url
??
''
).
trim
();
if
(
imageUrl
.
isEmpty
)
{
// Placeholder fallback
return
Container
(
height:
160
,
color:
const
Color
(
0xFFE9ECF1
),
...
...
@@ -287,22 +212,19 @@ class _BasePopupViewState extends State<_BasePopupView> {
);
}
return
GestureDetector
(
onTap:
_onImageTap
,
child:
ClipRRect
(
borderRadius:
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
cornerRadius
)),
child:
_NetworkImageWithInfo
(
imageUrl:
imageUrl
,
heightHint:
heightHint
,
onResolved:
(
width
,
height
)
{
if
(!
_imageLoaded
&&
width
>
0
&&
height
>
0
)
{
setState
(()
{
_imageLoaded
=
true
;
_imageAspectRatio
=
width
/
height
;
// width/height
});
}
},
),
return
ClipRRect
(
borderRadius:
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
cornerRadius
)),
child:
_NetworkImageWithInfo
(
imageUrl:
imageUrl
,
heightHint:
heightHint
,
onResolved:
(
width
,
height
)
{
if
(!
_imageLoaded
&&
width
>
0
&&
height
>
0
)
{
setState
(()
{
_imageLoaded
=
true
;
_imageAspectRatio
=
width
/
height
;
// width/height
});
}
},
),
);
}
...
...
@@ -313,12 +235,7 @@ class _NetworkImageWithInfo extends StatefulWidget {
final
String
imageUrl
;
final
double
?
heightHint
;
final
void
Function
(
int
width
,
int
height
)
onResolved
;
const
_NetworkImageWithInfo
({
required
this
.
imageUrl
,
required
this
.
onResolved
,
this
.
heightHint
,
});
const
_NetworkImageWithInfo
({
required
this
.
imageUrl
,
required
this
.
onResolved
,
this
.
heightHint
});
@override
State
<
_NetworkImageWithInfo
>
createState
()
=>
_NetworkImageWithInfoState
();
...
...
@@ -363,90 +280,9 @@ class _NetworkImageWithInfoState extends State<_NetworkImageWithInfo> {
@override
Widget
build
(
BuildContext
context
)
{
if
(
_info
==
null
&&
widget
.
heightHint
==
null
)
{
// loading skeleton
return
AspectRatio
(
aspectRatio:
16
/
9
,
child:
Container
(
color:
const
Color
(
0xFFE9ECF1
)),
);
return
AspectRatio
(
aspectRatio:
16
/
9
,
child:
Container
(
color:
const
Color
(
0xFFE9ECF1
)));
}
final
height
=
widget
.
heightHint
;
return
Image
.
network
(
widget
.
imageUrl
,
height:
height
,
width:
double
.
infinity
,
fit:
BoxFit
.
cover
,
);
}
}
/// ==== GiftMessageView tương đương (bottom sheet) ====
class
_GiftMessageSheet
extends
StatelessWidget
{
final
PopupManagerModel
model
;
final
PopupNavigator
onNavigate
;
const
_GiftMessageSheet
({
required
this
.
model
,
required
this
.
onNavigate
,
ff
});
@override
Widget
build
(
BuildContext
context
)
{
final
radius
=
const
Radius
.
circular
(
16
);
return
Container
(
decoration:
const
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
16
)),
),
padding:
const
EdgeInsets
.
fromLTRB
(
16
,
16
,
16
,
16
+
24
),
child:
SafeArea
(
top:
false
,
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
Container
(
width:
36
,
height:
4
,
decoration:
BoxDecoration
(
color:
Colors
.
grey
.
shade300
,
borderRadius:
BorderRadius
.
circular
(
2
))),
const
SizedBox
(
height:
12
),
Text
(
model
.
popupTitleTemplate
?.
isNotEmpty
==
true
?
model
.
popupTitleTemplate
!
:
'Quà tặng của bạn'
,
style:
const
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
w700
),
),
const
SizedBox
(
height:
8
),
Text
(
model
.
popupBodyTemplate
?.
isNotEmpty
==
true
?
model
.
popupBodyTemplate
!
:
'Nhấn "Sử dụng ngay" để tiếp tục.'
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Colors
.
black87
),
textAlign:
TextAlign
.
center
,
),
const
SizedBox
(
height:
16
),
SizedBox
(
width:
double
.
infinity
,
child:
ElevatedButton
(
onPressed:
()
{
if
((
model
.
clickActionType
??
''
).
isEmpty
)
{
Navigator
.
of
(
context
).
pop
();
return
;
}
onNavigate
(
name:
model
.
clickActionType
!,
identifier:
model
.
clickActionParam
??
''
,
title:
model
.
popupTitleTemplate
??
''
,
body:
model
.
popupBodyTemplate
??
''
,
);
Navigator
.
of
(
context
).
pop
();
},
style:
ElevatedButton
.
styleFrom
(
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
12
)),
padding:
const
EdgeInsets
.
symmetric
(
vertical:
14
),
),
child:
const
Text
(
'Sử dụng ngay'
),
),
),
const
SizedBox
(
height:
8
),
TextButton
(
onPressed:
()
=>
Navigator
.
of
(
context
).
pop
(),
child:
const
Text
(
'Để sau'
),
),
],
),
),
);
return
Image
.
network
(
widget
.
imageUrl
,
height:
height
,
width:
double
.
infinity
,
fit:
BoxFit
.
cover
);
}
}
lib/screen/popup_manager/popup_manager_viewmodel.dart
0 → 100644
View file @
cc202df4
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'package:mypoint_flutter_app/screen/popup_manager/popup_manager_model.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
class
PopupManagerViewModel
extends
RestfulApiViewModel
{
PopupManagerViewModel
.
_
();
static
final
PopupManagerViewModel
instance
=
PopupManagerViewModel
.
_
();
final
Set
<
String
>
_shownIds
=
{};
List
<
PopupManagerModel
>?
_popupData
;
bool
_loaded
=
false
;
Future
<
void
>?
_loadingFuture
;
Future
<
void
>
ensureLoaded
()
async
{
if
(
_loaded
)
return
;
if
(
_loadingFuture
!=
null
)
{
return
_loadingFuture
;
}
_loadingFuture
=
_getPopupManagerDataInternal
();
await
_loadingFuture
;
}
Future
<
void
>
_getPopupManagerDataInternal
()
async
{
try
{
const
Duration
(
seconds:
3
);
// Giả lập thời gian tải dữ liệu
final
response
=
await
client
.
getPopupManagerCommonScreen
();
_popupData
=
response
.
data
??
[];
// _popupData = [
// PopupManagerModel(
// id: '1',
// screenToShow: 'APP_SCREEN_HOME',
// clickActionType: 'VIEW_PRODUCT_VOUCHER',
// clickActionParam: '50760',
// posActionID: 'action1',
// posActionCode: 'code1',
// timeToShow: '10:00-18:00',
// timeCountDown: '30',
// hourStartInDay: '9',
// hourStopInDay: '17',
// afterPosID: 'pos1',
// afterPosCode: 'posCode1',
// afterPosName: 'POS Name 1',
// marketingRequestDescription: 'Marketing description here.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'daily',
// scheduleRunTypeName: 'Daily Schedule',
// scheduleAtTime: '12:00',
// popupTitleTemplate: 'APP_SCREEN_HOME',
// popupBodyTemplate: 'Enjoy your stay and check out our features.',
// imageID: 'image123',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '2',
// screenToShow: 'APP_SCREEN_POINTBACK',
// clickActionType: 'APP_SCREEN_SIM_SERVICE',
// clickActionParam: 'https://example.com/settings',
// posActionID: 'action2',
// posActionCode: 'code2',
// timeToShow: '08:00-20:00',
// timeCountDown: '60',
// hourStartInDay: '8',
// hourStopInDay: '20',
// afterPosID: 'pos2',
// afterPosCode: 'posCode2',
// afterPosName: 'POS Name 2',
// marketingRequestDescription: 'Settings popup description.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'weekly',
// scheduleRunTypeName: 'Weekly Schedule',
// scheduleAtTime: '10:00',
// popupTitleTemplate: 'APP_SCREEN_POINTBACK',
// popupBodyTemplate: 'Check out the new settings options.',
// imageID: 'image456',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '3',
// screenToShow: 'APP_SCREEN_PRODUCT_VOUCHER',
// clickActionType: 'APP_SCREEN_GIFTS',
// clickActionParam: 'Profile updated successfully.',
// posActionID: 'action3',
// posActionCode: 'code3',
// timeToShow: '09:00-21:00',
// timeCountDown: '45',
// hourStartInDay: '9',
// hourStopInDay: '21',
// afterPosID: 'pos3',
// afterPosCode: 'posCode3',
// afterPosName: 'POS Name 3',
// marketingRequestDescription: 'Profile update alert.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'monthly',
// scheduleRunTypeName: 'Monthly Schedule',
// scheduleAtTime: '15:00',
// popupTitleTemplate: 'APP_SCREEN_PRODUCT_VOUCHER',
// popupBodyTemplate: 'Your profile has been updated successfully.',
// imageID: 'image789',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '4',
// screenToShow: 'APP_SCREEN_GAME_BUNDLE',
// clickActionType: 'APP_SCREEN_CAMPAIGN_WALKING',
// clickActionParam: '1',
// posActionID: 'action3',
// posActionCode: 'code3',
// timeToShow: '09:00-21:00',
// timeCountDown: '45',
// hourStartInDay: '9',
// hourStopInDay: '21',
// afterPosID: 'pos3',
// afterPosCode: 'posCode3',
// afterPosName: 'POS Name 3',
// marketingRequestDescription: 'Profile update alert.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'monthly',
// scheduleRunTypeName: 'Monthly Schedule',
// scheduleAtTime: '15:00',
// popupTitleTemplate: 'APP_SCREEN_GAME_BUNDLE',
// popupBodyTemplate: 'Your profile has been updated successfully.',
// imageID: 'image789',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '5',
// screenToShow: 'APP_SCREEN_PERSONAL',
// clickActionType: 'APP_SCREEN_SIM_SERVICE',
// clickActionParam: 'Profile updated successfully.',
// posActionID: 'action3',
// posActionCode: 'code3',
// timeToShow: '09:00-21:00',
// timeCountDown: '45',
// hourStartInDay: '9',
// hourStopInDay: '21',
// afterPosID: 'pos3',
// afterPosCode: 'posCode3',
// afterPosName: 'POS Name 3',
// marketingRequestDescription: 'Profile update alert.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'monthly',
// scheduleRunTypeName: 'Monthly Schedule',
// scheduleAtTime: '15:00',
// popupTitleTemplate: 'APP_SCREEN_PERSONAL',
// popupBodyTemplate: 'Your profile has been updated successfully.',
// imageID: 'image789',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// ];
_loaded
=
true
;
}
catch
(
e
)
{
_popupData
=
[];
_loaded
=
true
;
rethrow
;
}
finally
{
_loadingFuture
=
null
;
}
}
PopupManagerModel
?
getForScreen
(
String
screenName
)
{
if
(
_popupData
==
null
||
_popupData
!.
isEmpty
)
return
null
;
final
idx
=
_popupData
!.
indexWhere
(
(
e
)
=>
(
e
.
screenToShow
??
''
).
trim
().
toUpperCase
()
==
screenName
.
trim
().
toUpperCase
(),
);
if
(
idx
<
0
)
return
null
;
final
found
=
_popupData
![
idx
];
if
(
_shownIds
.
contains
(
found
.
id
))
return
null
;
return
found
;
}
Future
<
void
>
markShownOnce
(
String
popupId
)
async
{
_shownIds
.
add
(
popupId
);
}
Future
<
void
>
reset
()
async
{
_shownIds
.
clear
();
_popupData
=
[];
_loaded
=
false
;
_loadingFuture
=
null
;
}
}
lib/screen/popup_manager/popup_runner_helper.dart
0 → 100644
View file @
cc202df4
import
'package:flutter/material.dart'
;
import
'package:mypoint_flutter_app/screen/popup_manager/popup_manager_screen.dart'
;
import
'../../directional/directional_action_type.dart'
;
import
'popup_manager_viewmodel.dart'
;
class
PopupRunner
{
static
Future
<
void
>
_showIfAvailable
(
BuildContext
context
,
{
required
String
screenName
})
async
{
await
PopupManagerViewModel
.
instance
.
ensureLoaded
();
final
popup
=
PopupManagerViewModel
.
instance
.
getForScreen
(
screenName
,);
if
(
popup
==
null
||
(
popup
.
id
??
''
).
isEmpty
)
return
;
if
(!
context
.
mounted
)
return
;
await
showPopupManagerScreen
(
context
,
modelPopup:
popup
,
);
}
}
mixin
PopupOnInit
<
T
extends
StatefulWidget
>
on
State
<
T
>
{
bool
_popupChecked
=
false
;
void
runPopupCheck
(
DirectionalScreenName
directional
)
{
if
(
_popupChecked
)
return
;
_popupChecked
=
true
;
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
async
{
if
(!
mounted
)
return
;
await
PopupRunner
.
_showIfAvailable
(
context
,
screenName:
directional
.
rawValue
,
);
});
}
}
\ No newline at end of file
lib/screen/setting/setting_screen.dart
View file @
cc202df4
// setting_screen.dart
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:get/get_core/src/get_main.dart'
;
import
'package:mypoint_flutter_app/screen/setting/setting_viewmodel.dart'
;
import
'../../shared/router_gage.dart'
;
import
'../../widgets/bottom_sheet_helper.dart'
;
import
'../../widgets/custom_
app
_bar.dart'
;
import
'../../widgets/custom_
navigation
_bar.dart'
;
import
'../change_pass/change_pass_screen.dart'
;
import
'../delete_account/delete_account_dialog.dart'
;
...
...
@@ -32,7 +31,7 @@ class _SettingScreenState extends State<SettingScreen> {
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
appBar:
Custom
AppBar
.
back
(
title:
"Cài đặt"
),
appBar:
Custom
NavigationBar
(
title:
"Cài đặt"
),
backgroundColor:
const
Color
(
0xFFF5F6F7
),
body:
Column
(
children:
[
...
...
lib/screen/splash/splash_screen_viewmodel.dart
View file @
cc202df4
import
'package:flutter/cupertino.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/base/restful_api_viewmodel.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'package:mypoint_flutter_app/shared/router_gage.dart'
;
import
'../../base/base_response_model.dart'
;
import
'../../
configs/constants
.dart'
;
import
'../../
model/auth/profile_response_model
.dart'
;
import
'../../model/update_response_model.dart'
;
import
'../../preference/data_preference.dart'
;
import
'../../preference/point/point_manager.dart'
;
import
'../main_tab_screen/main_tab_screen.dart'
;
import
'../onboarding/onboarding_screen.dart'
;
import
'package:url_launcher/url_launcher.dart'
;
import
'../popup_manager/popup_manager_viewmodel.dart'
;
class
SplashScreenViewModel
extends
RestfulApiViewModel
{
var
infoAppUpdate
=
BaseResponseModel
<
UpdateResponseModel
>().
obs
;
...
...
@@ -35,7 +35,7 @@ class SplashScreenViewModel extends RestfulApiViewModel {
}
Future
<
void
>
getUserProfile
()
async
{
if
(!(
await
DataPreference
.
instance
.
logged
))
{
if
(!(
DataPreference
.
instance
.
logged
))
{
Get
.
toNamed
(
onboardingScreen
);
return
;
}
...
...
@@ -44,20 +44,28 @@ class SplashScreenViewModel extends RestfulApiViewModel {
hideLoading
();
final
userProfile
=
value
.
data
;
if
(
value
.
isSuccess
&&
userProfile
!=
null
)
{
await
DataPreference
.
instance
.
saveUserProfile
(
userProfile
);
await
UserPointManager
().
fetchUserPoint
();
Get
.
toNamed
(
mainScreen
);
_freshDataAndToMainScreen
(
userProfile
);
}
else
{
DataPreference
.
instance
.
clearLoginToken
();
Get
.
toNamed
(
onboardingScreen
);
}
});
}
void
_freshDataAndToMainScreen
(
ProfileResponseModel
userProfile
)
async
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
async
{
await
DataPreference
.
instance
.
saveUserProfile
(
userProfile
);
await
UserPointManager
().
fetchUserPoint
();
await
PopupManagerViewModel
.
instance
.
ensureLoaded
();
Get
.
toNamed
(
mainScreen
);
});
}
}
class
EmptyCodable
{
EmptyCodable
.
fromJson
(
dynamic
json
);
Map
<
String
,
dynamic
>
toJson
()
{
return
{};
}
}
\ No newline at end of file
}
lib/screen/support/support_screen.dart
View file @
cc202df4
...
...
@@ -6,6 +6,7 @@ import 'package:mypoint_flutter_app/shared/router_gage.dart';
import
'package:url_launcher/url_launcher.dart'
;
import
'../../resources/base_color.dart'
;
import
'../../widgets/back_button.dart'
;
import
'../../widgets/custom_navigation_bar.dart'
;
import
'../faqs/faqs_screen.dart'
;
import
'../pageDetail/campaign_detail_screen.dart'
;
import
'../pageDetail/model/detail_page_rule_type.dart'
;
...
...
@@ -52,13 +53,7 @@ class _SupportScreenState extends State<SupportScreen> {
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
appBar:
AppBar
(
leading:
CustomBackButton
(),
title:
const
Text
(
"Hỗ trợ"
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
)),
backgroundColor:
Colors
.
white
,
foregroundColor:
Colors
.
black
,
elevation:
0
,
),
appBar:
CustomNavigationBar
(
title:
"Hỗ trợ"
),
body:
Obx
(()
{
if
(
controller
.
supportItems
.
isEmpty
)
{
return
const
Center
(
child:
CircularProgressIndicator
());
...
...
lib/screen/topup/topup_screen.dart
View file @
cc202df4
import
'package:contacts_service/contacts_service.dart'
;
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:intl/intl.dart'
;
...
...
@@ -8,6 +7,7 @@ import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import
'../../preference/data_preference.dart'
;
import
'../../resources/base_color.dart'
;
import
'../../shared/router_gage.dart'
;
import
'../contacts/contacts_picker.dart'
;
import
'brand_select_sheet_widget.dart'
;
class
PhoneTopUpScreen
extends
StatefulWidget
{
...
...
@@ -353,22 +353,19 @@ class _PhoneTopUpScreenState extends State<PhoneTopUpScreen> {
Future
<
void
>
pickContact
(
BuildContext
context
)
async
{
try
{
// Gọi sẽ tự động hiện dialog yêu cầu quyền (nếu cần)
final
Contact
?
contact
=
await
ContactsService
.
openDeviceContactPicker
();
if
(
contact
!=
null
&&
contact
.
phones
!=
null
&&
contact
.
phones
!.
isNotEmpty
)
{
String
phone
=
contact
.
phones
!.
first
.
value
??
''
;
phone
=
phone
.
replaceAll
(
RegExp
(
r'[\s\-\(\)]'
),
''
);
_phoneController
.
text
=
phone
;
_viewModel
.
phoneNumber
.
value
=
phone
;
_viewModel
.
checkMobileNetwork
();
}
else
{
ScaffoldMessenger
.
of
(
context
,
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
"Không tìm thấy số điện thoại hợp lệ"
)));
}
final
contact
=
await
showContactPicker
(
context
);
if
(
contact
==
null
)
return
;
if
(
contact
.
phones
.
isEmpty
)
return
;
String
phone
=
contact
.
phones
.
first
.
number
;
phone
=
phone
.
replaceAll
(
RegExp
(
r'[\s\-\(\)]'
),
''
);
_phoneController
.
text
=
phone
;
_viewModel
.
phoneNumber
.
value
=
phone
;
_viewModel
.
checkMobileNetwork
();
}
catch
(
e
)
{
print
(
"❌ Lỗi khi truy cập danh bạ:
$e
"
);
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
"Không thể truy cập danh bạ"
)));
debugPrint
(
'❌ pickContact error:
$e
'
);
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
'Không thể truy cập danh bạ'
)),
);
}
}
}
lib/screen/transaction/transactions_history_screen.dart
View file @
cc202df4
...
...
@@ -33,7 +33,7 @@ class _TransactionHistoryScreenState extends State<TransactionHistoryScreen> {
_buildCategoryTabs
(),
_buildSummaryBox
(
summary
),
const
SizedBox
(
height:
8
),
if
(
items
.
isEmpty
)
Expanded
(
child:
Center
(
child:
EmptyWidget
(
size:
Size
(
20
0
,
20
0
)))),
if
(
items
.
isEmpty
)
Expanded
(
child:
Center
(
child:
EmptyWidget
(
size:
Size
(
16
0
,
16
0
)))),
Expanded
(
child:
ListView
.
builder
(
physics:
const
AlwaysScrollableScrollPhysics
(),
...
...
lib/screen/voucher/detail/store_list_section.dart
View file @
cc202df4
...
...
@@ -4,16 +4,27 @@ import 'package:mypoint_flutter_app/shared/direction_google_map.dart';
import
'../models/product_store_model.dart'
;
class
StoreListSection
extends
State
less
Widget
{
class
StoreListSection
extends
State
ful
Widget
{
final
List
<
ProductStoreModel
>
stores
;
final
String
?
brandLogo
;
const
StoreListSection
({
Key
?
key
,
required
this
.
stores
,
this
.
brandLogo
})
:
super
(
key:
key
);
const
StoreListSection
({
super
.
key
,
required
this
.
stores
,
this
.
brandLogo
});
@override
State
<
StoreListSection
>
createState
()
=>
_StoreListSectionState
();
}
class
_StoreListSectionState
extends
State
<
StoreListSection
>
{
bool
_seeAllStore
=
false
;
final
int
_numberOfStore
=
4
;
List
<
ProductStoreModel
>
get
displayStores
{
return
_seeAllStore
?
widget
.
stores
:
widget
.
stores
.
take
(
_numberOfStore
).
toList
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
Obx
(()
{
if
(
s
tores
.
isEmpty
)
{
if
(
displayS
tores
.
isEmpty
)
{
return
const
SizedBox
.
shrink
();
}
return
Container
(
...
...
@@ -25,7 +36,7 @@ class StoreListSection extends StatelessWidget {
children:
[
const
Text
(
'Địa điểm áp dụng:'
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
16
)),
const
SizedBox
(
height:
10
),
...
s
tores
.
map
(
...
displayS
tores
.
map
(
(
store
)
=>
InkWell
(
onTap:
()
{
_onTapStore
(
store
);
...
...
@@ -33,6 +44,20 @@ class StoreListSection extends StatelessWidget {
child:
_buildStoreItem
(
store
),
),
),
if
(
widget
.
stores
.
length
>
_numberOfStore
)
GestureDetector
(
onTap:
()
{
setState
(()
{
_seeAllStore
=
!
_seeAllStore
;
});
},
child:
Center
(
child:
Text
(
_seeAllStore
?
'Thu gọn'
:
'Xem tất cả'
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
16
,
color:
Colors
.
blueAccent
),
),
),
),
],
),
);
...
...
@@ -53,7 +78,7 @@ class StoreListSection extends StatelessWidget {
children:
[
ClipOval
(
child:
Image
.
network
(
brandLogo
??
""
,
widget
.
brandLogo
??
""
,
width:
20
,
height:
20
,
fit:
BoxFit
.
cover
,
...
...
@@ -61,25 +86,28 @@ class StoreListSection extends StatelessWidget {
),
),
const
SizedBox
(
width:
8
),
Text
(
store
.
name
??
''
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w600
)),
],
),
const
SizedBox
(
height:
4
),
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
const
Icon
(
Icons
.
location_on_outlined
,
size:
18
,
color:
Colors
.
grey
),
const
SizedBox
(
width:
4
),
Expanded
(
child:
Text
(
store
.
address
??
''
,
style:
const
TextStyle
(
color:
Colors
.
grey
,
fontSize:
13
),
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
),
child:
Text
(
store
.
name
??
''
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w600
),
softWrap:
true
),
),
],
),
const
SizedBox
(
height:
4
),
if
((
store
.
address
??
''
).
isNotEmpty
)
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
const
Icon
(
Icons
.
location_on_outlined
,
size:
18
,
color:
Colors
.
grey
),
const
SizedBox
(
width:
4
),
Expanded
(
child:
Text
(
store
.
address
??
''
,
style:
const
TextStyle
(
color:
Colors
.
grey
,
fontSize:
13
),
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
),
),
],
),
],
),
);
...
...
lib/screen/voucher/models/my_mobile_card_response.dart
0 → 100644
View file @
cc202df4
import
'../../mobile_card/models/product_mobile_card_model.dart'
;
class
MyVoucherResponse
{
final
int
?
listStart
;
final
int
?
listLimit
;
final
int
?
listTotal
;
final
List
<
ProductMobileCardModel
>?
listItems
;
MyVoucherResponse
({
this
.
listStart
,
this
.
listLimit
,
this
.
listTotal
,
this
.
listItems
,
});
factory
MyVoucherResponse
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
MyVoucherResponse
(
listStart:
json
[
'list_start'
]
as
int
?,
listLimit:
json
[
'list_limit'
]
as
int
?,
listTotal:
json
[
'list_total'
]
as
int
?,
listItems:
(
json
[
'list_items'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
ProductMobileCardModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
(),
);
}
Map
<
String
,
dynamic
>
toJson
()
{
return
{
'list_start'
:
listStart
,
'list_limit'
:
listLimit
,
'list_total'
:
listTotal
,
'list_items'
:
listItems
?.
map
((
e
)
=>
e
.
toJson
()).
toList
(),
};
}
}
\ No newline at end of file
lib/screen/voucher/my_voucher/my_mobile_card_list_viewmodel.dart
0 → 100644
View file @
cc202df4
import
'package:get/get_rx/src/rx_types/rx_types.dart'
;
import
'package:mypoint_flutter_app/configs/constants.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'../../../base/restful_api_viewmodel.dart'
;
import
'../../mobile_card/models/product_mobile_card_model.dart'
;
import
'../models/my_product_status_type.dart'
;
class
MyMobileCardListViewModel
extends
RestfulApiViewModel
{
final
RxInt
selectedTabIndex
=
0
.
obs
;
var
myProducts
=
<
ProductMobileCardModel
>[].
obs
;
void
Function
(
String
message
)?
onShowAlertError
;
@override
void
onInit
()
{
super
.
onInit
();
freshData
(
isRefresh:
true
);
}
void
selectTab
(
int
index
)
{
selectedTabIndex
.
value
=
index
;
freshData
(
isRefresh:
true
);
}
void
freshData
({
bool
isRefresh
=
false
})
{
final
body
=
{
"index"
:
isRefresh
?
0
:
myProducts
.
length
,
"size"
:
20
,
};
final
status
=
selectedTabIndex
.
value
==
0
?
MyProductStatusType
.
waiting
:
MyProductStatusType
.
used
;
client
.
getMyMobileCards
(
status
,
body
).
then
((
response
)
{
if
(!
response
.
isSuccess
)
{
onShowAlertError
?.
call
(
response
.
errorMessage
??
Constants
.
commonError
);
}
final
result
=
response
.
data
?.
listItems
??
[];
if
(
isRefresh
)
{
myProducts
.
clear
();
}
myProducts
.
addAll
(
result
);
}).
catchError
((
error
)
{
myProducts
.
clear
();
print
(
'Error fetching products:
$error
'
);
});
}
}
\ No newline at end of file
lib/screen/voucher/my_voucher/my_mobile_card_list_widget.dart
0 → 100644
View file @
cc202df4
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'../../../widgets/custom_empty_widget.dart'
;
import
'../../../widgets/custom_navigation_bar.dart'
;
import
'../../../widgets/image_loader.dart'
;
import
'../../mobile_card/models/product_mobile_card_model.dart'
;
import
'my_mobile_card_list_viewmodel.dart'
;
import
'package:dotted_border/dotted_border.dart'
;
class
MyMobileCardListScreen
extends
StatefulWidget
{
const
MyMobileCardListScreen
({
super
.
key
});
@override
State
<
MyMobileCardListScreen
>
createState
()
=>
_MyMobileCardListScreenState
();
}
class
_MyMobileCardListScreenState
extends
State
<
MyMobileCardListScreen
>
{
late
final
MyMobileCardListViewModel
_viewModel
=
Get
.
put
(
MyMobileCardListViewModel
());
@override
void
initState
()
{
super
.
initState
();
// _viewModel = Get.put(MyProductListViewModel());
}
@override
Widget
build
(
BuildContext
context
)
{
final
screenWidth
=
MediaQuery
.
of
(
context
).
size
.
width
;
return
Scaffold
(
appBar:
CustomNavigationBar
(
title:
'Thẻ nạp của tôi'
,),
body:
Obx
(
()
=>
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Padding
(
padding:
const
EdgeInsets
.
all
(
8.0
),
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceAround
,
children:
[
_buildTab
(
'Đang có'
,
0
),
_buildTab
(
'Đã sử dụng'
,
1
)],
),
),
const
Divider
(
height:
1
),
if
(
_viewModel
.
myProducts
.
isEmpty
)
Expanded
(
child:
EmptyWidget
(
size:
Size
(
screenWidth
/
2
,
screenWidth
/
2
)))
else
Expanded
(
child:
RefreshIndicator
(
onRefresh:
()
async
{
_viewModel
.
freshData
(
isRefresh:
true
);
},
child:
ListView
.
builder
(
padding:
const
EdgeInsets
.
all
(
12
),
itemCount:
_viewModel
.
myProducts
.
length
,
itemBuilder:
(
_
,
index
)
{
if
(
index
>=
_viewModel
.
myProducts
.
length
)
{
_viewModel
.
freshData
(
isRefresh:
false
);
return
const
Center
(
child:
Padding
(
padding:
EdgeInsets
.
all
(
16
),
child:
CircularProgressIndicator
()),
);
}
final
product
=
_viewModel
.
myProducts
[
index
];
return
_buildVoucherItem
(
product
);
},
),
),
),
],
),
),
);
}
Widget
_buildTab
(
String
title
,
int
index
)
{
return
GestureDetector
(
onTap:
()
=>
_viewModel
.
selectTab
(
index
),
child:
Obx
(
()
=>
Column
(
children:
[
Text
(
title
,
style:
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
w600
,
color:
_viewModel
.
selectedTabIndex
.
value
==
index
?
Colors
.
red
:
Colors
.
black54
,
),
),
const
SizedBox
(
height:
4
),
if
(
_viewModel
.
selectedTabIndex
.
value
==
index
)
Container
(
height:
2
,
width:
60
,
color:
Colors
.
red
),
],
),
),
);
}
Widget
_buildVoucherItem
(
ProductMobileCardModel
product
)
{
return
GestureDetector
(
onTap:
()
{
// Get.toNamed(voucherDetailScreen, arguments: {"customerProductId": product.id});
},
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
vertical:
8
),
padding:
const
EdgeInsets
.
all
(
0
),
child:
DottedBorder
(
color:
Colors
.
redAccent
.
withOpacity
(
0.3
),
borderType:
BorderType
.
RRect
,
radius:
const
Radius
.
circular
(
12
),
dashPattern:
const
[
3
,
3
],
strokeWidth:
1
,
child:
Container
(
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
12
),
color:
Colors
.
redAccent
.
withOpacity
(
0.03
),
),
padding:
const
EdgeInsets
.
all
(
12
),
child:
Row
(
children:
[
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
8
),
child:
loadNetworkImage
(
url:
product
.
brandLogo
??
''
,
width:
64
,
height:
64
,
fit:
BoxFit
.
cover
,
placeholderAsset:
"assets/images/bg_default_11.png"
,
),
),
const
SizedBox
(
width:
16
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
product
.
brandName
??
''
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
bold
,
color:
Colors
.
black54
,
fontSize:
12
),
),
const
SizedBox
(
height:
6
),
Text
(
product
.
name
??
''
,
style:
const
TextStyle
(
fontSize:
15
,
fontWeight:
FontWeight
.
w500
)),
const
SizedBox
(
height:
6
),
Text
(
'HSD:
${product.expire}
'
,
style:
const
TextStyle
(
color:
Colors
.
black54
,
fontSize:
12
)),
],
),
),
],
),
),
),
),
);
}
}
lib/screen/voucher/my_voucher/my_product_list_widget.dart
View file @
cc202df4
...
...
@@ -99,12 +99,16 @@ class _MyVoucherListScreenState extends State<MyVoucherListScreen> {
margin:
const
EdgeInsets
.
symmetric
(
vertical:
8
),
padding:
const
EdgeInsets
.
all
(
0
),
child:
DottedBorder
(
color:
Colors
.
redAccent
.
withOpacity
(
0.
6
),
color:
Colors
.
redAccent
.
withOpacity
(
0.
3
),
borderType:
BorderType
.
RRect
,
radius:
const
Radius
.
circular
(
12
),
dashPattern:
const
[
6
,
4
],
dashPattern:
const
[
3
,
3
],
strokeWidth:
1
,
child:
Container
(
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
12
),
color:
Colors
.
redAccent
.
withOpacity
(
0.03
),
),
padding:
const
EdgeInsets
.
all
(
12
),
child:
Row
(
children:
[
...
...
@@ -127,9 +131,9 @@ class _MyVoucherListScreenState extends State<MyVoucherListScreen> {
product
.
brandName
??
''
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
bold
,
color:
Colors
.
black54
,
fontSize:
12
),
),
const
SizedBox
(
height:
4
),
const
SizedBox
(
height:
6
),
Text
(
product
.
title
??
''
,
style:
const
TextStyle
(
fontSize:
15
,
fontWeight:
FontWeight
.
w500
)),
const
SizedBox
(
height:
4
),
const
SizedBox
(
height:
6
),
Text
(
'HSD:
${product.expire}
'
,
style:
const
TextStyle
(
color:
Colors
.
black54
,
fontSize:
12
)),
],
),
...
...
lib/screen/voucher/voucher_tab_screen.dart
View file @
cc202df4
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/screen/voucher/voucher_list/voucher_list_screen.dart'
;
import
'../../directional/directional_action_type.dart'
;
import
'../../shared/router_gage.dart'
;
import
'../home/header_home_viewmodel.dart'
;
import
'../popup_manager/popup_manager_screen.dart'
;
import
'../popup_manager/popup_manager_viewmodel.dart'
;
import
'../popup_manager/popup_runner_helper.dart'
;
import
'voucher_tab_viewmodel.dart'
;
import
'sub_widget/voucher_action_menu.dart'
;
import
'sub_widget/voucher_item_grid.dart'
;
...
...
@@ -17,17 +21,22 @@ class VoucherTabScreen extends StatefulWidget {
State
<
VoucherTabScreen
>
createState
()
=>
_VoucherTabScreenState
();
}
class
_VoucherTabScreenState
extends
State
<
VoucherTabScreen
>
{
class
_VoucherTabScreenState
extends
State
<
VoucherTabScreen
>
with
PopupOnInit
{
final
_headerHomeVM
=
Get
.
find
<
HeaderHomeViewModel
>();
@override
void
initState
()
{
super
.
initState
();
runPopupCheck
(
DirectionalScreenName
.
productVoucher
);
}
@override
Widget
build
(
BuildContext
context
)
{
final
VoucherTabViewModel
viewModel
=
Get
.
put
(
VoucherTabViewModel
());
return
Scaffold
(
appBar:
CustomNavigationBar
(
title:
"Ưu đãi"
,
showBack
Button:
false
,
left
Button
s
:
[]
,
backgroundImage:
_headerHomeVM
.
headerData
.
background
??
"assets/images/bg_header_navi.png"
,
rightButtons:
[
IconButton
(
...
...
lib/screen/webview/web_view_screen.dart
View file @
cc202df4
...
...
@@ -8,6 +8,7 @@ import 'package:webview_flutter/webview_flutter.dart';
import
'../../base/base_screen.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../directional/directional_screen.dart'
;
import
'../../widgets/custom_navigation_bar.dart'
;
class
BaseWebViewInput
{
final
String
?
title
;
...
...
@@ -58,7 +59,7 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
onWebResourceError:
(
error
)
{
hideLoading
();
if
(
error
.
description
!=
'about:blank'
)
{
showAlertError
(
content:
error
.
description
);
//
showAlertError(content: error.description);
}
},
onNavigationRequest:
_handleNavigation
,
...
...
@@ -81,13 +82,19 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
appBar:
input
.
isFullScreen
?
null
:
AppBar
(
title:
Text
(
input
.
title
??
_dynamicTitle
??
Uri
.
parse
(
input
.
url
).
host
,
style:
const
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
bold
,
color:
Colors
.
black87
),
:
CustomNavigationBar
(
title:
input
.
title
??
_dynamicTitle
??
Uri
.
parse
(
input
.
url
).
host
,
leftButtons:
[
CustomBackButton
(
onPressed:
_handleBack
),
],
),
leading:
CustomBackButton
(
onPressed:
_handleBack
),
),
// AppBar(
// title: Text(
// input.title ?? _dynamicTitle ?? Uri.parse(input.url).host,
// style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
// ),
// leading: CustomBackButton(onPressed: _handleBack),
// ),
body:
Stack
(
children:
[
SafeArea
(
...
...
Prev
1
2
3
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