Commit cc202df4 authored by DatHV's avatar DatHV
Browse files

update change id, popup common manager

parent 417358c5
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/extensions/datetime_extensions.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
part 'product_mobile_card_model.g.dart'; part 'product_mobile_card_model.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
...@@ -56,6 +58,11 @@ class ProductMobileCardModel { ...@@ -56,6 +58,11 @@ class ProductMobileCardModel {
_$ProductMobileCardModelFromJson(json); _$ProductMobileCardModelFromJson(json);
Map<String, dynamic> toJson() => _$ProductMobileCardModelToJson(this); Map<String, dynamic> toJson() => _$ProductMobileCardModelToJson(this);
String get expire {
final ex = endTime ?? "";
return ex.toDate()?.toFormattedString() ?? "";
}
} }
@JsonSerializable() @JsonSerializable()
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart'; import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/screen/mobile_card/product_mobile_card_viewmodel.dart'; import 'package:mypoint_flutter_app/screen/mobile_card/product_mobile_card_viewmodel.dart';
import 'package:mypoint_flutter_app/screen/mobile_card/usable_mobile_card_popup.dart'; import 'package:mypoint_flutter_app/screen/mobile_card/usable_mobile_card_popup.dart';
import 'package:mypoint_flutter_app/widgets/custom_app_bar.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart'; import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../base/base_screen.dart'; import '../../base/base_screen.dart';
import '../../base/basic_state.dart'; import '../../base/basic_state.dart';
import '../../resources/base_color.dart'; import '../../resources/base_color.dart';
import '../../widgets/alert/custom_alert_dialog.dart'; import '../../widgets/alert/custom_alert_dialog.dart';
import '../../widgets/alert/data_alert_model.dart'; import '../../widgets/alert/data_alert_model.dart';
import '../../widgets/custom_navigation_bar.dart';
class ProductMobileCardScreen extends BaseScreen { class ProductMobileCardScreen extends BaseScreen {
const ProductMobileCardScreen({super.key}); const ProductMobileCardScreen({super.key});
...@@ -45,7 +43,7 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w ...@@ -45,7 +43,7 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w
@override @override
Widget createBody() { Widget createBody() {
return Scaffold( return Scaffold(
appBar: CustomAppBar.back(title: "Đổi mã thẻ nạp"), appBar: CustomNavigationBar(title: "Đổi mã thẻ nạp"),
body: Obx(() { body: Obx(() {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
......
...@@ -15,7 +15,7 @@ class OrderMenuScreen extends StatelessWidget { ...@@ -15,7 +15,7 @@ class OrderMenuScreen extends StatelessWidget {
OrderMenuScreen({super.key}); OrderMenuScreen({super.key});
final List<_OrderMenuItem> items = [ final List<_OrderMenuItem> items = [
_OrderMenuItem(title: 'Thẻ nạp của tôi', icon: Icons.credit_card, type: ''), _OrderMenuItem(title: 'Thẻ nạp của tôi', icon: Icons.credit_card, type: 'VIEW_MY_MOBILE_CARD'),
_OrderMenuItem(title: 'Sổ sức khỏe điện tử', icon: Icons.medical_services_outlined, type: ''), _OrderMenuItem(title: 'Sổ sức khỏe điện tử', icon: Icons.medical_services_outlined, type: ''),
_OrderMenuItem(title: 'Dịch vụ giao thông', icon: Icons.traffic_outlined, type: 'APP_SCREEN_MY_VNTRA_PACKAGE'), _OrderMenuItem(title: 'Dịch vụ giao thông', icon: Icons.traffic_outlined, type: 'APP_SCREEN_MY_VNTRA_PACKAGE'),
]; ];
......
...@@ -5,12 +5,14 @@ import 'package:mypoint_flutter_app/extensions/num_extension.dart'; ...@@ -5,12 +5,14 @@ import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart'; import 'package:mypoint_flutter_app/preference/data_preference.dart';
import '../../base/base_screen.dart'; import '../../base/base_screen.dart';
import '../../base/basic_state.dart'; import '../../base/basic_state.dart';
import '../../directional/directional_action_type.dart';
import '../../preference/package_info.dart'; import '../../preference/package_info.dart';
import '../../preference/point/header_home_model.dart'; import '../../preference/point/header_home_model.dart';
import '../../resources/base_color.dart'; import '../../resources/base_color.dart';
import '../../shared/router_gage.dart'; import '../../shared/router_gage.dart';
import '../../widgets/alert/data_alert_model.dart'; import '../../widgets/alert/data_alert_model.dart';
import '../home/header_home_viewmodel.dart'; import '../home/header_home_viewmodel.dart';
import '../popup_manager/popup_runner_helper.dart';
class PersonalScreen extends BaseScreen { class PersonalScreen extends BaseScreen {
const PersonalScreen({super.key}); const PersonalScreen({super.key});
...@@ -19,7 +21,7 @@ class PersonalScreen extends BaseScreen { ...@@ -19,7 +21,7 @@ class PersonalScreen extends BaseScreen {
State<PersonalScreen> createState() => _PersonalScreenState(); State<PersonalScreen> createState() => _PersonalScreenState();
} }
class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState { class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, PopupOnInit {
final _headerHomeVM = Get.find<HeaderHomeViewModel>(); final _headerHomeVM = Get.find<HeaderHomeViewModel>();
String? _version, _buildNumber; String? _version, _buildNumber;
...@@ -28,6 +30,18 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState { ...@@ -28,6 +30,18 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
super.initState(); super.initState();
_loadAppInfo(); _loadAppInfo();
_headerHomeVM.freshData(); _headerHomeVM.freshData();
// WidgetsBinding.instance.addPostFrameCallback((_) async {
// if (!mounted) return;
// await PopupManagerViewModel.instance.ensureLoaded();
// final popup = PopupManagerViewModel.instance.getForScreen(DirectionalScreenName.personal.rawValue);
// final id = popup?.id ?? '';
// if (id.isEmpty || popup == null) return;
// await showPopupManagerScreen(
// context,
// modelPopup: popup,
// );
// });
runPopupCheck(DirectionalScreenName.personal);
} }
Future<void> _loadAppInfo() async { Future<void> _loadAppInfo() async {
...@@ -189,7 +203,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState { ...@@ -189,7 +203,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
{'icon': Icons.receipt_long_outlined, 'title': 'Lịch sử giao dịch', 'sectionDivider': true, 'type': 'APP_SCREEN_TRANSACTION_HISTORIES'}, {'icon': Icons.receipt_long_outlined, 'title': 'Lịch sử giao dịch', 'sectionDivider': true, 'type': 'APP_SCREEN_TRANSACTION_HISTORIES'},
{'icon': Icons.history_outlined, 'title': 'Lịch sử điểm', 'type': 'APP_SCREEN_SURVERY_APP'}, {'icon': Icons.history_outlined, 'title': 'Lịch sử điểm', 'type': 'APP_SCREEN_SURVERY_APP'},
{'icon': Icons.history_outlined, 'title': 'Lịch sử hoàn điểm', 'type': 'APP_SCREEN_REFUND_HISTORY'}, {'icon': Icons.history_outlined, 'title': 'Lịch sử hoàn điểm', 'type': 'APP_SCREEN_REFUND_HISTORY'},
{'icon': Icons.account_balance_wallet_outlined, 'title': 'Quản lý tài khoản/thẻ', 'type': 'APP_SCREEN_ELECTRIC_BILL'}, {'icon': Icons.account_balance_wallet_outlined, 'title': 'Quản lý tài khoản/thẻ', 'type': 'BANK_ACCOUNT_MANAGER'},
{'icon': Icons.favorite_border, 'title': 'Yêu thích', 'type': 'APP_SCREEN_CATEGORY_TAB_FAVORITE'}, {'icon': Icons.favorite_border, 'title': 'Yêu thích', 'type': 'APP_SCREEN_CATEGORY_TAB_FAVORITE'},
{'icon': Icons.shopping_bag_outlined, 'title': 'Đơn mua', 'sectionDivider': true, 'type': 'APP_SCREEN_ORDER_MENU'}, {'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.info_outline, 'title': 'Giới thiệu MyPoint', 'sectionDivider': true, 'type': 'VIEW_WEBSITE_PAGE'},
...@@ -293,7 +307,8 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState { ...@@ -293,7 +307,8 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
}); });
final phone = DataPreference.instance.phoneNumberUsedForLoginScreen; final phone = DataPreference.instance.phoneNumberUsedForLoginScreen;
final displayName = DataPreference.instance.displayName; final displayName = DataPreference.instance.displayName;
if (phone != null && !found) { print("Safe back to login screen with phone: $phone, displayName: $displayName, found: $found");
if (phone != null && found) {
Get.offAllNamed(loginScreen, arguments: {"phone": phone, 'fullName': displayName}); Get.offAllNamed(loginScreen, arguments: {"phone": phone, 'fullName': displayName});
} else { } else {
DataPreference.instance.clearData(); DataPreference.instance.clearData();
......
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/directional/directional_screen.dart';
part 'popup_manager_model.g.dart'; part 'popup_manager_model.g.dart';
@JsonSerializable() @JsonSerializable()
...@@ -81,6 +82,13 @@ class PopupManagerModel { ...@@ -81,6 +82,13 @@ class PopupManagerModel {
this.requestId, this.requestId,
}); });
DirectionalScreen? get directional {
return DirectionalScreen.build(
clickActionType: clickActionType,
clickActionParam: clickActionParam,
);
}
factory PopupManagerModel.fromJson(Map<String, dynamic> json) => factory PopupManagerModel.fromJson(Map<String, dynamic> json) =>
_$PopupManagerModelFromJson(json); _$PopupManagerModelFromJson(json);
......
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/screen/popup_manager/popup_manager_model.dart'; import 'package:mypoint_flutter_app/screen/popup_manager/popup_manager_model.dart';
import 'package:mypoint_flutter_app/screen/popup_manager/popup_manager_viewmodel.dart';
/// Giao diện điều hướng để bạn nối với router của app
typedef PopupNavigator = void Function({
required String name,
required String identifier,
String? title,
String? body,
});
/// Logger tuỳ bạn hook vào hệ thống hiện có (Firebase, MoEngage, …) /// Logger tuỳ bạn hook vào hệ thống hiện có (Firebase, MoEngage, …)
void logPopupShowing({required String popupId, required String requestId}) { void logPopupShowing({required String popupId, required String requestId}) {
...@@ -22,56 +15,42 @@ void logPopupClick({required String popupId}) { ...@@ -22,56 +15,42 @@ void logPopupClick({required String popupId}) {
debugPrint('[Popup] click: popup_id=$popupId'); debugPrint('[Popup] click: popup_id=$popupId');
} }
/// ==== API hiển thị popup (gọi giống hàm Swift) ==== Future<void> showPopupManagerScreen(
Future<void> showPopup(
BuildContext context, { BuildContext context, {
required PopupManagerModel modelPopup, required PopupManagerModel modelPopup,
required PopupNavigator onNavigate, VoidCallback? onDismissed,
VoidCallback? onDismissed, // thay cho NotificationCenter }) async {
}) async {
int timeCountDown = int.tryParse(modelPopup.timeCountDown ?? '1000000') ?? 1000000; int timeCountDown = int.tryParse(modelPopup.timeCountDown ?? '1000000') ?? 1000000;
final popupId = modelPopup.id ?? ''; final popupId = modelPopup.id ?? '';
final requestId = modelPopup.requestId ?? ''; final requestId = modelPopup.requestId ?? '';
logPopupShowing(popupId: popupId, requestId: requestId); logPopupShowing(popupId: popupId, requestId: requestId);
await PopupManagerViewModel.instance.markShownOnce(popupId);
await showGeneralDialog( await showGeneralDialog(
context: context, context: context,
barrierDismissible: false, // giống SwiftEntryKit, user không chạm ra ngoài để tắt barrierDismissible: false,
barrierLabel: 'popup', barrierLabel: 'popup',
transitionDuration: const Duration(milliseconds: 220), transitionDuration: const Duration(milliseconds: 220),
pageBuilder: (_, __, ___) { pageBuilder: (_, __, ___) {
return _BasePopupView( return _BasePopupView(
model: modelPopup, model: modelPopup,
initialCountdown: timeCountDown, initialCountdown: timeCountDown,
onNavigate: onNavigate,
onDismissed: () { onDismissed: () {
onDismissed?.call(); onDismissed?.call();
}, },
); );
}, },
transitionBuilder: (_, anim, __, child) { transitionBuilder: (_, anim, __, child) {
return Transform.scale( return Transform.scale(scale: 0.96 + (0.04 * anim.value), child: Opacity(opacity: anim.value, child: child));
scale: 0.96 + (0.04 * anim.value),
child: Opacity(opacity: anim.value, child: child),
);
}, },
); );
} }
/// ==== Widget nền của popup (tương đương BasePopupView + SwiftEntryKit display) ====
class _BasePopupView extends StatefulWidget { class _BasePopupView extends StatefulWidget {
final PopupManagerModel model; final PopupManagerModel model;
final int initialCountdown; final int initialCountdown;
final PopupNavigator onNavigate;
final VoidCallback onDismissed; final VoidCallback onDismissed;
const _BasePopupView({ const _BasePopupView({required this.model, required this.initialCountdown, required this.onDismissed});
required this.model,
required this.initialCountdown,
required this.onNavigate,
required this.onDismissed,
});
@override @override
State<_BasePopupView> createState() => _BasePopupViewState(); State<_BasePopupView> createState() => _BasePopupViewState();
...@@ -80,7 +59,7 @@ class _BasePopupView extends StatefulWidget { ...@@ -80,7 +59,7 @@ class _BasePopupView extends StatefulWidget {
class _BasePopupViewState extends State<_BasePopupView> { class _BasePopupViewState extends State<_BasePopupView> {
Timer? _timer; Timer? _timer;
late int _countdown; late int _countdown;
double? _imageAspectRatio; // width / height double? _imageAspectRatio;
bool _imageLoaded = false; bool _imageLoaded = false;
@override @override
...@@ -113,35 +92,16 @@ class _BasePopupViewState extends State<_BasePopupView> { ...@@ -113,35 +92,16 @@ class _BasePopupViewState extends State<_BasePopupView> {
if (mounted) Navigator.of(context).pop(); if (mounted) Navigator.of(context).pop();
} }
void _onImageTap() { void _onContentTap() {
final model = widget.model; logPopupClick(popupId: widget.model.id ?? '');
if ((model.clickActionType ?? '').isEmpty) return; print(
'Popup clicked: ${widget.model.directional?.clickActionType ?? ''} - ${widget.model.directional?.clickActionParam ?? ''}',
_timer?.cancel();
logPopupClick(popupId: model.id ?? '');
// Điều hướng tương đương DirectionalScreen.begin(...)
widget.onNavigate(
name: model.clickActionType!,
identifier: model.clickActionParam ?? '',
title: model.popupTitleTemplate ?? '',
body: model.popupBodyTemplate ?? '',
);
_dismiss();
}
void _onCancelTap() async {
final model = widget.model;
if ((model.clickActionType ?? '') == 'VIEW_GIFT') {
// Show "GiftMessageView" dạng bottom sheet
await showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (_) => _GiftMessageSheet(model: model, onNavigate: widget.onNavigate),
); );
} _timer?.cancel();
_dismiss(); _dismiss();
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.model.directional?.begin();
});
} }
@override @override
...@@ -151,18 +111,10 @@ class _BasePopupViewState extends State<_BasePopupView> { ...@@ -151,18 +111,10 @@ class _BasePopupViewState extends State<_BasePopupView> {
final body = (model.popupBodyTemplate ?? '').trim(); final body = (model.popupBodyTemplate ?? '').trim();
final hasTitle = title.isNotEmpty; final hasTitle = title.isNotEmpty;
final hasBody = body.isNotEmpty; final hasBody = body.isNotEmpty;
// Tính phần height phụ theo Swift (55 + 50, trừ bớt khi ẩn)
int extra = 55 + 50;
if (!hasTitle) extra -= 55;
if (!hasBody) extra -= 50;
final media = MediaQuery.of(context); final media = MediaQuery.of(context);
final screenW = media.size.width; final screenW = media.size.width;
final maxPopupHeight = media.size.height - 200; final maxPopupHeight = media.size.height - 200;
final imageCornerRadius = 12.0; // ảnh trên cùng vẫn bo 12
// Card radius phụ thuộc điều kiện
final imageCornerRadius = (!hasTitle && !hasBody) ? 12.0 : 12.0; // ảnh trên cùng vẫn bo 12
final subHeaderRadius = (hasBody && !hasTitle) ? 12.0 : 0.0; final subHeaderRadius = (hasBody && !hasTitle) ? 12.0 : 0.0;
return Material( return Material(
...@@ -172,7 +124,6 @@ class _BasePopupViewState extends State<_BasePopupView> { ...@@ -172,7 +124,6 @@ class _BasePopupViewState extends State<_BasePopupView> {
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
// Nếu đã biết tỷ lệ ảnh: tính height theo công thức Swift
double imageHeight = 0; double imageHeight = 0;
if (_imageAspectRatio != null && _imageAspectRatio! > 0) { if (_imageAspectRatio != null && _imageAspectRatio! > 0) {
// image.size.height*(screenW - 40)/image.size.width // image.size.height*(screenW - 40)/image.size.width
...@@ -182,19 +133,14 @@ class _BasePopupViewState extends State<_BasePopupView> { ...@@ -182,19 +133,14 @@ class _BasePopupViewState extends State<_BasePopupView> {
final content = Column( final content = Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Nút đóng
Align( Align(
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: IconButton( child: IconButton(icon: const Icon(Icons.close, color: Colors.white), onPressed: _dismiss),
icon: const Icon(Icons.close, color: Colors.white),
onPressed: _onCancelTap,
),
), ),
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(maxHeight: maxPopupHeight, minWidth: double.infinity),
maxHeight: maxPopupHeight, child: GestureDetector(
minWidth: double.infinity, onTap: _onContentTap,
),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Material( child: Material(
...@@ -210,47 +156,31 @@ class _BasePopupViewState extends State<_BasePopupView> { ...@@ -210,47 +156,31 @@ class _BasePopupViewState extends State<_BasePopupView> {
heightHint: imageHeight > 0 ? imageHeight : null, heightHint: imageHeight > 0 ? imageHeight : null,
cornerRadius: imageCornerRadius, cornerRadius: imageCornerRadius,
), ),
// Header // Header
if (hasTitle) if (hasTitle)
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Text( child: Text(
title, title,
style: const TextStyle( style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
fontSize: 18,
fontWeight: FontWeight.w700,
),
), ),
), ),
// SubHeader // SubHeader
if (hasBody) if (hasBody)
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF6F7F9), color: const Color(0xFFF6F7F9),
borderRadius: BorderRadius.vertical( borderRadius: BorderRadius.vertical(bottom: Radius.circular(subHeaderRadius)),
bottom: Radius.circular(subHeaderRadius),
),
), ),
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16), padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
child: Text( child: Text(body, style: const TextStyle(fontSize: 15, height: 1.4)),
body,
style: const TextStyle(fontSize: 15, height: 1.4),
),
), ),
// Dòng trạng thái đếm ngược (tuỳ chọn) // Dòng trạng thái đếm ngược (tuỳ chọn)
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16), padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
child: Text( child: Text(
_countdown > 0 _countdown > 0 ? '$_countdown seconds dismiss popup' : 'Closing…',
? '$_countdown seconds dismiss popup' style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
: 'Closing…',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
textAlign: TextAlign.right, textAlign: TextAlign.right,
), ),
), ),
...@@ -260,9 +190,9 @@ class _BasePopupViewState extends State<_BasePopupView> { ...@@ -260,9 +190,9 @@ class _BasePopupViewState extends State<_BasePopupView> {
), ),
), ),
), ),
),
], ],
); );
return content; return content;
}, },
), ),
...@@ -271,14 +201,9 @@ class _BasePopupViewState extends State<_BasePopupView> { ...@@ -271,14 +201,9 @@ class _BasePopupViewState extends State<_BasePopupView> {
); );
} }
Widget _buildImage({ Widget _buildImage({required String? url, double? heightHint, required double cornerRadius}) {
required String? url,
double? heightHint,
required double cornerRadius,
}) {
final imageUrl = (url ?? '').trim(); final imageUrl = (url ?? '').trim();
if (imageUrl.isEmpty) { if (imageUrl.isEmpty) {
// Placeholder fallback
return Container( return Container(
height: 160, height: 160,
color: const Color(0xFFE9ECF1), color: const Color(0xFFE9ECF1),
...@@ -287,9 +212,7 @@ class _BasePopupViewState extends State<_BasePopupView> { ...@@ -287,9 +212,7 @@ class _BasePopupViewState extends State<_BasePopupView> {
); );
} }
return GestureDetector( return ClipRRect(
onTap: _onImageTap,
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(cornerRadius)), borderRadius: BorderRadius.vertical(top: Radius.circular(cornerRadius)),
child: _NetworkImageWithInfo( child: _NetworkImageWithInfo(
imageUrl: imageUrl, imageUrl: imageUrl,
...@@ -303,7 +226,6 @@ class _BasePopupViewState extends State<_BasePopupView> { ...@@ -303,7 +226,6 @@ class _BasePopupViewState extends State<_BasePopupView> {
} }
}, },
), ),
),
); );
} }
} }
...@@ -313,12 +235,7 @@ class _NetworkImageWithInfo extends StatefulWidget { ...@@ -313,12 +235,7 @@ class _NetworkImageWithInfo extends StatefulWidget {
final String imageUrl; final String imageUrl;
final double? heightHint; final double? heightHint;
final void Function(int width, int height) onResolved; final void Function(int width, int height) onResolved;
const _NetworkImageWithInfo({required this.imageUrl, required this.onResolved, this.heightHint});
const _NetworkImageWithInfo({
required this.imageUrl,
required this.onResolved,
this.heightHint,
});
@override @override
State<_NetworkImageWithInfo> createState() => _NetworkImageWithInfoState(); State<_NetworkImageWithInfo> createState() => _NetworkImageWithInfoState();
...@@ -363,90 +280,9 @@ class _NetworkImageWithInfoState extends State<_NetworkImageWithInfo> { ...@@ -363,90 +280,9 @@ class _NetworkImageWithInfoState extends State<_NetworkImageWithInfo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_info == null && widget.heightHint == null) { if (_info == null && widget.heightHint == null) {
// loading skeleton return AspectRatio(aspectRatio: 16 / 9, child: Container(color: const Color(0xFFE9ECF1)));
return AspectRatio(
aspectRatio: 16 / 9,
child: Container(color: const Color(0xFFE9ECF1)),
);
} }
final height = widget.heightHint; final height = widget.heightHint;
return Image.network( return Image.network(widget.imageUrl, height: height, width: double.infinity, fit: BoxFit.cover);
widget.imageUrl,
height: height,
width: double.infinity,
fit: BoxFit.cover,
);
}
}
/// ==== GiftMessageView tương đương (bottom sheet) ====
class _GiftMessageSheet extends StatelessWidget {
final PopupManagerModel model;
final PopupNavigator onNavigate;
const _GiftMessageSheet({
required this.model,
required this.onNavigate,ff
});
@override
Widget build(BuildContext context) {
final radius = const Radius.circular(16);
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16 + 24),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 36, height: 4, decoration: BoxDecoration(color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2))),
const SizedBox(height: 12),
Text(
model.popupTitleTemplate?.isNotEmpty == true ? model.popupTitleTemplate! : 'Quà tặng của bạn',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
Text(
model.popupBodyTemplate?.isNotEmpty == true ? model.popupBodyTemplate! : 'Nhấn "Sử dụng ngay" để tiếp tục.',
style: const TextStyle(fontSize: 14, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if ((model.clickActionType ?? '').isEmpty) {
Navigator.of(context).pop();
return;
}
onNavigate(
name: model.clickActionType!,
identifier: model.clickActionParam ?? '',
title: model.popupTitleTemplate ?? '',
body: model.popupBodyTemplate ?? '',
);
Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: const Text('Sử dụng ngay'),
),
),
const SizedBox(height: 8),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Để sau'),
),
],
),
),
);
} }
} }
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import 'package:mypoint_flutter_app/screen/popup_manager/popup_manager_model.dart';
import '../../base/restful_api_viewmodel.dart';
class PopupManagerViewModel extends RestfulApiViewModel {
PopupManagerViewModel._();
static final PopupManagerViewModel instance = PopupManagerViewModel._();
final Set<String> _shownIds = {};
List<PopupManagerModel>? _popupData;
bool _loaded = false;
Future<void>? _loadingFuture;
Future<void> ensureLoaded() async {
if (_loaded) return;
if (_loadingFuture != null) {
return _loadingFuture;
}
_loadingFuture = _getPopupManagerDataInternal();
await _loadingFuture;
}
Future<void> _getPopupManagerDataInternal() async {
try {
const Duration(seconds: 3); // Giả lập thời gian tải dữ liệu
final response = await client.getPopupManagerCommonScreen();
_popupData = response.data ?? [];
// _popupData = [
// PopupManagerModel(
// id: '1',
// screenToShow: 'APP_SCREEN_HOME',
// clickActionType: 'VIEW_PRODUCT_VOUCHER',
// clickActionParam: '50760',
// posActionID: 'action1',
// posActionCode: 'code1',
// timeToShow: '10:00-18:00',
// timeCountDown: '30',
// hourStartInDay: '9',
// hourStopInDay: '17',
// afterPosID: 'pos1',
// afterPosCode: 'posCode1',
// afterPosName: 'POS Name 1',
// marketingRequestDescription: 'Marketing description here.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'daily',
// scheduleRunTypeName: 'Daily Schedule',
// scheduleAtTime: '12:00',
// popupTitleTemplate: 'APP_SCREEN_HOME',
// popupBodyTemplate: 'Enjoy your stay and check out our features.',
// imageID: 'image123',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '2',
// screenToShow: 'APP_SCREEN_POINTBACK',
// clickActionType: 'APP_SCREEN_SIM_SERVICE',
// clickActionParam: 'https://example.com/settings',
// posActionID: 'action2',
// posActionCode: 'code2',
// timeToShow: '08:00-20:00',
// timeCountDown: '60',
// hourStartInDay: '8',
// hourStopInDay: '20',
// afterPosID: 'pos2',
// afterPosCode: 'posCode2',
// afterPosName: 'POS Name 2',
// marketingRequestDescription: 'Settings popup description.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'weekly',
// scheduleRunTypeName: 'Weekly Schedule',
// scheduleAtTime: '10:00',
// popupTitleTemplate: 'APP_SCREEN_POINTBACK',
// popupBodyTemplate: 'Check out the new settings options.',
// imageID: 'image456',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '3',
// screenToShow: 'APP_SCREEN_PRODUCT_VOUCHER',
// clickActionType: 'APP_SCREEN_GIFTS',
// clickActionParam: 'Profile updated successfully.',
// posActionID: 'action3',
// posActionCode: 'code3',
// timeToShow: '09:00-21:00',
// timeCountDown: '45',
// hourStartInDay: '9',
// hourStopInDay: '21',
// afterPosID: 'pos3',
// afterPosCode: 'posCode3',
// afterPosName: 'POS Name 3',
// marketingRequestDescription: 'Profile update alert.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'monthly',
// scheduleRunTypeName: 'Monthly Schedule',
// scheduleAtTime: '15:00',
// popupTitleTemplate: 'APP_SCREEN_PRODUCT_VOUCHER',
// popupBodyTemplate: 'Your profile has been updated successfully.',
// imageID: 'image789',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '4',
// screenToShow: 'APP_SCREEN_GAME_BUNDLE',
// clickActionType: 'APP_SCREEN_CAMPAIGN_WALKING',
// clickActionParam: '1',
// posActionID: 'action3',
// posActionCode: 'code3',
// timeToShow: '09:00-21:00',
// timeCountDown: '45',
// hourStartInDay: '9',
// hourStopInDay: '21',
// afterPosID: 'pos3',
// afterPosCode: 'posCode3',
// afterPosName: 'POS Name 3',
// marketingRequestDescription: 'Profile update alert.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'monthly',
// scheduleRunTypeName: 'Monthly Schedule',
// scheduleAtTime: '15:00',
// popupTitleTemplate: 'APP_SCREEN_GAME_BUNDLE',
// popupBodyTemplate: 'Your profile has been updated successfully.',
// imageID: 'image789',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '5',
// screenToShow: 'APP_SCREEN_PERSONAL',
// clickActionType: 'APP_SCREEN_SIM_SERVICE',
// clickActionParam: 'Profile updated successfully.',
// posActionID: 'action3',
// posActionCode: 'code3',
// timeToShow: '09:00-21:00',
// timeCountDown: '45',
// hourStartInDay: '9',
// hourStopInDay: '21',
// afterPosID: 'pos3',
// afterPosCode: 'posCode3',
// afterPosName: 'POS Name 3',
// marketingRequestDescription: 'Profile update alert.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'monthly',
// scheduleRunTypeName: 'Monthly Schedule',
// scheduleAtTime: '15:00',
// popupTitleTemplate: 'APP_SCREEN_PERSONAL',
// popupBodyTemplate: 'Your profile has been updated successfully.',
// imageID: 'image789',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// ];
_loaded = true;
} catch (e) {
_popupData = [];
_loaded = true;
rethrow;
} finally {
_loadingFuture = null;
}
}
PopupManagerModel? getForScreen(String screenName) {
if (_popupData == null || _popupData!.isEmpty) return null;
final idx = _popupData!.indexWhere(
(e) => (e.screenToShow ?? '').trim().toUpperCase() == screenName.trim().toUpperCase(),
);
if (idx < 0) return null;
final found = _popupData![idx];
if (_shownIds.contains(found.id)) return null;
return found;
}
Future<void> markShownOnce(String popupId) async {
_shownIds.add(popupId);
}
Future<void> reset() async {
_shownIds.clear();
_popupData = [];
_loaded = false;
_loadingFuture = null;
}
}
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/screen/popup_manager/popup_manager_screen.dart';
import '../../directional/directional_action_type.dart';
import 'popup_manager_viewmodel.dart';
class PopupRunner {
static Future<void> _showIfAvailable(BuildContext context, {required String screenName}) async {
await PopupManagerViewModel.instance.ensureLoaded();
final popup = PopupManagerViewModel.instance.getForScreen(screenName,);
if (popup == null || (popup.id ?? '').isEmpty) return;
if (!context.mounted) return;
await showPopupManagerScreen(
context,
modelPopup: popup,
);
}
}
mixin PopupOnInit<T extends StatefulWidget> on State<T> {
bool _popupChecked = false;
void runPopupCheck(DirectionalScreenName directional) {
if (_popupChecked) return;
_popupChecked = true;
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!mounted) return;
await PopupRunner._showIfAvailable(
context,
screenName: directional.rawValue,
);
});
}
}
\ No newline at end of file
// setting_screen.dart // setting_screen.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:mypoint_flutter_app/screen/setting/setting_viewmodel.dart'; import 'package:mypoint_flutter_app/screen/setting/setting_viewmodel.dart';
import '../../shared/router_gage.dart'; import '../../shared/router_gage.dart';
import '../../widgets/bottom_sheet_helper.dart'; import '../../widgets/bottom_sheet_helper.dart';
import '../../widgets/custom_app_bar.dart'; import '../../widgets/custom_navigation_bar.dart';
import '../change_pass/change_pass_screen.dart'; import '../change_pass/change_pass_screen.dart';
import '../delete_account/delete_account_dialog.dart'; import '../delete_account/delete_account_dialog.dart';
...@@ -32,7 +31,7 @@ class _SettingScreenState extends State<SettingScreen> { ...@@ -32,7 +31,7 @@ class _SettingScreenState extends State<SettingScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: CustomAppBar.back(title: "Cài đặt"), appBar: CustomNavigationBar(title: "Cài đặt"),
backgroundColor: const Color(0xFFF5F6F7), backgroundColor: const Color(0xFFF5F6F7),
body: Column( body: Column(
children: [ children: [
......
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/base/restful_api_viewmodel.dart'; import 'package:mypoint_flutter_app/base/restful_api_viewmodel.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart'; import 'package:mypoint_flutter_app/shared/router_gage.dart';
import '../../base/base_response_model.dart'; import '../../base/base_response_model.dart';
import '../../configs/constants.dart'; import '../../model/auth/profile_response_model.dart';
import '../../model/update_response_model.dart'; import '../../model/update_response_model.dart';
import '../../preference/data_preference.dart'; import '../../preference/data_preference.dart';
import '../../preference/point/point_manager.dart'; import '../../preference/point/point_manager.dart';
import '../main_tab_screen/main_tab_screen.dart';
import '../onboarding/onboarding_screen.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../popup_manager/popup_manager_viewmodel.dart';
class SplashScreenViewModel extends RestfulApiViewModel { class SplashScreenViewModel extends RestfulApiViewModel {
var infoAppUpdate = BaseResponseModel<UpdateResponseModel>().obs; var infoAppUpdate = BaseResponseModel<UpdateResponseModel>().obs;
...@@ -35,7 +35,7 @@ class SplashScreenViewModel extends RestfulApiViewModel { ...@@ -35,7 +35,7 @@ class SplashScreenViewModel extends RestfulApiViewModel {
} }
Future<void> getUserProfile() async { Future<void> getUserProfile() async {
if (!(await DataPreference.instance.logged)) { if (!(DataPreference.instance.logged)) {
Get.toNamed(onboardingScreen); Get.toNamed(onboardingScreen);
return; return;
} }
...@@ -44,16 +44,24 @@ class SplashScreenViewModel extends RestfulApiViewModel { ...@@ -44,16 +44,24 @@ class SplashScreenViewModel extends RestfulApiViewModel {
hideLoading(); hideLoading();
final userProfile = value.data; final userProfile = value.data;
if (value.isSuccess && userProfile != null) { if (value.isSuccess && userProfile != null) {
await DataPreference.instance.saveUserProfile(userProfile); _freshDataAndToMainScreen(userProfile);
await UserPointManager().fetchUserPoint();
Get.toNamed(mainScreen);
} else { } else {
DataPreference.instance.clearLoginToken(); DataPreference.instance.clearLoginToken();
Get.toNamed(onboardingScreen); Get.toNamed(onboardingScreen);
} }
}); });
} }
void _freshDataAndToMainScreen(ProfileResponseModel userProfile) async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await DataPreference.instance.saveUserProfile(userProfile);
await UserPointManager().fetchUserPoint();
await PopupManagerViewModel.instance.ensureLoaded();
Get.toNamed(mainScreen);
});
}
} }
class EmptyCodable { class EmptyCodable {
EmptyCodable.fromJson(dynamic json); EmptyCodable.fromJson(dynamic json);
......
...@@ -6,6 +6,7 @@ import 'package:mypoint_flutter_app/shared/router_gage.dart'; ...@@ -6,6 +6,7 @@ import 'package:mypoint_flutter_app/shared/router_gage.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../resources/base_color.dart'; import '../../resources/base_color.dart';
import '../../widgets/back_button.dart'; import '../../widgets/back_button.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../faqs/faqs_screen.dart'; import '../faqs/faqs_screen.dart';
import '../pageDetail/campaign_detail_screen.dart'; import '../pageDetail/campaign_detail_screen.dart';
import '../pageDetail/model/detail_page_rule_type.dart'; import '../pageDetail/model/detail_page_rule_type.dart';
...@@ -52,13 +53,7 @@ class _SupportScreenState extends State<SupportScreen> { ...@@ -52,13 +53,7 @@ class _SupportScreenState extends State<SupportScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: CustomNavigationBar(title: "Hỗ trợ"),
leading: CustomBackButton(),
title: const Text("Hỗ trợ", style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
),
body: Obx(() { body: Obx(() {
if (controller.supportItems.isEmpty) { if (controller.supportItems.isEmpty) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
......
import 'package:contacts_service/contacts_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
...@@ -8,6 +7,7 @@ import 'package:mypoint_flutter_app/widgets/image_loader.dart'; ...@@ -8,6 +7,7 @@ import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../preference/data_preference.dart'; import '../../preference/data_preference.dart';
import '../../resources/base_color.dart'; import '../../resources/base_color.dart';
import '../../shared/router_gage.dart'; import '../../shared/router_gage.dart';
import '../contacts/contacts_picker.dart';
import 'brand_select_sheet_widget.dart'; import 'brand_select_sheet_widget.dart';
class PhoneTopUpScreen extends StatefulWidget { class PhoneTopUpScreen extends StatefulWidget {
...@@ -353,22 +353,19 @@ class _PhoneTopUpScreenState extends State<PhoneTopUpScreen> { ...@@ -353,22 +353,19 @@ class _PhoneTopUpScreenState extends State<PhoneTopUpScreen> {
Future<void> pickContact(BuildContext context) async { Future<void> pickContact(BuildContext context) async {
try { try {
// Gọi sẽ tự động hiện dialog yêu cầu quyền (nếu cần) final contact = await showContactPicker(context);
final Contact? contact = await ContactsService.openDeviceContactPicker(); if (contact == null) return;
if (contact != null && contact.phones != null && contact.phones!.isNotEmpty) { if (contact.phones.isEmpty) return;
String phone = contact.phones!.first.value ?? ''; String phone = contact.phones.first.number;
phone = phone.replaceAll(RegExp(r'[\s\-\(\)]'), ''); phone = phone.replaceAll(RegExp(r'[\s\-\(\)]'), '');
_phoneController.text = phone; _phoneController.text = phone;
_viewModel.phoneNumber.value = phone; _viewModel.phoneNumber.value = phone;
_viewModel.checkMobileNetwork(); _viewModel.checkMobileNetwork();
} else {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("Không tìm thấy số điện thoại hợp lệ")));
}
} catch (e) { } catch (e) {
print("❌ Lỗi khi truy cập danh bạ: $e"); debugPrint('❌ pickContact error: $e');
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Không thể truy cập danh bạ"))); ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Không thể truy cập danh bạ')),
);
} }
} }
} }
...@@ -33,7 +33,7 @@ class _TransactionHistoryScreenState extends State<TransactionHistoryScreen> { ...@@ -33,7 +33,7 @@ class _TransactionHistoryScreenState extends State<TransactionHistoryScreen> {
_buildCategoryTabs(), _buildCategoryTabs(),
_buildSummaryBox(summary), _buildSummaryBox(summary),
const SizedBox(height: 8), const SizedBox(height: 8),
if (items.isEmpty) Expanded(child: Center(child: EmptyWidget(size: Size(200, 200)))), if (items.isEmpty) Expanded(child: Center(child: EmptyWidget(size: Size(160, 160)))),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
......
...@@ -4,16 +4,27 @@ import 'package:mypoint_flutter_app/shared/direction_google_map.dart'; ...@@ -4,16 +4,27 @@ import 'package:mypoint_flutter_app/shared/direction_google_map.dart';
import '../models/product_store_model.dart'; import '../models/product_store_model.dart';
class StoreListSection extends StatelessWidget { class StoreListSection extends StatefulWidget {
final List<ProductStoreModel> stores; final List<ProductStoreModel> stores;
final String? brandLogo; final String? brandLogo;
const StoreListSection({Key? key, required this.stores, this.brandLogo}) : super(key: key); const StoreListSection({super.key, required this.stores, this.brandLogo});
@override
State<StoreListSection> createState() => _StoreListSectionState();
}
class _StoreListSectionState extends State<StoreListSection> {
bool _seeAllStore = false;
final int _numberOfStore = 4;
List<ProductStoreModel> get displayStores {
return _seeAllStore ? widget.stores : widget.stores.take(_numberOfStore).toList();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() { return Obx(() {
if (stores.isEmpty) { if (displayStores.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Container( return Container(
...@@ -25,7 +36,7 @@ class StoreListSection extends StatelessWidget { ...@@ -25,7 +36,7 @@ class StoreListSection extends StatelessWidget {
children: [ children: [
const Text('Địa điểm áp dụng:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), const Text('Địa điểm áp dụng:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(height: 10), const SizedBox(height: 10),
...stores.map( ...displayStores.map(
(store) => InkWell( (store) => InkWell(
onTap: () { onTap: () {
_onTapStore(store); _onTapStore(store);
...@@ -33,6 +44,20 @@ class StoreListSection extends StatelessWidget { ...@@ -33,6 +44,20 @@ class StoreListSection extends StatelessWidget {
child: _buildStoreItem(store), child: _buildStoreItem(store),
), ),
), ),
if (widget.stores.length > _numberOfStore)
GestureDetector(
onTap: () {
setState(() {
_seeAllStore = !_seeAllStore;
});
},
child: Center(
child: Text(
_seeAllStore ? 'Thu gọn' : 'Xem tất cả',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.blueAccent),
),
),
),
], ],
), ),
); );
...@@ -53,7 +78,7 @@ class StoreListSection extends StatelessWidget { ...@@ -53,7 +78,7 @@ class StoreListSection extends StatelessWidget {
children: [ children: [
ClipOval( ClipOval(
child: Image.network( child: Image.network(
brandLogo ?? "", widget.brandLogo ?? "",
width: 20, width: 20,
height: 20, height: 20,
fit: BoxFit.cover, fit: BoxFit.cover,
...@@ -61,10 +86,13 @@ class StoreListSection extends StatelessWidget { ...@@ -61,10 +86,13 @@ class StoreListSection extends StatelessWidget {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(store.name ?? '', style: const TextStyle(fontWeight: FontWeight.w600)), Expanded(
child: Text(store.name ?? '', style: const TextStyle(fontWeight: FontWeight.w600), softWrap: true),
),
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
if ((store.address ?? '').isNotEmpty)
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
......
import '../../mobile_card/models/product_mobile_card_model.dart';
class MyVoucherResponse {
final int? listStart;
final int? listLimit;
final int? listTotal;
final List<ProductMobileCardModel>? listItems;
MyVoucherResponse({
this.listStart,
this.listLimit,
this.listTotal,
this.listItems,
});
factory MyVoucherResponse.fromJson(Map<String, dynamic> json) {
return MyVoucherResponse(
listStart: json['list_start'] as int?,
listLimit: json['list_limit'] as int?,
listTotal: json['list_total'] as int?,
listItems: (json['list_items'] as List<dynamic>?)
?.map((e) => ProductMobileCardModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> toJson() {
return {
'list_start': listStart,
'list_limit': listLimit,
'list_total': listTotal,
'list_items': listItems?.map((e) => e.toJson()).toList(),
};
}
}
\ No newline at end of file
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../../base/restful_api_viewmodel.dart';
import '../../mobile_card/models/product_mobile_card_model.dart';
import '../models/my_product_status_type.dart';
class MyMobileCardListViewModel extends RestfulApiViewModel {
final RxInt selectedTabIndex = 0.obs;
var myProducts = <ProductMobileCardModel>[].obs;
void Function(String message)? onShowAlertError;
@override
void onInit() {
super.onInit();
freshData(isRefresh: true);
}
void selectTab(int index) {
selectedTabIndex.value = index;
freshData(isRefresh: true);
}
void freshData({bool isRefresh = false}) {
final body = {
"index": isRefresh ? 0 : myProducts.length,
"size": 20,
};
final status = selectedTabIndex.value == 0 ? MyProductStatusType.waiting : MyProductStatusType.used;
client.getMyMobileCards(status, body).then((response) {
if (!response.isSuccess) {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
}
final result = response.data?.listItems ?? [];
if (isRefresh) {
myProducts.clear();
}
myProducts.addAll(result);
}).catchError((error) {
myProducts.clear();
print('Error fetching products: $error');
});
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../widgets/custom_empty_widget.dart';
import '../../../widgets/custom_navigation_bar.dart';
import '../../../widgets/image_loader.dart';
import '../../mobile_card/models/product_mobile_card_model.dart';
import 'my_mobile_card_list_viewmodel.dart';
import 'package:dotted_border/dotted_border.dart';
class MyMobileCardListScreen extends StatefulWidget {
const MyMobileCardListScreen({super.key});
@override
State<MyMobileCardListScreen> createState() => _MyMobileCardListScreenState();
}
class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
late final MyMobileCardListViewModel _viewModel = Get.put(MyMobileCardListViewModel());
@override
void initState() {
super.initState();
// _viewModel = Get.put(MyProductListViewModel());
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
appBar: CustomNavigationBar(title: 'Thẻ nạp của tôi',),
body: Obx(
() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [_buildTab('Đang có', 0), _buildTab('Đã sử dụng', 1)],
),
),
const Divider(height: 1),
if (_viewModel.myProducts.isEmpty)
Expanded(child: EmptyWidget(size: Size(screenWidth / 2, screenWidth / 2)))
else
Expanded(
child: RefreshIndicator(
onRefresh: () async {
_viewModel.freshData(isRefresh: true);
},
child: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _viewModel.myProducts.length,
itemBuilder: (_, index) {
if (index >= _viewModel.myProducts.length) {
_viewModel.freshData(isRefresh: false);
return const Center(
child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator()),
);
}
final product = _viewModel.myProducts[index];
return _buildVoucherItem(product);
},
),
),
),
],
),
),
);
}
Widget _buildTab(String title, int index) {
return GestureDetector(
onTap: () => _viewModel.selectTab(index),
child: Obx(
() => Column(
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: _viewModel.selectedTabIndex.value == index ? Colors.red : Colors.black54,
),
),
const SizedBox(height: 4),
if (_viewModel.selectedTabIndex.value == index) Container(height: 2, width: 60, color: Colors.red),
],
),
),
);
}
Widget _buildVoucherItem(ProductMobileCardModel product) {
return GestureDetector(
onTap: () {
// Get.toNamed(voucherDetailScreen, arguments: {"customerProductId": product.id});
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.all(0),
child: DottedBorder(
color: Colors.redAccent.withOpacity(0.3),
borderType: BorderType.RRect,
radius: const Radius.circular(12),
dashPattern: const [3, 3],
strokeWidth: 1,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.redAccent.withOpacity(0.03),
),
padding: const EdgeInsets.all(12),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: loadNetworkImage(
url: product.brandLogo ?? '',
width: 64,
height: 64,
fit: BoxFit.cover,
placeholderAsset: "assets/images/bg_default_11.png",
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.brandName ?? '',
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.black54, fontSize: 12),
),
const SizedBox(height: 6),
Text(product.name ?? '', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
Text('HSD: ${product.expire}', style: const TextStyle(color: Colors.black54, fontSize: 12)),
],
),
),
],
),
),
),
),
);
}
}
...@@ -99,12 +99,16 @@ class _MyVoucherListScreenState extends State<MyVoucherListScreen> { ...@@ -99,12 +99,16 @@ class _MyVoucherListScreenState extends State<MyVoucherListScreen> {
margin: const EdgeInsets.symmetric(vertical: 8), margin: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
child: DottedBorder( child: DottedBorder(
color: Colors.redAccent.withOpacity(0.6), color: Colors.redAccent.withOpacity(0.3),
borderType: BorderType.RRect, borderType: BorderType.RRect,
radius: const Radius.circular(12), radius: const Radius.circular(12),
dashPattern: const [6, 4], dashPattern: const [3, 3],
strokeWidth: 1, strokeWidth: 1,
child: Container( child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.redAccent.withOpacity(0.03),
),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Row( child: Row(
children: [ children: [
...@@ -127,9 +131,9 @@ class _MyVoucherListScreenState extends State<MyVoucherListScreen> { ...@@ -127,9 +131,9 @@ class _MyVoucherListScreenState extends State<MyVoucherListScreen> {
product.brandName ?? '', product.brandName ?? '',
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.black54, fontSize: 12), style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.black54, fontSize: 12),
), ),
const SizedBox(height: 4), const SizedBox(height: 6),
Text(product.title ?? '', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), Text(product.title ?? '', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
const SizedBox(height: 4), const SizedBox(height: 6),
Text('HSD: ${product.expire}', style: const TextStyle(color: Colors.black54, fontSize: 12)), Text('HSD: ${product.expire}', style: const TextStyle(color: Colors.black54, fontSize: 12)),
], ],
), ),
......
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/screen/voucher/voucher_list/voucher_list_screen.dart'; import 'package:mypoint_flutter_app/screen/voucher/voucher_list/voucher_list_screen.dart';
import '../../directional/directional_action_type.dart';
import '../../shared/router_gage.dart'; import '../../shared/router_gage.dart';
import '../home/header_home_viewmodel.dart'; import '../home/header_home_viewmodel.dart';
import '../popup_manager/popup_manager_screen.dart';
import '../popup_manager/popup_manager_viewmodel.dart';
import '../popup_manager/popup_runner_helper.dart';
import 'voucher_tab_viewmodel.dart'; import 'voucher_tab_viewmodel.dart';
import 'sub_widget/voucher_action_menu.dart'; import 'sub_widget/voucher_action_menu.dart';
import 'sub_widget/voucher_item_grid.dart'; import 'sub_widget/voucher_item_grid.dart';
...@@ -17,17 +21,22 @@ class VoucherTabScreen extends StatefulWidget { ...@@ -17,17 +21,22 @@ class VoucherTabScreen extends StatefulWidget {
State<VoucherTabScreen> createState() => _VoucherTabScreenState(); State<VoucherTabScreen> createState() => _VoucherTabScreenState();
} }
class _VoucherTabScreenState extends State<VoucherTabScreen> { class _VoucherTabScreenState extends State<VoucherTabScreen> with PopupOnInit {
final _headerHomeVM = Get.find<HeaderHomeViewModel>(); final _headerHomeVM = Get.find<HeaderHomeViewModel>();
@override
void initState() {
super.initState();
runPopupCheck(DirectionalScreenName.productVoucher);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final VoucherTabViewModel viewModel = Get.put(VoucherTabViewModel()); final VoucherTabViewModel viewModel = Get.put(VoucherTabViewModel());
return Scaffold( return Scaffold(
appBar: CustomNavigationBar( appBar: CustomNavigationBar(
title: "Ưu đãi", title: "Ưu đãi",
showBackButton: false, leftButtons: [],
backgroundImage: _headerHomeVM.headerData.background ?? "assets/images/bg_header_navi.png", backgroundImage: _headerHomeVM.headerData.background ?? "assets/images/bg_header_navi.png",
rightButtons: [ rightButtons: [
IconButton( IconButton(
......
...@@ -8,6 +8,7 @@ import 'package:webview_flutter/webview_flutter.dart'; ...@@ -8,6 +8,7 @@ import 'package:webview_flutter/webview_flutter.dart';
import '../../base/base_screen.dart'; import '../../base/base_screen.dart';
import '../../base/basic_state.dart'; import '../../base/basic_state.dart';
import '../../directional/directional_screen.dart'; import '../../directional/directional_screen.dart';
import '../../widgets/custom_navigation_bar.dart';
class BaseWebViewInput { class BaseWebViewInput {
final String? title; final String? title;
...@@ -58,7 +59,7 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta ...@@ -58,7 +59,7 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
onWebResourceError: (error) { onWebResourceError: (error) {
hideLoading(); hideLoading();
if (error.description != 'about:blank') { if (error.description != 'about:blank') {
showAlertError(content: error.description); // showAlertError(content: error.description);
} }
}, },
onNavigationRequest: _handleNavigation, onNavigationRequest: _handleNavigation,
...@@ -81,13 +82,19 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta ...@@ -81,13 +82,19 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
appBar: appBar:
input.isFullScreen input.isFullScreen
? null ? null
: AppBar( : CustomNavigationBar(
title: Text( title: input.title ?? _dynamicTitle ?? Uri.parse(input.url).host,
input.title ?? _dynamicTitle ?? Uri.parse(input.url).host, leftButtons: [
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87), CustomBackButton(onPressed: _handleBack),
), ],
leading: CustomBackButton(onPressed: _handleBack),
), ),
// AppBar(
// title: Text(
// input.title ?? _dynamicTitle ?? Uri.parse(input.url).host,
// style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
// ),
// leading: CustomBackButton(onPressed: _handleBack),
// ),
body: Stack( body: Stack(
children: [ children: [
SafeArea( SafeArea(
......
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