Commit c8abf95b authored by DatHV's avatar DatHV
Browse files

update screen logic

parent fda33894
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'membership_info_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
MembershipInfoResponse _$MembershipInfoResponseFromJson(
Map<String, dynamic> json,
) => MembershipInfoResponse(
levels:
(json['levels'] as List<dynamic>?)
?.map((e) => MembershipLevelModel.fromJson(e as Map<String, dynamic>))
.toList(),
membershipRule: json['membership_rule'] as String?,
);
Map<String, dynamic> _$MembershipInfoResponseToJson(
MembershipInfoResponse instance,
) => <String, dynamic>{
'levels': instance.levels,
'membership_rule': instance.membershipRule,
};
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/screen/home/models/image_model.dart';
import 'accumulated_counter_model.dart';
import 'membership_level_term_and_condition_model.dart';
part 'membership_level_model.g.dart';
@JsonSerializable()
class MembershipLevelModel {
final String? id;
@JsonKey(name: 'membership_level_rank')
final String? rank;
@JsonKey(name: 'membership_level_name')
final String? levelName;
@JsonKey(name: 'membership_level_description')
final String? description;
@JsonKey(name: 'membership_level_content')
final String? content;
@JsonKey(name: 'level_text_color')
final String? levelTextColor;
@JsonKey(name: 'logo')
final String? logo;
@JsonKey(name: 'level_start_at_date')
final String? levelStartAtDate;
@JsonKey(name: 'level_end_at_date')
final String? levelEndAtDate;
@JsonKey(name: 'refresh_level_after_months_from_start_date')
final String? refreshAfterMonths;
@JsonKey(name: 'upgrade_when_counter_is_greater_or_equal')
final String? upgradePointThreshold;
@JsonKey(name: 'upgrade_when_counter_gmv_is_greater_or_equal')
final String? upgradeGmvThreshold;
@JsonKey(name: 'downgrade_level_when_counter_is_less_than')
final String? downgradePointThreshold;
@JsonKey(name: 'downgrade_level_when_counter_gmv_is_less_than')
final String? downgradeGmvThreshold;
@JsonKey(name: 'upgrade_to_membership_level_id')
final String? upgradeToLevelId;
@JsonKey(name: 'downgrade_to_membership_level_id')
final String? downgradeToLevelId;
@JsonKey(name: 'membership_level_term_and_conditions')
final List<MembershipLevelTermAndConditionModel>? conditions;
@JsonKey(name: 'accumulated_counter')
final AccumulatedCounter? accumulatedCounter;
final List<ImageModel>? images;
MembershipLevelModel({
this.id,
this.rank,
this.levelName,
this.description,
this.content,
this.levelTextColor,
this.logo,
this.levelStartAtDate,
this.levelEndAtDate,
this.refreshAfterMonths,
this.upgradePointThreshold,
this.upgradeGmvThreshold,
this.downgradePointThreshold,
this.downgradeGmvThreshold,
this.upgradeToLevelId,
this.downgradeToLevelId,
this.conditions,
this.accumulatedCounter,
this.images,
});
factory MembershipLevelModel.fromJson(Map<String, dynamic> json) =>
_$MembershipLevelModelFromJson(json);
Map<String, dynamic> toJson() => _$MembershipLevelModelToJson(this);
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'membership_level_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
MembershipLevelModel _$MembershipLevelModelFromJson(
Map<String, dynamic> json,
) => MembershipLevelModel(
id: json['id'] as String?,
rank: json['membership_level_rank'] as String?,
levelName: json['membership_level_name'] as String?,
description: json['membership_level_description'] as String?,
content: json['membership_level_content'] as String?,
levelTextColor: json['level_text_color'] as String?,
logo: json['logo'] as String?,
levelStartAtDate: json['level_start_at_date'] as String?,
levelEndAtDate: json['level_end_at_date'] as String?,
refreshAfterMonths:
json['refresh_level_after_months_from_start_date'] as String?,
upgradePointThreshold:
json['upgrade_when_counter_is_greater_or_equal'] as String?,
upgradeGmvThreshold:
json['upgrade_when_counter_gmv_is_greater_or_equal'] as String?,
downgradePointThreshold:
json['downgrade_level_when_counter_is_less_than'] as String?,
downgradeGmvThreshold:
json['downgrade_level_when_counter_gmv_is_less_than'] as String?,
upgradeToLevelId: json['upgrade_to_membership_level_id'] as String?,
downgradeToLevelId: json['downgrade_to_membership_level_id'] as String?,
conditions:
(json['membership_level_term_and_conditions'] as List<dynamic>?)
?.map(
(e) => MembershipLevelTermAndConditionModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
accumulatedCounter:
json['accumulated_counter'] == null
? null
: AccumulatedCounter.fromJson(
json['accumulated_counter'] as Map<String, dynamic>,
),
images:
(json['images'] as List<dynamic>?)
?.map((e) => ImageModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$MembershipLevelModelToJson(
MembershipLevelModel instance,
) => <String, dynamic>{
'id': instance.id,
'membership_level_rank': instance.rank,
'membership_level_name': instance.levelName,
'membership_level_description': instance.description,
'membership_level_content': instance.content,
'level_text_color': instance.levelTextColor,
'logo': instance.logo,
'level_start_at_date': instance.levelStartAtDate,
'level_end_at_date': instance.levelEndAtDate,
'refresh_level_after_months_from_start_date': instance.refreshAfterMonths,
'upgrade_when_counter_is_greater_or_equal': instance.upgradePointThreshold,
'upgrade_when_counter_gmv_is_greater_or_equal': instance.upgradeGmvThreshold,
'downgrade_level_when_counter_is_less_than': instance.downgradePointThreshold,
'downgrade_level_when_counter_gmv_is_less_than':
instance.downgradeGmvThreshold,
'upgrade_to_membership_level_id': instance.upgradeToLevelId,
'downgrade_to_membership_level_id': instance.downgradeToLevelId,
'membership_level_term_and_conditions': instance.conditions,
'accumulated_counter': instance.accumulatedCounter,
'images': instance.images,
};
import 'package:json_annotation/json_annotation.dart';
part 'membership_level_term_and_condition_model.g.dart';
@JsonSerializable()
class MembershipLevelTermAndConditionModel {
final String? icon;
final String? title;
final String? content;
MembershipLevelTermAndConditionModel({
this.icon,
this.title,
this.content,
});
factory MembershipLevelTermAndConditionModel.fromJson(Map<String, dynamic> json) =>
_$MembershipLevelTermAndConditionModelFromJson(json);
Map<String, dynamic> toJson() => _$MembershipLevelTermAndConditionModelToJson(this);
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'membership_level_term_and_condition_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
MembershipLevelTermAndConditionModel
_$MembershipLevelTermAndConditionModelFromJson(Map<String, dynamic> json) =>
MembershipLevelTermAndConditionModel(
icon: json['icon'] as String?,
title: json['title'] as String?,
content: json['content'] as String?,
);
Map<String, dynamic> _$MembershipLevelTermAndConditionModelToJson(
MembershipLevelTermAndConditionModel instance,
) => <String, dynamic>{
'icon': instance.icon,
'title': instance.title,
'content': instance.content,
};
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/widgets/custom_app_bar.dart';
import '../../widgets/custom_navigation_bar.dart';
class _OrderMenuItem {
final String title;
final IconData icon;
_OrderMenuItem({required this.title, required this.icon});
}
class OrderMenuScreen extends StatelessWidget {
OrderMenuScreen({super.key});
final List<_OrderMenuItem> items = [
_OrderMenuItem(title: 'Thẻ nạp của tôi', icon: Icons.credit_card),
_OrderMenuItem(title: 'Sổ sức khỏe điện tử', icon: Icons.medical_services_outlined),
_OrderMenuItem(title: 'Dịch vụ giao thông', icon: Icons.traffic_outlined),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomNavigationBar(title: "Đơn mua",),
body: Container(
color: Colors.white,
child: ListView.separated(
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final item = items[index];
return InkWell(
onTap: () {
print("Tapped on ${item.title}");
// TODO: handle tap
},
child: Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Icon(item.icon, color: Colors.black54),
const SizedBox(width: 12),
Expanded(
child: Text(
item.title,
style: const TextStyle(fontSize: 16, color: Colors.black87),
),
),
const Icon(Icons.chevron_right, color: Colors.black54),
],
),
),
);
}
),
),
);
}
}
......@@ -176,7 +176,22 @@ class _CampaignDetailScreenState extends BaseState<CampaignDetailScreen> with Ba
}
} else if (mediaType == MediaTypeItemCampaign.text.key) {
if (item.contentText != null && item.contentText!.isNotEmpty) {
widgets.add(Padding(padding: const EdgeInsets.only(bottom: 16), child: HtmlWidget(item.contentText!)));
widgets.add(
Padding(padding: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if ((item.contentCaption ?? "").isNotEmpty)
Text(
item.contentCaption!,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
HtmlWidget(item.contentText!),
],
)
)
);
}
} else if (mediaType == MediaTypeItemCampaign.pageLink.key) {
if (item.pages?.isNotEmpty == true) {
......
......@@ -21,6 +21,22 @@ class CampaignDetailViewModel extends RestfulApiViewModel {
fetchWebsitePage(type!);
return;
}
fetchFAQItems();
}
Future<void> fetchFAQItems() async {
showLoading();
isLoading(true);
client.websiteFolderGetPageList({"folder_uri": "ABOUT"}).then((value) {
hideLoading();
isLoading(false);
final pageId = (value.data?.items ?? []).first.pageId ?? "";
if (pageId.isEmpty) {
errorMessage.value = Constants.commonError;
} else {
fetchWebsitePageGetDetail(pageId);
}
});
}
void fetchWebsitePage(DetailPageRuleType type) {
......
import 'package:flutter/cupertino.dart';
import 'package:mypoint_flutter_app/extensions/datetime_extensions.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import 'package:mypoint_flutter_app/screen/personal/personal_gender.dart';
import '../../networking/model_maker.dart';
import '../location_address/location_address_viewmodel.dart';
enum SectionPersonalEditType {
name,
nickname,
phone,
email,
identificationNumber,
birthday,
gender,
address,
province,
district,
}
class PersonalEditDataModel {
String? name;
String? nickname;
String? phone;
String? email;
String? identificationNumber;
DateTime? birthday;
PersonalGender? gender;
String? address;
AddressBaseModel? province;
AddressBaseModel? district;
String? avatar;
PersonalEditDataModel({
this.name,
this.nickname,
this.phone,
this.email,
this.identificationNumber,
this.birthday,
this.gender,
this.address,
this.province,
this.district,
this.avatar,
});
Json get body => <String, dynamic> {
"worker_site_id": DataPreference.instance.profile?.workerSite?.id,
"fullname": name,
"nickname": nickname,
"date_of_birth": birthday?.toFormattedString(format: "yyyy-MM-dd"),
"sex": gender?.value ?? "U",
"address_full": address,
"address_district_code": district?.code ?? "",
"address_province_code": province?.code ?? "",
"identification_number": identificationNumber,
"email": email,
"avatar": "",
"avatar_2": "",
};
}
class PersonalEditItemModel {
String? title;
String? value;
String? hintText;
bool? isRequired;
bool? showDropIcon;
String? warningText;
bool? isEditable = true;
TextInputType? keyboardType;
SectionPersonalEditType? sectionType;
PersonalEditItemModel({
this.title,
this.value,
this.hintText,
this.isRequired = false,
this.showDropIcon = false,
this.warningText,
this.isEditable = true,
this.keyboardType,
this.sectionType,
});
static List<PersonalEditItemModel> personalEditFields(PersonalEditDataModel data) {
return [
PersonalEditItemModel(
title: "Họ và tên",
value: data.name,
hintText: "Chưa cập nhật",
isRequired: true,
showDropIcon: false,
sectionType: SectionPersonalEditType.name,
),
PersonalEditItemModel(
title: "Biệt danh",
value: data.nickname,
hintText: "Chưa cập nhật",
isRequired: false,
showDropIcon: false,
sectionType: SectionPersonalEditType.nickname,
),
PersonalEditItemModel(
title: "Số điện thoại",
value: data.phone,
isRequired: false,
showDropIcon: false,
isEditable: false,
keyboardType: TextInputType.phone,
sectionType: SectionPersonalEditType.phone,
),
PersonalEditItemModel(
title: "Email",
value: data.email,
hintText: "Chưa cập nhật",
isRequired: false,
showDropIcon: false,
keyboardType: TextInputType.emailAddress,
sectionType: SectionPersonalEditType.email,
),
PersonalEditItemModel(
title: "Số CMND/CCCD",
value: data.identificationNumber,
hintText: "Chưa cập nhật",
isRequired: false,
showDropIcon: false,
keyboardType: TextInputType.number,
sectionType: SectionPersonalEditType.identificationNumber,
),
PersonalEditItemModel(
title: "Ngày sinh",
value: data.birthday?.toFormattedString() ?? "",
hintText: "Ngày/Tháng/Năm",
isRequired: true,
showDropIcon: true,
warningText: "Ngày sinh chỉ được cập nhật 1 lần duy nhất",
sectionType: SectionPersonalEditType.birthday,
),
PersonalEditItemModel(
title: "Giới tính",
value: data.gender?.display ?? "",
hintText: "Khác",
isRequired: true,
showDropIcon: true,
sectionType: SectionPersonalEditType.gender,
),
PersonalEditItemModel(
title: "Địa chỉ",
value: data.address,
hintText: "Số nhà, đường, phường/xã",
isRequired: false,
showDropIcon: false,
sectionType: SectionPersonalEditType.address,
),
PersonalEditItemModel(
title: "Tỉnh, Thành phố",
value: data.province?.name ?? "",
hintText: "Chọn tỉnh thành",
isRequired: false,
showDropIcon: true,
sectionType: SectionPersonalEditType.province,
),
PersonalEditItemModel(
title: "Quận, Huyện",
value: data.district?.name ?? "",
hintText: "Chọn quận/huyện",
isRequired: false,
showDropIcon: true,
sectionType: SectionPersonalEditType.district,
),
];
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import 'package:mypoint_flutter_app/screen/personal/personal_edit_item_model.dart';
import 'package:mypoint_flutter_app/screen/personal/personal_edit_viewmodel.dart';
import 'package:mypoint_flutter_app/screen/personal/personal_gender.dart';
import 'package:mypoint_flutter_app/widgets/custom_app_bar.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../resouce/base_color.dart';
import '../../shared/router_gage.dart';
import '../../widgets/alert/data_alert_model.dart';
import '../../widgets/bottom_sheet_helper.dart';
import '../../widgets/time_picker_widget.dart';
class PersonalEditScreen extends BaseScreen {
const PersonalEditScreen({super.key});
@override
State<PersonalEditScreen> createState() => _PersonalEditScreenState();
}
class _PersonalEditScreenState extends BaseState<PersonalEditScreen> with BasicState {
final viewModel = Get.put(PersonalEditViewModel());
@override
initState() {
super.initState();
viewModel.onShowAlertError = (message) {
showAlertError(content: message, barrierDismissible: true, onConfirmed: null);
};
viewModel.updateProfileResponseSuccess = () {
DataAlertModel alertData = DataAlertModel(
localHeaderImage: "assets/images/ic_pipi_05.png",
title: "Thông báo",
description: "Cập nhật thông tin cá nhân thành công!",
buttons: [
AlertButton(
text: "Đã hiểu",
onPressed: () => Get.back(),
bgColor: BaseColor.primary500,
textColor: Colors.white,
),
],
);
showAlert(data: alertData);
};
}
@override
Widget createBody() {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
behavior: HitTestBehavior.translucent,
child: Scaffold(
appBar: CustomAppBar.back(title: "Chỉnh sửa thông tin cá nhân"),
body: Obx(() {
List<PersonalEditItemModel> items;
final editDataModel = viewModel.editDataModel.value;
if (editDataModel == null) {
return const SizedBox.shrink();
}
items = PersonalEditItemModel.personalEditFields(editDataModel);
return CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
child: _buildAvatarItem(),
),
),
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final item = items[index];
return _editPersonalItem(item);
}, childCount: items.length),
),
],
);
}),
bottomNavigationBar: _buildContinueButton(),
),
);
}
Widget _buildContinueButton() {
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: Obx(() {
return SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed:
viewModel.isValidate.value
? () {
viewModel.updateProfile();
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: viewModel.isValidate.value ? BaseColor.primary500 : Colors.grey,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text(
'Cập Nhật Ngay',
style: TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.bold),
),
),
);
}),
),
);
}
Widget _buildAvatarItem() {
return Center(
child: Stack(
alignment: Alignment.bottomRight,
children: [
ClipOval(child: Image.asset("assets/images/bg_default_11.png", width: 100, height: 100, fit: BoxFit.cover)),
Positioned(
bottom: 4,
right: 4,
child: GestureDetector(
onTap: () {
print("Change avatar tapped");
},
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
child: const Icon(Icons.camera_alt, color: Colors.white, size: 18),
),
),
),
],
),
);
}
_onTapItemChangeValue(PersonalEditItemModel item) async {
if (item.sectionType == SectionPersonalEditType.province || item.sectionType == SectionPersonalEditType.district) {
viewModel.navigateToLocationScreen(item);
} else if (item.sectionType == SectionPersonalEditType.birthday) {
if ((DataPreference.instance.profile?.workerSite?.birthday ?? "").isNotEmpty) return;
final now = DateTime.now();
final picked = await showDatePicker(
context: context,
initialDate: viewModel.birthday ?? now,
firstDate: DateTime(1900),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() {
viewModel.birthday = picked;
viewModel.editDataModel.value?.birthday = picked;
viewModel.isValidate.value = viewModel.validate();
});
}
} else if (item.sectionType == SectionPersonalEditType.gender) {
showGenderPicker(
context: context,
selected: viewModel.gender ?? PersonalGender.unknown,
onSelected: (gender) {
setState(() {
viewModel.gender = gender;
viewModel.editDataModel.value?.gender = gender;
});
},
);
}
}
Widget _editPersonalItem(PersonalEditItemModel item) {
final isTapField =
item.sectionType == SectionPersonalEditType.province ||
item.sectionType == SectionPersonalEditType.district ||
item.sectionType == SectionPersonalEditType.gender;
final isDate = item.sectionType == SectionPersonalEditType.birthday;
final isTappableItem = isTapField || isDate;
return Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8, left: 16, right: 16), // all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// const SizedBox(height: 8),
RichText(
text: TextSpan(
text: item.title,
style: const TextStyle(fontSize: 14, color: Colors.black, fontWeight: FontWeight.w600),
children: [
if (item.isRequired == true)
const TextSpan(text: ' (*)', style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)),
],
),
),
const SizedBox(height: 6),
GestureDetector(
onTap: isTappableItem ? () => _onTapItemChangeValue(item) : null,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decoration: BoxDecoration(
color: item.isEditable == true ? Colors.white : Colors.grey.shade100,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
// color: Colors.grey.shade50,
),
child: Row(
children: [
Expanded(
child: TextField(
keyboardType: item.keyboardType ?? TextInputType.text,
controller: TextEditingController(text: item.value ?? ""),
enabled: isTappableItem ? false : (item.isEditable ?? true),
decoration: InputDecoration.collapsed(
hintText: item.hintText ?? "",
hintStyle: TextStyle(color: Colors.grey, fontSize: 14),
),
style: TextStyle(color: item.isEditable ?? true ? Colors.black87 : Colors.grey, fontSize: 16),
onChanged: (value) {
viewModel.updateItemEditData(item, value);
viewModel.isValidate.value = viewModel.validate();
},
),
),
if (item.showDropIcon == true) const Icon(Icons.expand_more, size: 20, color: Colors.grey),
],
),
),
),
const SizedBox(height: 6),
if (item.warningText != null)
Row(
children: [
const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 14),
const SizedBox(width: 4),
Text(item.warningText ?? '', style: const TextStyle(fontSize: 12, color: Colors.orange)),
],
),
// const SizedBox(height: 12),
],
),
);
}
void showGenderPicker({
required BuildContext context,
required PersonalGender selected,
required Function(PersonalGender gender) onSelected,
}) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (_) {
final genderList = PersonalGender.values.toList();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(
child: Center(
child: const Text('Giới tính', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
),
),
GestureDetector(onTap: () => Navigator.of(context).pop(), child: const Icon(Icons.close, size: 20)),
],
),
),
const Divider(height: 1),
...genderList.map((gender) {
final isSelected = selected == gender;
return ListTile(
title: Text(
gender.display,
style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? Colors.blue : Colors.black,
),
),
trailing: isSelected ? const Icon(Icons.check, color: Colors.blue) : null,
onTap: () {
Navigator.of(context).pop();
onSelected(gender);
},
);
}).toList(),
const Divider(height: 100),
],
);
},
);
}
}
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import 'package:mypoint_flutter_app/screen/personal/personal_edit_item_model.dart';
import 'package:mypoint_flutter_app/screen/personal/personal_gender.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../configs/constants.dart';
import '../../preference/data_preference.dart';
import '../../shared/router_gage.dart';
import '../location_address/location_address_viewmodel.dart';
class PersonalEditViewModel extends RestfulApiViewModel {
var editDataModel = Rxn<PersonalEditDataModel>();
var province = Rxn<AddressBaseModel>();
var district = Rxn<AddressBaseModel>();
DateTime? birthday;
PersonalGender? gender;
RxBool isValidate = false.obs;
void Function(String message)? onShowAlertError;
void Function()? updateProfileResponseSuccess;
@override
void onInit() {
super.onInit();
final profile = DataPreference.instance.profile;
if (profile == null) return;
province.value = AddressBaseModel(
code: profile?.workerSite?.locationProvinceCode,
name: profile?.workerSite?.locationProvinceName,
);
district.value = AddressBaseModel(
code: profile?.workerSite?.locationDistrictCode,
name: profile?.workerSite?.locationDistrictName,
);
birthday = profile?.workerSite?.birthday?.toDateFormat('yyyy-MM-dd');
gender = PersonalGender.from(profile.workerSite?.sex ?? "U");
editDataModel.value = PersonalEditDataModel(
name: DataPreference.instance.fullName,
nickname: profile?.workerSite?.nickname,
phone: profile?.workerSite?.phoneNumber,
email: profile?.workerSite?.email,
identificationNumber: profile?.workerSite?.identificationNumber,
birthday: birthday,
gender: gender,
address: profile?.workerSite?.addressFull,
province: province.value,
district: district.value,
);
isValidate.value = validate();
}
updateItemEditData(PersonalEditItemModel item, String value) {
if (editDataModel.value == null) return;
switch (item.sectionType ?? SectionPersonalEditType.nickname) {
case SectionPersonalEditType.name:
editDataModel.value?.name = value;
break;
case SectionPersonalEditType.nickname:
editDataModel.value?.nickname = value;
break;
case SectionPersonalEditType.phone:
editDataModel.value?.phone = value;
break;
case SectionPersonalEditType.email:
editDataModel.value?.email = value;
break;
case SectionPersonalEditType.identificationNumber:
editDataModel.value?.identificationNumber = value;
break;
case SectionPersonalEditType.address:
editDataModel.value?.address = value;
break;
default:
break;
}
}
Future<void> updateProfile() async {
showLoading();
try {
final body = editDataModel.value?.body ?? {};
final response = await client.updateWorkerSiteProfile(body);
hideLoading();
if (response.status?.toLowerCase() == "success") {
updateProfileResponseSuccess?.call();
_getUserProfile();
} else {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
}
} catch (error) {
hideLoading();
onShowAlertError?.call(Constants.commonError);
}
}
void _getUserProfile() {
client.getUserProfile().then((value) async {
final userProfile = value.data;
if (value.isSuccess && userProfile != null) {
DataPreference.instance.saveUserProfile(userProfile);
}
});
}
navigateToLocationScreen(PersonalEditItemModel item) async {
if (item.sectionType == null) return;
if (item.sectionType == SectionPersonalEditType.province) {
final result = await Get.toNamed(
locationAddressScreen,
arguments: {"type": "province", "selectedCode": province.value?.code ?? ""},
);
if (result is AddressBaseModel && result.code != province.value?.code) {
province.value = result;
district.value = null;
editDataModel.value?.district = null;
editDataModel.value?.province = result;
editDataModel.refresh();
}
} else if (item.sectionType == SectionPersonalEditType.district && (province.value?.code ?? "").isNotEmpty) {
final result = await Get.toNamed(
locationAddressScreen,
arguments: {
"type": "district",
"selectedCode": district.value?.code ?? "",
"provinceCode": province.value?.code ?? "",
},
);
if (result is AddressBaseModel) {
district.value = result;
editDataModel.value?.district = result;
editDataModel.refresh();
}
}
isValidate.value = validate();
}
bool validate() {
final model = editDataModel.value;
if (model == null) return false;
if ((model.name ?? '').isEmpty) {
return false;
}
if (model.birthday == null) {
return false;
}
if (model.gender == null || model.gender == 'notAllowed') {
return false;
}
if ((model.email ?? '').isNotEmpty && !isValidEmail(model.email!)) {
return false;
}
return true;
}
bool isValidEmail(String email) {
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
return emailRegex.hasMatch(email);
}
}
enum PersonalGender {
female,
male,
unknown;
static PersonalGender from(String gender) {
switch (gender) {
case 'F':
return PersonalGender.female;
case 'M':
return PersonalGender.male;
default:
return PersonalGender.unknown;
}
}
String get value {
switch (this) {
case PersonalGender.female:
return 'F';
case PersonalGender.male:
return 'M';
case PersonalGender.unknown:
return 'U';
}
}
String get display {
switch (this) {
case PersonalGender.female:
return 'Nữ';
case PersonalGender.male:
return 'Nam';
case PersonalGender.unknown:
return 'Khác';
}
}
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/directional/directional_screen.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../preference/package_info.dart';
import '../../preference/point/header_home_model.dart';
import '../../resouce/base_color.dart';
import '../../shared/router_gage.dart';
import '../../widgets/alert/data_alert_model.dart';
import '../home/header_home_viewmodel.dart';
class PersonalScreen extends StatelessWidget {
class PersonalScreen extends BaseScreen {
const PersonalScreen({super.key});
@override
Widget build(BuildContext context) {
// Set status bar to transparent to allow banner to extend under it
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
),
);
State<PersonalScreen> createState() => _PersonalScreenState();
}
class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
final _headerHomeVM = Get.find<HeaderHomeViewModel>();
String? _version, _buildNumber;
@override
void initState() {
super.initState();
_loadAppInfo();
_headerHomeVM.freshData();
}
Future<void> _loadAppInfo() async {
final v = await AppInfoHelper.version;
final b = await AppInfoHelper.buildNumber;
setState(() {
_version = v ?? "";
_buildNumber = b ?? "";
});
}
@override
Widget createBody() {
return Scaffold(
body: Column(
children: [
// Header with gradient that extends under status bar
_buildHeader(),
// Scrollable content
_buildHeaderPersonal(_headerHomeVM.headerData),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
// Special invitation section
_buildInvitationSection(),
// Regular menu items
_buildMenuItems(),
_buildVersionInfo(),
_buildVersionInfo(_version, _buildNumber),
Container(color: Colors.grey[200], height: MediaQuery.of(context).padding.bottom + 16),
],
),
),
......@@ -41,286 +61,183 @@ class PersonalScreen extends StatelessWidget {
);
}
Widget _buildHeader() {
Widget _buildHeaderPersonal(HeaderHomeModel data) {
final width = MediaQuery.of(context).size.width;
final topPadding = MediaQuery.of(context).padding.top;
final name = DataPreference.instance.profile?.workerSite?.fullname ?? "Quý Khách";
final level = DataPreference.instance.rankName ?? "Hạng Đồng";
final email = DataPreference.instance.profile?.workerSite?.email ?? "";
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFE83A5D),
Color(0xFFD13A5D),
],
),
),
child: SafeArea(
top: false, // Extend under status bar
bottom: false,
height: width * 163 / 375,
decoration: BoxDecoration(image: DecorationImage(image: NetworkImage(data.background ?? ""), fit: BoxFit.cover)),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 40, 16, 16),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
GestureDetector(
onTap: () async {
await Get.toNamed(personalEditScreen);
setState(() {});
},
child: Container(
margin: EdgeInsets.only(top: topPadding),
child: Row(
children: [
Container(
width: 40,
height: 40,
width: 64,
height: 64,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.amber, width: 2),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: const Center(
child: Text(
"U",
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 20,
child: ClipOval(child: Image.asset("assets/images/ic_logo.png", width: 64, height: 64)),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white)),
if (email.isNotEmpty)
Text(
email,
style: const TextStyle(fontSize: 16, color: Colors.white70, fontWeight: FontWeight.w600),
),
],
),
const Spacer(),
const Icon(Icons.chevron_right, color: Colors.white, size: 22),
],
),
const SizedBox(width: 12),
const Text(
"Quý khách",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
const Spacer(),
const Icon(
Icons.chevron_right,
color: Colors.white,
),
],
),
const SizedBox(height: 16),
Row(
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
Icons.star,
color: Colors.red,
size: 16,
),
),
const SizedBox(width: 8),
const Text(
"Chi tiết đối tác",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 14,
),
Row(
children: [
Image.asset("assets/images/ic_rank_gray.png", width: 30, height: 30, color: Colors.white),
const SizedBox(width: 4),
Text(level, style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
],
),
const Spacer(),
const Text(
"0 điểm",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
Row(
children: [
Text(
"${data.totalPointActive.toString()} điểm",
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(width: 4),
const Icon(
Icons.chevron_right,
color: Colors.white,
size: 20,
),
const Icon(Icons.chevron_right, color: Colors.white, size: 22),
],
),
],
),
],
),
),
);
}
Widget _buildInvitationSection() {
return Container(
color: Colors.white,
child: ListTile(
leading: Row(
mainAxisSize: MainAxisSize.min,
return GestureDetector(
onTap: () {
print("Invitation tapped");
},
child: Container(
color: Colors.grey[100],
child: Container(
margin: const EdgeInsets.only(top: 12, bottom: 12),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(color: Colors.pink.shade50),
child: Row(
children: [
Image.asset(
'assets/gift_characters.png',
width: 40,
height: 40,
errorBuilder: (context, error, stackTrace) {
// Fallback if image is not available
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.pink[50],
borderRadius: BorderRadius.circular(20),
),
child: Icon(Icons.people, color: Colors.red[300], size: 24),
);
},
),
],
'assets/images/ic_pipi_02.png',
width: 72,
// height: 56,
fit: BoxFit.contain,
),
title: const Text(
const SizedBox(width: 12),
const Expanded(
child: Text(
'Mời bạn nhận quà liền tay 🎁',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Colors.black87),
),
),
trailing: const Icon(
Icons.chevron_right,
color: Colors.grey,
size: 20,
const Icon(Icons.chevron_right, color: Colors.black87, size: 24),
],
),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
minLeadingWidth: 40,
),
);
}
Widget _buildMenuItems() {
final menuItems = [
{
'icon': Icons.monetization_on,
'title': 'Săn điểm',
'iconColor': Colors.amber,
'showDivider': true,
},
{
'icon': Icons.check_box_outlined,
'title': 'Check-in nhận quà',
'iconColor': Colors.blue[300],
'showDivider': true,
},
{
'icon': Icons.emoji_events_outlined,
'title': 'Bảng xếp hạng',
'iconColor': Colors.amber[700],
'showDivider': true,
},
{
'icon': Icons.local_offer_outlined,
'title': 'Ưu đãi của tôi',
'iconColor': Colors.purple[300],
'showDivider': true,
},
{
'icon': Icons.history_outlined,
'title': 'Lịch sử điểm',
'iconColor': Colors.blue[300],
'showDivider': true,
},
{
'icon': Icons.history_outlined,
'title': 'Lịch sử hoàn điểm',
'iconColor': Colors.blue[300],
'showDivider': true,
},
{
'icon': Icons.account_balance_wallet_outlined,
'title': 'Quản lý tài khoản/thẻ',
'iconColor': Colors.green[300],
'showDivider': true,
},
{
'icon': Icons.favorite_border,
'title': 'Yêu thích',
'iconColor': Colors.red[300],
'showDivider': true,
'sectionDivider': false,
},
{
'icon': Icons.receipt_long_outlined,
'title': 'Lịch sử giao dịch',
'iconColor': Colors.blue[300],
'showDivider': true,
'sectionDivider': true,
},
{
'icon': Icons.shopping_bag_outlined,
'title': 'Đơn mua',
'iconColor': Colors.orange[300],
'showDivider': true,
},
{
'icon': Icons.info_outline,
'title': 'Giới thiệu MyPoint',
'iconColor': Colors.blue[300],
'showDivider': true,
'sectionDivider': true
},
{
'icon': Icons.settings_outlined,
'title': 'Cài đặt',
'iconColor': Colors.grey[600],
'showDivider': true,
},
{
'icon': Icons.headset_mic_outlined,
'title': 'Hỗ trợ',
'iconColor': Colors.blue[300],
'showDivider': true,
},
{
'icon': Icons.logout,
'title': 'Đăng xuất',
'iconColor': Colors.red[400],
'showDivider': false,
},
{'icon': Icons.monetization_on_outlined, 'title': 'Săn điểm', 'type': 'APP_SCREEN_POINT_HUNTING'},
{'icon': Icons.check_box_outlined, 'title': 'Check-in nhận quà', 'type': ''},
{'icon': Icons.emoji_events_outlined, 'title': 'Bảng xếp hạng', 'type': ''},
{'icon': Icons.gif_box_outlined, 'title': 'Ưu đãi của tôi', 'type': 'APP_SCREEN_MY_PURCHASE_ITEMS'},
{'icon': Icons.receipt_long_outlined, 'title': 'Lịch sử giao dịch', 'sectionDivider': true, 'type': ''},
{'icon': Icons.history_outlined, 'title': 'Lịch sử điểm', 'type': ''},
{'icon': Icons.history_outlined, 'title': 'Lịch sử hoàn điểm', 'type': ''},
{'icon': Icons.account_balance_wallet_outlined, 'title': 'Quản lý tài khoản/thẻ', 'type': ''},
{'icon': Icons.favorite_border, 'title': 'Yêu thích', 'type': ''},
{'icon': Icons.shopping_bag_outlined, 'title': 'Đơn mua', 'sectionDivider': true, 'type': 'APP_SCREEN_ORDER_MENU'},
{'icon': Icons.info_outline, 'title': 'Giới thiệu MyPoint', 'sectionDivider': true, 'type': 'VIEW_WEBSITE_PAGE'},
{'icon': Icons.headset_mic_outlined, 'title': 'Hỗ trợ', 'type': 'APP_SCREEN_CUSTOMER_FEEDBACK'},
{'icon': Icons.settings_outlined, 'title': 'Cài đặt', 'type': 'APP_SCREEN_SETTING'},
{'icon': Icons.logout, 'title': 'Đăng xuất', 'color': Colors.red[400], 'type': 'LOGOUT'},
];
return Container(
color: Colors.white,
child: Column(
children: menuItems.map((item) {
// Check if this item needs a section divider before it
children:
menuItems.map((item) {
final needsSectionDivider = item['sectionDivider'] == true;
return Column(
children: [
// Add section divider if needed
if (needsSectionDivider)
Container(
height: 8,
color: Colors.grey[100],
),
ListTile(
leading: Icon(
if (needsSectionDivider) Container(height: 12, color: Colors.grey[100]),
GestureDetector(
onTap: () {
final type = item['type'] as String?;
if (type == "LOGOUT") {
_showAlertConfirmLogout();
return;
}
DirectionalScreen.build(clickActionType: type)?.begin();
},
child: SizedBox(
height: 48,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Icon(
item['icon'] as IconData,
color: item['iconColor'] as Color?,
color: (item['color'] as Color?) ?? Colors.black54,
size: 24,
),
title: Text(
const SizedBox(width: 12),
Expanded(
child: Text(
item['title'] as String,
style: const TextStyle(
fontSize: 14,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: (item['color'] as Color?) ?? Colors.black87,
),
),
),
const Icon(Icons.chevron_right, color: Colors.black54, size: 24),
],
),
),
trailing: const Icon(
Icons.chevron_right,
color: Colors.grey,
size: 20,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
minLeadingWidth: 24,
),
// Add regular divider if needed
if (item['showDivider'] == true)
const Divider(height: 1, indent: 56, endIndent: 0),
],
);
}).toList(),
......@@ -328,17 +245,54 @@ class PersonalScreen extends StatelessWidget {
);
}
Widget _buildVersionInfo() {
_buildVersionInfo(String? version, String? buildNumber) {
return Container(
color: Colors.grey[200],
padding: const EdgeInsets.symmetric(vertical: 12),
alignment: Alignment.center,
child: const Text(
"Phiên bản: 1.21.10(25032101)",
style: TextStyle(
color: Colors.grey,
fontSize: 12,
child: Text(
"Phiên bản: $version($buildNumber)",
style: TextStyle(color: Colors.black87, fontSize: 14, fontWeight: FontWeight.bold),
),
);
}
_showAlertConfirmLogout() {
final dataAlert = DataAlertModel(
title: "Xác nhận",
description: "Bạn có chắc muốn đăng xuất?",
localHeaderImage: "assets/images/ic_pipi_03.png",
buttons: [
AlertButton(
text: "Đồng ý",
onPressed: () {
DataPreference.instance.clearLoginToken();
_safeBackToLogin();
},
bgColor: BaseColor.primary500,
textColor: Colors.white,
),
AlertButton(text: "Huỷ", onPressed: () => Get.back(), bgColor: Colors.white, textColor: BaseColor.second500),
],
);
showAlert(data: dataAlert);
}
void _safeBackToLogin() {
bool found = false;
Navigator.popUntil(Get.context!, (route) {
final matched = route.settings.name == loginScreen;
if (matched) found = true;
return matched;
});
final phone = DataPreference.instance.phone;
if (phone != null) {
if (!found) {
Get.offAllNamed(loginScreen, arguments: phone);
}
} else {
DataPreference.instance.clearData();
Get.offAllNamed(loginScreen);
}
}
}
......@@ -8,6 +8,7 @@ import '../../base/basic_state.dart';
import '../../resouce/base_color.dart';
import '../../widgets/bottom_sheet_helper.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../home/header_home_viewmodel.dart';
import 'affiliate_overview.dart';
import 'affiliate_tab_viewmodel.dart';
......@@ -20,6 +21,7 @@ class AffiliateTabScreen extends BaseScreen {
class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with BasicState {
final AffiliateTabViewModel viewModel = Get.put(AffiliateTabViewModel());
final _headerHomeVM = Get.find<HeaderHomeViewModel>();
@override
Widget createBody() {
......@@ -28,6 +30,7 @@ class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with Basic
appBar: CustomNavigationBar(
title: "Mua sắm",
showBackButton: false,
backgroundImage: _headerHomeVM.headerData.background ?? "assets/images/bg_header_navi.png",
rightButtons: [
IconButton(
icon: const Icon(Icons.info, color: Colors.white),
......@@ -56,10 +59,7 @@ class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with Basic
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.shade100,
borderRadius: BorderRadius.circular(12),
),
decoration: BoxDecoration(color: Colors.amber.shade100, borderRadius: BorderRadius.circular(12)),
child: Row(
children: [
const Text("Điểm hoàn:", style: TextStyle(fontSize: 18, fontWeight: FontWeight.normal)),
......@@ -67,7 +67,9 @@ class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with Basic
Image.asset('assets/images/ic_point.png', width: 20, height: 20),
const SizedBox(width: 4),
const Text(
"0", style: TextStyle(fontSize: 20, color: BaseColor.primary400, fontWeight: FontWeight.bold)),
"0",
style: TextStyle(fontSize: 20, color: BaseColor.primary400, fontWeight: FontWeight.bold),
),
const Spacer(),
Icon(Icons.arrow_forward_ios, color: BaseColor.primary400, size: 16),
],
......@@ -89,9 +91,9 @@ class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with Basic
],
),
),
AffiliateBrand(brands: viewModel.affiliateBrands,),
AffiliateCategory(categories: viewModel.affiliateCategories,),
AffiliateProductTopSale(products: viewModel.affiliateProducts,),
AffiliateBrand(brands: viewModel.affiliateBrands),
AffiliateCategory(categories: viewModel.affiliateCategories),
AffiliateProductTopSale(products: viewModel.affiliateProducts),
],
),
),
......
......@@ -12,6 +12,8 @@ class AffiliateBrand extends StatelessWidget {
if (brands.isEmpty) {
return const SizedBox.shrink();
}
final space = 8.0;
final width = (MediaQuery.of(context).size.width - space * 2 - 32)/3;
return Column(
children: [
const SizedBox(height: 24),
......@@ -29,9 +31,9 @@ class AffiliateBrand extends StatelessWidget {
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: brands.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 3 / 3.2,
childAspectRatio: width/(width + 30),
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
......
This diff is collapsed.
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