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
8d264762
Commit
8d264762
authored
Apr 29, 2025
by
DatHV
Browse files
update voucher detail
parent
560b49dd
Changes
11
Hide whitespace changes
Inline
Side-by-side
lib/configs/api_paths.dart
View file @
8d264762
...
@@ -29,4 +29,6 @@ class APIPaths {
...
@@ -29,4 +29,6 @@ class APIPaths {
static
const
String
getSearchProducts
=
"/product/api/v2.0/products/search"
;
static
const
String
getSearchProducts
=
"/product/api/v2.0/products/search"
;
static
const
String
getProductDetail
=
"/product/api/v2.0/products/%@"
;
static
const
String
getProductDetail
=
"/product/api/v2.0/products/%@"
;
static
const
String
getProductStores
=
"/product/api/v2.0/product/stores"
;
static
const
String
getProductStores
=
"/product/api/v2.0/product/stores"
;
static
const
String
productCustomerLikes
=
"/product/api/v2.0/customer/likes"
;
static
const
String
productCustomerUnlikes
=
"/product/api/v2.0/customer/likes/%@"
;
}
}
\ No newline at end of file
lib/networking/restful_api.dart
View file @
8d264762
...
@@ -7,7 +7,7 @@ import '../configs/constants.dart';
...
@@ -7,7 +7,7 @@ import '../configs/constants.dart';
import
'model_maker.dart'
;
import
'model_maker.dart'
;
enum
Method
{
enum
Method
{
GET
,
POST
,
PUT
GET
,
POST
,
PUT
,
DELETE
}
}
class
RestfulAPIClient
{
class
RestfulAPIClient
{
...
...
lib/networking/restful_api_request.dart
View file @
8d264762
...
@@ -20,6 +20,7 @@ import '../screen/otp/model/otp_verify_response_model.dart';
...
@@ -20,6 +20,7 @@ import '../screen/otp/model/otp_verify_response_model.dart';
import
'../screen/pageDetail/model/campaign_detail_model.dart'
;
import
'../screen/pageDetail/model/campaign_detail_model.dart'
;
import
'../screen/pageDetail/model/detail_page_rule_type.dart'
;
import
'../screen/pageDetail/model/detail_page_rule_type.dart'
;
import
'../screen/splash/splash_screen_viewmodel.dart'
;
import
'../screen/splash/splash_screen_viewmodel.dart'
;
import
'../screen/voucher/models/like_product_reponse_model.dart'
;
import
'../screen/voucher/models/product_store_model.dart'
;
import
'../screen/voucher/models/product_store_model.dart'
;
import
'../screen/voucher/models/search_product_response_model.dart'
;
import
'../screen/voucher/models/search_product_response_model.dart'
;
import
'model_maker.dart'
;
import
'model_maker.dart'
;
...
@@ -296,4 +297,21 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
...
@@ -296,4 +297,21 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
return
list
.
map
((
e
)
=>
ProductStoreModel
.
fromJson
(
e
)).
toList
();
return
list
.
map
((
e
)
=>
ProductStoreModel
.
fromJson
(
e
)).
toList
();
});
});
}
}
Future
<
BaseResponseModel
<
LikeProductReponseModel
>>
likeProduct
(
int
id
)
async
{
final
body
=
{
"product_id"
:
id
};
return
requestNormal
(
APIPaths
.
productCustomerLikes
,
Method
.
POST
,
body
,
(
data
)
{
return
LikeProductReponseModel
.
fromJson
(
data
as
Json
);
});
}
Future
<
BaseResponseModel
<
EmptyCodable
>>
unlikeProduct
(
int
id
)
async
{
final
path
=
APIPaths
.
productCustomerUnlikes
.
replaceAll
(
"%@"
,
id
.
toString
());
return
requestNormal
(
path
,
Method
.
DELETE
,
{},
(
data
)
=>
EmptyCodable
.
fromJson
(
data
as
Json
),
);
}
}
}
\ No newline at end of file
lib/screen/voucher/detail/store_list_section.dart
View file @
8d264762
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:mypoint_flutter_app/shared/direction_google_map.dart'
;
import
'../models/product_store_model.dart'
;
import
'../models/product_store_model.dart'
;
class
StoreListSection
extends
StatelessWidget
{
class
StoreListSection
extends
StatelessWidget
{
final
List
<
ProductStoreModel
>
stores
;
final
List
<
ProductStoreModel
>
stores
;
final
String
?
brandLogo
;
const
StoreListSection
({
Key
?
key
,
required
this
.
stores
})
:
super
(
key:
key
);
const
StoreListSection
({
Key
?
key
,
required
this
.
stores
,
this
.
brandLogo
})
:
super
(
key:
key
);
@override
@override
Widget
build
(
BuildContext
context
)
{
Widget
build
(
BuildContext
context
)
{
...
@@ -16,68 +18,64 @@ class StoreListSection extends StatelessWidget {
...
@@ -16,68 +18,64 @@ class StoreListSection extends StatelessWidget {
return
Container
(
return
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
padding:
const
EdgeInsets
.
all
(
16
),
padding:
const
EdgeInsets
.
all
(
16
),
decoration:
BoxDecoration
(
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
16
)),
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
16
),
),
child:
Column
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
children:
[
const
Text
(
const
Text
(
'Địa điểm áp dụng:'
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
16
)),
'Địa điểm áp dụng:'
,
const
SizedBox
(
height:
10
),
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
,
fontSize:
16
),
// ...stores.map((store) => _buildStoreItem(store)).toList(),
),
...
stores
.
map
((
store
)
=>
InkWell
(
const
SizedBox
(
height:
12
),
onTap:
()
{
...
stores
.
map
((
store
)
=>
_buildStoreItem
(
store
)).
toList
(),
_onTapStore
(
store
);
},
child:
_buildStoreItem
(
store
),
)),
],
],
),
),
);
);
}
}
_onTapStore
(
ProductStoreModel
store
)
{
showGoogleMap
(
lat:
store
.
latitude
,
lng:
store
.
longitude
);
}
Widget
_buildStoreItem
(
ProductStoreModel
store
)
{
Widget
_buildStoreItem
(
ProductStoreModel
store
)
{
return
Padding
(
return
Padding
(
padding:
const
EdgeInsets
.
only
(
bottom:
12
),
padding:
const
EdgeInsets
.
only
(
bottom:
12
),
child:
Row
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
children:
[
// Logo Brand
Row
(
ClipOval
(
children:
[
child:
Image
.
network
(
ClipOval
(
""
,
child:
Image
.
network
(
width:
20
,
brandLogo
??
""
,
height:
20
,
width:
20
,
fit:
BoxFit
.
cover
,
height:
20
,
errorBuilder:
(
_
,
__
,
___
)
=>
fit:
BoxFit
.
cover
,
Image
.
asset
(
'assets/images/sample.png'
,
width:
20
,
height:
20
),
errorBuilder:
(
_
,
__
,
___
)
=>
Image
.
asset
(
'assets/images/ic_logo.png'
,
width:
20
,
height:
20
),
),
),
const
SizedBox
(
width:
8
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
store
.
name
??
''
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w600
),
),
),
const
SizedBox
(
height:
4
),
),
Row
(
const
SizedBox
(
width:
8
),
crossAxisAlignment:
CrossAxisAlignment
.
start
,
Text
(
store
.
name
??
''
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w600
)),
children:
[
],
const
Icon
(
Icons
.
location_on_outlined
,
size:
18
,
color:
Colors
.
grey
),
),
const
SizedBox
(
width:
4
),
const
SizedBox
(
height:
4
),
Expanded
(
Row
(
child:
Text
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
store
.
address
??
''
,
children:
[
style:
const
TextStyle
(
color:
Colors
.
grey
,
fontSize:
13
),
const
Icon
(
Icons
.
location_on_outlined
,
size:
18
,
color:
Colors
.
grey
),
maxLines:
2
,
const
SizedBox
(
width:
4
),
overflow:
TextOverflow
.
ellipsis
,
Expanded
(
),
child:
Text
(
),
store
.
address
??
''
,
],
style:
const
TextStyle
(
color:
Colors
.
grey
,
fontSize:
13
),
maxLines:
2
,
overflow:
TextOverflow
.
ellipsis
,
),
),
]
,
)
,
)
,
]
,
),
),
],
],
),
),
...
...
lib/screen/voucher/detail/voucher_detail_screen.dart
View file @
8d264762
...
@@ -2,6 +2,7 @@ import 'dart:math';
...
@@ -2,6 +2,7 @@ import 'dart:math';
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_widget_from_html/flutter_widget_from_html.dart'
;
import
'package:flutter_widget_from_html/flutter_widget_from_html.dart'
;
import
'package:mypoint_flutter_app/screen/voucher/detail/store_list_section.dart'
;
import
'package:mypoint_flutter_app/screen/voucher/detail/store_list_section.dart'
;
import
'package:url_launcher/url_launcher.dart'
;
import
'../../../resouce/base_color.dart'
;
import
'../../../resouce/base_color.dart'
;
import
'../../../widgets/back_button.dart'
;
import
'../../../widgets/back_button.dart'
;
import
'../../../widgets/custom_empty_widget.dart'
;
import
'../../../widgets/custom_empty_widget.dart'
;
...
@@ -9,6 +10,8 @@ import '../../../widgets/custom_price_tag.dart';
...
@@ -9,6 +10,8 @@ import '../../../widgets/custom_price_tag.dart';
import
'../../../widgets/dashed_line.dart'
;
import
'../../../widgets/dashed_line.dart'
;
import
'../../../widgets/image_loader.dart'
;
import
'../../../widgets/image_loader.dart'
;
import
'../../../widgets/measure_size.dart'
;
import
'../../../widgets/measure_size.dart'
;
import
'../models/cash_type.dart'
;
import
'../models/my_product_status_type.dart'
;
import
'../models/product_model.dart'
;
import
'../models/product_model.dart'
;
import
'voucher_detail_viewmodel.dart'
;
import
'voucher_detail_viewmodel.dart'
;
import
'package:get/get.dart'
;
import
'package:get/get.dart'
;
...
@@ -23,8 +26,8 @@ class VoucherDetailScreen extends StatefulWidget {
...
@@ -23,8 +26,8 @@ class VoucherDetailScreen extends StatefulWidget {
class
_VoucherDetailScreenState
extends
State
<
VoucherDetailScreen
>
{
class
_VoucherDetailScreenState
extends
State
<
VoucherDetailScreen
>
{
late
final
int
productId
;
late
final
int
productId
;
late
final
VoucherDetailViewModel
_viewModel
;
late
final
VoucherDetailViewModel
_viewModel
;
final
GlobalKey
_infoKey
=
GlobalKey
();
double
_infoHeight
=
0
;
double
_infoHeight
=
0
;
final
_quantity
=
1
.
obs
;
@override
@override
void
initState
()
{
void
initState
()
{
...
@@ -41,6 +44,7 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
...
@@ -41,6 +44,7 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
@override
@override
Widget
build
(
BuildContext
context
)
{
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
return
Scaffold
(
backgroundColor:
Colors
.
grey
.
shade100
,
body:
Obx
(()
{
body:
Obx
(()
{
if
(
_viewModel
.
isLoading
.
value
)
{
if
(
_viewModel
.
isLoading
.
value
)
{
return
const
Center
(
child:
CircularProgressIndicator
());
return
const
Center
(
child:
CircularProgressIndicator
());
...
@@ -51,26 +55,45 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
...
@@ -51,26 +55,45 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
}
}
return
Stack
(
return
Stack
(
children:
[
children:
[
Container
(
SingleChildScrollView
(
color:
Colors
.
grey
.
shade100
,
child:
Column
(
child:
SingleChildScrollView
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
child:
Column
(
children:
[
_buildHeaderWithInfo
(
product
),
SizedBox
(
height:
max
(
_infoHeight
-
36
,
0
)),
_buildTextBlock
(
"Chi tiết ưu đãi:"
,
product
.
content
?.
detail
),
_buildTextBlock
(
"Điều kiện áp dụng:"
,
product
.
content
?.
termAndCondition
),
SizedBox
(
height:
8
),
StoreListSection
(
stores:
_viewModel
.
stores
,
brandLogo:
product
.
brand
?.
logo
??
""
),
_buildSupportBlock
(
product
),
Container
(
color:
Colors
.
grey
.
shade100
,
child:
SizedBox
(
height:
64
)),
],
),
),
SafeArea
(
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
8
),
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
crossAxisAlignment:
CrossAxisAlignment
.
start
,
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
children:
[
_buildHeaderWithInfo
(
product
),
CustomBackButton
(),
SizedBox
(
height:
max
(
_infoHeight
-
36
,
0
)),
_buildFavoriteButton
(),
_buildDetailBlock
(
product
),
_buildConditionBlock
(
product
),
SizedBox
(
height:
8
),
StoreListSection
(
stores:
_viewModel
.
stores
),
],
],
),
),
),
),
),
),
SafeArea
(
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
8
),
child:
CustomBackButton
())),
//
SafeArea(child: Padding(padding: const EdgeInsets.all(8), child: CustomBackButton())),
],
],
);
);
}),
}),
bottomNavigationBar:
Obx
(()
{
final
product
=
_viewModel
.
product
.
value
;
if
(
product
==
null
)
{
return
const
SizedBox
.
shrink
();
}
return
_buildBottomAction
(
product
);
}),
);
);
}
}
...
@@ -116,12 +139,16 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
...
@@ -116,12 +139,16 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
decoration:
BoxDecoration
(
decoration:
BoxDecoration
(
color:
Colors
.
white
,
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
16
),
borderRadius:
BorderRadius
.
circular
(
16
),
boxShadow:
[
BoxShadow
(
color:
Colors
.
black
.
withOpacity
(
0.
05
),
blurRadius:
8
,
offset:
const
Offset
(
0
,
4
))],
boxShadow:
[
BoxShadow
(
color:
Colors
.
black
.
withOpacity
(
0.
1
),
blurRadius:
8
,
offset:
const
Offset
(
0
,
4
))],
),
),
child:
Column
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
center
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
children:
[
Text
(
product
.
content
?.
name
??
''
,
style:
const
TextStyle
(
fontSize:
24
,
fontWeight:
FontWeight
.
bold
)),
Text
(
product
.
content
?.
name
??
''
,
textAlign:
TextAlign
.
center
,
style:
const
TextStyle
(
fontSize:
24
,
fontWeight:
FontWeight
.
bold
)
),
const
SizedBox
(
height:
8
),
const
SizedBox
(
height:
8
),
_buildExpireAndStock
(
product
),
_buildExpireAndStock
(
product
),
const
SizedBox
(
height:
16
),
const
SizedBox
(
height:
16
),
...
@@ -155,37 +182,19 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
...
@@ -155,37 +182,19 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
Widget
_buildExpireAndStock
(
ProductModel
product
)
{
Widget
_buildExpireAndStock
(
ProductModel
product
)
{
final
bool
isOutOfStock
=
!(
product
.
inStock
??
true
);
final
bool
isOutOfStock
=
!(
product
.
inStock
??
true
);
final
bool
hasExpire
=
product
.
expire
.
isNotEmpty
;
final
bool
hasExpire
=
product
.
expire
.
isNotEmpty
;
return
Row
(
return
Row
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
mainAxisAlignment:
MainAxisAlignment
.
center
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
children:
[
if
(
hasExpire
)
if
(
hasExpire
)
Text
(
Text
(
'Hạn dùng: '
,
style:
const
TextStyle
(
color:
Colors
.
grey
,
fontWeight:
FontWeight
.
bold
,
fontSize:
12
)),
'Hạn dùng: '
,
style:
const
TextStyle
(
color:
Colors
.
grey
,
fontWeight:
FontWeight
.
bold
,
fontSize:
12
,
),
),
if
(
hasExpire
)
if
(
hasExpire
)
Text
(
Text
(
product
.
expire
,
style:
const
TextStyle
(
color:
Colors
.
red
,
fontWeight:
FontWeight
.
bold
,
fontSize:
12
)),
product
.
expire
,
style:
const
TextStyle
(
color:
Colors
.
red
,
fontWeight:
FontWeight
.
bold
,
fontSize:
12
,
),
),
if
(
isOutOfStock
)
if
(
isOutOfStock
)
Container
(
Container
(
margin:
const
EdgeInsets
.
only
(
left:
8
),
margin:
const
EdgeInsets
.
only
(
left:
8
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
4
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
4
),
decoration:
BoxDecoration
(
decoration:
BoxDecoration
(
color:
Colors
.
grey
,
borderRadius:
BorderRadius
.
circular
(
4
)),
color:
Colors
.
grey
,
borderRadius:
BorderRadius
.
circular
(
4
),
),
child:
const
Text
(
child:
const
Text
(
'Tạm hết'
,
'Tạm hết'
,
style:
TextStyle
(
color:
Colors
.
white
,
fontSize:
12
,
fontWeight:
FontWeight
.
bold
),
style:
TextStyle
(
color:
Colors
.
white
,
fontSize:
12
,
fontWeight:
FontWeight
.
bold
),
...
@@ -196,11 +205,11 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
...
@@ -196,11 +205,11 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
}
}
Widget
_buildDetailBlock
(
ProductModel
product
)
{
Widget
_buildDetailBlock
(
ProductModel
product
)
{
return
_buildTextBlock
(
"Chi tiết ưu đãi"
,
product
.
content
?.
detail
);
return
_buildTextBlock
(
"Chi tiết ưu đãi
:
"
,
product
.
content
?.
detail
);
}
}
Widget
_buildConditionBlock
(
ProductModel
product
)
{
Widget
_buildConditionBlock
(
ProductModel
product
)
{
return
_buildTextBlock
(
"Điều kiện áp dụng"
,
product
.
content
?.
termAndCondition
);
return
_buildTextBlock
(
"Điều kiện áp dụng
:
"
,
product
.
content
?.
termAndCondition
);
}
}
Widget
_buildTextBlock
(
String
title
,
String
?
content
)
{
Widget
_buildTextBlock
(
String
title
,
String
?
content
)
{
...
@@ -214,9 +223,249 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
...
@@ -214,9 +223,249 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
children:
[
children:
[
Text
(
title
,
style:
const
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
bold
)),
Text
(
title
,
style:
const
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
bold
)),
const
SizedBox
(
height:
8
),
const
SizedBox
(
height:
8
),
HtmlWidget
(
content
),
HtmlWidget
(
content
,
textStyle:
const
TextStyle
(
fontSize:
13
,
color:
Colors
.
black54
)
),
],
],
),
),
);
);
}
}
Widget
_buildSupportBlock
(
ProductModel
product
)
{
final
brand
=
product
.
brand
;
if
(
brand
==
null
)
return
const
SizedBox
.
shrink
();
return
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
padding:
const
EdgeInsets
.
all
(
16
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
circular
(
16
)),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'Hỗ trợ:'
,
style:
const
TextStyle
(
fontSize:
16
,
fontWeight:
FontWeight
.
bold
)),
const
SizedBox
(
height:
4
),
Text
(
'Nếu bạn gặp bất kỳ vấn đề gì với voucher này, xin vui lòng liên hệ
${brand.name ?? ''}
'
,
style:
const
TextStyle
(
fontSize:
13
,
color:
Colors
.
black54
),
),
const
SizedBox
(
height:
12
),
if
((
brand
.
phone
??
''
).
isNotEmpty
)
_buildContactRow
(
Icons
.
phone
,
brand
.
phone
??
''
,
onTap:
()
async
{
final
Uri
phoneUri
=
Uri
.
parse
(
'tel:
${brand.phone}
'
);
_launchUri
(
phoneUri
);
},
),
if
((
brand
.
email
??
''
).
isNotEmpty
)
_buildContactRow
(
Icons
.
email_outlined
,
brand
.
email
??
''
,
onTap:
()
async
{
final
Uri
emailUri
=
Uri
.
parse
(
'mailto:
${brand.email}
'
);
_launchUri
(
emailUri
);
},
),
if
((
brand
.
website
??
''
).
isNotEmpty
)
_buildContactRow
(
Icons
.
language
,
brand
.
website
??
''
,
onTap:
()
{
final
url
=
brand
.
website
!.
startsWith
(
'http'
)
?
brand
.
website
!
:
'https://
${brand.website}
'
;
_launchUri
(
Uri
(
scheme:
url
));
},
),
],
),
);
}
_launchUri
(
Uri
uri
)
async
{
if
(
await
canLaunchUrl
(
uri
))
{
await
launchUrl
(
uri
);
}
else
{
throw
'Could not launch
$uri
'
;
}
}
Widget
_buildContactRow
(
IconData
icon
,
String
value
,
{
VoidCallback
?
onTap
})
{
return
InkWell
(
onTap:
onTap
,
child:
Padding
(
padding:
const
EdgeInsets
.
only
(
bottom:
8
),
child:
Row
(
children:
[
Icon
(
icon
,
size:
18
,
color:
Colors
.
black54
),
const
SizedBox
(
width:
8
),
Expanded
(
child:
Text
(
value
,
style:
const
TextStyle
(
fontSize:
13
,
color:
Colors
.
black54
))),
const
Icon
(
Icons
.
chevron_right
,
color:
Colors
.
black54
),
],
),
),
);
}
Widget
_buildBottomAction
(
ProductModel
product
)
{
// if (!(product.isMyProduct
// ? product.customerInfoModel?.status == MyProductStatusType.waiting
// : (product.inStock == true && !(product.expired == true)))) {
// return const SizedBox.shrink();
// }
if
(
product
.
isMyProduct
)
{
return
_buildUseButton
();
}
else
if
(
product
.
price
?.
method
==
CashType
.
point
)
{
return
_buildExchangeButton
();
}
else
{
return
_buildBuyButtonWithCounter
();
}
}
Widget
_buildBottomActionContainer
({
required
Widget
child
})
{
return
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
16
),
decoration:
const
BoxDecoration
(
color:
Colors
.
white
,
boxShadow:
[
BoxShadow
(
color:
Colors
.
black54
,
blurRadius:
8
,
offset:
Offset
(
0
,
4
))],
),
child:
SafeArea
(
top:
false
,
child:
child
),
);
}
Widget
_buildUseButton
()
{
return
_buildBottomActionContainer
(
child:
SizedBox
(
width:
double
.
infinity
,
height:
48
,
child:
ElevatedButton
(
onPressed:
()
{
// TODO: Handle sử dụng voucher
},
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
Colors
.
green
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
12
)),
),
child:
const
Text
(
'Sử Dụng'
,
style:
TextStyle
(
fontSize:
16
,
color:
Colors
.
white
,
fontWeight:
FontWeight
.
bold
)),
),
),
);
}
Widget
_buildExchangeButton
()
{
return
_buildBottomActionContainer
(
child:
SizedBox
(
width:
double
.
infinity
,
height:
48
,
child:
ElevatedButton
(
onPressed:
()
{
// TODO: Handle đổi ưu đãi
},
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
BaseColor
.
primary500
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
12
)),
),
child:
const
Text
(
'Đổi Ưu Đãi'
,
style:
TextStyle
(
fontSize:
16
,
color:
Colors
.
white
,
fontWeight:
FontWeight
.
bold
),
),
),
),
);
}
Widget
_buildBuyButtonWithCounter
()
{
return
_buildBottomActionContainer
(
child:
Row
(
children:
[
Row
(
children:
[
Container
(
decoration:
BoxDecoration
(
color:
Colors
.
white
,
border:
Border
.
all
(
color:
Colors
.
grey
.
shade300
),
borderRadius:
BorderRadius
.
circular
(
50
),
),
child:
IconButton
(
icon:
const
Icon
(
Icons
.
remove
,
color:
Colors
.
black
),
onPressed:
()
{
if
(
_quantity
.
value
>
1
)
{
_quantity
.
value
--;
// TODO: update state
}
},
),
),
const
SizedBox
(
width:
12
),
Obx
(()
=>
Text
(
'
${_quantity.value}
'
,
style:
const
TextStyle
(
fontSize:
16
),
)),
const
SizedBox
(
width:
12
),
Container
(
decoration:
BoxDecoration
(
color:
BaseColor
.
primary500
,
border:
Border
.
all
(
color:
Colors
.
grey
.
shade300
),
borderRadius:
BorderRadius
.
circular
(
50
),
),
child:
IconButton
(
icon:
const
Icon
(
Icons
.
add
,
color:
Colors
.
white
),
onPressed:
()
{
_quantity
.
value
++;
// TODO: update state
},
),
),
],
),
const
SizedBox
(
width:
36
),
Expanded
(
child:
SizedBox
(
height:
48
,
child:
ElevatedButton
(
onPressed:
()
{
// TODO: Handle mua ngay
},
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
BaseColor
.
primary500
,
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
12
)),
),
child:
const
Text
(
'Mua ngay'
,
style:
TextStyle
(
fontSize:
16
,
color:
Colors
.
white
,
fontWeight:
FontWeight
.
bold
),
),
),
),
),
],
),
);
}
Widget
_buildFavoriteButton
()
{
return
Obx
(()
{
final
isFavorite
=
_viewModel
.
liked
.
value
;
return
Align
(
alignment:
Alignment
.
topRight
,
child:
Padding
(
padding:
const
EdgeInsets
.
only
(
top:
8
,
right:
8
),
child:
GestureDetector
(
onTap:
()
{
_viewModel
.
toggleFavorite
();
},
child:
Container
(
width:
40
,
height:
40
,
decoration:
BoxDecoration
(
color:
Colors
.
grey
.
withOpacity
(
0.6
),
shape:
BoxShape
.
circle
,
),
child:
Icon
(
Icons
.
favorite
,
color:
isFavorite
?
BaseColor
.
primary600
:
Colors
.
white
,
size:
24
,
),
),
),
),
);
});
}
}
}
lib/screen/voucher/detail/voucher_detail_viewmodel.dart
View file @
8d264762
...
@@ -10,6 +10,7 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
...
@@ -10,6 +10,7 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
var
stores
=
RxList
<
ProductStoreModel
>();
var
stores
=
RxList
<
ProductStoreModel
>();
var
product
=
Rxn
<
ProductModel
>();
var
product
=
Rxn
<
ProductModel
>();
var
isLoading
=
false
.
obs
;
var
isLoading
=
false
.
obs
;
var
liked
=
false
.
obs
;
@override
@override
void
onInit
()
{
void
onInit
()
{
...
@@ -18,12 +19,33 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
...
@@ -18,12 +19,33 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
_getProductStores
();
_getProductStores
();
}
}
Future
<
void
>
toggleFavorite
()
async
{
// if (liked.value) {
// liked.value = false;
// } else {
// liked.value = true;
// }
final
value
=
product
.
value
;
if
(
value
==
null
)
return
;
if
(
value
!.
liked
==
true
)
{
await
client
.
unlikeProduct
(
value
?.
likeId
??
0
);
value
?.
likeId
=
0
;
Future
.
microtask
(()
=>
liked
.
value
=
false
);
}
else
{
final
response
=
await
client
.
likeProduct
(
productId
);
value
?.
likeId
=
response
.
data
?.
id
;
Future
.
microtask
(()
=>
liked
.
value
=
(
response
.
data
?.
id
??
0
)
!=
0
);
}
// product.refresh();
}
Future
<
void
>
_getProductDetail
()
async
{
Future
<
void
>
_getProductDetail
()
async
{
if
(
isLoading
.
value
)
return
;
if
(
isLoading
.
value
)
return
;
try
{
try
{
isLoading
.
value
=
true
;
isLoading
.
value
=
true
;
final
response
=
await
client
.
getProduct
(
productId
);
final
response
=
await
client
.
getProduct
(
productId
);
product
.
value
=
response
.
data
;
product
.
value
=
response
.
data
;
liked
.
value
=
product
.
value
?.
liked
==
true
;
}
catch
(
error
)
{
}
catch
(
error
)
{
print
(
"Error fetching product detail:
$error
"
);
print
(
"Error fetching product detail:
$error
"
);
}
finally
{
}
finally
{
...
...
lib/screen/voucher/models/like_product_reponse_model.dart
0 → 100644
View file @
8d264762
import
'package:json_annotation/json_annotation.dart'
;
part
'like_product_reponse_model.g.dart'
;
@JsonSerializable
()
class
LikeProductReponseModel
{
@JsonKey
(
name:
'like_id'
)
final
int
?
id
;
LikeProductReponseModel
({
this
.
id
,
});
factory
LikeProductReponseModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$LikeProductReponseModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$LikeProductReponseModelToJson
(
this
);
}
lib/screen/voucher/models/like_product_reponse_model.g.dart
0 → 100644
View file @
8d264762
// GENERATED CODE - DO NOT MODIFY BY HAND
part of
'like_product_reponse_model.dart'
;
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
LikeProductReponseModel
_$LikeProductReponseModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
LikeProductReponseModel
(
id:
(
json
[
'like_id'
]
as
num
?)?.
toInt
());
Map
<
String
,
dynamic
>
_$LikeProductReponseModelToJson
(
LikeProductReponseModel
instance
,
)
=>
<
String
,
dynamic
>{
'like_id'
:
instance
.
id
};
lib/screen/voucher/models/product_model.dart
View file @
8d264762
...
@@ -17,6 +17,8 @@ part 'product_model.g.dart';
...
@@ -17,6 +17,8 @@ part 'product_model.g.dart';
@JsonSerializable
()
@JsonSerializable
()
class
ProductModel
{
class
ProductModel
{
final
int
?
id
;
final
int
?
id
;
@JsonKey
(
name:
'like_id'
)
late
final
int
?
likeId
;
@JsonKey
(
name:
'quantity_available'
)
@JsonKey
(
name:
'quantity_available'
)
final
int
?
quantityAvailable
;
final
int
?
quantityAvailable
;
final
ProductContentModel
?
content
;
final
ProductContentModel
?
content
;
...
@@ -36,6 +38,7 @@ class ProductModel {
...
@@ -36,6 +38,7 @@ class ProductModel {
ProductModel
({
ProductModel
({
this
.
id
,
this
.
id
,
this
.
likeId
,
this
.
quantityAvailable
,
this
.
quantityAvailable
,
this
.
content
,
this
.
content
,
this
.
price
,
this
.
price
,
...
@@ -72,6 +75,10 @@ class ProductModel {
...
@@ -72,6 +75,10 @@ class ProductModel {
return
(
quantityAvailable
??
1
)
!=
0
;
return
(
quantityAvailable
??
1
)
!=
0
;
}
}
bool
get
liked
{
return
(
likeId
??
0
)
!=
0
;
}
bool
get
expired
{
bool
get
expired
{
if
(
customerInfoModel
!=
null
)
{
if
(
customerInfoModel
!=
null
)
{
return
customerInfoModel
?.
status
==
MyProductStatusType
.
expired
;
return
customerInfoModel
?.
status
==
MyProductStatusType
.
expired
;
...
@@ -85,5 +92,6 @@ class ProductModel {
...
@@ -85,5 +92,6 @@ class ProductModel {
}
}
factory
ProductModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$ProductModelFromJson
(
json
);
factory
ProductModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$ProductModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$ProductModelToJson
(
this
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$ProductModelToJson
(
this
);
}
}
\ No newline at end of file
lib/screen/voucher/models/product_model.g.dart
View file @
8d264762
...
@@ -8,6 +8,7 @@ part of 'product_model.dart';
...
@@ -8,6 +8,7 @@ part of 'product_model.dart';
ProductModel
_$ProductModelFromJson
(
Map
<
String
,
dynamic
>
json
)
=>
ProductModel
(
ProductModel
_$ProductModelFromJson
(
Map
<
String
,
dynamic
>
json
)
=>
ProductModel
(
id:
(
json
[
'id'
]
as
num
?)?.
toInt
(),
id:
(
json
[
'id'
]
as
num
?)?.
toInt
(),
likeId:
(
json
[
'like_id'
]
as
num
?)?.
toInt
(),
quantityAvailable:
(
json
[
'quantity_available'
]
as
num
?)?.
toInt
(),
quantityAvailable:
(
json
[
'quantity_available'
]
as
num
?)?.
toInt
(),
content:
content:
json
[
'content'
]
==
null
json
[
'content'
]
==
null
...
@@ -57,6 +58,7 @@ ProductModel _$ProductModelFromJson(Map<String, dynamic> json) => ProductModel(
...
@@ -57,6 +58,7 @@ ProductModel _$ProductModelFromJson(Map<String, dynamic> json) => ProductModel(
Map
<
String
,
dynamic
>
_$ProductModelToJson
(
ProductModel
instance
)
=>
Map
<
String
,
dynamic
>
_$ProductModelToJson
(
ProductModel
instance
)
=>
<
String
,
dynamic
>{
<
String
,
dynamic
>{
'id'
:
instance
.
id
,
'id'
:
instance
.
id
,
'like_id'
:
instance
.
likeId
,
'quantity_available'
:
instance
.
quantityAvailable
,
'quantity_available'
:
instance
.
quantityAvailable
,
'content'
:
instance
.
content
,
'content'
:
instance
.
content
,
'price'
:
instance
.
price
,
'price'
:
instance
.
price
,
...
...
lib/shared/direction_google_map.dart
0 → 100644
View file @
8d264762
import
'package:url_launcher/url_launcher.dart'
;
Future
<
void
>
showGoogleMap
({
required
double
?
lat
,
required
double
?
lng
})
async
{
if
(
lat
==
null
||
lng
==
null
)
return
;
final
googleMapsSchemeUrl
=
Uri
.
parse
(
'comgooglemaps://'
);
final
googleMapsAppUrl
=
Uri
.
parse
(
'comgooglemaps-x-callback://?saddr=&daddr=
$lat
,
$lng
&directionsmode=driving'
);
final
googleMapsWebUrl
=
Uri
.
parse
(
'https://www.google.com/maps/dir/?saddr=&daddr=
$lat
,
$lng
&directionsmode=driving'
);
// Kiểm tra xem device có cài app Google Maps không
if
(
await
canLaunchUrl
(
googleMapsSchemeUrl
))
{
// Có app Google Maps -> mở app
if
(
await
canLaunchUrl
(
googleMapsAppUrl
))
{
await
launchUrl
(
googleMapsAppUrl
);
}
}
else
{
// Không có app -> mở trên trình duyệt
if
(
await
canLaunchUrl
(
googleMapsWebUrl
))
{
await
launchUrl
(
googleMapsWebUrl
,
mode:
LaunchMode
.
externalApplication
);
}
}
}
\ No newline at end of file
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