Commit 97763d9b authored by DatHV's avatar DatHV
Browse files

update health book

parent fc2caf86
...@@ -16,6 +16,8 @@ extension NullableString on String? { ...@@ -16,6 +16,8 @@ extension NullableString on String? {
} }
bool get hasText => (this?.trim().isNotEmpty ?? false); bool get hasText => (this?.trim().isNotEmpty ?? false);
bool get isNullOrBlank => (this == null || this!.trim().isEmpty);
} }
extension StringUrlExtension on String { extension StringUrlExtension on String {
......
...@@ -1048,9 +1048,9 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient { ...@@ -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'; 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); return VerifyRegisterCampaignModel.fromJson(data as Json);
}); });
} }
......
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)),
),
);
}
}
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
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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/custom_empty_widget.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../extensions/date_format.dart';
import '../../resources/base_color.dart'; import '../../resources/base_color.dart';
import '../../shared/router_gage.dart'; import '../../shared/router_gage.dart';
import '../../widgets/custom_navigation_bar.dart'; import '../../widgets/custom_navigation_bar.dart';
import '../traffic_service/traffic_service_model.dart';
import 'health_book_viewmodel.dart'; import 'health_book_viewmodel.dart';
import 'widgets/health_book_item.dart';
class HealthBookScreen extends StatefulWidget { class HealthBookScreen extends StatefulWidget {
const HealthBookScreen({super.key}); const HealthBookScreen({super.key});
...@@ -28,139 +26,114 @@ class _HealthBookScreenState extends State<HealthBookScreen> { ...@@ -28,139 +26,114 @@ class _HealthBookScreenState extends State<HealthBookScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tags = _viewModel.headerFilterOrder;
return Scaffold( return Scaffold(
appBar: CustomNavigationBar(title: "Sổ sức khoẻ điện tử"), appBar: CustomNavigationBar(title: "Sổ sức khoẻ điện tử"),
body: Column( body: Column(
children: [ children: [
const SizedBox(height: 8), const SizedBox(height: 8),
Obx(() Obx(() => SizedBox(child: _buildTag())),
=> SizedBox( const Divider(height: 14, color: Colors.black12),
child: SingleChildScrollView( const SizedBox(height: 8),
Expanded(
child: Obx(() {
final products = _viewModel.healthBookData.value?.products ?? [];
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: HealthBookItem(
item: item,
onTap: () {
Get.toNamed(healthBookCardDetail, arguments: {'health_book_card': item, 'id': item.itemId.toString() ?? ''});
},
),
);
},
);
}),
),
],
),
);
}
Widget _buildTag() {
final tags = _viewModel.headerFilterOrder;
return SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row( child: Row(
children: List.generate(tags.length, (index) { children: List.generate(tags.length, (index) {
final item = tags[index];
final isSelected = index == _viewModel.selectedIndex.value; final isSelected = index == _viewModel.selectedIndex.value;
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
if (_viewModel.selectedIndex.value == index) return; if (_viewModel.selectedIndex.value == index && item.sort == null) return;
setState(() { 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.selectedIndex.value = index;
_viewModel.getHealthBookCards(); _viewModel.getHealthBookCards();
}); });
}, },
child: Container( child: Container(
height: 52,
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(color: isSelected ? BaseColor.primary500 : Colors.grey.shade300),
color: isSelected ? BaseColor.primary500 : Colors.grey.shade300,
),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text( child: Row(
tags[index].title, children: [
Text(
item.title,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
softWrap: false, softWrap: false,
style: TextStyle( style: TextStyle(color: isSelected ? BaseColor.primary500 : Colors.black87),
color: isSelected ? BaseColor.primary500 : Colors.black87,
),
),
),
);
}),
),
), ),
) if (item.sort != null)
), Row(
const Divider(height: 14, color: Colors.black12),
const SizedBox(height: 8),
Expanded(
child: Obx(() {
final products = _viewModel.healthBookData.value?.products ?? [];
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',
),
),
),
title: Text(item.fullName ?? '', style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 4), const SizedBox(width: 4),
Text( SizedBox(
item.cardName ?? '', height: 24,
style: const TextStyle( child: Column(
fontSize: 14, children: [
color: Colors.black87, Icon(
fontWeight: FontWeight.w500, Icons.keyboard_double_arrow_up_sharp,
), size: 12,
), color:
const SizedBox(height: 4), (isSelected && item.sort == SortFilter.asc) ? BaseColor.primary500 : Colors.black54,
Text( ),
(item.expireDate ?? '').toDate()?.toFormattedString() ?? '', Icon(
style: const TextStyle( Icons.keyboard_double_arrow_down_sharp,
fontSize: 14, size: 12,
color: Colors.black87, color:
fontWeight: FontWeight.w500, (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),
), ),
],
), ),
],
), ),
), ),
); );
},
);
}), }),
), ),
],
),
); );
} }
} }
...@@ -10,16 +10,21 @@ class HealthBookViewModel extends RestfulApiViewModel { ...@@ -10,16 +10,21 @@ class HealthBookViewModel extends RestfulApiViewModel {
var healthBookDataDetail = Rxn<HealthBookCardItemModel>(); var healthBookDataDetail = Rxn<HealthBookCardItemModel>();
void Function(String message)? onShowAlertError; void Function(String message)? onShowAlertError;
RxInt selectedIndex = 0.obs; RxInt selectedIndex = 0.obs;
late List<HeaderFilterOrderModel> headerFilterOrder;
List<HeaderFilterOrderModel> get headerFilterOrder { @override
return [ onInit() {
super.onInit();
headerFilterOrder = [
HeaderFilterOrderModel( HeaderFilterOrderModel(
title: 'Tất cả', title: 'Tất cả',
suffixChecking: 'tatca', expired: '',
suffixChecking: 'all',
selected: true, selected: true,
), ),
HeaderFilterOrderModel( HeaderFilterOrderModel(
title: 'Hiệu lực', title: 'Hiệu lực',
expired: "false",
suffixChecking: 'hieuluc', suffixChecking: 'hieuluc',
), ),
HeaderFilterOrderModel( HeaderFilterOrderModel(
...@@ -44,6 +49,8 @@ class HealthBookViewModel extends RestfulApiViewModel { ...@@ -44,6 +49,8 @@ class HealthBookViewModel extends RestfulApiViewModel {
try { try {
final response = await client.getHealthBookCards(body); final response = await client.getHealthBookCards(body);
hideLoading(); hideLoading();
// var data = HealthBookResponseModel(total: 20, products: makeFakeHealthBookCards(20));
// healthBookData.value = data;
if (response.isSuccess) { if (response.isSuccess) {
healthBookData.value = response.data; healthBookData.value = response.data;
} else { } else {
...@@ -55,19 +62,41 @@ class HealthBookViewModel extends RestfulApiViewModel { ...@@ -55,19 +62,41 @@ class HealthBookViewModel extends RestfulApiViewModel {
} }
} }
Future<void> getDetailHealthBookCard(String id) async { // Future<void> getDetailHealthBookCard(String id) async {
showLoading(); // showLoading();
try { // try {
final response = await client.getDetailHealthBookCard(id); // final response = await client.getDetailHealthBookCard(id);
hideLoading(); // hideLoading();
if (response.isSuccess) { // if (response.isSuccess) {
healthBookDataDetail.value = response.data; // healthBookDataDetail.value = response.data;
} else { // } else {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError); // onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
} // }
} catch (error) { // } catch (error) {
hideLoading(); // hideLoading();
onShowAlertError?.call("Error fetching product detail: $error"); // 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
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),
),
),
),
],
),
],
),
),
);
}
}
...@@ -23,15 +23,15 @@ class VerifyRegisteredPackageModel { ...@@ -23,15 +23,15 @@ class VerifyRegisteredPackageModel {
} }
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class VerifyRegisterVnTraAlertModel { class VerifyRegisterCampaignAlertModel {
final bool? alert; final bool? alert;
final String? description; final String? description;
const VerifyRegisterVnTraAlertModel({this.alert, this.description}); const VerifyRegisterCampaignAlertModel({this.alert, this.description});
factory VerifyRegisterVnTraAlertModel.fromJson(Map<String, dynamic> json) factory VerifyRegisterCampaignAlertModel.fromJson(Map<String, dynamic> json)
=> _$VerifyRegisterVnTraAlertModelFromJson(json); => _$VerifyRegisterCampaignAlertModelFromJson(json);
Map<String, dynamic> toJson() => _$VerifyRegisterVnTraAlertModelToJson(this); Map<String, dynamic> toJson() => _$VerifyRegisterCampaignAlertModelToJson(this);
} }
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
...@@ -45,7 +45,7 @@ class VerifyRegisterCampaignModel { ...@@ -45,7 +45,7 @@ class VerifyRegisterCampaignModel {
final String? licensePlate; final String? licensePlate;
@JsonKey(name: 'registed_package') @JsonKey(name: 'registed_package')
final VerifyRegisteredPackageModel? registeredPackage; final VerifyRegisteredPackageModel? registeredPackage;
final VerifyRegisterVnTraAlertModel? alert; final VerifyRegisterCampaignAlertModel? alert;
const VerifyRegisterCampaignModel({ const VerifyRegisterCampaignModel({
this.valid, this.valid,
...@@ -57,8 +57,8 @@ class VerifyRegisterCampaignModel { ...@@ -57,8 +57,8 @@ class VerifyRegisterCampaignModel {
}); });
factory VerifyRegisterCampaignModel.fromJson(Map<String, dynamic> json) factory VerifyRegisterCampaignModel.fromJson(Map<String, dynamic> json)
=> _$VerifyRegisterVnTraModelFromJson(json); => _$VerifyRegisterCampaignModelFromJson(json);
Map<String, dynamic> toJson() => _$VerifyRegisterVnTraModelToJson(this); Map<String, dynamic> toJson() => _$VerifyRegisterCampaignModelToJson(this);
} }
class VerifyFormRegisterModel { class VerifyFormRegisterModel {
......
...@@ -24,15 +24,15 @@ Map<String, dynamic> _$VerifyRegisteredPackageModelToJson( ...@@ -24,15 +24,15 @@ Map<String, dynamic> _$VerifyRegisteredPackageModelToJson(
'description': instance.description, 'description': instance.description,
}; };
VerifyRegisterVnTraAlertModel _$VerifyRegisterVnTraAlertModelFromJson( VerifyRegisterCampaignAlertModel _$VerifyRegisterCampaignAlertModelFromJson(
Map<String, dynamic> json, Map<String, dynamic> json,
) => VerifyRegisterVnTraAlertModel( ) => VerifyRegisterCampaignAlertModel(
alert: json['alert'] as bool?, alert: json['alert'] as bool?,
description: json['description'] as String?, description: json['description'] as String?,
); );
Map<String, dynamic> _$VerifyRegisterVnTraAlertModelToJson( Map<String, dynamic> _$VerifyRegisterCampaignAlertModelToJson(
VerifyRegisterVnTraAlertModel instance, VerifyRegisterCampaignAlertModel instance,
) => <String, dynamic>{ ) => <String, dynamic>{
'alert': instance.alert, 'alert': instance.alert,
'description': instance.description, 'description': instance.description,
...@@ -54,7 +54,7 @@ VerifyRegisterCampaignModel _$VerifyRegisterCampaignModelFromJson( ...@@ -54,7 +54,7 @@ VerifyRegisterCampaignModel _$VerifyRegisterCampaignModelFromJson(
alert: alert:
json['alert'] == null json['alert'] == null
? null ? null
: VerifyRegisterVnTraAlertModel.fromJson( : VerifyRegisterCampaignAlertModel.fromJson(
json['alert'] as Map<String, dynamic>, json['alert'] as Map<String, dynamic>,
), ),
); );
......
...@@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; ...@@ -2,6 +2,8 @@ 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:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/screen/register_campaign/register_form_input_viewmodel.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 '../../resources/base_color.dart';
import '../../shared/router_gage.dart'; import '../../shared/router_gage.dart';
import '../../widgets/custom_navigation_bar.dart'; import '../../widgets/custom_navigation_bar.dart';
...@@ -9,14 +11,14 @@ import '../voucher/models/product_model.dart'; ...@@ -9,14 +11,14 @@ import '../voucher/models/product_model.dart';
import 'input_form_cell.dart'; import 'input_form_cell.dart';
import 'model/registration_form_package_model.dart'; import 'model/registration_form_package_model.dart';
class RegisterFormInputScreen extends StatefulWidget { class RegisterFormInputScreen extends BaseScreen {
const RegisterFormInputScreen({super.key}); const RegisterFormInputScreen({super.key});
@override @override
State<RegisterFormInputScreen> createState() => _RegisterFormInputScreenState(); State<RegisterFormInputScreen> createState() => _RegisterFormInputScreenState();
} }
class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> { class _RegisterFormInputScreenState extends BaseState<RegisterFormInputScreen> with BasicState {
late final RegisterFormInputViewModel _viewModel; late final RegisterFormInputViewModel _viewModel;
String _title = ''; String _title = '';
final _isFormValid = false.obs; final _isFormValid = false.obs;
...@@ -27,14 +29,28 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> { ...@@ -27,14 +29,28 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
void initState() { void initState() {
super.initState(); super.initState();
_viewModel = Get.put(RegisterFormInputViewModel()); _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>?; final args = Get.arguments as Map<String, dynamic>?;
_product = args?['product'] as ProductModel?; _product = args?['product'] as ProductModel?;
if (args?['formConfirm'] != null) { if (args?['formConfirm'] != null) {
_isConfirmScreen = true; _isConfirmScreen = true;
final data = args!['formConfirm'] as RegistrationFormPackageModel; final data = args?['formConfirm'] as RegistrationFormPackageModel;
_title = data?.formConfirm?.title ?? ''; _title = data.formConfirm?.title ?? '';
_viewModel.form.value = data; _viewModel.form.value = data;
} else { } else {
_isConfirmScreen = false; _isConfirmScreen = false;
...@@ -49,7 +65,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> { ...@@ -49,7 +65,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
} }
@override @override
Widget build(BuildContext context) { Widget createBody() {
return GestureDetector( return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(), onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold( child: Scaffold(
...@@ -67,18 +83,18 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> { ...@@ -67,18 +83,18 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header HTML // Header HTML
if (form?.headerDescription?.title?.isNotEmpty == true) if (form.headerDescription?.title?.isNotEmpty == true)
Container( Container(
color: BaseColor.primary50, color: BaseColor.primary50,
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: HtmlWidget(form!.headerDescription!.title!), child: HtmlWidget(form.headerDescription!.title!),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Inputs // Inputs
if (inputItem != null) if (inputItem != null)
...inputItem!.map( ...inputItem.map(
(item) => Padding( (item) => Padding(
padding: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.only(bottom: 16),
child: InputFormCell(model: item, onChanged: _validateForm, isViewOnly: _isConfirmScreen), child: InputFormCell(model: item, onChanged: _validateForm, isViewOnly: _isConfirmScreen),
...@@ -93,7 +109,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> { ...@@ -93,7 +109,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (form?.checked == true && _isConfirmScreen == false) if (form.checked == true && _isConfirmScreen == false)
Obx( Obx(
() => Checkbox( () => Checkbox(
activeColor: BaseColor.primary400, activeColor: BaseColor.primary400,
...@@ -109,7 +125,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> { ...@@ -109,7 +125,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
onTap: () { onTap: () {
form.footerDescription?.directional?.begin(); form.footerDescription?.directional?.begin();
}, },
child: HtmlWidget(form!.footerDescription!.title!)), child: HtmlWidget(form.footerDescription!.title!)),
), ),
], ],
), ),
...@@ -148,7 +164,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> { ...@@ -148,7 +164,7 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: ElevatedButton( child: ElevatedButton(
onPressed: _gotoPaymentScreen, onPressed: _continueToPaymentHandle,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: BaseColor.primary400, backgroundColor: BaseColor.primary400,
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
...@@ -186,6 +202,15 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> { ...@@ -186,6 +202,15 @@ class _RegisterFormInputScreenState extends State<RegisterFormInputScreen> {
void _onSubmit() { void _onSubmit() {
if (_viewModel.form.value?.formConfirm?.checked == true) { if (_viewModel.form.value?.formConfirm?.checked == true) {
Get.toNamed(registerFormInputScreen, arguments: {"formConfirm": _viewModel.form.value, "product": _product}, preventDuplicates: false); 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 { } else {
_gotoPaymentScreen(); _gotoPaymentScreen();
} }
......
import 'package:get/get_rx/src/rx_types/rx_types.dart'; 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 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../networking/restful_api_viewmodel.dart'; import '../../networking/restful_api_viewmodel.dart';
import 'model/registration_form_package_model.dart'; import 'model/registration_form_package_model.dart';
...@@ -7,32 +8,29 @@ import 'model/verify_register_model.dart'; ...@@ -7,32 +8,29 @@ import 'model/verify_register_model.dart';
class RegisterFormInputViewModel extends RestfulApiViewModel { class RegisterFormInputViewModel extends RestfulApiViewModel {
var form = Rxn<RegistrationFormPackageModel>(); var form = Rxn<RegistrationFormPackageModel>();
var verifyData = Rxn<VerifyRegisterCampaignModel>(); var verifyData = Rxn<VerifyRegisterCampaignModel>();
var isLoading = false.obs;
var isChecked = true.obs; var isChecked = true.obs;
void Function(String message)? onShowAlertError;
void Function(VerifyRegisterCampaignModel data)? verifyRegisterFormSuccess;
Future<void> fetchRegisterFormInput(String id) async { Future<void> fetchRegisterFormInput(String id) async {
try {
isLoading.value = true;
final response = await client.getRegistrationForm(id); final response = await client.getRegistrationForm(id);
form.value = response.data; form.value = response.data;
} catch (error) { if (!response.isSuccess) {
// onShowAlertError?.call("Error fetching product detail: $error"); onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
print("Error fetching product detail: $error");
} finally {
isLoading.value = false;
} }
} }
Future<void> verifyRegisterForm() async { Future<void> verifyRegisterForm() async {
final path = form.value?.formRegistration?.verify?.url ?? ''; showProgressIndicator();
try { final path = form.value?.formRegistration?.verify?.url ?? '/accountPasswordReset/1.0.0';
isLoading.value = true; final metaData = (form.value?.submitParams ?? {}).toJsonString();
final response = await client.verifyRegisterForm(path); final response = await client.verifyRegisterForm(path, {'metadata': metaData});
verifyData.value = response.data; hideProgressIndicator();
} catch (error) { final data = response.data;
print("Error fetching product detail: $error"); if (!response.isSuccess && data != null) {
} finally { onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
isLoading.value = false; return;
} }
verifyRegisterFormSuccess?.call(data!);
} }
} }
\ No newline at end of file
import 'package:mypoint_flutter_app/directional/directional_screen.dart';
import 'package:mypoint_flutter_app/extensions/datetime_extensions.dart'; import 'package:mypoint_flutter_app/extensions/datetime_extensions.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart'; import 'package:mypoint_flutter_app/extensions/string_extension.dart';
...@@ -188,19 +189,45 @@ class ProductMediaItem { ...@@ -188,19 +189,45 @@ class ProductMediaItem {
class ButtonConfigModel { class ButtonConfigModel {
String? text; String? text;
String? color;
String? action; 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) { factory ButtonConfigModel.fromJson(Map<String, dynamic> json) {
return ButtonConfigModel( return ButtonConfigModel(
text: json['text'], text: json['text'],
color: json['color'],
action: json['action'], action: json['action'],
clickActionType: json['click_action_type'],
clickActionParam: json['click_action_param'],
hiden: json['hiden'],
); );
} }
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'text': text, 'text': text,
'color': color,
'action': action, 'action': action,
'click_action_type': clickActionType,
'click_action_param': clickActionParam,
'hiden': hiden,
}; };
} }
\ No newline at end of file
...@@ -16,6 +16,7 @@ import '../screen/electric_payment/electric_payment_history_screen.dart'; ...@@ -16,6 +16,7 @@ import '../screen/electric_payment/electric_payment_history_screen.dart';
import '../screen/electric_payment/electric_payment_screen.dart'; import '../screen/electric_payment/electric_payment_screen.dart';
import '../screen/game/game_cards/game_card_screen.dart'; import '../screen/game/game_cards/game_card_screen.dart';
import '../screen/game/game_tab_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/health_book/health_book_screen.dart';
import '../screen/history_point/history_point_screen.dart'; import '../screen/history_point/history_point_screen.dart';
import '../screen/history_point_cashback/history_point_cashback_screen.dart'; import '../screen/history_point_cashback/history_point_cashback_screen.dart';
...@@ -101,6 +102,7 @@ const bankAccountManagerScreen = '/bankAccountManagerScreen'; ...@@ -101,6 +102,7 @@ const bankAccountManagerScreen = '/bankAccountManagerScreen';
const historyPointScreen = '/historyPointScreen'; const historyPointScreen = '/historyPointScreen';
const qrCodeScreen = '/qrCodeScreen'; const qrCodeScreen = '/qrCodeScreen';
const myMobileCardDetailScreen = '/myMobileCardDetailScreen'; const myMobileCardDetailScreen = '/myMobileCardDetailScreen';
const healthBookCardDetail = '/healthBookCardDetail';
class RouterPage { class RouterPage {
static List<GetPage> pages() { static List<GetPage> pages() {
...@@ -168,6 +170,7 @@ class RouterPage { ...@@ -168,6 +170,7 @@ class RouterPage {
GetPage(name: qrCodeScreen, page: () => QRCodeScreen()), GetPage(name: qrCodeScreen, page: () => QRCodeScreen()),
GetPage(name: myMobileCardDetailScreen, page: () => MyMobileCardDetailScreen()), GetPage(name: myMobileCardDetailScreen, page: () => MyMobileCardDetailScreen()),
GetPage(name: healthBookScreen, page: () => HealthBookScreen()), GetPage(name: healthBookScreen, page: () => HealthBookScreen()),
GetPage(name: healthBookCardDetail, page: () => HealthBookCardDetail()),
]; ];
} }
} }
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment