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
97763d9b
Commit
97763d9b
authored
Oct 10, 2025
by
DatHV
Browse files
update health book
parent
fc2caf86
Changes
14
Hide whitespace changes
Inline
Side-by-side
assets/images/bg_medon_card.png
0 → 100644
View file @
97763d9b
30.8 KB
lib/extensions/string_extension.dart
View file @
97763d9b
...
...
@@ -16,6 +16,8 @@ extension NullableString on String? {
}
bool
get
hasText
=>
(
this
?.
trim
().
isNotEmpty
??
false
);
bool
get
isNullOrBlank
=>
(
this
==
null
||
this
!.
trim
().
isEmpty
);
}
extension
StringUrlExtension
on
String
{
...
...
lib/networking/restful_api_client_all_request.dart
View file @
97763d9b
...
...
@@ -1048,9 +1048,9 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
});
}
Future
<
BaseResponseModel
<
VerifyRegisterCampaignModel
>>
verifyRegisterForm
(
String
path
)
async
{
Future
<
BaseResponseModel
<
VerifyRegisterCampaignModel
>>
verifyRegisterForm
(
String
path
,
Json
body
)
async
{
var
path_
=
path
.
startsWith
(
'/'
)
?
path
:
'/
$path
'
;
return
requestNormal
(
path_
,
Method
.
POST
,
{}
,
(
data
)
{
return
requestNormal
(
path_
,
Method
.
POST
,
body
,
(
data
)
{
return
VerifyRegisterCampaignModel
.
fromJson
(
data
as
Json
);
});
}
...
...
lib/screen/health_book/health_book_card_detail.dart
0 → 100644
View file @
97763d9b
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/extensions/datetime_extensions.dart'
;
import
'package:mypoint_flutter_app/extensions/string_extension.dart'
;
import
'package:mypoint_flutter_app/widgets/custom_empty_widget.dart'
;
import
'../../base/base_screen.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../extensions/date_format.dart'
;
import
'../../widgets/custom_navigation_bar.dart'
;
import
'../../widgets/custom_toast_message.dart'
;
import
'health_book_card_detail_viewmodel.dart'
;
import
'health_book_model.dart'
;
class
HealthBookCardDetail
extends
BaseScreen
{
const
HealthBookCardDetail
({
super
.
key
});
@override
State
<
HealthBookCardDetail
>
createState
()
=>
_HealthBookCardDetailState
();
}
class
_HealthBookCardDetailState
extends
BaseState
<
HealthBookCardDetail
>
with
BasicState
{
final
_viewModel
=
Get
.
put
(
HealthBookCardDetailViewModel
());
@override
void
initState
()
{
super
.
initState
();
HealthBookCardItemModel
?
data
;
String
?
cardId
;
final
args
=
Get
.
arguments
;
if
(
args
is
Map
)
{
data
=
args
[
'health_book_card'
];
cardId
=
args
[
'id'
];
}
if
(
data
==
null
&&
cardId
.
isNullOrBlank
)
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
Get
.
back
();
});
return
;
}
if
(
data
!=
null
)
{
_viewModel
?.
card
.
value
=
data
;
}
else
if
(
cardId
.
hasText
)
{
_viewModel
?.
getHealthBookCardDetail
(
cardId
!);
}
_viewModel
.
onShowAlertError
=
(
message
)
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
showAlertError
(
content:
message
,
onConfirmed:
()
=>
Get
.
back
());
});
};
}
@override
Widget
createBody
()
{
final
theme
=
Theme
.
of
(
context
);
return
Scaffold
(
appBar:
CustomNavigationBar
(
title:
'Chi tiết thẻ'
),
body:
SafeArea
(
child:
Obx
(()
{
final
card
=
_viewModel
?.
card
.
value
;
if
(
card
==
null
)
return
EmptyWidget
();
final
name
=
card
.
fullName
.
orIfBlank
(
'--'
);
final
phone
=
card
.
phoneNumber
.
orIfBlank
(
'--'
);
final
cardCode
=
card
.
cardCode
.
orIfBlank
(
'--'
);
final
expireDate
=
(
card
.
expireDate
??
''
).
toDate
()?.
toFormattedString
()
??
'--'
;
final
lastUpdated
=
(
card
.
updatedAt
??
''
).
toDate
()?.
toFormattedString
(
format:
DateFormat
.
ddMMyyyyhhmm
)
??
'--'
;
final
remaining
=
(
card
.
countCheckupUnused
??
0
).
toString
();
return
SingleChildScrollView
(
padding:
const
EdgeInsets
.
all
(
16
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
_CardPreview
(
cardName:
card
.
cardName
.
orIfBlank
(
'--'
),
fullName:
name
,
expireDate:
expireDate
),
const
SizedBox
(
height:
20
),
Text
(
'Thông tin thẻ'
,
style:
theme
.
textTheme
.
titleMedium
?.
copyWith
(
fontWeight:
FontWeight
.
w600
)),
const
SizedBox
(
height:
12
),
_InfoField
(
label:
'Họ và tên'
,
value:
name
),
const
SizedBox
(
height:
12
),
_InfoField
(
label:
'Số điện thoại'
,
value:
phone
),
const
SizedBox
(
height:
12
),
_InfoField
(
label:
'Mã thẻ'
,
value:
cardCode
,
trailing:
cardCode
==
'--'
?
null
:
_CopyChip
(
onTap:
()
=>
_copy
(
context
,
cardCode
)),
),
const
SizedBox
(
height:
12
),
Row
(
children:
[
Expanded
(
child:
_InfoField
(
label:
'Lượt chưa sử dụng'
,
value:
remaining
)),
const
SizedBox
(
width:
12
),
Expanded
(
child:
_InfoField
(
label:
'Hạn sử dụng'
,
value:
expireDate
)),
],
),
const
SizedBox
(
height:
12
),
_InfoField
(
label:
'Cập nhật gần nhất'
,
value:
lastUpdated
),
if
(
card
.
bottomButton
?.
hiden
!=
true
&&
card
.
bottomButton
?.
text
.
hasText
==
true
)
...[
const
SizedBox
(
height:
24
),
SizedBox
(
width:
double
.
infinity
,
child:
ElevatedButton
(
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
parseHexColor
(
card
.
bottomButton
?.
color
??
''
,
fallbackColor:
theme
.
primaryColor
,
),
padding:
const
EdgeInsets
.
symmetric
(
vertical:
14
),
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
8
)),
),
onPressed:
()
=>
card
.
bottomButton
?.
directional
?.
begin
(),
child:
Text
(
card
.
bottomButton
?.
text
??
''
,
style:
const
TextStyle
(
color:
Colors
.
white
,
fontWeight:
FontWeight
.
w600
),
),
),
),
],
],
),
);
}),
),
);
}
void
_copy
(
BuildContext
context
,
String
text
)
{
Clipboard
.
setData
(
ClipboardData
(
text:
text
));
showToastMessage
(
'Đã sao chép mã thẻ'
);
}
}
class
_CardPreview
extends
StatelessWidget
{
final
String
cardName
;
final
String
fullName
;
final
String
expireDate
;
const
_CardPreview
({
required
this
.
cardName
,
required
this
.
fullName
,
required
this
.
expireDate
});
@override
Widget
build
(
BuildContext
context
)
{
return
AspectRatio
(
aspectRatio:
343
/
230
,
child:
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
12
),
child:
Stack
(
fit:
StackFit
.
expand
,
children:
[
const
Image
(
image:
AssetImage
(
'assets/images/bg_medon_card.png'
),
fit:
BoxFit
.
cover
),
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
24
,
vertical:
8
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
mainAxisAlignment:
MainAxisAlignment
.
end
,
children:
[
Text
(
cardName
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Colors
.
white
,
fontWeight:
FontWeight
.
w600
),
),
// Holder name
Text
(
fullName
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
style:
TextStyle
(
fontSize:
12
,
color:
'FBF09C'
.
toColor
()
??
Colors
.
white
,
fontWeight:
FontWeight
.
w600
,
),
),
Text
(
'Hạn sử dụng:
$expireDate
'
,
style:
TextStyle
(
fontSize:
12
,
color:
'FBF09C'
.
toColor
()
??
Colors
.
white
,
fontWeight:
FontWeight
.
w600
,
),
),
],
),
),
],
),
),
);
}
}
class
_InfoField
extends
StatelessWidget
{
final
String
label
;
final
String
value
;
final
Widget
?
trailing
;
const
_InfoField
({
required
this
.
label
,
required
this
.
value
,
this
.
trailing
});
@override
Widget
build
(
BuildContext
context
)
{
final
theme
=
Theme
.
of
(
context
);
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
label
,
style:
theme
.
textTheme
.
bodyMedium
?.
copyWith
(
fontWeight:
FontWeight
.
w500
)),
const
SizedBox
(
height:
6
),
Container
(
height:
48
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
),
decoration:
BoxDecoration
(
color:
Colors
.
grey
.
shade100
,
borderRadius:
BorderRadius
.
circular
(
10
),
border:
Border
.
all
(
color:
Colors
.
grey
.
shade300
),
),
child:
Row
(
children:
[
Expanded
(
child:
Text
(
value
,
style:
theme
.
textTheme
.
bodyMedium
)),
if
(
trailing
!=
null
)
...[
const
SizedBox
(
width:
12
),
trailing
!],
],
),
),
],
);
}
}
class
_CopyChip
extends
StatelessWidget
{
final
VoidCallback
onTap
;
const
_CopyChip
({
required
this
.
onTap
});
@override
Widget
build
(
BuildContext
context
)
{
return
GestureDetector
(
onTap:
onTap
,
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
6
),
decoration:
BoxDecoration
(
color:
const
Color
(
0xFFE5F8F1
),
borderRadius:
BorderRadius
.
circular
(
8
)),
child:
const
Text
(
'COPY'
,
style:
TextStyle
(
color:
Color
(
0xFF00A676
),
fontWeight:
FontWeight
.
w600
)),
),
);
}
}
lib/screen/health_book/health_book_card_detail_viewmodel.dart
0 → 100644
View file @
97763d9b
import
'package:get/get_rx/src/rx_types/rx_types.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart'
;
import
'../../configs/constants.dart'
;
import
'../../networking/restful_api_viewmodel.dart'
;
import
'health_book_model.dart'
;
class
HealthBookCardDetailViewModel
extends
RestfulApiViewModel
{
var
card
=
Rxn
<
HealthBookCardItemModel
>();
void
Function
(
String
message
)?
onShowAlertError
;
Future
<
void
>
getHealthBookCardDetail
(
String
cardId
)
async
{
showProgressIndicator
();
final
response
=
await
client
.
getDetailHealthBookCard
(
cardId
);
showProgressIndicator
();
if
(
response
.
isSuccess
)
{
card
.
value
=
response
.
data
;
}
else
{
onShowAlertError
?.
call
(
response
.
errorMessage
??
Constants
.
commonError
);
}
}
}
\ No newline at end of file
lib/screen/health_book/health_book_screen.dart
View file @
97763d9b
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/extensions/datetime_extensions.dart'
;
import
'package:mypoint_flutter_app/extensions/string_extension.dart'
;
import
'package:mypoint_flutter_app/widgets/custom_empty_widget.dart'
;
import
'package:mypoint_flutter_app/widgets/image_loader.dart'
;
import
'../../extensions/date_format.dart'
;
import
'../../resources/base_color.dart'
;
import
'../../shared/router_gage.dart'
;
import
'../../widgets/custom_navigation_bar.dart'
;
import
'../traffic_service/traffic_service_model.dart'
;
import
'health_book_viewmodel.dart'
;
import
'widgets/health_book_item.dart'
;
class
HealthBookScreen
extends
StatefulWidget
{
const
HealthBookScreen
({
super
.
key
});
...
...
@@ -28,55 +26,12 @@ class _HealthBookScreenState extends State<HealthBookScreen> {
@override
Widget
build
(
BuildContext
context
)
{
final
tags
=
_viewModel
.
headerFilterOrder
;
return
Scaffold
(
appBar:
CustomNavigationBar
(
title:
"Sổ sức khoẻ điện tử"
),
body:
Column
(
children:
[
const
SizedBox
(
height:
8
),
Obx
(()
=>
SizedBox
(
child:
SingleChildScrollView
(
scrollDirection:
Axis
.
horizontal
,
physics:
const
BouncingScrollPhysics
(),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
4
),
child:
Row
(
children:
List
.
generate
(
tags
.
length
,
(
index
)
{
final
isSelected
=
index
==
_viewModel
.
selectedIndex
.
value
;
return
GestureDetector
(
onTap:
()
{
if
(
_viewModel
.
selectedIndex
.
value
==
index
)
return
;
setState
(()
{
_viewModel
.
selectedIndex
.
value
=
index
;
_viewModel
.
getHealthBookCards
();
});
},
child:
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
8
,
vertical:
4
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
12
),
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
isSelected
?
BaseColor
.
primary500
:
Colors
.
grey
.
shade300
,
),
borderRadius:
BorderRadius
.
circular
(
8
),
),
child:
Text
(
tags
[
index
].
title
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
softWrap:
false
,
style:
TextStyle
(
color:
isSelected
?
BaseColor
.
primary500
:
Colors
.
black87
,
),
),
),
);
}),
),
),
)
),
Obx
(()
=>
SizedBox
(
child:
_buildTag
())),
const
Divider
(
height:
14
,
color:
Colors
.
black12
),
const
SizedBox
(
height:
8
),
Expanded
(
...
...
@@ -85,81 +40,99 @@ class _HealthBookScreenState extends State<HealthBookScreen> {
return
products
.
isEmpty
?
Center
(
child:
EmptyWidget
())
:
ListView
.
builder
(
itemCount:
products
.
length
,
itemBuilder:
(
context
,
index
)
{
final
item
=
products
[
index
];
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
4
),
child:
Container
(
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
Colors
.
grey
.
shade300
),
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
ListTile
(
onTap:
()
{
print
(
'TODO Tapped on item:
${item.phoneNumber}
'
);
// Get.toNamed(trafficServiceDetailScreen, arguments: {'serviceId': item.itemId});
},
leading:
SizedBox
(
width:
60
,
// <= giới hạn rõ
height:
60
,
child:
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
8
),
child:
loadNetworkImage
(
url:
item
.
media
?.
firstOrNull
?.
url
??
''
,
fit:
BoxFit
.
cover
,
placeholderAsset:
'assets/images/bg_default_11.png'
,
),
),
itemCount:
products
.
length
,
itemBuilder:
(
context
,
index
)
{
final
item
=
products
[
index
];
return
Padding
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
4
),
child:
HealthBookItem
(
item:
item
,
onTap:
()
{
Get
.
toNamed
(
healthBookCardDetail
,
arguments:
{
'health_book_card'
:
item
,
'id'
:
item
.
itemId
.
toString
()
??
''
});
},
),
title:
Text
(
item
.
fullName
??
''
,
style:
TextStyle
(
fontWeight:
FontWeight
.
bold
)),
subtitle:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
const
SizedBox
(
height:
4
),
Text
(
item
.
cardName
??
''
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Colors
.
black87
,
fontWeight:
FontWeight
.
w500
,
);
},
);
}),
),
],
),
);
}
Widget
_buildTag
()
{
final
tags
=
_viewModel
.
headerFilterOrder
;
return
SingleChildScrollView
(
scrollDirection:
Axis
.
horizontal
,
physics:
const
BouncingScrollPhysics
(),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
4
),
child:
Row
(
children:
List
.
generate
(
tags
.
length
,
(
index
)
{
final
item
=
tags
[
index
];
final
isSelected
=
index
==
_viewModel
.
selectedIndex
.
value
;
return
GestureDetector
(
onTap:
()
{
if
(
_viewModel
.
selectedIndex
.
value
==
index
&&
item
.
sort
==
null
)
return
;
setState
(()
{
if
(
item
.
sort
==
SortFilter
.
asc
)
{
tags
[
index
].
sort
=
SortFilter
.
desc
;
}
else
if
(
item
.
sort
==
SortFilter
.
desc
)
{
tags
[
index
].
sort
=
SortFilter
.
asc
;
}
_viewModel
.
selectedIndex
.
value
=
index
;
_viewModel
.
getHealthBookCards
();
});
},
child:
Container
(
height:
52
,
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
8
,
vertical:
4
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
12
),
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
isSelected
?
BaseColor
.
primary500
:
Colors
.
grey
.
shade300
),
borderRadius:
BorderRadius
.
circular
(
8
),
),
child:
Row
(
children:
[
Text
(
item
.
title
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
softWrap:
false
,
style:
TextStyle
(
color:
isSelected
?
BaseColor
.
primary500
:
Colors
.
black87
),
),
if
(
item
.
sort
!=
null
)
Row
(
children:
[
const
SizedBox
(
width:
4
),
SizedBox
(
height:
24
,
child:
Column
(
children:
[
Icon
(
Icons
.
keyboard_double_arrow_up_sharp
,
size:
12
,
color:
(
isSelected
&&
item
.
sort
==
SortFilter
.
asc
)
?
BaseColor
.
primary500
:
Colors
.
black54
,
),
),
const
SizedBox
(
height:
4
),
Text
(
(
item
.
expireDate
??
''
).
toDate
()?.
toFormattedString
()
??
''
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Colors
.
black87
,
fontWeight:
FontWeight
.
w500
,
Icon
(
Icons
.
keyboard_double_arrow_down_sharp
,
size:
12
,
color:
(
isSelected
&&
item
.
sort
==
SortFilter
.
desc
)
?
BaseColor
.
primary500
:
Colors
.
black54
,
),
),
const
SizedBox
(
height:
4
),
Text
(
'Cập nhật lúc
${(item.updatedAt ?? '').toDate()?.toFormattedString(format: DateFormat.ddMMyyyyhhmm) ?? ''}
'
,
style:
const
TextStyle
(
fontSize:
11
,
color:
Colors
.
black54
),
),
],
),
trailing:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
8
,
vertical:
4
),
decoration:
BoxDecoration
(
color:
Colors
.
green
.
shade50
,
borderRadius:
BorderRadius
.
circular
(
4
),
),
child:
Text
(
item
.
active
?.
text
??
''
,
style:
const
TextStyle
(
fontSize:
12
,
color:
Colors
.
green
,
fontWeight:
FontWeight
.
w500
),
],
),
),
)
,
]
,
),
);
},
);
}),
),
],
],
),
),
);
}),
),
);
}
...
...
lib/screen/health_book/health_book_viewmodel.dart
View file @
97763d9b
...
...
@@ -10,16 +10,21 @@ class HealthBookViewModel extends RestfulApiViewModel {
var
healthBookDataDetail
=
Rxn
<
HealthBookCardItemModel
>();
void
Function
(
String
message
)?
onShowAlertError
;
RxInt
selectedIndex
=
0
.
obs
;
late
List
<
HeaderFilterOrderModel
>
headerFilterOrder
;
List
<
HeaderFilterOrderModel
>
get
headerFilterOrder
{
return
[
@override
onInit
()
{
super
.
onInit
();
headerFilterOrder
=
[
HeaderFilterOrderModel
(
title:
'Tất cả'
,
suffixChecking:
'tatca'
,
expired:
''
,
suffixChecking:
'all'
,
selected:
true
,
),
HeaderFilterOrderModel
(
title:
'Hiệu lực'
,
expired:
"false"
,
suffixChecking:
'hieuluc'
,
),
HeaderFilterOrderModel
(
...
...
@@ -44,6 +49,8 @@ class HealthBookViewModel extends RestfulApiViewModel {
try
{
final
response
=
await
client
.
getHealthBookCards
(
body
);
hideLoading
();
// var data = HealthBookResponseModel(total: 20, products: makeFakeHealthBookCards(20));
// healthBookData.value = data;
if
(
response
.
isSuccess
)
{
healthBookData
.
value
=
response
.
data
;
}
else
{
...
...
@@ -55,19 +62,41 @@ class HealthBookViewModel extends RestfulApiViewModel {
}
}
Future
<
void
>
getDetailHealthBookCard
(
String
id
)
async
{
showLoading
();
try
{
final
response
=
await
client
.
getDetailHealthBookCard
(
id
);
hideLoading
();
if
(
response
.
isSuccess
)
{
healthBookDataDetail
.
value
=
response
.
data
;
}
else
{
onShowAlertError
?.
call
(
response
.
errorMessage
??
Constants
.
commonError
);
}
}
catch
(
error
)
{
hideLoading
();
onShowAlertError
?.
call
(
"Error fetching product detail:
$error
"
);
}
}
// Future<void> getDetailHealthBookCard(String id) async {
// showLoading();
// try {
// final response = await client.getDetailHealthBookCard(id);
// hideLoading();
// if (response.isSuccess) {
// healthBookDataDetail.value = response.data;
// } else {
// onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
// }
// } catch (error) {
// hideLoading();
// onShowAlertError?.call("Error fetching product detail: $error");
// }
// }
// List<HealthBookCardItemModel> makeFakeHealthBookCards(int n) {
// return List.generate(n, (i) {
// final id = 2000 + i;
// return HealthBookCardItemModel(
// itemId: id,
// cardName: 'Thẻ Khám #$id',
// fullName: 'User #$id',
// cardCode: 'MP-${id.toString().padLeft(4, '0')}',
// expireDate: '2025-12-31T00:00:00Z',
// phoneNumber: '09${(10000000 + i).toString().padLeft(8, '0')}',
// updatedAt: '2025-10-10T10:00:00Z',
// countCheckupUnused: i % 6,
// bottomButton: ButtonConfigModel(text: 'Sử dụng', action: 'use'),
// media: [
// ProductMediaItem(url: 'https://picsum.photos/seed/$id/300/300'),
// ],
// buyMoreNote: ButtonConfigModel(text: 'Mua thêm', action: 'buy_more'),
// active: ActiveTextConfig(text: 'Còn hiệu lực', textColor: '#0F9D58', bgColor: '#E6F4EA'),
// );
// });
// }
}
\ No newline at end of file
lib/screen/health_book/widgets/health_book_item.dart
0 → 100644
View file @
97763d9b
import
'package:flutter/material.dart'
;
import
'package:mypoint_flutter_app/extensions/datetime_extensions.dart'
;
import
'package:mypoint_flutter_app/extensions/string_extension.dart'
;
import
'package:mypoint_flutter_app/widgets/image_loader.dart'
;
import
'../../../extensions/date_format.dart'
;
import
'../health_book_model.dart'
;
class
HealthBookItem
extends
StatelessWidget
{
final
HealthBookCardItemModel
item
;
final
VoidCallback
?
onTap
;
const
HealthBookItem
({
super
.
key
,
required
this
.
item
,
this
.
onTap
});
@override
Widget
build
(
BuildContext
context
)
{
return
InkWell
(
onTap:
onTap
,
borderRadius:
BorderRadius
.
circular
(
12
),
child:
Container
(
padding:
const
EdgeInsets
.
all
(
12
),
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
Colors
.
grey
.
shade300
),
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
SizedBox
(
width:
60
,
height:
60
,
child:
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
8
),
child:
loadNetworkImage
(
url:
item
.
media
?.
firstOrNull
?.
url
??
''
,
fit:
BoxFit
.
cover
,
placeholderAsset:
'assets/images/bg_default_11.png'
,
),
),
),
const
SizedBox
(
width:
12
),
Expanded
(
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
item
.
fullName
??
''
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
bold
),
),
const
SizedBox
(
height:
4
),
Text
(
item
.
cardName
??
''
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Colors
.
black87
,
fontWeight:
FontWeight
.
w500
),
),
const
SizedBox
(
height:
4
),
Text
(
(
item
.
expireDate
??
''
).
toDate
()?.
toFormattedString
()
??
''
,
style:
const
TextStyle
(
fontSize:
14
,
color:
Colors
.
black87
,
fontWeight:
FontWeight
.
w500
),
),
const
SizedBox
(
height:
4
),
Text
(
'Cập nhật lúc
${(item.updatedAt ?? '').toDate()?.toFormattedString(format: DateFormat.ddMMyyyyhhmm) ?? ''}
'
,
style:
const
TextStyle
(
fontSize:
11
,
color:
Colors
.
black54
),
),
],
),
),
const
SizedBox
(
width:
8
),
Column
(
mainAxisSize:
MainAxisSize
.
min
,
crossAxisAlignment:
CrossAxisAlignment
.
end
,
children:
[
const
Icon
(
Icons
.
keyboard_arrow_right_outlined
,
size:
20
),
const
SizedBox
(
height:
4
),
Text
(
(
item
.
countCheckupUnused
??
0
)
>
0
?
'Còn
${item.countCheckupUnused}
lượt khám'
:
'Hết lượt'
,
style:
TextStyle
(
fontSize:
12
,
color:
(
item
.
countCheckupUnused
??
0
)
>
0
?
Colors
.
green
:
Colors
.
red
,
fontWeight:
FontWeight
.
w500
,
),
),
const
SizedBox
(
height:
4
),
Text
(
item
.
active
?.
text
??
''
,
style:
TextStyle
(
fontSize:
12
,
color:
parseHexColor
(
item
.
active
?.
textColor
??
''
,
fallbackColor:
Colors
.
green
),
fontWeight:
FontWeight
.
w500
,
),
),
const
SizedBox
(
height:
6
),
GestureDetector
(
onTap:
()
{
item
.
buyMoreNote
?.
directional
?.
begin
();
},
child:
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
8
,
vertical:
4
),
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
Colors
.
green
),
borderRadius:
BorderRadius
.
circular
(
16
),
color:
Colors
.
blue
.
shade50
,
),
child:
Text
(
item
.
buyMoreNote
?.
text
??
'Mua thêm'
,
style:
const
TextStyle
(
fontSize:
12
,
color:
Colors
.
green
,
fontWeight:
FontWeight
.
w600
),
),
),
),
],
),
],
),
),
);
}
}
lib/screen/register_campaign/model/verify_register_model.dart
View file @
97763d9b
...
...
@@ -23,15 +23,15 @@ class VerifyRegisteredPackageModel {
}
@JsonSerializable
(
explicitToJson:
true
)
class
VerifyRegister
VnTra
AlertModel
{
class
VerifyRegister
Campaign
AlertModel
{
final
bool
?
alert
;
final
String
?
description
;
const
VerifyRegister
VnTra
AlertModel
({
this
.
alert
,
this
.
description
});
const
VerifyRegister
Campaign
AlertModel
({
this
.
alert
,
this
.
description
});
factory
VerifyRegister
VnTra
AlertModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$VerifyRegister
VnTra
AlertModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$VerifyRegister
VnTra
AlertModelToJson
(
this
);
factory
VerifyRegister
Campaign
AlertModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$VerifyRegister
Campaign
AlertModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$VerifyRegister
Campaign
AlertModelToJson
(
this
);
}
@JsonSerializable
(
explicitToJson:
true
)
...
...
@@ -45,7 +45,7 @@ class VerifyRegisterCampaignModel {
final
String
?
licensePlate
;
@JsonKey
(
name:
'registed_package'
)
final
VerifyRegisteredPackageModel
?
registeredPackage
;
final
VerifyRegister
VnTra
AlertModel
?
alert
;
final
VerifyRegister
Campaign
AlertModel
?
alert
;
const
VerifyRegisterCampaignModel
({
this
.
valid
,
...
...
@@ -57,8 +57,8 @@ class VerifyRegisterCampaignModel {
});
factory
VerifyRegisterCampaignModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$VerifyRegister
VnTra
ModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$VerifyRegister
VnTra
ModelToJson
(
this
);
=>
_$VerifyRegister
Campaign
ModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$VerifyRegister
Campaign
ModelToJson
(
this
);
}
class
VerifyFormRegisterModel
{
...
...
lib/screen/register_campaign/model/verify_register_model.g.dart
View file @
97763d9b
...
...
@@ -24,15 +24,15 @@ Map<String, dynamic> _$VerifyRegisteredPackageModelToJson(
'description'
:
instance
.
description
,
};
VerifyRegister
VnTra
AlertModel
_$VerifyRegister
VnTra
AlertModelFromJson
(
VerifyRegister
Campaign
AlertModel
_$VerifyRegister
Campaign
AlertModelFromJson
(
Map
<
String
,
dynamic
>
json
,
)
=>
VerifyRegister
VnTra
AlertModel
(
)
=>
VerifyRegister
Campaign
AlertModel
(
alert:
json
[
'alert'
]
as
bool
?,
description:
json
[
'description'
]
as
String
?,
);
Map
<
String
,
dynamic
>
_$VerifyRegister
VnTra
AlertModelToJson
(
VerifyRegister
VnTra
AlertModel
instance
,
Map
<
String
,
dynamic
>
_$VerifyRegister
Campaign
AlertModelToJson
(
VerifyRegister
Campaign
AlertModel
instance
,
)
=>
<
String
,
dynamic
>{
'alert'
:
instance
.
alert
,
'description'
:
instance
.
description
,
...
...
@@ -54,7 +54,7 @@ VerifyRegisterCampaignModel _$VerifyRegisterCampaignModelFromJson(
alert:
json
[
'alert'
]
==
null
?
null
:
VerifyRegister
VnTra
AlertModel
.
fromJson
(
:
VerifyRegister
Campaign
AlertModel
.
fromJson
(
json
[
'alert'
]
as
Map
<
String
,
dynamic
>,
),
);
...
...
lib/screen/register_campaign/register_form_input_screen.dart
View file @
97763d9b
...
...
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import
'package:flutter_widget_from_html/flutter_widget_from_html.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/screen/register_campaign/register_form_input_viewmodel.dart'
;
import
'../../base/base_screen.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../resources/base_color.dart'
;
import
'../../shared/router_gage.dart'
;
import
'../../widgets/custom_navigation_bar.dart'
;
...
...
@@ -9,14 +11,14 @@ import '../voucher/models/product_model.dart';
import
'input_form_cell.dart'
;
import
'model/registration_form_package_model.dart'
;
class
RegisterFormInputScreen
extends
StatefulWidget
{
class
RegisterFormInputScreen
extends
BaseScreen
{
const
RegisterFormInputScreen
({
super
.
key
});
@override
State
<
RegisterFormInputScreen
>
createState
()
=>
_RegisterFormInputScreenState
();
}
class
_RegisterFormInputScreenState
extends
State
<
RegisterFormInputScreen
>
{
class
_RegisterFormInputScreenState
extends
Base
State
<
RegisterFormInputScreen
>
with
BasicState
{
late
final
RegisterFormInputViewModel
_viewModel
;
String
_title
=
''
;
final
_isFormValid
=
false
.
obs
;
...
...
@@ -27,14 +29,28 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
void
initState
()
{
super
.
initState
();
_viewModel
=
Get
.
put
(
RegisterFormInputViewModel
());
_viewModel
.
onShowAlertError
=
(
message
)
{
showAlertError
(
content:
message
);
};
_viewModel
.
verifyRegisterFormSuccess
=
(
data
)
{
final
alert
=
data
.
alert
;
final
des
=
alert
?.
description
??
''
;
if
(
alert
?.
alert
==
true
&&
des
.
isNotEmpty
)
{
showAlertError
(
content:
des
,
onConfirmed:
()
{
_gotoPaymentScreen
();
});
}
else
{
_gotoPaymentScreen
();
}
};
final
args
=
Get
.
arguments
as
Map
<
String
,
dynamic
>?;
_product
=
args
?[
'product'
]
as
ProductModel
?;
if
(
args
?[
'formConfirm'
]
!=
null
)
{
_isConfirmScreen
=
true
;
final
data
=
args
!
[
'formConfirm'
]
as
RegistrationFormPackageModel
;
_title
=
data
?
.
formConfirm
?.
title
??
''
;
final
data
=
args
?
[
'formConfirm'
]
as
RegistrationFormPackageModel
;
_title
=
data
.
formConfirm
?.
title
??
''
;
_viewModel
.
form
.
value
=
data
;
}
else
{
_isConfirmScreen
=
false
;
...
...
@@ -49,7 +65,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
}
@override
Widget
build
(
BuildContext
context
)
{
Widget
createBody
(
)
{
return
GestureDetector
(
onTap:
()
=>
FocusScope
.
of
(
context
).
unfocus
(),
child:
Scaffold
(
...
...
@@ -67,18 +83,18 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
// Header HTML
if
(
form
?
.
headerDescription
?.
title
?.
isNotEmpty
==
true
)
if
(
form
.
headerDescription
?.
title
?.
isNotEmpty
==
true
)
Container
(
color:
BaseColor
.
primary50
,
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
8.0
),
child:
HtmlWidget
(
form
!
.
headerDescription
!.
title
!),
child:
HtmlWidget
(
form
.
headerDescription
!.
title
!),
),
),
const
SizedBox
(
height:
12
),
// Inputs
if
(
inputItem
!=
null
)
...
inputItem
!
.
map
(
...
inputItem
.
map
(
(
item
)
=>
Padding
(
padding:
const
EdgeInsets
.
only
(
bottom:
16
),
child:
InputFormCell
(
model:
item
,
onChanged:
_validateForm
,
isViewOnly:
_isConfirmScreen
),
...
...
@@ -93,7 +109,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
mainAxisAlignment:
MainAxisAlignment
.
start
,
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
if
(
form
?
.
checked
==
true
&&
_isConfirmScreen
==
false
)
if
(
form
.
checked
==
true
&&
_isConfirmScreen
==
false
)
Obx
(
()
=>
Checkbox
(
activeColor:
BaseColor
.
primary400
,
...
...
@@ -109,7 +125,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
onTap:
()
{
form
.
footerDescription
?.
directional
?.
begin
();
},
child:
HtmlWidget
(
form
!
.
footerDescription
!.
title
!)),
child:
HtmlWidget
(
form
.
footerDescription
!.
title
!)),
),
],
),
...
...
@@ -148,7 +164,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
const
SizedBox
(
width:
12
),
Expanded
(
child:
ElevatedButton
(
onPressed:
_
got
oPayment
Screen
,
onPressed:
_
continueT
oPayment
Handle
,
style:
ElevatedButton
.
styleFrom
(
backgroundColor:
BaseColor
.
primary400
,
padding:
const
EdgeInsets
.
symmetric
(
vertical:
14
),
...
...
@@ -186,6 +202,15 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
void
_onSubmit
()
{
if
(
_viewModel
.
form
.
value
?.
formConfirm
?.
checked
==
true
)
{
Get
.
toNamed
(
registerFormInputScreen
,
arguments:
{
"formConfirm"
:
_viewModel
.
form
.
value
,
"product"
:
_product
},
preventDuplicates:
false
);
}
else
{
_continueToPaymentHandle
();
}
}
void
_continueToPaymentHandle
()
{
final
verifyURL
=
_viewModel
.
form
.
value
?.
verify
?.
url
??
''
;
if
(
verifyURL
.
isNotEmpty
)
{
_viewModel
.
verifyRegisterForm
();
}
else
{
_gotoPaymentScreen
();
}
...
...
lib/screen/register_campaign/register_form_input_viewmodel.dart
View file @
97763d9b
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_client_all_request.dart'
;
import
'../../networking/restful_api_viewmodel.dart'
;
import
'model/registration_form_package_model.dart'
;
...
...
@@ -7,32 +8,29 @@ import 'model/verify_register_model.dart';
class
RegisterFormInputViewModel
extends
RestfulApiViewModel
{
var
form
=
Rxn
<
RegistrationFormPackageModel
>();
var
verifyData
=
Rxn
<
VerifyRegisterCampaignModel
>();
var
isLoading
=
false
.
obs
;
var
isChecked
=
true
.
obs
;
void
Function
(
String
message
)?
onShowAlertError
;
void
Function
(
VerifyRegisterCampaignModel
data
)?
verifyRegisterFormSuccess
;
Future
<
void
>
fetchRegisterFormInput
(
String
id
)
async
{
try
{
isLoading
.
value
=
true
;
final
response
=
await
client
.
getRegistrationForm
(
id
);
form
.
value
=
response
.
data
;
}
catch
(
error
)
{
// onShowAlertError?.call("Error fetching product detail: $error");
print
(
"Error fetching product detail:
$error
"
);
}
finally
{
isLoading
.
value
=
false
;
final
response
=
await
client
.
getRegistrationForm
(
id
);
form
.
value
=
response
.
data
;
if
(!
response
.
isSuccess
)
{
onShowAlertError
?.
call
(
response
.
errorMessage
??
Constants
.
commonError
);
}
}
Future
<
void
>
verifyRegisterForm
()
async
{
final
path
=
form
.
value
?.
formRegistration
?.
verify
?.
url
??
''
;
try
{
isLoading
.
value
=
true
;
final
response
=
await
client
.
verifyRegisterForm
(
path
);
verifyData
.
value
=
response
.
data
;
}
catch
(
error
)
{
print
(
"Error fetching product
d
e
ta
il:
$error
"
);
}
finally
{
isLoading
.
value
=
false
;
showProgressIndicator
()
;
final
path
=
form
.
value
?.
formRegistration
?.
verify
?.
url
??
'/accountPasswordReset/1.0.0'
;
final
metaData
=
(
form
.
value
?.
submitParams
??
{}).
toJsonString
()
;
final
response
=
await
client
.
verifyRegisterForm
(
path
,
{
'metadata'
:
metaData
}
);
hideProgressIndicator
()
;
final
data
=
response
.
data
;
if
(!
response
.
isSuccess
&&
d
a
ta
!=
null
)
{
onShowAlertError
?.
call
(
response
.
errorMessage
??
Constants
.
commonError
);
return
;
}
verifyRegisterFormSuccess
?.
call
(
data
!);
}
}
\ No newline at end of file
lib/screen/traffic_service/traffic_service_model.dart
View file @
97763d9b
import
'package:mypoint_flutter_app/directional/directional_screen.dart'
;
import
'package:mypoint_flutter_app/extensions/datetime_extensions.dart'
;
import
'package:mypoint_flutter_app/extensions/string_extension.dart'
;
...
...
@@ -188,19 +189,45 @@ class ProductMediaItem {
class
ButtonConfigModel
{
String
?
text
;
String
?
color
;
String
?
action
;
String
?
clickActionType
;
String
?
clickActionParam
;
bool
?
hiden
;
DirectionalScreen
?
get
directional
{
return
DirectionalScreen
.
build
(
clickActionType:
clickActionType
,
clickActionParam:
clickActionParam
,
);
}
ButtonConfigModel
({
this
.
text
,
this
.
action
});
ButtonConfigModel
({
this
.
text
,
this
.
color
,
this
.
action
,
this
.
clickActionType
,
this
.
clickActionParam
,
this
.
hiden
,
});
factory
ButtonConfigModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
ButtonConfigModel
(
text:
json
[
'text'
],
color:
json
[
'color'
],
action:
json
[
'action'
],
clickActionType:
json
[
'click_action_type'
],
clickActionParam:
json
[
'click_action_param'
],
hiden:
json
[
'hiden'
],
);
}
Map
<
String
,
dynamic
>
toJson
()
=>
{
'text'
:
text
,
'color'
:
color
,
'action'
:
action
,
'click_action_type'
:
clickActionType
,
'click_action_param'
:
clickActionParam
,
'hiden'
:
hiden
,
};
}
\ No newline at end of file
lib/shared/router_gage.dart
View file @
97763d9b
...
...
@@ -16,6 +16,7 @@ import '../screen/electric_payment/electric_payment_history_screen.dart';
import
'../screen/electric_payment/electric_payment_screen.dart'
;
import
'../screen/game/game_cards/game_card_screen.dart'
;
import
'../screen/game/game_tab_screen.dart'
;
import
'../screen/health_book/health_book_card_detail.dart'
;
import
'../screen/health_book/health_book_screen.dart'
;
import
'../screen/history_point/history_point_screen.dart'
;
import
'../screen/history_point_cashback/history_point_cashback_screen.dart'
;
...
...
@@ -101,6 +102,7 @@ const bankAccountManagerScreen = '/bankAccountManagerScreen';
const
historyPointScreen
=
'/historyPointScreen'
;
const
qrCodeScreen
=
'/qrCodeScreen'
;
const
myMobileCardDetailScreen
=
'/myMobileCardDetailScreen'
;
const
healthBookCardDetail
=
'/healthBookCardDetail'
;
class
RouterPage
{
static
List
<
GetPage
>
pages
()
{
...
...
@@ -168,6 +170,7 @@ class RouterPage {
GetPage
(
name:
qrCodeScreen
,
page:
()
=>
QRCodeScreen
()),
GetPage
(
name:
myMobileCardDetailScreen
,
page:
()
=>
MyMobileCardDetailScreen
()),
GetPage
(
name:
healthBookScreen
,
page:
()
=>
HealthBookScreen
()),
GetPage
(
name:
healthBookCardDetail
,
page:
()
=>
HealthBookCardDetail
()),
];
}
}
\ 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