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
33ec1dde
Commit
33ec1dde
authored
May 23, 2025
by
DatHV
Browse files
update game, notify
parent
8bef97c9
Changes
39
Hide whitespace changes
Inline
Side-by-side
lib/configs/api_paths.dart
View file @
33ec1dde
...
...
@@ -43,4 +43,11 @@ class APIPaths {
static
const
String
getPreviewOrderInfo
=
"/order/api/v1.0/preview-order"
;
static
const
String
getPreviewOrderBankAccounts
=
"/order/api/v1.0/payment/bank-accounts"
;
static
const
String
getPreviewPaymentMethods
=
"/order/api/v1.0/payment/payment-methods"
;
static
const
String
orderSubmitPayment
=
"/order/api/v1.0/submit-order"
;
static
const
String
getTransactionHistoryDetail
=
"/order/api/v1.0/my-orders/%@"
;
static
const
String
getNotificationCategories
=
"/dynamic-home/api/v1.0/notification_groups"
;
static
const
String
getNotifications
=
"/notificationGetList/1.0.0"
;
static
const
String
deleteNotification
=
"/notificationDeleteOne/1.0.0"
;
static
const
String
deleteAllNotifications
=
"/notificationDeleteAll/1.0.0"
;
static
const
String
notificationMarkAsSeen
=
"/notificationMarkAsSeen/1.0.0"
;
}
\ No newline at end of file
lib/networking/restful_api_request.dart
View file @
33ec1dde
...
...
@@ -15,6 +15,8 @@ import '../model/update_response_model.dart';
import
'../preference/point/header_home_model.dart'
;
import
'../screen/faqs/faqs_model.dart'
;
import
'../screen/game/models/game_bundle_item_model.dart'
;
import
'../screen/notification/models/category_notify_item_model.dart'
;
import
'../screen/notification/models/notification_list_data_model.dart'
;
import
'../screen/onboarding/model/check_phone_response_model.dart'
;
import
'../screen/onboarding/model/onboarding_info_model.dart'
;
import
'../screen/otp/model/create_otp_response_model.dart'
;
...
...
@@ -27,8 +29,11 @@ import '../screen/shopping/model/affiliate_category_model.dart';
import
'../screen/shopping/model/affiliate_product_top_sale_model.dart'
;
import
'../screen/shopping/model/cashback_overview_model.dart'
;
import
'../screen/splash/splash_screen_viewmodel.dart'
;
import
'../screen/transaction/history/transaction_history_model.dart'
;
import
'../screen/transaction/model/order_product_payment_response_model.dart'
;
import
'../screen/transaction/model/payment_bank_account_info_model.dart'
;
import
'../screen/transaction/model/payment_method_model.dart'
;
import
'../screen/transaction/model/payment_method_type.dart'
;
import
'../screen/transaction/model/preview_order_payment_model.dart'
;
import
'../screen/voucher/models/like_product_reponse_model.dart'
;
import
'../screen/voucher/models/product_store_model.dart'
;
...
...
@@ -360,13 +365,9 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
}
Future
<
BaseResponseModel
<
PreviewOrderPaymentModel
>>
getPreviewOrderInfo
(
Json
body
)
async
{
return
requestNormal
(
APIPaths
.
getPreviewOrderInfo
,
Method
.
POST
,
body
,
(
data
)
{
return
PreviewOrderPaymentModel
.
fromJson
(
data
as
Json
);
});
return
requestNormal
(
APIPaths
.
getPreviewOrderInfo
,
Method
.
POST
,
body
,
(
data
)
{
return
PreviewOrderPaymentModel
.
fromJson
(
data
as
Json
);
});
}
Future
<
BaseResponseModel
<
List
<
PaymentBankAccountInfoModel
>>>
getPreviewOrderBankAccounts
()
async
{
...
...
@@ -382,4 +383,91 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
return
list
.
map
((
e
)
=>
PaymentMethodModel
.
fromJson
(
e
)).
toList
();
});
}
Future
<
BaseResponseModel
<
List
<
CategoryNotifyItemModel
>>>
getNotificationCategories
()
async
{
return
requestNormal
(
APIPaths
.
getNotificationCategories
,
Method
.
GET
,
{},
(
data
)
{
final
list
=
data
as
List
<
dynamic
>;
return
list
.
map
((
e
)
=>
CategoryNotifyItemModel
.
fromJson
(
e
)).
toList
();
});
}
Future
<
BaseResponseModel
<
NotificationListDataModel
>>
getNotifications
(
Json
body
)
async
{
return
requestNormal
(
APIPaths
.
getNotifications
,
Method
.
POST
,
body
,
(
data
)
{
return
NotificationListDataModel
.
fromJson
(
data
as
Json
);
});
}
Future
<
BaseResponseModel
<
EmptyCodable
>>
deleteNotification
(
String
id
)
async
{
return
requestNormal
(
APIPaths
.
deleteNotification
,
Method
.
POST
,
{
"notification_id"
:
id
},
(
data
)
{
return
EmptyCodable
.
fromJson
(
data
as
Json
);
});
}
Future
<
BaseResponseModel
<
EmptyCodable
>>
deleteAllNotifications
()
async
{
String
?
token
=
DataPreference
.
instance
.
token
??
""
;
final
body
=
{
"access_token"
:
token
};
return
requestNormal
(
APIPaths
.
deleteAllNotifications
,
Method
.
POST
,
body
,
(
data
)
{
return
EmptyCodable
.
fromJson
(
data
as
Json
);
});
}
Future
<
BaseResponseModel
<
EmptyCodable
>>
notificationMarkAsSeen
()
async
{
String
?
token
=
DataPreference
.
instance
.
token
??
""
;
final
body
=
{
"access_token"
:
token
};
return
requestNormal
(
APIPaths
.
notificationMarkAsSeen
,
Method
.
POST
,
body
,
(
data
)
{
return
EmptyCodable
.
fromJson
(
data
as
Json
);
});
}
Future
<
BaseResponseModel
<
TransactionHistoryModel
>>
getTransactionHistoryDetail
(
String
id
)
async
{
final
path
=
APIPaths
.
getTransactionHistoryDetail
.
replaceAll
(
"%@"
,
id
);
return
requestNormal
(
path
,
Method
.
GET
,
{},
(
data
)
{
return
TransactionHistoryModel
.
fromJson
(
data
as
Json
);
});
}
Future
<
BaseResponseModel
<
OrderProductPaymentResponseModel
>>
orderSubmitPayment
({
required
List
<
ProductModel
>
products
,
required
int
quantity
,
required
String
requestId
,
int
?
point
,
int
?
cash
,
PaymentMethodModel
?
method
,
int
?
paymentTokenId
,
bool
?
saveToken
,
String
?
metadata
,
})
async
{
final
items
=
products
.
map
((
product
)
{
return
{
'product_id'
:
product
.
id
,
'product_type'
:
product
.
type
??
''
,
'quantity'
:
quantity
};
}).
toList
();
final
Map
<
String
,
dynamic
>
params
=
{
'request_id'
:
requestId
,
'items'
:
items
,
'flow'
:
'21'
};
// flash_sale
final
firstProduct
=
products
.
first
;
if
(
firstProduct
.
previewFlashSale
?.
isFlashSalePrice
==
true
&&
firstProduct
.
previewFlashSale
?.
id
!=
null
)
{
params
[
'flash_sale_id'
]
=
firstProduct
.
previewFlashSale
!.
id
;
}
// Optional parameters
if
(
method
!=
null
)
{
params
[
'payment_method'
]
=
method
.
code
;
}
if
(
point
!=
null
&&
point
!=
0
)
{
params
[
'pay_point'
]
=
point
;
}
if
(
cash
!=
null
&&
cash
!=
0
)
{
params
[
'pay_cash'
]
=
cash
;
}
if
(
paymentTokenId
!=
null
)
{
params
[
'payment_token_id'
]
=
paymentTokenId
;
}
if
(
saveToken
!=
null
)
{
params
[
'save_token'
]
=
saveToken
;
}
if
(
metadata
!=
null
)
{
params
[
'metadata'
]
=
metadata
;
}
return
requestNormal
(
APIPaths
.
orderSubmitPayment
,
Method
.
POST
,
params
,
(
data
)
{
return
OrderProductPaymentResponseModel
.
fromJson
(
data
as
Json
);
});
}
}
lib/screen/home/home_screen.dart
View file @
33ec1dde
...
...
@@ -20,12 +20,17 @@ class HomeScreen extends StatelessWidget {
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'
)),
],
),
),
);
}
void
_showNotify
(
BuildContext
context
)
async
{
Get
.
toNamed
(
notificationScreen
);
}
void
_showMiniGame
(
BuildContext
context
)
async
{
Navigator
.
push
(
context
,
...
...
lib/screen/notification/models/category_notify_item_model.dart
0 → 100644
View file @
33ec1dde
import
'package:json_annotation/json_annotation.dart'
;
part
'category_notify_item_model.g.dart'
;
@JsonSerializable
()
class
CategoryNotifyItemModel
{
int
?
id
;
@JsonKey
(
name:
'name_vi'
)
String
?
nameVi
;
@JsonKey
(
name:
'total_unread'
)
int
?
totalUnread
;
bool
?
isSelected
;
CategoryNotifyItemModel
({
this
.
id
,
this
.
nameVi
,
this
.
totalUnread
,
this
.
isSelected
,
});
factory
CategoryNotifyItemModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$CategoryNotifyItemModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$CategoryNotifyItemModelToJson
(
this
);
}
\ No newline at end of file
lib/screen/notification/models/category_notify_item_model.g.dart
0 → 100644
View file @
33ec1dde
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'category_notify_item_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CategoryNotifyItemModel
_$CategoryNotifyItemModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
CategoryNotifyItemModel
(
id:
(
json
[
'id'
]
as
num
?)?.
toInt
(),
nameVi:
json
[
'name_vi'
]
as
String
?,
totalUnread:
(
json
[
'total_unread'
]
as
num
?)?.
toInt
(),
isSelected:
json
[
'isSelected'
]
as
bool
?,
);
Map
<
String
,
dynamic
>
_$CategoryNotifyItemModelToJson
(
CategoryNotifyItemModel
instance
,
)
=>
<
String
,
dynamic
>{
'id'
:
instance
.
id
,
'name_vi'
:
instance
.
nameVi
,
'total_unread'
:
instance
.
totalUnread
,
'isSelected'
:
instance
.
isSelected
,
};
lib/screen/notification/models/notification_item_model.dart
0 → 100644
View file @
33ec1dde
import
'package:json_annotation/json_annotation.dart'
;
part
'notification_item_model.g.dart'
;
@JsonSerializable
()
class
NotificationItemModel
{
@JsonKey
(
name:
'notification_id'
)
final
String
?
notificationId
;
final
String
?
title
;
final
String
?
body
;
final
String
?
type
;
@JsonKey
(
name:
'click_action_type'
)
final
String
?
clickActionType
;
@JsonKey
(
name:
'click_action_param'
)
final
String
?
clickActionParam
;
@JsonKey
(
name:
'seen_at'
)
final
String
?
seenAt
;
final
String
?
status
;
@JsonKey
(
name:
'create_time'
)
final
String
?
createTime
;
@JsonKey
(
name:
'working_site'
)
final
WorkingSiteModel
?
workingSite
;
bool
get
hasSeen
=>
(
seenAt
??
""
).
isNotEmpty
;
NotificationItemModel
({
this
.
notificationId
,
this
.
title
,
this
.
body
,
this
.
type
,
this
.
clickActionType
,
this
.
clickActionParam
,
this
.
seenAt
,
this
.
status
,
this
.
createTime
,
this
.
workingSite
,
});
factory
NotificationItemModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$NotificationItemModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$NotificationItemModelToJson
(
this
);
}
class
WorkingSiteModel
{
final
String
?
avatar
;
WorkingSiteModel
({
this
.
avatar
,
});
factory
WorkingSiteModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
WorkingSiteModel
(
avatar:
json
[
'avatar'
]
as
String
?,
);
}
Map
<
String
,
dynamic
>
toJson
()
{
return
{
'avatar'
:
avatar
,
};
}
}
\ No newline at end of file
lib/screen/notification/models/notification_item_model.g.dart
0 → 100644
View file @
33ec1dde
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'notification_item_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
NotificationItemModel
_$NotificationItemModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
NotificationItemModel
(
notificationId:
json
[
'notification_id'
]
as
String
?,
title:
json
[
'title'
]
as
String
?,
body:
json
[
'body'
]
as
String
?,
type:
json
[
'type'
]
as
String
?,
clickActionType:
json
[
'click_action_type'
]
as
String
?,
clickActionParam:
json
[
'click_action_param'
]
as
String
?,
seenAt:
json
[
'seen_at'
]
as
String
?,
status:
json
[
'status'
]
as
String
?,
createTime:
json
[
'create_time'
]
as
String
?,
workingSite:
json
[
'working_site'
]
==
null
?
null
:
WorkingSiteModel
.
fromJson
(
json
[
'working_site'
]
as
Map
<
String
,
dynamic
>,
),
);
Map
<
String
,
dynamic
>
_$NotificationItemModelToJson
(
NotificationItemModel
instance
,
)
=>
<
String
,
dynamic
>{
'notification_id'
:
instance
.
notificationId
,
'title'
:
instance
.
title
,
'body'
:
instance
.
body
,
'type'
:
instance
.
type
,
'click_action_type'
:
instance
.
clickActionType
,
'click_action_param'
:
instance
.
clickActionParam
,
'seen_at'
:
instance
.
seenAt
,
'status'
:
instance
.
status
,
'create_time'
:
instance
.
createTime
,
'working_site'
:
instance
.
workingSite
,
};
lib/screen/notification/models/notification_list_data_model.dart
0 → 100644
View file @
33ec1dde
import
'package:json_annotation/json_annotation.dart'
;
import
'notification_item_model.dart'
;
part
'notification_list_data_model.g.dart'
;
@JsonSerializable
()
class
NotificationListDataModel
{
final
String
?
start
;
final
String
?
limit
;
final
String
?
total
;
final
String
?
unread
;
@JsonKey
(
name:
'list_items'
)
final
List
<
NotificationItemModel
>?
items
;
NotificationListDataModel
({
this
.
start
,
this
.
limit
,
this
.
total
,
this
.
unread
,
this
.
items
,
});
factory
NotificationListDataModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$NotificationListDataModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$NotificationListDataModelToJson
(
this
);
}
\ No newline at end of file
lib/screen/notification/models/notification_list_data_model.g.dart
0 → 100644
View file @
33ec1dde
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'notification_list_data_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
NotificationListDataModel
_$NotificationListDataModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
NotificationListDataModel
(
start:
json
[
'start'
]
as
String
?,
limit:
json
[
'limit'
]
as
String
?,
total:
json
[
'total'
]
as
String
?,
unread:
json
[
'unread'
]
as
String
?,
items:
(
json
[
'list_items'
]
as
List
<
dynamic
>?)
?.
map
(
(
e
)
=>
NotificationItemModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>),
)
.
toList
(),
);
Map
<
String
,
dynamic
>
_$NotificationListDataModelToJson
(
NotificationListDataModel
instance
,
)
=>
<
String
,
dynamic
>{
'start'
:
instance
.
start
,
'limit'
:
instance
.
limit
,
'total'
:
instance
.
total
,
'unread'
:
instance
.
unread
,
'list_items'
:
instance
.
items
,
};
lib/screen/notification/notification_screen.dart
0 → 100644
View file @
33ec1dde
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:get/get.dart'
;
import
'../../base/base_screen.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../resouce/base_color.dart'
;
import
'../../widgets/alert/data_alert_model.dart'
;
import
'../../widgets/back_button.dart'
;
import
'../../widgets/custom_empty_widget.dart'
;
import
'../../widgets/image_loader.dart'
;
import
'models/notification_item_model.dart'
;
import
'notification_viewmodel.dart'
;
class
NotificationScreen
extends
BaseScreen
{
const
NotificationScreen
({
super
.
key
});
@override
State
<
NotificationScreen
>
createState
()
=>
_NotificationScreenState
();
}
class
_NotificationScreenState
extends
BaseState
<
NotificationScreen
>
with
BasicState
{
final
_scrollController
=
ScrollController
();
final
_viewModel
=
Get
.
put
(
NotificationViewModel
());
final
LayerLink
_layerLink
=
LayerLink
();
final
GlobalKey
_infoKey
=
GlobalKey
();
OverlayEntry
?
_popupEntry
;
bool
_isPopupShown
=
false
;
@override
void
initState
()
{
super
.
initState
();
_scrollController
.
addListener
(()
{
if
(
_scrollController
.
position
.
pixels
>=
_scrollController
.
position
.
maxScrollExtent
-
100
)
{
_viewModel
.
fetchNotifications
(
refresh:
false
);
}
});
}
@override
Widget
createBody
()
{
return
Scaffold
(
appBar:
AppBar
(
scrolledUnderElevation:
0
,
backgroundColor:
Colors
.
white
,
elevation:
0
,
centerTitle:
true
,
title:
const
Text
(
'Thông báo'
,
style:
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
bold
,
color:
Colors
.
black87
),
),
leading:
CustomBackButton
(),
actions:
[
CompositedTransformTarget
(
link:
_layerLink
,
child:
IconButton
(
key:
_infoKey
,
icon:
const
Icon
(
Icons
.
settings
,
color:
Colors
.
black
),
onPressed:
_toggleSetting
,
),
),
],
),
body:
Obx
(()
{
final
items
=
_viewModel
.
notifications
;
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
_buildNotificationCategory
(),
const
Divider
(
height:
1
),
if
(
items
.
isEmpty
)
const
Expanded
(
child:
EmptyWidget
(),
)
else
Expanded
(
child:
Container
(
color:
Colors
.
grey
.
shade100
,
child:
RefreshIndicator
(
onRefresh:
()
async
{
_viewModel
.
fetchNotifications
(
refresh:
true
);
},
child:
ListView
.
builder
(
controller:
_scrollController
,
padding:
const
EdgeInsets
.
all
(
16
),
itemCount:
items
.
length
,
itemBuilder:
(
_
,
index
)
{
final
item
=
items
[
index
];
return
_buildNotificationItem
(
item
);
},
),
),
),
),
],
);
}),
);
}
void
_toggleSetting
()
{
if
(
_isPopupShown
)
{
_hidePopup
();
}
else
{
_showPopup
();
}
}
void
_showPopup
()
{
final
overlay
=
Overlay
.
of
(
context
);
final
renderBox
=
_infoKey
.
currentContext
?.
findRenderObject
()
as
RenderBox
?;
final
offset
=
renderBox
?.
localToGlobal
(
Offset
.
zero
)
??
Offset
.
zero
;
final
size
=
renderBox
?.
size
??
Size
.
zero
;
final
double
widthSize
=
270
;
_popupEntry
=
OverlayEntry
(
builder:
(
context
)
=>
Stack
(
children:
[
Positioned
.
fill
(
child:
GestureDetector
(
onTap:
_hidePopup
,
behavior:
HitTestBehavior
.
translucent
,
child:
Container
(
color:
Colors
.
transparent
),
),
),
Positioned
(
top:
offset
.
dy
+
size
.
height
+
8
,
left:
MediaQuery
.
of
(
context
).
size
.
width
-
widthSize
-
16
,
child:
Material
(
borderRadius:
BorderRadius
.
circular
(
16
),
elevation:
4
,
child:
Container
(
width:
widthSize
,
padding:
const
EdgeInsets
.
all
(
16
),
decoration:
BoxDecoration
(
color:
Colors
.
grey
.
shade50
,
borderRadius:
BorderRadius
.
circular
(
16
),
),
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
ListTile
(
title:
const
Text
(
'Đánh dấu tất cả đã đọc'
,
style:
TextStyle
(
fontWeight:
FontWeight
.
w500
)),
onTap:
()
{
_hidePopup
();
_viewModel
.
notificationMarkAsSeen
();
},
),
const
Divider
(
height:
1
,
color:
Colors
.
grey
),
ListTile
(
title:
const
Text
(
'Xoá tất cả'
,
style:
TextStyle
(
color:
Colors
.
red
,
fontWeight:
FontWeight
.
w500
)),
onTap:
()
{
_hidePopup
();
_confirmDeleteAllNotifications
();
}
),
],
),
),
),
),
],
),
);
overlay
.
insert
(
_popupEntry
!);
_isPopupShown
=
true
;
}
_confirmDeleteAllNotifications
()
{
final
dataAlert
=
DataAlertModel
(
title:
"Quên mật khẩu"
,
description:
"Bạn cần đăng xuất khỏi tài khoản này để đặt lại mật khẩu. Bạn chắc chứ?."
,
localHeaderImage:
"assets/images/ic_pipi_03.png"
,
buttons:
[
AlertButton
(
text:
"Đồng ý"
,
onPressed:
()
{
Get
.
back
();
_viewModel
.
deleteAllNotifications
();
},
bgColor:
BaseColor
.
primary500
,
textColor:
Colors
.
white
,
),
AlertButton
(
text:
"Huỷ"
,
onPressed:
()
=>
Get
.
back
(),
bgColor:
Colors
.
white
,
textColor:
BaseColor
.
second500
,
),],
);
showAlert
(
data:
dataAlert
);
}
void
_hidePopup
()
{
_popupEntry
?.
remove
();
_popupEntry
=
null
;
_isPopupShown
=
false
;
}
Widget
_buildNotificationCategory
()
{
final
categories
=
_viewModel
.
categories
;
return
SizedBox
(
height:
60
,
child:
Center
(
child:
ListView
.
separated
(
scrollDirection:
Axis
.
horizontal
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
),
itemCount:
categories
.
length
,
separatorBuilder:
(
_
,
__
)
=>
const
SizedBox
(
width:
12
),
itemBuilder:
(
_
,
index
)
{
final
cat
=
categories
[
index
];
final
selected
=
cat
.
isSelected
??
false
;
return
GestureDetector
(
onTap:
()
=>
_viewModel
.
selectCategory
(
index
),
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Stack
(
clipBehavior:
Clip
.
none
,
children:
[
Row
(
children:
[
const
SizedBox
(
width:
4
),
Text
(
"
${cat.nameVi ?? ''}
"
,
style:
TextStyle
(
color:
selected
?
Colors
.
red
:
Colors
.
black87
,
fontSize:
16
,
fontWeight:
selected
?
FontWeight
.
bold
:
FontWeight
.
normal
,
),
),
const
SizedBox
(
width:
4
),
],
),
if
((
cat
.
totalUnread
??
0
)
>
0
)
Positioned
(
top:
-
6
,
right:
0
,
child:
Container
(
width:
6
,
height:
6
,
decoration:
const
BoxDecoration
(
shape:
BoxShape
.
circle
,
color:
Colors
.
red
),
),
),
],
),
const
SizedBox
(
height:
4
),
AnimatedContainer
(
duration:
const
Duration
(
milliseconds:
200
),
height:
2
,
width:
selected
?
32
:
0
,
color:
Colors
.
red
,
),
],
),
);
},
),
),
);
}
Widget
_buildNotificationItem
(
NotificationItemModel
item
)
{
return
Container
(
margin:
const
EdgeInsets
.
only
(
bottom:
12
),
padding:
const
EdgeInsets
.
all
(
12
),
decoration:
BoxDecoration
(
color:
item
.
hasSeen
?
Colors
.
white
:
Colors
.
pink
.
shade50
,
borderRadius:
BorderRadius
.
circular
(
12
)
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
8
),
child:
loadNetworkImage
(
url:
item
.
workingSite
?.
avatar
??
""
,
fit:
BoxFit
.
cover
,
width:
40
,
height:
40
,
placeholderAsset:
'assets/images/ic_logo.png'
,
),
),
const
SizedBox
(
width:
12
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
item
.
title
??
''
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
15
)),
const
SizedBox
(
height:
8
),
Text
(
item
.
body
??
''
,
style:
const
TextStyle
(
fontSize:
14
)),
const
SizedBox
(
height:
8
),
Text
(
item
.
createTime
??
''
,
style:
const
TextStyle
(
fontSize:
13
,
color:
Colors
.
black54
)),
],
),
),
],
),
],
),
);
}
}
lib/screen/notification/notification_viewmodel.dart
0 → 100644
View file @
33ec1dde
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../../preference/data_preference.dart'
;
import
'models/category_notify_item_model.dart'
;
import
'models/notification_item_model.dart'
;
class
NotificationViewModel
extends
RestfulApiViewModel
{
var
categories
=
RxList
<
CategoryNotifyItemModel
>();
var
notifications
=
RxList
<
NotificationItemModel
>();
final
RxBool
isLoading
=
false
.
obs
;
CategoryNotifyItemModel
?
get
selectedCategory
=>
categories
.
isNotEmpty
?
categories
.
firstWhere
((
item
)
=>
item
.
isSelected
??
false
)
:
null
;
@override
void
onInit
()
{
super
.
onInit
();
_fetchCategories
();
}
void
_fetchCategories
()
async
{
showLoading
();
client
.
getNotificationCategories
().
then
((
value
)
{
final
results
=
value
.
data
??
[];
if
(
results
.
isNotEmpty
)
{
results
[
0
].
isSelected
=
true
;
}
categories
.
value
=
results
;
fetchNotifications
(
refresh:
true
);
});
}
void
fetchNotifications
({
bool
refresh
=
false
})
async
{
if
(
isLoading
.
value
)
return
;
isLoading
.
value
=
true
;
String
?
token
=
DataPreference
.
instance
.
token
??
""
;
final
body
=
{
"access_token"
:
token
,
"start"
:
refresh
?
0
:
notifications
.
length
,
"limit"
:
10
,
"noti_group_id"
:
selectedCategory
?.
id
??
""
,
};
client
.
getNotifications
(
body
).
then
((
value
)
{
isLoading
.
value
=
false
;
hideLoading
();
if
(
refresh
)
{
notifications
.
value
=
value
.
data
?.
items
??
[];
}
else
{
notifications
.
addAll
(
value
.
data
?.
items
??
[]);
}
});
}
void
selectCategory
(
int
index
)
{
for
(
var
i
=
0
;
i
<
categories
.
length
;
i
++)
{
categories
[
i
].
isSelected
=
i
==
index
;
}
fetchNotifications
(
refresh:
true
);
}
notificationMarkAsSeen
()
{
client
.
notificationMarkAsSeen
().
then
((
value
)
{
_fetchCategories
();
});
}
deleteAllNotifications
()
{
client
.
deleteAllNotifications
().
then
((
value
)
{
_fetchCategories
();
});
}
}
lib/screen/register_campaign/register_form_input_screen.dart
View file @
33ec1dde
...
...
@@ -5,6 +5,7 @@ import 'package:mypoint_flutter_app/resouce/base_color.dart';
import
'package:mypoint_flutter_app/screen/register_campaign/register_form_input_viewmodel.dart'
;
import
'../../shared/router_gage.dart'
;
import
'../../widgets/custom_app_bar.dart'
;
import
'../voucher/models/product_model.dart'
;
import
'input_form_cell.dart'
;
import
'model/registration_form_package_model.dart'
;
...
...
@@ -33,7 +34,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
_title
=
data
?.
formConfirm
?.
title
??
''
;
_viewModel
.
form
.
value
=
data
;
}
else
{
final
id
=
args
?[
'
id'
]
as
int
?
;
final
id
=
(
args
?[
'
product'
]
as
ProductModel
?)?.
id
;
if
(
id
!=
null
)
{
_isConfirmScreen
=
false
;
_viewModel
.
fetchRegisterFormInput
(
id
.
toString
()).
then
((
_
)
{
...
...
lib/screen/splash/splash_screen_viewmodel.dart
View file @
33ec1dde
...
...
@@ -6,6 +6,7 @@ import '../../base/base_response_model.dart';
import
'../../configs/constants.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'
;
...
...
@@ -44,6 +45,7 @@ class SplashScreenViewModel extends RestfulApiViewModel {
final
userProfile
=
value
.
data
;
if
(
value
.
isSuccess
&&
userProfile
!=
null
)
{
await
DataPreference
.
instance
.
saveUserProfile
(
userProfile
);
await
UserPointManager
().
fetchUserPoint
();
Get
.
toNamed
(
mainScreen
);
}
else
{
DataPreference
.
instance
.
clearLoginToken
();
...
...
lib/screen/support/transaction_history_screen.dart
deleted
100644 → 0
View file @
8bef97c9
import
'package:flutter/material.dart'
;
import
'package:intl/intl.dart'
;
import
'transaction_service.dart'
;
class
TransactionHistoryScreen
extends
StatefulWidget
{
const
TransactionHistoryScreen
({
super
.
key
});
@override
State
<
TransactionHistoryScreen
>
createState
()
=>
_TransactionHistoryScreenState
();
}
class
_TransactionHistoryScreenState
extends
State
<
TransactionHistoryScreen
>
{
final
List
<
String
>
categories
=
[
'Tất cả'
,
'Viện thông'
,
'Mua sắm'
,
'Ưu đãi'
,
'Hóa đơn'
,
];
int
selectedCategoryIndex
=
0
;
DateTime
selectedMonth
=
DateTime
.
now
();
bool
isLoading
=
false
;
List
<
Transaction
>
transactions
=
[];
final
TransactionService
_transactionService
=
TransactionService
();
@override
void
initState
()
{
super
.
initState
();
fetchTransactions
();
}
Future
<
void
>
fetchTransactions
()
async
{
setState
(()
{
isLoading
=
true
;
});
try
{
final
result
=
await
_transactionService
.
getTransactions
(
category:
categories
[
selectedCategoryIndex
],
month:
selectedMonth
,
);
setState
(()
{
transactions
=
result
;
isLoading
=
false
;
});
}
catch
(
e
)
{
setState
(()
{
isLoading
=
false
;
});
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
content:
Text
(
'Không thể tải dữ liệu:
$e
'
)),
);
}
}
void
selectCategory
(
int
index
)
{
setState
(()
{
selectedCategoryIndex
=
index
;
});
fetchTransactions
();
}
void
selectMonth
()
async
{
final
DateTime
?
picked
=
await
showDatePicker
(
context:
context
,
initialDate:
selectedMonth
,
firstDate:
DateTime
(
2020
),
lastDate:
DateTime
(
2026
),
initialDatePickerMode:
DatePickerMode
.
year
,
builder:
(
context
,
child
)
{
return
Theme
(
data:
Theme
.
of
(
context
).
copyWith
(
colorScheme:
ColorScheme
.
light
(
primary:
Colors
.
red
.
shade400
,
onPrimary:
Colors
.
white
,
onSurface:
Colors
.
black
,
),
),
child:
child
!,
);
},
);
if
(
picked
!=
null
)
{
setState
(()
{
selectedMonth
=
picked
;
});
fetchTransactions
();
}
}
@override
Widget
build
(
BuildContext
context
)
{
final
transactionCount
=
transactions
.
length
;
final
hasTransactions
=
transactionCount
>
0
;
// Tính tổng tiền và điểm
int
totalAmount
=
0
;
int
totalPoints
=
0
;
for
(
var
transaction
in
transactions
)
{
totalAmount
+=
transaction
.
amount
;
totalPoints
+=
transaction
.
points
;
}
return
Scaffold
(
backgroundColor:
Colors
.
grey
.
shade50
,
appBar:
AppBar
(
backgroundColor:
Colors
.
white
,
elevation:
0
,
centerTitle:
true
,
title:
const
Text
(
'Lịch sử giao dịch'
,
style:
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
w500
,
color:
Colors
.
black87
,
),
),
leading:
IconButton
(
icon:
const
Icon
(
Icons
.
arrow_back_ios
,
size:
20
,
color:
Colors
.
black54
),
onPressed:
()
=>
Navigator
.
of
(
context
).
pop
(),
),
),
body:
Column
(
children:
[
// Danh mục
Container
(
height:
60
,
color:
Colors
.
white
,
child:
ListView
.
builder
(
scrollDirection:
Axis
.
horizontal
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
),
itemCount:
categories
.
length
,
itemBuilder:
(
context
,
index
)
{
final
isSelected
=
selectedCategoryIndex
==
index
;
return
GestureDetector
(
onTap:
()
=>
selectCategory
(
index
),
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
4
,
vertical:
10
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
decoration:
BoxDecoration
(
color:
isSelected
?
Colors
.
red
.
shade50
:
Colors
.
grey
.
shade100
,
borderRadius:
BorderRadius
.
circular
(
20
),
border:
isSelected
?
Border
.
all
(
color:
Colors
.
red
.
shade100
)
:
null
,
),
alignment:
Alignment
.
center
,
child:
Text
(
categories
[
index
],
style:
TextStyle
(
color:
isSelected
?
Colors
.
red
:
Colors
.
grey
.
shade700
,
fontWeight:
isSelected
?
FontWeight
.
w500
:
FontWeight
.
normal
,
),
),
),
);
},
),
),
const
SizedBox
(
height:
8
),
// Tháng và số giao dịch
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
Text
(
'Tháng
${DateFormat('MM/yyyy').format(selectedMonth)}
(
${transactionCount}
giao dịch)'
,
style:
const
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
w500
,
color:
Colors
.
black87
,
),
),
InkWell
(
onTap:
selectMonth
,
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
6
),
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
Colors
.
grey
.
shade300
),
borderRadius:
BorderRadius
.
circular
(
4
),
),
child:
Row
(
children:
[
const
Text
(
'Tháng'
,
style:
TextStyle
(
fontSize:
14
,
color:
Colors
.
black87
,
),
),
const
SizedBox
(
width:
4
),
Icon
(
Icons
.
keyboard_arrow_down
,
size:
18
,
color:
Colors
.
grey
.
shade700
),
],
),
),
),
],
),
),
// Tổng tiền và điểm
Container
(
margin:
const
EdgeInsets
.
all
(
16
),
padding:
const
EdgeInsets
.
all
(
16
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
12
),
border:
Border
.
all
(
color:
Colors
.
grey
.
shade200
),
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'
${totalAmount}
đ'
,
style:
const
TextStyle
(
fontSize:
24
,
fontWeight:
FontWeight
.
bold
,
),
),
const
SizedBox
(
height:
8
),
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
const
Text
(
'Giao dịch bằng tiền'
,
style:
TextStyle
(
color:
Colors
.
black54
,
fontSize:
14
,
),
),
Text
(
'
${totalAmount}
đ'
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w500
,
),
),
],
),
const
SizedBox
(
height:
4
),
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
const
Text
(
'Giao dịch bằng điểm'
,
style:
TextStyle
(
color:
Colors
.
black54
,
fontSize:
14
,
),
),
Row
(
children:
[
Container
(
width:
18
,
height:
18
,
decoration:
const
BoxDecoration
(
color:
Colors
.
amber
,
shape:
BoxShape
.
circle
,
),
child:
const
Center
(
child:
Icon
(
Icons
.
currency_exchange
,
size:
12
,
color:
Colors
.
white
,
),
),
),
const
SizedBox
(
width:
4
),
Text
(
'
$totalPoints
'
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w500
,
),
),
],
),
],
),
],
),
),
// Danh sách giao dịch hoặc trạng thái trống
Expanded
(
child:
isLoading
?
const
Center
(
child:
CircularProgressIndicator
())
:
hasTransactions
?
ListView
.
builder
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
itemCount:
transactions
.
length
,
itemBuilder:
(
context
,
index
)
{
final
transaction
=
transactions
[
index
];
return
TransactionItem
(
transaction:
transaction
);
},
)
:
const
EmptyTransactionState
(),
),
],
),
);
}
}
class
TransactionItem
extends
StatelessWidget
{
final
Transaction
transaction
;
const
TransactionItem
({
super
.
key
,
required
this
.
transaction
,
});
@override
Widget
build
(
BuildContext
context
)
{
return
Container
(
margin:
const
EdgeInsets
.
only
(
bottom:
12
),
child:
Row
(
children:
[
// Icon
Container
(
width:
40
,
height:
40
,
decoration:
BoxDecoration
(
color:
Colors
.
red
.
shade50
,
borderRadius:
BorderRadius
.
circular
(
8
),
),
child:
Stack
(
children:
[
Center
(
child:
Icon
(
transaction
.
icon
,
color:
Colors
.
red
,
size:
20
,
),
),
if
(
transaction
.
status
==
TransactionStatus
.
completed
)
Positioned
(
right:
0
,
bottom:
0
,
child:
Container
(
width:
16
,
height:
16
,
decoration:
BoxDecoration
(
color:
Colors
.
green
,
shape:
BoxShape
.
circle
,
border:
Border
.
all
(
color:
Colors
.
white
,
width:
2
),
),
child:
const
Center
(
child:
Icon
(
Icons
.
check
,
size:
8
,
color:
Colors
.
white
,
),
),
),
),
],
),
),
const
SizedBox
(
width:
12
),
// Thông tin giao dịch
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
transaction
.
title
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w500
,
fontSize:
15
,
),
),
const
SizedBox
(
height:
2
),
Text
(
'
${DateFormat('HH:mm').format(transaction.date)}
-
${DateFormat('dd/MM/yyyy').format(transaction.date)}
'
,
style:
TextStyle
(
color:
Colors
.
grey
.
shade600
,
fontSize:
13
,
),
),
],
),
),
// Điểm
Row
(
children:
[
Container
(
width:
20
,
height:
20
,
decoration:
const
BoxDecoration
(
color:
Colors
.
amber
,
shape:
BoxShape
.
circle
,
),
child:
const
Center
(
child:
Icon
(
Icons
.
currency_exchange
,
size:
12
,
color:
Colors
.
white
,
),
),
),
const
SizedBox
(
width:
4
),
Text
(
'
${transaction.points}
'
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w500
,
fontSize:
15
,
),
),
],
),
],
),
);
}
}
class
EmptyTransactionState
extends
StatelessWidget
{
const
EmptyTransactionState
({
super
.
key
});
@override
Widget
build
(
BuildContext
context
)
{
return
Center
(
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Container
(
width:
120
,
height:
120
,
decoration:
BoxDecoration
(
color:
Colors
.
grey
.
shade100
,
shape:
BoxShape
.
circle
,
),
child:
Center
(
child:
Icon
(
Icons
.
receipt_long
,
size:
50
,
color:
Colors
.
grey
.
shade400
,
),
),
),
const
SizedBox
(
height:
16
),
const
Text
(
'Bạn hiện chưa có giao dịch nào'
,
style:
TextStyle
(
fontSize:
16
,
color:
Colors
.
grey
,
),
),
],
),
);
}
}
\ No newline at end of file
lib/screen/support/transaction_service.dart
deleted
100644 → 0
View file @
8bef97c9
import
'dart:convert'
;
import
'package:flutter/material.dart'
;
import
'package:http/http.dart'
as
http
;
enum
TransactionStatus
{
pending
,
completed
,
failed
,
}
class
Transaction
{
final
String
id
;
final
String
title
;
final
int
amount
;
final
int
points
;
final
DateTime
date
;
final
String
category
;
final
IconData
icon
;
final
TransactionStatus
status
;
Transaction
({
required
this
.
id
,
required
this
.
title
,
required
this
.
amount
,
required
this
.
points
,
required
this
.
date
,
required
this
.
category
,
required
this
.
icon
,
required
this
.
status
,
});
factory
Transaction
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
IconData
getIconForCategory
(
String
category
)
{
switch
(
category
)
{
case
'Viện thông'
:
return
Icons
.
phone_android
;
case
'Mua sắm'
:
return
Icons
.
shopping_bag
;
case
'Ưu đãi'
:
return
Icons
.
card_giftcard
;
case
'Hóa đơn'
:
return
Icons
.
receipt
;
default
:
return
Icons
.
receipt_long
;
}
}
TransactionStatus
getStatus
(
String
statusStr
)
{
switch
(
statusStr
)
{
case
'completed'
:
return
TransactionStatus
.
completed
;
case
'pending'
:
return
TransactionStatus
.
pending
;
case
'failed'
:
return
TransactionStatus
.
failed
;
default
:
return
TransactionStatus
.
pending
;
}
}
return
Transaction
(
id:
json
[
'id'
],
title:
json
[
'title'
],
amount:
json
[
'amount'
],
points:
json
[
'points'
],
date:
DateTime
.
parse
(
json
[
'date'
]),
category:
json
[
'category'
],
icon:
getIconForCategory
(
json
[
'category'
]),
status:
getStatus
(
json
[
'status'
]),
);
}
}
class
TransactionService
{
static
const
String
baseUrl
=
'https://api.example.com'
;
Future
<
List
<
Transaction
>>
getTransactions
({
required
String
category
,
required
DateTime
month
,
})
async
{
try
{
final
response
=
await
http
.
get
(
Uri
.
parse
(
'
$baseUrl
/transactions?category=
${Uri.encodeComponent(category)}
&month=
${month.year}
-
${month.month}
'
,
),
headers:
{
'Content-Type'
:
'application/json'
},
);
if
(
response
.
statusCode
==
200
)
{
final
List
<
dynamic
>
data
=
json
.
decode
(
response
.
body
);
return
data
.
map
((
json
)
=>
Transaction
.
fromJson
(
json
)).
toList
();
}
else
{
throw
Exception
(
'Failed to load transactions:
${response.statusCode}
'
);
}
}
catch
(
e
)
{
// Trong môi trường thực tế, bạn nên xử lý lỗi một cách phù hợp
print
(
'Error fetching transactions:
$e
'
);
// Trả về dữ liệu mẫu cho mục đích demo
if
(
category
==
'Ưu đãi'
)
{
return
[
Transaction
(
id:
'1'
,
title:
'Thanh toán mua ưu đãi'
,
amount:
0
,
points:
0
,
date:
DateTime
.
now
(),
category:
'Ưu đãi'
,
icon:
Icons
.
card_giftcard
,
status:
TransactionStatus
.
completed
,
),
Transaction
(
id:
'2'
,
title:
'Thanh toán mua ưu đãi'
,
amount:
0
,
points:
0
,
date:
DateTime
.
now
(),
category:
'Ưu đãi'
,
icon:
Icons
.
card_giftcard
,
status:
TransactionStatus
.
completed
,
),
];
}
return
[];
}
}
}
\ No newline at end of file
lib/screen/transaction/history/transaction_history_detail_screen.dart
0 → 100644
View file @
33ec1dde
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/screen/transaction/history/transaction_history_emun.dart'
;
import
'package:mypoint_flutter_app/shared/router_gage.dart'
;
import
'package:mypoint_flutter_app/widgets/dashed_line.dart'
;
import
'../../../base/base_screen.dart'
;
import
'../../../base/basic_state.dart'
;
import
'../../../resouce/base_color.dart'
;
import
'../../../widgets/back_button.dart'
;
import
'../../../widgets/image_loader.dart'
;
import
'transaction_history_detail_viewmodel.dart'
;
import
'transaction_history_model.dart'
;
class
TransactionHistoryDetailScreen
extends
BaseScreen
{
const
TransactionHistoryDetailScreen
({
super
.
key
});
@override
State
<
TransactionHistoryDetailScreen
>
createState
()
=>
_TransactionHistoryDetailScreenState
();
}
class
_TransactionHistoryDetailScreenState
extends
BaseState
<
TransactionHistoryDetailScreen
>
with
BasicState
{
late
final
TransactionHistoryDetailViewModel
_viewModel
;
late
var
canBack
=
true
;
@override
void
initState
()
{
super
.
initState
();
final
args
=
Get
.
arguments
;
canBack
=
args
[
'canBack'
]
as
bool
?
??
true
;
final
orderId
=
args
[
'orderId'
]
as
String
?
??
''
;
_viewModel
=
Get
.
put
(
TransactionHistoryDetailViewModel
(
orderID:
orderId
));
_viewModel
.
onShowAlertError
=
(
message
)
{
if
(
message
.
isNotEmpty
)
{
showAlertError
(
content:
message
);
}
};
}
@override
Widget
createBody
()
{
return
Scaffold
(
backgroundColor:
Colors
.
grey
.
shade100
,
appBar:
AppBar
(
backgroundColor:
Colors
.
white
,
elevation:
0
,
centerTitle:
true
,
title:
const
Text
(
'Chi tiết giao dịch'
,
style:
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
bold
,
color:
Colors
.
black87
),
),
leading:
canBack
?
CustomBackButton
()
:
SizedBox
.
shrink
(),
actions:
[
IconButton
(
icon:
const
Icon
(
Icons
.
headset_mic
,
size:
24
,
color:
Colors
.
black
),
onPressed:
()
{
Get
.
toNamed
(
supportScreen
);
// Xử lý khi nhấn nút hỗ trợ
},
),
],
),
body:
Obx
(()
{
final
isLoading
=
_viewModel
.
isLoading
.
value
;
final
data
=
_viewModel
.
transactionData
.
value
;
if
(
isLoading
)
{
return
const
Center
(
child:
CircularProgressIndicator
());
}
if
(
data
==
null
)
{
return
const
Center
(
child:
Text
(
'Không tìm thấy dữ liệu giao dịch'
));
}
return
Column
(
children:
[
Expanded
(
child:
IntrinsicHeight
(
child:
SingleChildScrollView
(
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
16
),
margin:
const
EdgeInsets
.
all
(
20
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
12
)),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
_buildMainInfoSection
(
data
),
DashedLine
(),
const
SizedBox
(
height:
12
),
_buildDetailInfoSection
(
data
),
const
SizedBox
(
height:
12
),
DashedLine
(),
const
Divider
(
height:
12
),
_buildProductInfoSection
(
data
),
],
),
),
),
),
),
_buildBottomButton
(
data
),
],
);
}),
);
}
Widget
_buildMainInfoSection
(
TransactionHistoryModel
data
)
{
return
Center
(
child:
Container
(
padding:
const
EdgeInsets
.
all
(
16
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
12
)),
child:
Column
(
children:
[
SizedBox
(
width:
60
,
height:
60
,
child:
loadNetworkImage
(
url:
data
.
logo
??
""
,
fit:
BoxFit
.
cover
,
placeholderAsset:
'assets/images/ic_logo.png'
,
),
),
const
SizedBox
(
height:
8
),
const
Text
(
"Thanh toán mua ưu đãi"
,
style:
TextStyle
(
fontSize:
20
,
fontWeight:
FontWeight
.
bold
)),
const
SizedBox
(
height:
4
),
Text
(
data
.
payCash
?.
isNotEmpty
==
true
?
'
${data.payCash}
'
:
data
.
payPoint
??
'0đ'
,
style:
const
TextStyle
(
fontSize:
20
,
color:
Colors
.
red
,
fontWeight:
FontWeight
.
bold
),
),
],
),
),
);
}
Widget
_buildDetailInfoSection
(
TransactionHistoryModel
data
)
{
return
Container
(
margin:
const
EdgeInsets
.
only
(
top:
8
),
color:
Colors
.
white
,
child:
Column
(
children:
[
_buildDetailRow
(
title:
"Trạng thái"
,
value:
data
.
status
??
''
,
valueColor:
_getStatusColor
(
data
.
statusT
)),
_buildDetailRow
(
title:
"Thời gian"
,
value:
data
.
createdAt
??
''
),
const
SizedBox
(
height:
12
),
DashedLine
(),
const
SizedBox
(
height:
12
),
_buildDetailRow
(
title:
"Mã giao dịch"
,
value:
data
.
transactionId
??
''
,
trailing:
SizedBox
(
width:
32
,
height:
32
,
child:
Center
(
child:
IconButton
(
icon:
const
Icon
(
Icons
.
copy
,
size:
16
,
color:
BaseColor
.
primary500
),
onPressed:
()
{
Clipboard
.
setData
(
ClipboardData
(
text:
data
.
transactionId
??
''
));
Get
.
snackbar
(
'✔️'
,
'Đã sao chép mã giao dịch'
);
},
),
),
),
),
_buildDetailRow
(
title:
"Nguồn tiền"
,
value:
data
.
sourceCash
??
''
),
_buildDetailRow
(
title:
"Phí thanh toán"
,
value:
data
.
feesPrice
??
''
),
],
),
);
}
Widget
_buildProductInfoSection
(
TransactionHistoryModel
data
)
{
final
productInfo
=
data
.
productInfo
;
if
(
productInfo
==
null
||
productInfo
.
isEmpty
)
{
return
const
SizedBox
.
shrink
();
}
return
Container
(
margin:
const
EdgeInsets
.
only
(
top:
8
),
color:
Colors
.
white
,
child:
Column
(
children:
productInfo
.
map
((
info
)
{
return
Column
(
children:
[
_buildDetailRow
(
title:
info
.
name
??
''
,
value:
info
.
value
??
''
),
// if (productInfo.last != info) _buildDivider(),
],
);
}).
toList
(),
),
);
}
Widget
_buildDetailRow
({
required
String
title
,
required
String
value
,
Widget
?
trailing
,
Color
?
valueColor
})
{
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
vertical:
6
),
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
SizedBox
(
width:
150
,
child:
Text
(
title
,
style:
const
TextStyle
(
fontSize:
15
,
color:
Colors
.
black87
,
fontWeight:
FontWeight
.
w400
),
),
),
const
SizedBox
(
width:
12
),
Expanded
(
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
end
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
Expanded
(
child:
Text
(
value
,
textAlign:
TextAlign
.
right
,
style:
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
w500
,
color:
valueColor
??
Colors
.
black
),
),
),
if
(
trailing
!=
null
)
...[
// const SizedBox(height: 4),
trailing
,
],
],
),
),
],
),
);
}
Widget
_buildBottomButton
(
TransactionHistoryModel
transaction
)
{
return
SafeArea
(
top:
false
,
minimum:
EdgeInsets
.
only
(
bottom:
MediaQuery
.
of
(
context
).
viewInsets
.
bottom
),
child:
Container
(
width:
double
.
infinity
,
padding:
const
EdgeInsets
.
all
(
16
),
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
if
(
transaction
.
titleRedButton
!=
null
)
ElevatedButton
(
onPressed:
()
{
Get
.
until
((
route
)
=>
Get
.
currentRoute
==
mainScreen
);
// Navigator.of(context).pop();
},
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
BaseColor
.
primary600
,
foregroundColor:
Colors
.
white
,
minimumSize:
const
Size
(
double
.
infinity
,
50
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
8
)),
),
child:
Text
(
transaction
.
titleRedButton
,
style:
const
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
bold
),
),
),
const
SizedBox
(
height:
12
),
if
(
transaction
.
titleClearButton
!=
null
)
TextButton
(
onPressed:
()
{
Get
.
until
((
route
)
=>
Get
.
currentRoute
==
mainScreen
);
},
style:
TextButton
.
styleFrom
(
minimumSize:
const
Size
(
double
.
infinity
,
50
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
8
)),
),
child:
Text
(
transaction
.
titleClearButton
??
''
,
style:
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
w500
,
color:
Colors
.
grey
.
shade700
),
),
),
],
),
),
);
}
Color
_getStatusColor
(
TransactionStatusOrder
status
)
{
switch
(
status
)
{
case
TransactionStatusOrder
.
success
:
return
Colors
.
green
;
case
TransactionStatusOrder
.
failed
:
return
Colors
.
red
;
case
TransactionStatusOrder
.
processing
:
default
:
return
Colors
.
orange
;
}
}
}
lib/screen/transaction/history/transaction_history_detail_viewmodel.dart
0 → 100644
View file @
33ec1dde
import
'package:get/get_rx/src/rx_types/rx_types.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'package:mypoint_flutter_app/screen/transaction/history/transaction_history_model.dart'
;
import
'../../../base/restful_api_viewmodel.dart'
;
import
'../../../configs/constants.dart'
;
class
TransactionHistoryDetailViewModel
extends
RestfulApiViewModel
{
String
orderID
;
TransactionHistoryDetailViewModel
({
required
this
.
orderID
});
var
transactionData
=
Rxn
<
TransactionHistoryModel
>();
final
RxBool
isLoading
=
false
.
obs
;
void
Function
(
String
message
)?
onShowAlertError
;
@override
void
onInit
()
{
super
.
onInit
();
_loadData
();
}
Future
<
void
>
_loadData
()
async
{
showLoading
();
client
.
getTransactionHistoryDetail
(
orderID
).
then
((
value
)
{
hideLoading
();
if
(
value
.
isSuccess
)
{
transactionData
.
value
=
value
.
data
;
}
else
{
onShowAlertError
?.
call
(
value
.
errorMessage
??
Constants
.
commonError
);
}
});
}
}
lib/screen/transaction/history/transaction_history_emun.dart
0 → 100644
View file @
33ec1dde
enum
TransactionStatusOrder
{
success
,
failed
,
processing
,
pending
;
factory
TransactionStatusOrder
.
fromRawValue
(
String
?
value
)
{
switch
(
value
)
{
case
'success'
:
return
TransactionStatusOrder
.
success
;
case
'failed'
:
return
TransactionStatusOrder
.
failed
;
case
'processing'
:
default
:
return
TransactionStatusOrder
.
processing
;
}
}
}
lib/screen/transaction/history/transaction_history_model.dart
0 → 100644
View file @
33ec1dde
import
'package:json_annotation/json_annotation.dart'
;
import
'package:mypoint_flutter_app/screen/transaction/history/transaction_history_emun.dart'
;
import
'../../../directional/directional_screen.dart'
;
import
'../../voucher/models/product_type.dart'
;
part
'transaction_history_model.g.dart'
;
@JsonSerializable
()
class
TransactionHistoryModel
{
final
String
?
id
;
@JsonKey
(
name:
'transaction_id'
)
final
String
?
transactionId
;
@JsonKey
(
name:
'item_id'
)
final
String
?
itemId
;
final
String
?
name
;
@JsonKey
(
name:
'status_code'
)
final
String
?
statusCode
;
final
String
?
status
;
@JsonKey
(
name:
'pay_point'
)
final
String
?
payPoint
;
@JsonKey
(
name:
'pay_cash'
)
final
String
?
payCash
;
final
String
?
logo
;
@JsonKey
(
name:
'pay_total'
)
final
String
?
payTotal
;
@JsonKey
(
name:
'created_at'
)
final
String
?
createdAt
;
@JsonKey
(
name:
'source_cash'
)
final
String
?
sourceCash
;
@JsonKey
(
name:
'fees_price'
)
final
String
?
feesPrice
;
@JsonKey
(
name:
'product_type'
)
final
String
?
productTypeRaw
;
@JsonKey
(
name:
'product_info'
)
final
List
<
ProductInfoModel
>?
productInfo
;
TransactionHistoryModel
({
this
.
id
,
this
.
transactionId
,
this
.
itemId
,
this
.
name
,
this
.
statusCode
,
this
.
status
,
this
.
payPoint
,
this
.
payCash
,
this
.
logo
,
this
.
payTotal
,
this
.
createdAt
,
this
.
sourceCash
,
this
.
feesPrice
,
this
.
productTypeRaw
,
this
.
productInfo
,
});
factory
TransactionHistoryModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$TransactionHistoryModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$TransactionHistoryModelToJson
(
this
);
TransactionStatusOrder
get
statusT
=>
TransactionStatusOrder
.
fromRawValue
(
statusCode
);
bool
get
orderCompleted
=>
statusT
==
TransactionStatusOrder
.
success
||
statusT
==
TransactionStatusOrder
.
failed
;
ProductType
?
get
productType
=>
ProductTypeExt
.
from
(
productTypeRaw
)
??
ProductType
.
voucher
;
String
get
titleRedButton
{
switch
(
statusT
)
{
case
TransactionStatusOrder
.
success
:
switch
(
productType
)
{
case
ProductType
.
voucher
:
return
'Xem ưu đãi đã mua'
;
case
ProductType
.
topupMobile
:
return
'Tiếp tục nạp tiền'
;
case
ProductType
.
typeCard
:
return
'Xem thông tin thẻ'
;
case
ProductType
.
vnTra
:
return
'Xem chi tiết'
;
default
:
return
'Về trang chủ'
;
}
case
TransactionStatusOrder
.
failed
:
return
'Liên hệ hỗ trợ'
;
default
:
return
'Về trang chủ'
;
}
}
String
?
get
titleClearButton
=>
orderCompleted
?
'Về trang chủ'
:
null
;
//
// String? get iconSupport =>
// statusT == TransactionStatusOrder.failed ? 'ic_support_transaction' : null;
// DirectionalScreen? get directionScreenRedButton {
// switch (statusT) {
// case TransactionStatusOrder.success:
// switch (productType) {
// case ProductType.voucher:
// return DirectionalScreen(clickActionType: 'productOwnVoucher', clickActionParam: itemId);
// case ProductType.topupMobile:
// return DirectionalScreen(clickActionType: 'mobileTopup', clickActionParam: itemId);
// case ProductType.typeCard:
// return DirectionalScreen(clickActionType: 'familyMedonDetailCard', clickActionParam: itemId);
// case ProductType.vnTra:
// return DirectionalScreen(clickActionType: 'detailTrafficService', clickActionParam: itemId);
// default:
// return null;
// }
// case TransactionStatusOrder.failed:
// return DirectionalScreen(clickActionType: 'customerSupport');
// default:
// return DirectionalScreen(clickActionType: 'home');
// }
// }
}
class
ProductInfoModel
{
final
String
?
name
;
final
String
?
value
;
ProductInfoModel
({
this
.
name
,
this
.
value
});
factory
ProductInfoModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
ProductInfoModel
(
name:
json
[
'name'
],
value:
json
[
'value'
],
);
}
Map
<
String
,
dynamic
>
toJson
()
=>
{
'name'
:
name
,
'value'
:
value
,
};
}
lib/screen/transaction/history/transaction_history_model.g.dart
0 → 100644
View file @
33ec1dde
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'transaction_history_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
TransactionHistoryModel
_$TransactionHistoryModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
TransactionHistoryModel
(
id:
json
[
'id'
]
as
String
?,
transactionId:
json
[
'transaction_id'
]
as
String
?,
itemId:
json
[
'item_id'
]
as
String
?,
name:
json
[
'name'
]
as
String
?,
statusCode:
json
[
'status_code'
]
as
String
?,
status:
json
[
'status'
]
as
String
?,
payPoint:
json
[
'pay_point'
]
as
String
?,
payCash:
json
[
'pay_cash'
]
as
String
?,
logo:
json
[
'logo'
]
as
String
?,
payTotal:
json
[
'pay_total'
]
as
String
?,
createdAt:
json
[
'created_at'
]
as
String
?,
sourceCash:
json
[
'source_cash'
]
as
String
?,
feesPrice:
json
[
'fees_price'
]
as
String
?,
productTypeRaw:
json
[
'product_type'
]
as
String
?,
productInfo:
(
json
[
'product_info'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
ProductInfoModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
(),
);
Map
<
String
,
dynamic
>
_$TransactionHistoryModelToJson
(
TransactionHistoryModel
instance
,
)
=>
<
String
,
dynamic
>{
'id'
:
instance
.
id
,
'transaction_id'
:
instance
.
transactionId
,
'item_id'
:
instance
.
itemId
,
'name'
:
instance
.
name
,
'status_code'
:
instance
.
statusCode
,
'status'
:
instance
.
status
,
'pay_point'
:
instance
.
payPoint
,
'pay_cash'
:
instance
.
payCash
,
'logo'
:
instance
.
logo
,
'pay_total'
:
instance
.
payTotal
,
'created_at'
:
instance
.
createdAt
,
'source_cash'
:
instance
.
sourceCash
,
'fees_price'
:
instance
.
feesPrice
,
'product_type'
:
instance
.
productTypeRaw
,
'product_info'
:
instance
.
productInfo
,
};
Prev
1
2
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