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
fa01087d
Commit
fa01087d
authored
Jul 04, 2025
by
DatHV
Browse files
update brand, detail..
parent
c8abf95b
Changes
108
Hide whitespace changes
Inline
Side-by-side
lib/screen/mobile_card/models/product_mobile_card_model.g.dart
0 → 100644
View file @
fa01087d
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'product_mobile_card_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProductMobileCardModel
_$ProductMobileCardModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
ProductMobileCardModel
(
id:
json
[
'id'
]
as
String
?,
productModelCode:
json
[
'product_model_code'
]
as
String
?,
code:
json
[
'code'
]
as
String
?,
name:
json
[
'name'
]
as
String
?,
dataDurationApply:
json
[
'data_duration_apply'
]
as
String
?,
productDescription:
json
[
'description'
]
as
String
?,
startTime:
json
[
'start_time'
]
as
String
?,
endTime:
json
[
'end_time'
]
as
String
?,
limitQuantityPerTransaction:
json
[
'limit_quantity_per_transaction'
]
as
String
?,
brandId:
json
[
'brand_id'
]
as
String
?,
brandCode:
json
[
'brand_code'
]
as
String
?,
brandName:
json
[
'brand_name'
]
as
String
?,
pointReward:
json
[
'point_reward'
]
as
String
?,
brandLogo:
json
[
'brand_logo'
]
as
String
?,
images:
(
json
[
'images'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
MobileCardImageModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
(),
language:
json
[
'language'
]
==
null
?
null
:
MobileCardLanguageModel
.
fromJson
(
json
[
'language'
]
as
Map
<
String
,
dynamic
>,
),
prices:
(
json
[
'prices'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
MobileCardPriceModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
(),
);
Map
<
String
,
dynamic
>
_$ProductMobileCardModelToJson
(
ProductMobileCardModel
instance
,
)
=>
<
String
,
dynamic
>{
'id'
:
instance
.
id
,
'product_model_code'
:
instance
.
productModelCode
,
'code'
:
instance
.
code
,
'name'
:
instance
.
name
,
'data_duration_apply'
:
instance
.
dataDurationApply
,
'description'
:
instance
.
productDescription
,
'start_time'
:
instance
.
startTime
,
'end_time'
:
instance
.
endTime
,
'limit_quantity_per_transaction'
:
instance
.
limitQuantityPerTransaction
,
'brand_id'
:
instance
.
brandId
,
'brand_code'
:
instance
.
brandCode
,
'brand_name'
:
instance
.
brandName
,
'point_reward'
:
instance
.
pointReward
,
'brand_logo'
:
instance
.
brandLogo
,
'images'
:
instance
.
images
?.
map
((
e
)
=>
e
.
toJson
()).
toList
(),
'language'
:
instance
.
language
?.
toJson
(),
'prices'
:
instance
.
prices
?.
map
((
e
)
=>
e
.
toJson
()).
toList
(),
};
ProductMobileCardResponse
_$ProductMobileCardResponseFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
ProductMobileCardResponse
(
products:
(
json
[
'products'
]
as
List
<
dynamic
>?)
?.
map
(
(
e
)
=>
ProductMobileCardModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>),
)
.
toList
(),
);
Map
<
String
,
dynamic
>
_$ProductMobileCardResponseToJson
(
ProductMobileCardResponse
instance
,
)
=>
<
String
,
dynamic
>{
'products'
:
instance
.
products
};
MobileCardImageModel
_$MobileCardImageModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
MobileCardImageModel
(
id:
json
[
'id'
]
as
String
?,
caption:
json
[
'caption'
]
as
String
?,
imageUrl:
json
[
'image_url'
]
as
String
?,
);
Map
<
String
,
dynamic
>
_$MobileCardImageModelToJson
(
MobileCardImageModel
instance
,
)
=>
<
String
,
dynamic
>{
'id'
:
instance
.
id
,
'caption'
:
instance
.
caption
,
'image_url'
:
instance
.
imageUrl
,
};
MobileCardLanguageModel
_$MobileCardLanguageModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
MobileCardLanguageModel
(
content:
json
[
'content'
]
as
String
?,
termAndCondition:
json
[
'term_and_condition'
]
as
String
?,
stockRemark:
json
[
'stock_remark'
]
as
String
?,
);
Map
<
String
,
dynamic
>
_$MobileCardLanguageModelToJson
(
MobileCardLanguageModel
instance
,
)
=>
<
String
,
dynamic
>{
'content'
:
instance
.
content
,
'term_and_condition'
:
instance
.
termAndCondition
,
'stock_remark'
:
instance
.
stockRemark
,
};
MobileCardPriceModel
_$MobileCardPriceModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
MobileCardPriceModel
(
channelCode:
json
[
'channel_code'
]
as
String
?,
channelName:
json
[
'channel_name'
]
as
String
?,
payPoint:
json
[
'pay_point'
]
as
String
?,
originalPrice:
json
[
'original_price'
]
as
String
?,
poolCode:
json
[
'pool_code'
]
as
String
?,
subPoolCode:
json
[
'sub_pool_code'
]
as
String
?,
currencyCode:
json
[
'currency_code'
]
as
String
?,
);
Map
<
String
,
dynamic
>
_$MobileCardPriceModelToJson
(
MobileCardPriceModel
instance
,
)
=>
<
String
,
dynamic
>{
'channel_code'
:
instance
.
channelCode
,
'channel_name'
:
instance
.
channelName
,
'pay_point'
:
instance
.
payPoint
,
'original_price'
:
instance
.
originalPrice
,
'pool_code'
:
instance
.
poolCode
,
'sub_pool_code'
:
instance
.
subPoolCode
,
'currency_code'
:
instance
.
currencyCode
,
};
lib/screen/mobile_card/models/usable_voucher_model.dart
0 → 100644
View file @
fa01087d
import
'package:flutter/foundation.dart'
;
import
'package:json_annotation/json_annotation.dart'
;
import
'package:mypoint_flutter_app/screen/mobile_card/models/product_mobile_card_model.dart'
;
import
'../../home/models/brand_model.dart'
;
part
'usable_voucher_model.g.dart'
;
@JsonSerializable
()
class
UsableVoucherModel
{
@JsonKey
(
name:
'voucher_id'
)
final
String
?
voucherID
;
@JsonKey
(
name:
'product_item_id'
)
final
String
?
voucherItemID
;
@JsonKey
(
name:
'action_time'
)
final
String
?
actionTime
;
final
String
?
code
;
final
String
?
serial
;
String
?
name
;
String
?
description
;
@JsonKey
(
name:
'voucher_type_code'
)
final
String
?
voucherTypeCode
;
@JsonKey
(
name:
'voucher_type_name'
)
final
String
?
voucherTypeName
;
@JsonKey
(
name:
'voucher_value'
)
final
String
?
voucherValue
;
@JsonKey
(
name:
'content'
)
final
String
?
voucherContent
;
@JsonKey
(
name:
'term_and_condition'
)
final
String
?
voucherTermAndCondition
;
@JsonKey
(
name:
'voucher_stock_remark'
)
final
String
?
voucherStockRemark
;
@JsonKey
(
name:
'expired_time'
)
final
String
?
expiredTime
;
@JsonKey
(
name:
'status_code'
)
final
String
?
statusCode
;
final
String
?
status
;
@JsonKey
(
name:
'beneficiary_site_name'
)
final
String
?
beneficiarySiteName
;
final
List
<
MobileCardPriceModel
>?
prices
;
final
List
<
MobileCardImageModel
>?
images
;
final
BrandModel
?
brand
;
@JsonKey
(
name:
'like_id'
)
final
String
?
likeId
;
@JsonKey
(
name:
'code_secret'
)
String
?
codeSecret
;
final
String
?
password
;
UsableVoucherModel
({
this
.
voucherID
,
this
.
voucherItemID
,
this
.
actionTime
,
this
.
code
,
this
.
serial
,
this
.
name
,
this
.
description
,
this
.
voucherTypeCode
,
this
.
voucherTypeName
,
this
.
voucherValue
,
this
.
voucherContent
,
this
.
voucherTermAndCondition
,
this
.
voucherStockRemark
,
this
.
expiredTime
,
this
.
statusCode
,
this
.
status
,
this
.
beneficiarySiteName
,
this
.
prices
,
this
.
images
,
this
.
brand
,
this
.
likeId
,
this
.
codeSecret
,
this
.
password
,
});
factory
UsableVoucherModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$UsableVoucherModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$UsableVoucherModelToJson
(
this
);
}
@JsonSerializable
()
class
RedeemProductResponseModel
{
final
UsableVoucherModel
?
item
;
RedeemProductResponseModel
({
this
.
item
});
factory
RedeemProductResponseModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$RedeemProductResponseModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$RedeemProductResponseModelToJson
(
this
);
}
lib/screen/mobile_card/models/usable_voucher_model.g.dart
0 → 100644
View file @
fa01087d
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'usable_voucher_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UsableVoucherModel
_$UsableVoucherModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
UsableVoucherModel
(
voucherID:
json
[
'voucher_id'
]
as
String
?,
voucherItemID:
json
[
'product_item_id'
]
as
String
?,
actionTime:
json
[
'action_time'
]
as
String
?,
code:
json
[
'code'
]
as
String
?,
serial:
json
[
'serial'
]
as
String
?,
name:
json
[
'name'
]
as
String
?,
description:
json
[
'description'
]
as
String
?,
voucherTypeCode:
json
[
'voucher_type_code'
]
as
String
?,
voucherTypeName:
json
[
'voucher_type_name'
]
as
String
?,
voucherValue:
json
[
'voucher_value'
]
as
String
?,
voucherContent:
json
[
'content'
]
as
String
?,
voucherTermAndCondition:
json
[
'term_and_condition'
]
as
String
?,
voucherStockRemark:
json
[
'voucher_stock_remark'
]
as
String
?,
expiredTime:
json
[
'expired_time'
]
as
String
?,
statusCode:
json
[
'status_code'
]
as
String
?,
status:
json
[
'status'
]
as
String
?,
beneficiarySiteName:
json
[
'beneficiary_site_name'
]
as
String
?,
prices:
(
json
[
'prices'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
MobileCardPriceModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
(),
images:
(
json
[
'images'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
MobileCardImageModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
(),
brand:
json
[
'brand'
]
==
null
?
null
:
BrandModel
.
fromJson
(
json
[
'brand'
]
as
Map
<
String
,
dynamic
>),
likeId:
json
[
'like_id'
]
as
String
?,
codeSecret:
json
[
'code_secret'
]
as
String
?,
password:
json
[
'password'
]
as
String
?,
);
Map
<
String
,
dynamic
>
_$UsableVoucherModelToJson
(
UsableVoucherModel
instance
)
=>
<
String
,
dynamic
>{
'voucher_id'
:
instance
.
voucherID
,
'product_item_id'
:
instance
.
voucherItemID
,
'action_time'
:
instance
.
actionTime
,
'code'
:
instance
.
code
,
'serial'
:
instance
.
serial
,
'name'
:
instance
.
name
,
'description'
:
instance
.
description
,
'voucher_type_code'
:
instance
.
voucherTypeCode
,
'voucher_type_name'
:
instance
.
voucherTypeName
,
'voucher_value'
:
instance
.
voucherValue
,
'content'
:
instance
.
voucherContent
,
'term_and_condition'
:
instance
.
voucherTermAndCondition
,
'voucher_stock_remark'
:
instance
.
voucherStockRemark
,
'expired_time'
:
instance
.
expiredTime
,
'status_code'
:
instance
.
statusCode
,
'status'
:
instance
.
status
,
'beneficiary_site_name'
:
instance
.
beneficiarySiteName
,
'prices'
:
instance
.
prices
,
'images'
:
instance
.
images
,
'brand'
:
instance
.
brand
,
'like_id'
:
instance
.
likeId
,
'code_secret'
:
instance
.
codeSecret
,
'password'
:
instance
.
password
,
};
RedeemProductResponseModel
_$RedeemProductResponseModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
RedeemProductResponseModel
(
item:
json
[
'item'
]
==
null
?
null
:
UsableVoucherModel
.
fromJson
(
json
[
'item'
]
as
Map
<
String
,
dynamic
>),
);
Map
<
String
,
dynamic
>
_$RedeemProductResponseModelToJson
(
RedeemProductResponseModel
instance
,
)
=>
<
String
,
dynamic
>{
'item'
:
instance
.
item
};
lib/screen/mobile_card/product_mobile_card_screen.dart
0 → 100644
View file @
fa01087d
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/resouce/base_color.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
'../../widgets/alert/custom_alert_dialog.dart'
;
import
'../../widgets/alert/data_alert_model.dart'
;
class
ProductMobileCardScreen
extends
BaseScreen
{
const
ProductMobileCardScreen
({
super
.
key
});
@override
State
<
ProductMobileCardScreen
>
createState
()
=>
_ProductMobileCardScreenState
();
}
class
_ProductMobileCardScreenState
extends
BaseState
<
ProductMobileCardScreen
>
with
BasicState
{
late
final
ProductMobileCardViewModel
_viewModel
;
@override
void
initState
()
{
super
.
initState
();
_viewModel
=
ProductMobileCardViewModel
();
_viewModel
.
getProductMobileCard
();
_viewModel
.
onShowAlertError
=
(
message
)
{
if
(
message
.
isNotEmpty
)
{
showAlertError
(
content:
message
);
}
};
_viewModel
.
onRedeemProductMobileSuccess
=
(
data
)
{
if
(
data
!=
null
)
{
showVoucherPopup
(
context
,
data
);
}
else
{
showAlertError
(
content:
"Đổi mã thẻ nạp thất bại, vui lòng thử lại sau!"
);
}
};
}
@override
Widget
createBody
()
{
return
Scaffold
(
appBar:
CustomAppBar
.
back
(
title:
"Đổi mã thẻ nạp"
),
body:
Obx
(()
{
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
const
Padding
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
12
),
child:
Text
(
"Chọn nhà mạng"
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
16
)),
),
_buildSectionNetwork
(),
const
SizedBox
(
height:
24
),
const
Padding
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
16
),
child:
Text
(
"Mệnh giá thẻ"
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
16
)),
),
const
SizedBox
(
height:
12
),
_buildProductItem
(),
SafeArea
(
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
16
),
child:
ElevatedButton
(
onPressed:
_viewModel
.
selectedProduct
==
null
?
null
:
_redeemProductMobileCard
,
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
_viewModel
.
selectedProduct
==
null
?
Colors
.
grey
:
BaseColor
.
primary500
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
10
)),
minimumSize:
const
Size
.
fromHeight
(
48
),
),
child:
const
Text
(
"Xác nhận"
,
style:
TextStyle
(
fontSize:
16
,
color:
Colors
.
white
,
fontWeight:
FontWeight
.
bold
),
),
),
),
),
],
);
}),
);
}
Widget
_buildSectionNetwork
()
{
final
widthCardItem
=
MediaQuery
.
of
(
context
).
size
.
width
/
2.5
;
return
SizedBox
(
height:
widthCardItem
*
9
/
16
,
child:
ListView
.
separated
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
scrollDirection:
Axis
.
horizontal
,
itemCount:
_viewModel
.
mobileCardSections
.
value
.
length
,
separatorBuilder:
(
_
,
__
)
=>
const
SizedBox
(
width:
12
),
itemBuilder:
(
_
,
index
)
{
final
mobileCard
=
_viewModel
.
mobileCardSections
.
value
[
index
];
final
isSelected
=
mobileCard
.
brandCode
==
_viewModel
.
selectedBrandCode
.
value
;
return
GestureDetector
(
onTap:
()
{
setState
(()
{
if
(
_viewModel
.
selectedBrandCode
.
value
==
mobileCard
.
brandCode
)
return
;
_viewModel
.
selectedBrandCode
.
value
=
mobileCard
.
brandCode
??
""
;
_viewModel
.
selectedProduct
=
null
;
});
},
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
20
),
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
12
),
border:
Border
.
all
(
color:
isSelected
?
Colors
.
orange
:
Colors
.
grey
.
shade300
,
width:
2
),
color:
Colors
.
white
,
),
alignment:
Alignment
.
center
,
child:
loadNetworkImage
(
url:
mobileCard
.
brandLogo
,
width:
widthCardItem
,
placeholderAsset:
"assets/images/bg_default_169.png"
,
),
),
);
},
),
);
}
Widget
_buildProductItem
()
{
return
Expanded
(
child:
GridView
.
count
(
crossAxisCount:
2
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
crossAxisSpacing:
12
,
mainAxisSpacing:
12
,
childAspectRatio:
2.4
,
children:
_viewModel
.
products
.
map
((
product
)
{
final
isSelected
=
_viewModel
.
selectedProduct
?.
id
==
product
.
id
;
final
amount
=
int
.
tryParse
(
(
product
.
prices
?.
isNotEmpty
==
true
)
?
product
.
prices
?.
first
.
originalPrice
??
"0"
:
"0"
,
)
??
0
;
final
price
=
int
.
tryParse
((
product
.
prices
?.
isNotEmpty
==
true
)
?
product
.
prices
?.
first
.
payPoint
??
"0"
:
"0"
)
??
0
;
return
GestureDetector
(
onTap:
()
{
setState
(()
{
_viewModel
.
selectedProduct
=
product
;
});
},
child:
Container
(
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
isSelected
?
Colors
.
orange
:
Colors
.
grey
.
shade300
),
borderRadius:
BorderRadius
.
circular
(
12
),
color:
isSelected
?
Colors
.
orange
.
withOpacity
(
0.1
)
:
Colors
.
white
,
),
padding:
const
EdgeInsets
.
all
(
12
),
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
amount
.
money
(
CurrencyUnit
.
vnd
),
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
16
,
color:
isSelected
?
Colors
.
orange
:
Colors
.
black87
,
),
),
const
SizedBox
(
height:
4
),
Row
(
children:
[
Text
(
"Giá:
${price.money(CurrencyUnit.none)}
"
,
style:
TextStyle
(
fontSize:
14
,
color:
isSelected
?
Colors
.
orange
:
Colors
.
black54
),
),
Image
.
asset
(
"assets/images/ic_point.png"
,
width:
16
,
height:
16
),
],
),
],
),
),
);
}).
toList
(),
),
);
}
_redeemProductMobileCard
()
{
if
(
_viewModel
.
selectedProduct
==
null
)
return
;
if
(!
_viewModel
.
isValidBalance
)
{
showAlertError
(
content:
"Bạn chưa đủ điểm để đổi ưu đãi này, vui lòng tích lũy thêm điểm nhé!"
);
return
;
}
_showAlertConfirmRedeemProduct
();
}
_showAlertConfirmRedeemProduct
()
{
final
dataAlert
=
DataAlertModel
(
title:
"Xác nhận"
,
description:
"Bạn có muốn sử dụng
${_viewModel.payPoint.money(CurrencyUnit.point)}
MyPoint để đổi lấy mã thẻ điện thoại này không?"
,
localHeaderImage:
"assets/images/ic_pipi_02.png"
,
buttons:
[
AlertButton
(
text:
"Đồng ý"
,
onPressed:
()
{
Get
.
back
();
_viewModel
.
redeemProductMobileCard
();
},
bgColor:
BaseColor
.
primary500
,
textColor:
Colors
.
white
,
),
AlertButton
(
text:
"Huỷ"
,
onPressed:
()
=>
Get
.
back
(),
bgColor:
Colors
.
white
,
textColor:
BaseColor
.
second500
),
],
);
showAlert
(
data:
dataAlert
,
direction:
ButtonsDirection
.
row
);
}
}
lib/screen/mobile_card/product_mobile_card_viewmodel.dart
0 → 100644
View file @
fa01087d
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
'package:mypoint_flutter_app/screen/mobile_card/models/product_mobile_card_model.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../../preference/point/point_manager.dart'
;
import
'models/usable_voucher_model.dart'
;
class
ProductMobileCardViewModel
extends
RestfulApiViewModel
{
void
Function
(
String
message
)?
onShowAlertError
;
void
Function
(
UsableVoucherModel
data
)?
onRedeemProductMobileSuccess
;
RxMap
<
String
,
List
<
ProductMobileCardModel
>>
groupedSection
=
RxMap
<
String
,
List
<
ProductMobileCardModel
>>();
var
mobileCardSections
=
RxList
<
ProductMobileCardModel
>();
RxString
selectedBrandCode
=
""
.
obs
;
List
<
ProductMobileCardModel
>
get
products
{
return
groupedSection
[
selectedBrandCode
.
value
]
??
[];
}
ProductMobileCardModel
?
selectedProduct
;
int
get
payPoint
{
return
int
.
tryParse
(
selectedProduct
?.
prices
?.
firstOrNull
?.
payPoint
??
"0"
)
??
0
;
}
bool
get
isValidBalance
{
return
UserPointManager
().
point
>=
(
int
.
tryParse
(
selectedProduct
?.
prices
?.
firstOrNull
?.
payPoint
??
"0"
)
??
0
);
}
@override
onInit
()
{
super
.
onInit
();
UserPointManager
().
fetchUserPoint
();
}
redeemProductMobileCard
()
async
{
showLoading
();
try
{
final
response
=
await
client
.
redeemMobileCard
((
selectedProduct
?.
id
??
0
).
toString
());
final
itemId
=
response
.
data
?.
itemId
??
""
;
hideLoading
();
if
(
itemId
.
isEmpty
)
{
hideLoading
();
print
(
"redeemMobileCard failed:
${response.errorMessage}
"
);
onShowAlertError
?.
call
(
response
.
errorMessage
??
Constants
.
commonError
);
return
;
}
_getMobileCardCode
(
itemId
);
}
catch
(
error
)
{
hideLoading
();
onShowAlertError
?.
call
(
error
.
toString
());
return
;
}
}
_getMobileCardCode
(
String
itemId
)
async
{
showLoading
();
try
{
final
response
=
await
client
.
getMobileCardCode
(
itemId
);
hideLoading
();
final
data
=
response
.
data
?.
item
;
if
(
response
.
isSuccess
&&
data
!=
null
)
{
onRedeemProductMobileSuccess
?.
call
(
data
);
return
;
}
onShowAlertError
?.
call
(
response
.
message
??
Constants
.
commonError
);
}
catch
(
error
)
{
hideLoading
();
onShowAlertError
?.
call
(
error
.
toString
());
return
;
}
}
getProductMobileCard
()
async
{
showLoading
();
try
{
final
response
=
await
client
.
productMobileCardGetList
();
final
result
=
response
.
data
?.
products
??
[];
final
seen
=
<
String
>{};
final
uniqueBrandCode
=
<
ProductMobileCardModel
>[];
for
(
final
p
in
result
)
{
final
code
=
p
.
brandCode
??
""
;
if
(
code
.
isNotEmpty
&&
seen
.
add
(
code
))
{
uniqueBrandCode
.
add
(
p
);
}
}
selectedBrandCode
.
value
=
uniqueBrandCode
.
isNotEmpty
?
uniqueBrandCode
.
first
.
brandCode
??
""
:
""
;
mobileCardSections
.
value
=
uniqueBrandCode
;
final
Map
<
String
,
List
<
ProductMobileCardModel
>>
grouped
=
{};
for
(
final
product
in
result
)
{
final
code
=
product
.
brandCode
??
'unknown'
;
if
(!
grouped
.
containsKey
(
code
))
{
grouped
[
code
]
=
[];
}
grouped
[
code
]!.
add
(
product
);
}
groupedSection
.
value
=
grouped
;
hideLoading
();
if
(!
response
.
isSuccess
)
{
onShowAlertError
?.
call
(
response
.
message
??
Constants
.
commonError
);
}
}
catch
(
error
)
{
onShowAlertError
?.
call
(
error
.
toString
());
}
}
}
lib/screen/mobile_card/usable_mobile_card_popup.dart
0 → 100644
View file @
fa01087d
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:mypoint_flutter_app/extensions/datetime_extensions.dart'
;
import
'package:mypoint_flutter_app/extensions/string_extension.dart'
;
import
'package:mypoint_flutter_app/resouce/base_color.dart'
;
import
'models/usable_voucher_model.dart'
;
class
UsableMobileCardPopup
extends
StatelessWidget
{
final
UsableVoucherModel
usableVoucher
;
const
UsableMobileCardPopup
({
super
.
key
,
required
this
.
usableVoucher
});
@override
Widget
build
(
BuildContext
context
)
{
final
String
titleNonExpired
=
"Bạn đã đổi mã thẻ điện thoại thành công."
;
final
String
titleSuccess
=
"Bạn đã đổi mã thẻ điện thoại thành công. Giá trị tới ngày %@. Vui lòng sử dụng trước ngày hết hạn."
;
final
expiredTime
=
(
usableVoucher
.
expiredTime
??
""
).
toDate
()?.
toFormattedString
()
??
""
;
final
String
content
=
expiredTime
.
isNotEmpty
?
titleSuccess
.
replaceAll
(
"%@"
,
usableVoucher
.
expiredTime
!)
:
titleNonExpired
;
return
Dialog
(
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
16
)),
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
16.0
),
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
Align
(
alignment:
Alignment
.
topRight
,
child:
GestureDetector
(
onTap:
()
=>
Navigator
.
of
(
context
).
pop
(),
child:
const
Icon
(
Icons
.
close
,
size:
24
),
),
),
Image
.
asset
(
'assets/images/ic_pipi_02.png'
,
height:
200
,
),
const
SizedBox
(
height:
8
),
Text
(
"Thành Công"
,
style:
const
TextStyle
(
fontSize:
18
,
fontWeight:
FontWeight
.
bold
),
),
const
SizedBox
(
height:
8
),
Text
(
content
,
textAlign:
TextAlign
.
center
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Colors
.
black87
),
),
const
SizedBox
(
height:
16
),
Container
(
height:
48
,
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
8
),
color:
Colors
.
grey
.
shade200
,
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
12
),
child:
Row
(
children:
[
Expanded
(
child:
Text
(
usableVoucher
.
codeSecret
??
'---'
,
style:
const
TextStyle
(
fontSize:
16
),
),
),
GestureDetector
(
onTap:
()
{
Clipboard
.
setData
(
ClipboardData
(
text:
usableVoucher
.
codeSecret
??
''
),
);
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
'Đã sao chép mã'
)),
);
},
child:
const
Icon
(
Icons
.
copy
),
),
],
),
),
const
SizedBox
(
height:
20
),
ElevatedButton
(
onPressed:
()
=>
Navigator
.
of
(
context
).
pop
(),
style:
ElevatedButton
.
styleFrom
(
minimumSize:
const
Size
(
double
.
infinity
,
48
),
backgroundColor:
BaseColor
.
primary500
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
8
)),
),
child:
const
Text
(
"Đã hiểu"
,
style:
TextStyle
(
fontSize:
16
,
color:
Colors
.
white
,
fontWeight:
FontWeight
.
bold
)),
),
],
),
),
);
}
}
void
showVoucherPopup
(
BuildContext
context
,
UsableVoucherModel
usableVoucher
)
{
showDialog
(
context:
context
,
barrierDismissible:
true
,
builder:
(
_
)
=>
UsableMobileCardPopup
(
usableVoucher:
usableVoucher
),
);
}
lib/screen/personal/personal_edit_viewmodel.dart
View file @
fa01087d
...
...
@@ -34,9 +34,10 @@ class PersonalEditViewModel extends RestfulApiViewModel {
);
birthday
=
profile
?.
workerSite
?.
birthday
?.
toDateFormat
(
'yyyy-MM-dd'
);
gender
=
PersonalGender
.
from
(
profile
.
workerSite
?.
sex
??
"U"
);
var
name
=
profile
?.
workerSite
?.
fullname
??
""
;
editDataModel
.
value
=
PersonalEditDataModel
(
name:
DataPreference
.
instance
.
fullN
ame
,
name:
n
ame
,
nickname:
profile
?.
workerSite
?.
nickname
,
phone:
profile
?.
workerSite
?.
phoneNumber
,
email:
profile
?.
workerSite
?.
email
,
...
...
lib/screen/personal/personal_screen.dart
View file @
fa01087d
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/directional/directional_screen.dart'
;
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'
;
...
...
@@ -64,14 +65,14 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
Widget
_buildHeaderPersonal
(
HeaderHomeModel
data
)
{
final
width
=
MediaQuery
.
of
(
context
).
size
.
width
;
final
topPadding
=
MediaQuery
.
of
(
context
).
padding
.
top
;
final
name
=
DataPreference
.
instance
.
profile
?.
workerSite
?.
fullname
??
"Quý Khách"
;
final
name
=
DataPreference
.
instance
.
displayName
;
final
level
=
DataPreference
.
instance
.
rankName
??
"Hạng Đồng"
;
final
email
=
DataPreference
.
instance
.
profile
?.
workerSite
?.
email
??
""
;
return
Container
(
height:
width
*
163
/
375
,
decoration:
BoxDecoration
(
image:
DecorationImage
(
image:
NetworkImage
(
data
.
background
??
""
),
fit:
BoxFit
.
cover
)),
child:
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
12
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
8
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
...
...
@@ -125,7 +126,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
Row
(
children:
[
Text
(
"
${
data.totalPointActive
.toString()}
điểm"
,
(
data
.
totalPointActive
??
0
).
money
(
CurrencyUnit
.
point
)
,
style:
const
TextStyle
(
color:
Colors
.
white
,
fontSize:
16
,
fontWeight:
FontWeight
.
bold
),
),
const
SizedBox
(
width:
4
),
...
...
@@ -182,7 +183,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
{
'icon'
:
Icons
.
gif_box_outlined
,
'title'
:
'Ưu đãi của tôi'
,
'type'
:
'APP_SCREEN_MY_PURCHASE_ITEMS'
},
{
'icon'
:
Icons
.
receipt_long_outlined
,
'title'
:
'Lịch sử giao dịch'
,
'sectionDivider'
:
true
,
'type'
:
''
},
{
'icon'
:
Icons
.
history_outlined
,
'title'
:
'Lịch sử điểm'
,
'type'
:
''
},
{
'icon'
:
Icons
.
history_outlined
,
'title'
:
'Lịch sử hoàn điểm'
,
'type'
:
''
},
{
'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'
:
''
},
{
'icon'
:
Icons
.
favorite_border
,
'title'
:
'Yêu thích'
,
'type'
:
''
},
{
'icon'
:
Icons
.
shopping_bag_outlined
,
'title'
:
'Đơn mua'
,
'sectionDivider'
:
true
,
'type'
:
'APP_SCREEN_ORDER_MENU'
},
...
...
lib/screen/topup/brand_select_sheet_widget.dart
0 → 100644
View file @
fa01087d
import
'package:flutter/material.dart'
;
import
'package:mypoint_flutter_app/widgets/image_loader.dart'
;
import
'../voucher/models/product_brand_model.dart'
;
class
BrandSelectSheet
extends
StatelessWidget
{
final
List
<
ProductBrandModel
>
brands
;
final
Function
(
ProductBrandModel
)
onSelected
;
final
ProductBrandModel
?
selectedBrand
;
const
BrandSelectSheet
({
super
.
key
,
required
this
.
brands
,
this
.
selectedBrand
,
required
this
.
onSelected
,
});
@override
Widget
build
(
BuildContext
context
)
{
return
Container
(
padding:
const
EdgeInsets
.
fromLTRB
(
16
,
16
,
16
,
32
),
decoration:
const
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
24
)),
),
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
const
Text
(
"Chọn nhà mạng"
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
18
)),
const
SizedBox
(
height:
16
),
GridView
.
builder
(
shrinkWrap:
true
,
itemCount:
brands
.
length
,
physics:
const
NeverScrollableScrollPhysics
(),
gridDelegate:
const
SliverGridDelegateWithFixedCrossAxisCount
(
crossAxisCount:
3
,
mainAxisSpacing:
16
,
crossAxisSpacing:
16
,
childAspectRatio:
1.7
,
),
itemBuilder:
(
context
,
index
)
{
final
brand
=
brands
[
index
];
bool
isFocused
=
selectedBrand
?.
id
==
brand
.
id
;
return
GestureDetector
(
onTap:
()
=>
onSelected
(
brand
),
child:
Container
(
padding:
const
EdgeInsets
.
all
(
8
),
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
isFocused
?
Colors
.
orange
:
Colors
.
grey
.
shade300
),
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
loadNetworkImage
(
url:
brand
.
logo
,
fit:
BoxFit
.
contain
,
placeholderAsset:
"assets/images/bg_default_169.png"
,
),
),
);
},
),
],
),
);
}
}
lib/screen/topup/models/brand_network_model.dart
0 → 100644
View file @
fa01087d
import
'package:json_annotation/json_annotation.dart'
;
part
'brand_network_model.g.dart'
;
@JsonSerializable
()
class
BrandNetworkModel
{
final
int
?
id
;
final
int
?
stock
;
final
String
?
code
;
final
String
?
name
;
final
String
?
logo
;
BrandNetworkModel
({
this
.
id
,
this
.
stock
,
this
.
code
,
this
.
name
,
this
.
logo
,
});
factory
BrandNetworkModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$BrandNetworkModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$BrandNetworkModelToJson
(
this
);
}
@JsonSerializable
()
class
BrandNameCheckResponse
{
final
String
?
brand
;
BrandNameCheckResponse
({
this
.
brand
});
factory
BrandNameCheckResponse
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$BrandNameCheckResponseFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$BrandNameCheckResponseToJson
(
this
);
}
lib/screen/topup/models/brand_network_model.g.dart
0 → 100644
View file @
fa01087d
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'brand_network_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
BrandNetworkModel
_$BrandNetworkModelFromJson
(
Map
<
String
,
dynamic
>
json
)
=>
BrandNetworkModel
(
id:
(
json
[
'id'
]
as
num
?)?.
toInt
(),
stock:
(
json
[
'stock'
]
as
num
?)?.
toInt
(),
code:
json
[
'code'
]
as
String
?,
name:
json
[
'name'
]
as
String
?,
logo:
json
[
'logo'
]
as
String
?,
);
Map
<
String
,
dynamic
>
_$BrandNetworkModelToJson
(
BrandNetworkModel
instance
)
=>
<
String
,
dynamic
>{
'id'
:
instance
.
id
,
'stock'
:
instance
.
stock
,
'code'
:
instance
.
code
,
'name'
:
instance
.
name
,
'logo'
:
instance
.
logo
,
};
BrandNameCheckResponse
_$BrandNameCheckResponseFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
BrandNameCheckResponse
(
brand:
json
[
'brand'
]
as
String
?);
Map
<
String
,
dynamic
>
_$BrandNameCheckResponseToJson
(
BrandNameCheckResponse
instance
,
)
=>
<
String
,
dynamic
>{
'brand'
:
instance
.
brand
};
lib/screen/topup/topup_screen.dart
0 → 100644
View file @
fa01087d
import
'package:contacts_service/contacts_service.dart'
;
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:intl/intl.dart'
;
import
'package:mypoint_flutter_app/screen/topup/topup_viewmodel.dart'
;
import
'package:mypoint_flutter_app/widgets/custom_navigation_bar.dart'
;
import
'package:mypoint_flutter_app/widgets/image_loader.dart'
;
import
'../../preference/data_preference.dart'
;
import
'../../resouce/base_color.dart'
;
import
'../../shared/router_gage.dart'
;
import
'brand_select_sheet_widget.dart'
;
class
PhoneTopUpScreen
extends
StatefulWidget
{
const
PhoneTopUpScreen
({
super
.
key
});
@override
State
<
PhoneTopUpScreen
>
createState
()
=>
_PhoneTopUpScreenState
();
}
class
_PhoneTopUpScreenState
extends
State
<
PhoneTopUpScreen
>
{
final
TopUpViewModel
_viewModel
=
Get
.
put
(
TopUpViewModel
());
late
final
TextEditingController
_phoneController
;
@override
void
initState
()
{
super
.
initState
();
_phoneController
=
TextEditingController
(
text:
_viewModel
.
phoneNumber
.
value
);
_viewModel
.
firstLoadTopUpData
();
}
String
get
formattedAmount
{
return
NumberFormat
.
currency
(
locale:
'vi_VN'
,
symbol:
''
,
decimalDigits:
0
,
).
format
(
_viewModel
.
selectedProduct
.
value
?.
amountToBePaid
??
0
);
}
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
appBar:
CustomNavigationBar
(
title:
"Nạp tiền điện thoại"
),
body:
Obx
(()
{
return
Column
(
children:
[
_buildHeaderPhone
(),
Container
(
height:
6
,
color:
Colors
.
grey
.
shade200
),
const
Divider
(
height:
8
),
_buildItemTypeProduct
(),
const
Divider
(
height:
1
),
],
);
}),
bottomNavigationBar:
Obx
(()
{
return
_buildBottomAction
();
}),
);
}
Widget
_buildHeaderPhone
()
{
return
Obx
(()
{
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
const
SizedBox
(
height:
8
),
const
Text
(
"Số điện thoại"
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
16
)),
const
SizedBox
(
height:
8
),
Row
(
children:
[
Expanded
(
child:
TextField
(
controller:
_phoneController
,
decoration:
InputDecoration
(
filled:
true
,
fillColor:
Colors
.
grey
.
shade100
,
suffixIcon:
InkWell
(
onTap:
()
=>
pickContact
(
context
),
child:
const
Icon
(
Icons
.
contacts
,
color:
Colors
.
orange
),
),
border:
OutlineInputBorder
(
borderRadius:
BorderRadius
.
circular
(
12
),
borderSide:
BorderSide
.
none
),
),
keyboardType:
TextInputType
.
phone
,
onChanged:
(
value
)
{
_viewModel
.
phoneNumber
.
value
=
value
;
_viewModel
.
checkMobileNetwork
();
},
),
),
const
SizedBox
(
width:
8
),
GestureDetector
(
onTap:
_viewModel
.
topUpBrands
.
value
.
isEmpty
?
null
:
()
{
showModalBottomSheet
(
context:
context
,
backgroundColor:
Colors
.
transparent
,
isScrollControlled:
true
,
builder:
(
_
)
=>
BrandSelectSheet
(
brands:
_viewModel
.
topUpBrands
.
value
,
selectedBrand:
_viewModel
.
selectedBrand
.
value
,
onSelected:
(
brand
)
{
Navigator
.
pop
(
context
);
if
(
brand
==
null
&&
brand
.
id
!=
_viewModel
.
selectedBrand
.
value
?.
id
)
return
;
_viewModel
.
selectedProduct
.
value
=
null
;
_viewModel
.
selectedBrand
.
value
=
brand
;
_viewModel
.
getTelcoDetail
();
},
),
);
},
child:
Container
(
padding:
const
EdgeInsets
.
all
(
4
),
height:
48
,
width:
64
,
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
8
),
border:
Border
.
all
(
color:
Colors
.
grey
.
shade300
),
),
child:
loadNetworkImage
(
url:
_viewModel
.
selectedBrand
.
value
?.
logo
,
fit:
BoxFit
.
fitWidth
,
placeholderAsset:
"assets/images/bg_default_169.png"
,
),
),
),
],
),
const
SizedBox
(
height:
16
),
_buildTagHistory
(),
const
SizedBox
(
height:
8
),
],
),
);
});
}
Widget
_buildTagHistory
()
{
final
histories
=
_viewModel
.
histories
;
return
Obx
(()
{
return
SizedBox
(
height:
36
,
child:
Center
(
child:
ListView
.
separated
(
scrollDirection:
Axis
.
horizontal
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
8
),
itemCount:
histories
.
length
,
separatorBuilder:
(
_
,
__
)
=>
const
SizedBox
(
width:
8
),
itemBuilder:
(
_
,
index
)
{
final
phone
=
histories
[
index
];
final
myPhone
=
DataPreference
.
instance
.
phone
??
''
;
final
isMyPhone
=
phone
==
myPhone
;
final
selected
=
phone
==
_viewModel
.
phoneNumber
.
value
;
return
GestureDetector
(
onTap:
()
{
setState
(()
{
_viewModel
.
phoneNumber
.
value
=
phone
;
_phoneController
.
text
=
phone
;
_viewModel
.
checkMobileNetwork
();
});
},
child:
Container
(
padding:
EdgeInsets
.
all
(
4
),
decoration:
BoxDecoration
(
color:
selected
?
Colors
.
orange
.
shade50
:
Colors
.
grey
.
shade100
,
borderRadius:
BorderRadius
.
circular
(
8
),
border:
Border
.
all
(
color:
selected
?
Colors
.
orange
:
Colors
.
grey
.
shade300
),
),
child:
Center
(
child:
Text
(
isMyPhone
?
" Số của tôi "
:
"
$phone
"
,
textAlign:
TextAlign
.
center
,
style:
TextStyle
(
color:
selected
?
Colors
.
orange
:
Colors
.
black54
,
fontSize:
16
,
fontWeight:
selected
?
FontWeight
.
bold
:
FontWeight
.
normal
,
),
),
),
),
);
},
),
),
);
});
}
Widget
_buildItemTypeProduct
()
{
return
Expanded
(
child:
SingleChildScrollView
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
const
Text
(
"Mệnh giá"
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
16
)),
const
SizedBox
(
height:
12
),
GridView
.
count
(
shrinkWrap:
true
,
physics:
const
NeverScrollableScrollPhysics
(),
crossAxisCount:
2
,
crossAxisSpacing:
12
,
mainAxisSpacing:
12
,
childAspectRatio:
3
,
children:
_viewModel
.
products
.
value
.
map
((
product
)
{
final
isSelected
=
product
.
id
==
_viewModel
.
selectedProduct
.
value
?.
id
;
final
preview
=
product
.
previewCampaign
;
return
GestureDetector
(
onTap:
()
=>
setState
(()
=>
_viewModel
.
selectedProduct
.
value
=
product
),
child:
Container
(
decoration:
BoxDecoration
(
borderRadius:
BorderRadius
.
circular
(
12
),
border:
Border
.
all
(
color:
isSelected
?
Colors
.
orange
:
Colors
.
grey
.
shade300
),
color:
isSelected
?
Colors
.
orange
.
withOpacity
(
0.1
)
:
Colors
.
white
,
),
child:
Stack
(
children:
[
// Gift icon
if
(
preview
?.
hasGift
==
true
)
Positioned
(
top:
8
,
left:
0
,
child:
Image
.
asset
(
'assets/images/ic_mark_give_voucher.png'
,
height:
16
),
),
// Point icon
if
((
preview
?.
rewardPoint
??
0
)
>
0
)
Positioned
(
top:
0
,
right:
8
,
child:
Image
.
asset
(
'assets/images/ic_mark_give_point.png'
,
width:
24
),
),
// Text center
Center
(
child:
Text
(
"
${NumberFormat.currency(locale: 'vi_VN', symbol: '', decimalDigits: 0).format(product.amountToBePaid ?? 0)}
đ"
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
14
,
color:
isSelected
?
Colors
.
orange
:
Colors
.
black87
,
),
),
),
],
),
),
);
}).
toList
(),
),
],
),
),
);
}
Widget
_buildBottomAction
()
{
final
product
=
_viewModel
.
selectedProduct
.
value
;
final
preview
=
product
?.
previewCampaign
;
final
rewardPoint
=
preview
?.
rewardPoint
??
0
;
final
hasGift
=
preview
?.
hasGift
==
true
;
return
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
8
,
vertical:
8
),
decoration:
const
BoxDecoration
(
color:
Colors
.
white
,
boxShadow:
[
BoxShadow
(
color:
Colors
.
black54
,
blurRadius:
8
,
offset:
Offset
(
0
,
4
))],
),
child:
SafeArea
(
top:
false
,
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
8
,
vertical:
4
),
color:
Colors
.
white
,
child:
Row
(
children:
[
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
mainAxisSize:
MainAxisSize
.
min
,
children:
[
Row
(
children:
[
Text
(
"Tổng: "
,
style:
TextStyle
(
color:
Colors
.
grey
.
shade600
,
fontSize:
16
)),
Text
(
"
$formattedAmount
đ"
,
style:
const
TextStyle
(
color:
BaseColor
.
primary500
,
fontWeight:
FontWeight
.
bold
,
fontSize:
20
),
),
],
),
const
SizedBox
(
height:
4
),
Row
(
children:
[
if
(
rewardPoint
>
0
)
Row
(
children:
[
Text
(
"+"
,
style:
const
TextStyle
(
color:
Colors
.
orange
,
fontSize:
16
,
fontWeight:
FontWeight
.
bold
),
),
const
SizedBox
(
width:
2
),
Image
.
asset
(
'assets/images/ic_point.png'
,
width:
16
,
height:
16
,
fit:
BoxFit
.
cover
),
const
SizedBox
(
width:
2
),
Text
(
NumberFormat
.
decimalPattern
(
'vi_VN'
).
format
(
rewardPoint
),
style:
const
TextStyle
(
color:
Colors
.
orange
,
fontSize:
16
,
fontWeight:
FontWeight
.
bold
),
),
],
),
if
(
rewardPoint
>
0
&&
hasGift
)
const
SizedBox
(
width:
12
),
if
(
hasGift
)
Row
(
children:
[
Text
(
"+"
,
style:
const
TextStyle
(
color:
Colors
.
red
,
fontSize:
16
,
fontWeight:
FontWeight
.
bold
),
),
SizedBox
(
width:
2
),
Image
.
asset
(
'assets/images/ic_gift_red.png'
,
width:
16
,
height:
16
,
fit:
BoxFit
.
cover
),
],
),
],
),
],
),
const
Spacer
(),
ElevatedButton
(
onPressed:
()
{
Get
.
toNamed
(
transactionDetailScreen
,
arguments:
{
"product"
:
product
,
"quantity"
:
1
,
"targetPhoneNumber"
:
_viewModel
.
phoneNumber
.
value
},
);
},
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
BaseColor
.
primary500
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
10
)),
),
child:
const
Padding
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
10
),
child:
Text
(
"Nạp ngay"
,
style:
TextStyle
(
fontSize:
16
,
color:
Colors
.
white
,
fontWeight:
FontWeight
.
bold
),
),
),
),
],
),
),
),
);
}
Future
<
void
>
pickContact
(
BuildContext
context
)
async
{
try
{
// Gọi sẽ tự động hiện dialog yêu cầu quyền (nếu cần)
final
Contact
?
contact
=
await
ContactsService
.
openDeviceContactPicker
();
if
(
contact
!=
null
&&
contact
.
phones
!=
null
&&
contact
.
phones
!.
isNotEmpty
)
{
String
phone
=
contact
.
phones
!.
first
.
value
??
''
;
phone
=
phone
.
replaceAll
(
RegExp
(
r'[\s\-\(\)]'
),
''
);
_phoneController
.
text
=
phone
;
_viewModel
.
phoneNumber
.
value
=
phone
;
_viewModel
.
checkMobileNetwork
();
}
else
{
ScaffoldMessenger
.
of
(
context
,
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
"Không tìm thấy số điện thoại hợp lệ"
)));
}
}
catch
(
e
)
{
print
(
"❌ Lỗi khi truy cập danh bạ:
$e
"
);
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
const
SnackBar
(
content:
Text
(
"Không thể truy cập danh bạ"
)));
}
}
}
lib/screen/topup/topup_viewmodel.dart
0 → 100644
View file @
fa01087d
import
'package:get/get.dart'
;
import
'package:get/get_rx/src/rx_types/rx_types.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../../preference/contact_storage_service.dart'
;
import
'../voucher/models/product_brand_model.dart'
;
import
'../voucher/models/product_model.dart'
;
import
'../voucher/models/product_type.dart'
;
class
TopUpViewModel
extends
RestfulApiViewModel
{
var
histories
=
RxList
<
String
>();
final
RxList
<
ProductBrandModel
>
topUpBrands
=
<
ProductBrandModel
>[].
obs
;
final
RxList
<
ProductModel
>
products
=
<
ProductModel
>[].
obs
;
var
selectedBrand
=
Rxn
<
ProductBrandModel
>();
var
selectedProduct
=
Rxn
<
ProductModel
>();
final
Map
<
String
,
List
<
ProductModel
>>
_allValue
=
{};
var
phoneNumber
=
''
.
obs
;
@override
void
onInit
()
{
super
.
onInit
();
final
myPhone
=
DataPreference
.
instance
.
phone
??
''
;
phoneNumber
.
value
=
myPhone
;
ContactStorageService
().
getUsedContacts
().
then
((
value
)
{
if
(
value
.
isNotEmpty
)
{
histories
.
value
=
value
;
}
else
{
histories
.
value
=
[
myPhone
];
}
});
if
(!
histories
.
contains
(
myPhone
))
{
histories
.
value
.
insert
(
0
,
myPhone
);
ContactStorageService
().
saveUsedContact
(
myPhone
);
}
}
firstLoadTopUpData
()
async
{
showLoading
();
await
getTopUpBrands
();
await
checkMobileNetwork
();
hideLoading
();
}
getTopUpBrands
()
{
client
.
getTopUpBrands
(
ProductType
.
topupMobile
).
then
((
response
)
{
topUpBrands
.
value
=
response
.
data
??
[];
}).
catchError
((
error
)
{
print
(
'Error fetching brands topup:
$error
'
);
});
}
checkMobileNetwork
()
{
client
.
checkMobileNetwork
(
phoneNumber
.
value
).
then
((
response
)
{
final
brandCode
=
response
.
data
?.
brand
??
''
;
final
brand
=
topUpBrands
.
isNotEmpty
?
topUpBrands
.
firstWhere
(
(
brand
)
=>
brand
.
code
==
brandCode
,
orElse:
()
=>
topUpBrands
.
first
,
)
:
null
;
selectedBrand
.
value
=
brand
;
getTelcoDetail
();
}).
catchError
((
error
)
{
final
first
=
topUpBrands
.
value
.
firstOrNull
;
if
(
first
!=
null
)
{
selectedBrand
.
value
=
first
;
}
getTelcoDetail
();
print
(
'Error checking mobile network:
$error
'
);
});
}
Future
<
void
>
getTelcoDetail
({
String
?
selected
})
async
{
final
code
=
selectedBrand
.
value
?.
code
;
final
id
=
selectedBrand
.
value
?.
id
;
if
(
code
==
null
||
id
==
null
)
return
;
void
makeSelected
(
List
<
ProductModel
>
list
)
{
bool
didSelect
=
false
;
if
(
selected
!=
null
&&
selected
.
isNotEmpty
)
{
for
(
var
item
in
list
)
{
final
isMatch
=
item
.
id
==
int
.
tryParse
(
selected
);
if
(
isMatch
)
{
selectedProduct
.
value
=
item
;
didSelect
=
true
;
}
}
}
// Nếu chưa có item nào được chọn → mặc định chọn 100k
if
(!
didSelect
&&
selectedProduct
.
value
==
null
)
{
final
item100k
=
list
.
isNotEmpty
?
list
.
firstWhere
(
(
e
)
=>
e
.
amountToBePaid
==
100000
,
orElse:
()
=>
list
.
first
,
)
:
null
;
selectedProduct
.
value
=
item100k
;
}
}
// Dùng cache nếu có
if
(
_allValue
.
containsKey
(
code
))
{
final
cached
=
_allValue
[
code
]!;
products
.
value
=
cached
;
makeSelected
(
cached
);
return
;
}
showLoading
();
final
body
=
{
"type"
:
ProductType
.
topupMobile
.
value
,
"size"
:
200
,
"index"
:
0
,
"brand_id"
:
selectedBrand
.
value
?.
id
??
0
,
};
try
{
final
result
=
await
client
.
getProducts
(
body
);
final
data
=
result
.
data
??
[];
_allValue
[
code
]
=
data
;
products
.
value
=
result
.
data
??
[];
makeSelected
(
data
);
hideLoading
();
}
catch
(
error
)
{
print
(
"Error fetching all products:
$error
"
);
hideLoading
();
}
}
}
\ No newline at end of file
lib/screen/transaction/transaction_detail_screen.dart
View file @
fa01087d
...
...
@@ -25,6 +25,7 @@ class _TransactionDetailScreenState extends BaseState<TransactionDetailScreen> w
final
currencyFormatter
=
NumberFormat
.
currency
(
locale:
'vi_VN'
,
symbol:
'đ'
,
decimalDigits:
0
);
ProductModel
?
_product
;
int
_quantity
=
1
;
String
?
_targetPhoneNumber
;
bool
_isPaymentMethodsExpanded
=
true
;
bool
shouldDisablePaymentMethods
=
false
;
...
...
@@ -35,6 +36,7 @@ class _TransactionDetailScreenState extends BaseState<TransactionDetailScreen> w
if
(
args
is
Map
)
{
_product
=
args
[
'product'
];
_quantity
=
args
[
'quantity'
]
??
1
;
_targetPhoneNumber
=
args
[
'targetPhoneNumber'
];
}
if
(
_product
==
null
)
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
...
...
@@ -42,7 +44,9 @@ class _TransactionDetailScreenState extends BaseState<TransactionDetailScreen> w
});
return
;
}
_viewModel
=
Get
.
put
(
TransactionDetailViewModel
(
product:
_product
!,
quantity:
_quantity
));
_viewModel
=
Get
.
put
(
TransactionDetailViewModel
(
product:
_product
!,
quantity:
_quantity
,
targetPhoneNumber:
_targetPhoneNumber
),
);
_viewModel
.
refreshData
();
_viewModel
.
onShowAlertError
=
(
message
)
{
...
...
lib/screen/transaction/transaction_detail_viewmodel.dart
View file @
fa01087d
...
...
@@ -6,6 +6,7 @@ import 'package:mypoint_flutter_app/shared/router_gage.dart';
import
'package:uuid/uuid.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../../configs/constants.dart'
;
import
'../../preference/contact_storage_service.dart'
;
import
'../../preference/data_preference.dart'
;
import
'../voucher/models/product_model.dart'
;
import
'../webview/payment_web_view_screen.dart'
;
...
...
@@ -22,6 +23,7 @@ class TransactionDetailViewModel extends RestfulApiViewModel {
final
RxBool
isLoading
=
false
.
obs
;
final
ProductModel
product
;
final
int
quantity
;
final
String
?
targetPhoneNumber
;
final
RxBool
usePoints
=
true
.
obs
;
var
selectedPaymentMethodIndex
=
-
1
.
obs
;
void
Function
(
String
message
)?
onShowAlertError
;
...
...
@@ -29,13 +31,12 @@ class TransactionDetailViewModel extends RestfulApiViewModel {
int
get
finalTotal
{
final
totalPrice
=
previewData
.
value
?.
totalPrice
??
0
;
final
pointValue
=
previewData
.
value
?.
pointData
?.
point
??
0
;
final
finalTotal
=
usePoints
.
value
&&
previewData
.
value
?.
pointData
?.
status
==
1
?
totalPrice
-
pointValue
:
totalPrice
;
final
finalTotal
=
usePoints
.
value
&&
previewData
.
value
?.
pointData
?.
status
==
1
?
totalPrice
-
pointValue
:
totalPrice
;
return
finalTotal
;
}
TransactionDetailViewModel
({
required
this
.
product
,
required
this
.
quantity
});
TransactionDetailViewModel
({
required
this
.
product
,
required
this
.
quantity
,
this
.
targetPhoneNumber
});
@override
void
onInit
()
{
...
...
@@ -75,12 +76,16 @@ class TransactionDetailViewModel extends RestfulApiViewModel {
paymentTokenId:
selectedBankAccount
?.
id
,
saveToken:
saveToken
,
metadata:
""
,
targetPhoneNumber:
targetPhoneNumber
,
)
.
then
((
value
)
{
hideLoading
();
if
(
value
.
isSuccess
)
{
final
data
=
value
.
data
;
if
((
data
?.
paymentUrl
??
""
).
isNotEmpty
)
{
if
((
targetPhoneNumber
??
""
).
isNotEmpty
)
{
ContactStorageService
().
saveUsedContact
(
targetPhoneNumber
??
""
);
}
Get
.
toNamed
(
paymentWebViewScreen
,
arguments:
PaymentWebViewInput
(
...
...
@@ -88,16 +93,20 @@ class TransactionDetailViewModel extends RestfulApiViewModel {
isContract:
false
,
orderId:
data
?.
id
??
""
,
showAlertBack:
true
,
callback:
(
result
)
{},
callback:
(
result
)
{
if
(
result
==
PaymentProcess
.
success
)
{
print
(
"PaymentProcess.success"
);
print
(
data
?.
id
??
""
);
Get
.
offNamed
(
transactionHistoryDetailScreen
,
arguments:
{
"orderId"
:
data
?.
id
??
""
,
"canBack"
:
true
},
);
}
},
)
);
}
else
if
((
data
?.
redeemId
??
""
).
isNotEmpty
&&
(
data
?.
id
??
""
).
isNotEmpty
)
{
Get
.
offNamed
(
transactionHistoryDetailScreen
,
arguments:
{
"orderId"
:
data
?.
id
??
""
,
"canBack"
:
true
,
}
);
Get
.
offNamed
(
transactionHistoryDetailScreen
,
arguments:
{
"orderId"
:
data
?.
id
??
""
,
"canBack"
:
true
});
}
else
{
onShowAlertError
?.
call
(
value
.
errorMessage
??
Constants
.
commonError
);
}
...
...
@@ -115,6 +124,7 @@ class TransactionDetailViewModel extends RestfulApiViewModel {
"quantity"
:
quantity
,
"price"
:
product
.
amountToBePaid
??
0
,
"access_token"
:
token
,
"target_phone_number"
:
targetPhoneNumber
??
""
,
};
if
(
product
.
previewFlashSale
?.
isFlashSalePrice
==
true
&&
product
.
previewFlashSale
?.
id
!=
null
)
{
body
[
"flash_sale_id"
]
=
product
.
previewFlashSale
!.
id
;
...
...
lib/screen/voucher/detail/voucher_detail_screen.dart
View file @
fa01087d
...
...
@@ -57,10 +57,7 @@ class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with Basi
});
return
;
}
_viewModel
=
Get
.
put
(
VoucherDetailViewModel
(
productId:
productId
,
customerProductId:
customerProductId
,
));
_viewModel
=
Get
.
put
(
VoucherDetailViewModel
(
productId:
productId
,
customerProductId:
customerProductId
));
_viewModel
.
onShowAlertError
=
(
message
)
{
if
(
message
.
isNotEmpty
)
{
showAlertError
(
content:
message
);
...
...
@@ -367,7 +364,7 @@ class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with Basi
child:
ElevatedButton
(
onPressed:
()
{
if
(
_viewModel
.
product
.
value
==
null
)
return
;
Get
.
to
(()
=>
VoucherCodeCardScreen
(
product:
_viewModel
.
product
.
value
!
,
));
Get
.
to
(()
=>
VoucherCodeCardScreen
(
product:
_viewModel
.
product
.
value
!));
},
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
Colors
.
green
,
...
...
@@ -442,9 +439,9 @@ class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with Basi
},
),
),
const
SizedBox
(
width:
24
),
],
),
const
SizedBox
(
width:
36
),
Expanded
(
child:
SizedBox
(
height:
48
,
...
...
lib/screen/voucher/models/product_brand_model.dart
View file @
fa01087d
...
...
@@ -25,3 +25,29 @@ class ProductBrandModel {
factory
ProductBrandModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$ProductBrandModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$ProductBrandModelToJson
(
this
);
}
class
PrefixMobileCarrier
{
static
const
String
mobiFoneCode
=
"MOBIFONE"
;
static
const
String
viettelCode
=
"VIETTEL"
;
static
const
String
vinaphoneCode
=
"VINAPHONE"
;
static
const
String
vietNamMobile
=
"VNM"
;
static
List
<
String
>
viettelPrefixNumbers
()
=>
[
"086"
,
"096"
,
"097"
,
"098"
,
"032"
,
"033"
,
"034"
,
"035"
,
"036"
,
"037"
,
"038"
,
"039"
,
];
static
List
<
String
>
vinaPhonePrefixNumbers
()
=>
[
"088"
,
"091"
,
"094"
,
"083"
,
"084"
,
"085"
,
"081"
,
"082"
,
];
static
List
<
String
>
mobiPhonePrefixNumbers
()
=>
[
"089"
,
"090"
,
"093"
,
"070"
,
"079"
,
"077"
,
"076"
,
"078"
,
];
static
List
<
String
>
vietNamMobilePrefixNumbers
()
=>
[
"092"
,
"056"
,
"058"
,
];
}
lib/screen/voucher/models/product_model.dart
View file @
fa01087d
...
...
@@ -15,6 +15,7 @@ import 'media_type.dart';
import
'my_product_status_type.dart'
;
part
'product_model.g.dart'
;
@JsonSerializable
()
class
ProductModel
{
final
int
?
id
;
...
...
@@ -28,6 +29,8 @@ class ProductModel {
@JsonKey
(
name:
'voucher_properties'
)
final
ProductPropertiesModel
?
properties
;
final
List
<
ProductMediaItem
>?
media
;
@JsonKey
(
name:
'preview_campaign'
)
final
ProductPreviewCampaignModel
?
previewCampaign
;
@JsonKey
(
name:
'preview_campaign_flash_sale'
)
final
PreviewFlashSale
?
previewFlashSale
;
@JsonKey
(
name:
'customer_product_info'
)
...
...
@@ -49,6 +52,7 @@ class ProductModel {
this
.
brand
,
this
.
properties
,
this
.
media
,
this
.
previewCampaign
,
this
.
previewFlashSale
,
this
.
customerInfoModel
,
this
.
item
,
...
...
@@ -109,7 +113,9 @@ class ProductModel {
}
double
get
progress
{
if
(
previewFlashSale
?.
fsQuantityTotal
!=
null
&&
previewFlashSale
?.
fsQuantitySold
!=
null
&&
previewFlashSale
!.
fsQuantityTotal
!
>
0
)
{
if
(
previewFlashSale
?.
fsQuantityTotal
!=
null
&&
previewFlashSale
?.
fsQuantitySold
!=
null
&&
previewFlashSale
!.
fsQuantityTotal
!
>
0
)
{
return
previewFlashSale
!.
fsQuantitySold
!
/
previewFlashSale
!.
fsQuantityTotal
!;
}
return
0.0
;
...
...
@@ -139,4 +145,20 @@ class ProductModel {
factory
ProductModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$ProductModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$ProductModelToJson
(
this
);
}
\ No newline at end of file
}
@JsonSerializable
()
class
ProductPreviewCampaignModel
{
@JsonKey
(
name:
'reward_point'
)
int
?
rewardPoint
;
@JsonKey
(
name:
'has_gift'
)
bool
?
hasGift
;
ProductPreviewCampaignModel
({
this
.
rewardPoint
,
this
.
hasGift
,
});
factory
ProductPreviewCampaignModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$ProductPreviewCampaignModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$ProductPreviewCampaignModelToJson
(
this
);
}
lib/screen/voucher/models/product_model.g.dart
View file @
fa01087d
...
...
@@ -34,6 +34,12 @@ ProductModel _$ProductModelFromJson(Map<String, dynamic> json) => ProductModel(
(
json
[
'media'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
ProductMediaItem
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
(),
previewCampaign:
json
[
'preview_campaign'
]
==
null
?
null
:
ProductPreviewCampaignModel
.
fromJson
(
json
[
'preview_campaign'
]
as
Map
<
String
,
dynamic
>,
),
previewFlashSale:
json
[
'preview_campaign_flash_sale'
]
==
null
?
null
...
...
@@ -67,6 +73,7 @@ Map<String, dynamic> _$ProductModelToJson(ProductModel instance) =>
'brand'
:
instance
.
brand
,
'voucher_properties'
:
instance
.
properties
,
'media'
:
instance
.
media
,
'preview_campaign'
:
instance
.
previewCampaign
,
'preview_campaign_flash_sale'
:
instance
.
previewFlashSale
,
'customer_product_info'
:
instance
.
customerInfoModel
,
'product_item'
:
instance
.
item
,
...
...
@@ -74,3 +81,17 @@ Map<String, dynamic> _$ProductModelToJson(ProductModel instance) =>
'require_form_regis'
:
instance
.
requireFormRegis
,
'type'
:
instance
.
type
,
};
ProductPreviewCampaignModel
_$ProductPreviewCampaignModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
ProductPreviewCampaignModel
(
rewardPoint:
(
json
[
'reward_point'
]
as
num
?)?.
toInt
(),
hasGift:
json
[
'has_gift'
]
as
bool
?,
);
Map
<
String
,
dynamic
>
_$ProductPreviewCampaignModelToJson
(
ProductPreviewCampaignModel
instance
,
)
=>
<
String
,
dynamic
>{
'reward_point'
:
instance
.
rewardPoint
,
'has_gift'
:
instance
.
hasGift
,
};
lib/screen/voucher/sub_widget/voucher_action_menu.dart
View file @
fa01087d
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:get/get_core/src/get_main.dart'
;
import
'../../../directional/directional_action_type.dart'
;
import
'../../../directional/directional_screen.dart'
;
import
'../../../resouce/base_color.dart'
;
import
'../../../shared/router_gage.dart'
;
class
VoucherActionMenu
extends
StatelessWidget
{
const
VoucherActionMenu
({
super
.
key
});
...
...
@@ -14,10 +19,10 @@ class VoucherActionMenu extends StatelessWidget {
padding:
const
EdgeInsets
.
symmetric
(
vertical:
12
),
child:
Row
(
children:
const
[
_ActionItem
(
icon:
"assets/images/ic_topup.png"
,
label:
'Nạp tiền
\n
diện thoại'
),
_ActionItem
(
icon:
"assets/images/ic_card_code.png"
,
label:
'Đổi mã
\n
thẻ nạp'
),
_ActionItem
(
icon:
"assets/images/ic_sim_service.png"
,
label:
'Gói cước
\n
nhà mạng'
),
_ActionItem
(
icon:
"assets/images/ic_topup_data.png"
,
label:
'Ưu đãi
\n
Data'
),
_ActionItem
(
icon:
"assets/images/ic_topup.png"
,
label:
'Nạp tiền
\n
diện thoại'
,
type:
DirectionalScreenName
.
topup
,
),
_ActionItem
(
icon:
"assets/images/ic_card_code.png"
,
label:
'Đổi mã
\n
thẻ nạp'
,
type:
DirectionalScreenName
.
productMobileCard
,
),
_ActionItem
(
icon:
"assets/images/ic_sim_service.png"
,
label:
'Gói cước
\n
nhà mạng'
,
type:
DirectionalScreenName
.
carrierPackage
,
),
_ActionItem
(
icon:
"assets/images/ic_topup_data.png"
,
label:
'Ưu đãi
\n
Data'
,
type:
DirectionalScreenName
.
simService
,
),
],
),
);
...
...
@@ -27,34 +32,45 @@ class VoucherActionMenu extends StatelessWidget {
class
_ActionItem
extends
StatelessWidget
{
final
String
icon
;
final
String
label
;
final
DirectionalScreenName
type
;
const
_ActionItem
({
required
this
.
icon
,
required
this
.
label
});
const
_ActionItem
({
required
this
.
icon
,
required
this
.
label
,
required
this
.
type
});
@override
Widget
build
(
BuildContext
context
)
{
final
screenWidth
=
MediaQuery
.
of
(
context
).
size
.
width
;
final
itemWidth
=
screenWidth
/
4
;
return
SizedBox
(
width:
itemWidth
,
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Image
.
asset
(
icon
,
fit:
BoxFit
.
cover
,
width:
40
,
height:
40
,
),
const
SizedBox
(
height:
8
),
Text
(
label
,
style:
const
TextStyle
(
fontSize:
12
),
textAlign:
TextAlign
.
center
,
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
),
],
return
GestureDetector
(
onTap:
()
{
final
param
=
type
==
DirectionalScreenName
.
carrierPackage
?
"https://mypoint.uudaigoicuoc.com/"
:
null
;
DirectionalScreen
?
screen
=
DirectionalScreen
.
build
(
clickActionType:
type
.
rawValue
,
clickActionParam:
param
,
);
screen
?.
begin
();
},
child:
SizedBox
(
width:
itemWidth
,
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Image
.
asset
(
icon
,
fit:
BoxFit
.
cover
,
width:
40
,
height:
40
,
),
const
SizedBox
(
height:
8
),
Text
(
label
,
style:
const
TextStyle
(
fontSize:
12
),
textAlign:
TextAlign
.
center
,
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
),
],
),
),
);
}
...
...
Prev
1
2
3
4
5
6
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