Commit 4bd580fa authored by DatHV's avatar DatHV
Browse files

update game card detail, mobile card

parent b75a9279
import 'dart:io'; import 'dart:io';
import 'dart:convert';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:mypoint_flutter_app/firebase/push_notification.dart';
import 'firebase_options.dart'; import 'firebase_options.dart';
import 'notification_parse_payload.dart';
@pragma('vm:entry-point') // bắt buộc cho background isolate @pragma('vm:entry-point') // bắt buộc cho background isolate
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// TODO: xử lý dữ liệu background nếu cần print('_firebaseMessagingBackgroundHandler ${message.toMap()}');
// Android: data-only message sẽ không tự hiển thị. Tự show local notification
if (Platform.isAndroid) {
final data = message.data;
final title = message.notification?.title ?? data['title']?.toString();
final body = message.notification?.body ?? (data['body']?.toString() ?? data['content']?.toString());
if ((title ?? body) != null) {
await _ensureBgLocalNotifications();
await _flnp.show(
message.hashCode,
title,
body,
const NotificationDetails(
android: AndroidNotificationDetails(
'default_channel',
'General',
importance: Importance.high,
priority: Priority.high,
),
),
payload: data.isNotEmpty ? jsonEncode(data) : null,
);
}
}
} }
final _flnp = FlutterLocalNotificationsPlugin(); final _flnp = FlutterLocalNotificationsPlugin();
bool _bgLocalInit = false;
Future<void> _initLocalNotifications() async { Future<void> _ensureBgLocalNotifications() async {
if (_bgLocalInit) return;
const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher'); const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosInit = DarwinInitializationSettings(); const iosInit = DarwinInitializationSettings();
const init = InitializationSettings(android: androidInit, iOS: iosInit); const init = InitializationSettings(android: androidInit, iOS: iosInit);
await _flnp.initialize(init); await _flnp.initialize(init);
const channel = AndroidNotificationChannel( const channel = AndroidNotificationChannel(
'default_channel', 'General', 'default_channel',
'General',
description: 'Default notifications',
importance: Importance.high,
);
await _flnp
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
_bgLocalInit = true;
}
Future<void> _initLocalNotifications() async {
const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosInit = DarwinInitializationSettings();
const init = InitializationSettings(android: androidInit, iOS: iosInit);
// Thêm callback xử lý khi click notification
await _flnp.initialize(
init,
onDidReceiveNotificationResponse: (response) {
print('Response: $response, payload: ${response.payload}');
final info = parseNotificationPayload(response.payload);
NotificationRouter.handleDirectionNotification(PushNotification(info: info));
},
);
const channel = AndroidNotificationChannel(
'default_channel',
'General',
description: 'Default notifications', description: 'Default notifications',
importance: Importance.high, importance: Importance.high,
); );
...@@ -28,11 +84,26 @@ Future<void> _initLocalNotifications() async { ...@@ -28,11 +84,26 @@ Future<void> _initLocalNotifications() async {
?.createNotificationChannel(channel); ?.createNotificationChannel(channel);
} }
/// Kiểm tra nếu app được mở do click vào local notification (từ killed state)
Future<void> handleLocalNotificationLaunchIfAny() async {
try {
final details = await _flnp.getNotificationAppLaunchDetails();
if (details == null) return;
if (details.didNotificationLaunchApp) {
final payload = details.notificationResponse?.payload;
if (payload != null && payload.isNotEmpty) {
final info = parseNotificationPayload(payload);
Future.delayed(const Duration(seconds: 1), () {
NotificationRouter.handleDirectionNotification(PushNotification(info: info));
});
}
}
} catch (_) {}
}
Future<void> initFirebaseAndFcm() async { Future<void> initFirebaseAndFcm() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
final messaging = FirebaseMessaging.instance; final messaging = FirebaseMessaging.instance;
// Quyền iOS / Android 13+ // Quyền iOS / Android 13+
...@@ -41,39 +112,46 @@ Future<void> initFirebaseAndFcm() async { ...@@ -41,39 +112,46 @@ Future<void> initFirebaseAndFcm() async {
} else { } else {
await messaging.requestPermission(); // Android 13+ POST_NOTIFICATIONS await messaging.requestPermission(); // Android 13+ POST_NOTIFICATIONS
} }
await _initLocalNotifications(); await _initLocalNotifications();
// Foreground: tự hiện local notification // Foreground: Android không tự hiển thị -> ta show local notification
FirebaseMessaging.onMessage.listen((message) { FirebaseMessaging.onMessage.listen((message) {
final n = message.notification; if (kDebugMode) {
if (n != null) { print('=== FOREGROUND MESSAGE RECEIVED ===');
_flnp.show( print('Message: ${message.messageId}');
n.hashCode, print('Data: ${message.data}');
n.title, print('Notification: ${message.notification?.title} - ${message.notification?.body}');
n.body,
const NotificationDetails(
android: AndroidNotificationDetails('default_channel', 'General',
importance: Importance.high, priority: Priority.high),
iOS: DarwinNotificationDetails(),
),
payload: message.data.isNotEmpty ? message.data.toString() : null,
);
} }
// if (Platform.isAndroid) {
final n = message.notification;
final title = n?.title ?? (message.data['title']?.toString());
final body = n?.body ?? (message.data['body']?.toString());
if ((title ?? body) != null) {
_flnp.show(
message.hashCode,
title,
body,
const NotificationDetails(
android: AndroidNotificationDetails(
'default_channel',
'General',
importance: Importance.high,
priority: Priority.high,
),
),
payload: message.data.isNotEmpty ? jsonEncode(message.data) : null,
);
}
// }
}); });
// User click notification mở app (khi app đang chạy ở background)
// User click notification mở app
FirebaseMessaging.onMessageOpenedApp.listen((message) { FirebaseMessaging.onMessageOpenedApp.listen((message) {
// TODO: điều hướng theo message.data['screen'] (nếu có) NotificationRouter.handleRemoteMessage(message);
}); });
// Initial message sẽ được xử lý sau khi runApp trong main.dart
// Nếu app mở từ trạng thái terminated do user bấm notify
final initial = await FirebaseMessaging.instance.getInitialMessage();
if (initial != null) {
// TODO: xử lý điều hướng
}
// Lấy token để test gửi // Lấy token để test gửi
final token = await messaging.getToken(); final token = await messaging.getToken();
print('FCM token: $token'); // if (kDebugMode) {
print('FCM token: $token');
// }
} }
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import 'package:mypoint_flutter_app/networking/restful_api_viewmodel.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
class PushTokenService extends RestfulApiViewModel {
static final PushTokenService _instance = PushTokenService._internal();
PushTokenService._internal();
factory PushTokenService() => _instance;
static Future<void> uploadIfLogged({String? fcmToken}) async {
final isLogged = DataPreference.instance.logged;
if (!isLogged) return;
final token = fcmToken ?? await FirebaseMessaging.instance.getToken();
if (token == null || token.isEmpty) return;
await _instance.client.pushNotificationDeviceUpdateToken(token);
}
}
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/app_navigator.dart'; import 'package:mypoint_flutter_app/networking/app_navigator.dart';
...@@ -10,18 +11,26 @@ import 'package:mypoint_flutter_app/shared/router_gage.dart'; ...@@ -10,18 +11,26 @@ import 'package:mypoint_flutter_app/shared/router_gage.dart';
import 'base/app_loading.dart'; import 'base/app_loading.dart';
import 'env_loader.dart'; import 'env_loader.dart';
import 'networking/dio_http_service.dart'; import 'networking/dio_http_service.dart';
import 'push_setup.dart'; import 'firebase/push_notification.dart';
import 'firebase/push_setup.dart';
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await loadEnv(); await loadEnv();
await initFirebaseAndFcm(); await DataPreference.instance.init();
DioHttpService(); DioHttpService();
Get.put(HeaderThemeController(), permanent: true); Get.put(HeaderThemeController(), permanent: true);
await DataPreference.instance.init(); await initFirebaseAndFcm();
await UserPointManager().fetchUserPoint(); await UserPointManager().fetchUserPoint();
runApp(const MyApp()); runApp(const MyApp());
AppLoading().attach(); WidgetsBinding.instance.addPostFrameCallback((_) {
AppLoading().attach();
});
// Handle launch from notification when app was killed
_handleInitialNotificationLaunch();
// Handle launch from local notification tap when app was killed
handleLocalNotificationLaunchIfAny();
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
...@@ -31,6 +40,7 @@ class MyApp extends StatelessWidget { ...@@ -31,6 +40,7 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetMaterialApp( return GetMaterialApp(
navigatorKey: AppNavigator.key, navigatorKey: AppNavigator.key,
navigatorObservers: [routeObserver],
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
initialRoute: '/splash', initialRoute: '/splash',
theme: ThemeData( theme: ThemeData(
...@@ -50,4 +60,17 @@ class MyApp extends StatelessWidget { ...@@ -50,4 +60,17 @@ class MyApp extends StatelessWidget {
getPages: RouterPage.pages(), getPages: RouterPage.pages(),
); );
} }
}
Future<void> _handleInitialNotificationLaunch() async {
try {
final initial = await FirebaseMessaging.instance.getInitialMessage();
print('Checking initial message for app launch from terminated state...$initial');
if (initial == null) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(const Duration(seconds: 1), () {
NotificationRouter.handleRemoteMessage(initial);
});
});
} catch (_) {}
} }
\ No newline at end of file
...@@ -98,6 +98,7 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient { ...@@ -98,6 +98,7 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
var deviceKey = await DeviceInfo.getDeviceId(); var deviceKey = await DeviceInfo.getDeviceId();
var key = "$phone+_=$deviceKey/*8854"; var key = "$phone+_=$deviceKey/*8854";
final body = {"device_key": deviceKey, "phone_number": phone, "key": key.toSha256()}; final body = {"device_key": deviceKey, "phone_number": phone, "key": key.toSha256()};
print('body: $body');
return requestNormal( return requestNormal(
APIPaths.checkPhoneNumber, APIPaths.checkPhoneNumber,
Method.POST, Method.POST,
...@@ -367,12 +368,20 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient { ...@@ -367,12 +368,20 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
} }
Future<BaseResponseModel<GameBundleItemModel>> getGameDetail(String id) async { Future<BaseResponseModel<GameBundleItemModel>> getGameDetail(String id) async {
print("RestfulAPIClientAllRequest getGameDetail - id: $id");
final path = APIPaths.getGameDetail.replaceAll("%@", id); final path = APIPaths.getGameDetail.replaceAll("%@", id);
return requestNormal(path, Method.POST, {}, (data) { return requestNormal(path, Method.POST, {}, (data) {
return GameBundleItemModel.fromJson(data as Json); return GameBundleItemModel.fromJson(data as Json);
}); });
} }
Future<BaseResponseModel<GameBundleItemModel>> submitGameCard(String gameId, String itemId) async {
final path = APIPaths.submitGameCard.replaceFirst("%@", gameId).replaceFirst("%@", itemId);
return requestNormal(path, Method.POST, {}, (data) {
return GameBundleItemModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<List<AffiliateCategoryModel>>> affiliateCategoryGetList() async { Future<BaseResponseModel<List<AffiliateCategoryModel>>> affiliateCategoryGetList() async {
String? token = DataPreference.instance.token ?? ""; String? token = DataPreference.instance.token ?? "";
final body = {"access_token": token}; final body = {"access_token": token};
...@@ -660,7 +669,7 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient { ...@@ -660,7 +669,7 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
Future<BaseResponseModel<RedeemProductResponseModel>> getMobileCardCode(String itemId) async { Future<BaseResponseModel<RedeemProductResponseModel>> getMobileCardCode(String itemId) async {
String? token = DataPreference.instance.token ?? ""; String? token = DataPreference.instance.token ?? "";
final body = {"product_item_id": itemId, "access_token": token}; final body = {"product_item_id": itemId, "access_token": token};
return requestNormal(APIPaths.redeemMobileCard, Method.POST, body, (data) { return requestNormal(APIPaths.getMobileCardCode, Method.POST, body, (data) {
return RedeemProductResponseModel.fromJson(data as Json); return RedeemProductResponseModel.fromJson(data as Json);
}); });
} }
...@@ -916,10 +925,8 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient { ...@@ -916,10 +925,8 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
switch (status) { switch (status) {
case MyProductStatusType.waiting: case MyProductStatusType.waiting:
path = APIPaths.getMyProductGetWaitingList; path = APIPaths.getMyProductGetWaitingList;
break;
case MyProductStatusType.used: case MyProductStatusType.used:
path = APIPaths.getMyProductGetUsedList; path = APIPaths.getMyProductGetUsedList;
break;
case MyProductStatusType.expired: case MyProductStatusType.expired:
path = APIPaths.getMyProductGetExpiredList; path = APIPaths.getMyProductGetExpiredList;
} }
...@@ -968,4 +975,45 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient { ...@@ -968,4 +975,45 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
return DirectionalScreen.fromJson(data as Json); return DirectionalScreen.fromJson(data as Json);
}); });
} }
Future<BaseResponseModel<EmptyCodable>> pushNotificationDeviceUpdateToken(String token) async {
print("pushNotificationDeviceUpdateToken FCM: $token");
var deviceKey = await DeviceInfo.getDeviceId();
final details = await DeviceInfo.getDetails();
String? accessToken = DataPreference.instance.token ?? "";
var body = details.toMap();
body["access_token"] = accessToken;
body["push_notification_token"] = token;
body["device_key"] = deviceKey;
body["lang"] = 'vi';
body["software_type"] = "Application";
body["software_model"] = "MyPoint";
return requestNormal(APIPaths.pushNotificationDeviceUpdateToken, Method.POST, body, (data) {
return EmptyCodable.fromJson(data as Json);
});
}
Future<BaseResponseModel<EmptyCodable>> myProductMarkAsUsed(String id) async {
String? accessToken = DataPreference.instance.token ?? "";
final body = {
"product_item_id": id,
"lang": "vi",
"access_token": accessToken,
};
return requestNormal(APIPaths.myProductMarkAsUsed, Method.POST, body, (data) {
return EmptyCodable.fromJson(data as Json);
});
}
Future<BaseResponseModel<EmptyCodable>> myProductMarkAsNotUsedYet(String id) async {
String? accessToken = DataPreference.instance.token ?? "";
final body = {
"product_item_id": id,
"lang": "vi",
"access_token": accessToken,
};
return requestNormal(APIPaths.myProductMarkAsNotUsedYet, Method.POST, body, (data) {
return EmptyCodable.fromJson(data as Json);
});
}
} }
\ No newline at end of file
...@@ -15,6 +15,7 @@ class UserPointManager extends RestfulApiViewModel { ...@@ -15,6 +15,7 @@ class UserPointManager extends RestfulApiViewModel {
int get point => _userPoint.value; int get point => _userPoint.value;
Future<int?> fetchUserPoint() async { Future<int?> fetchUserPoint() async {
print("fetchUserPoint");
if (!DataPreference.instance.logged) return null; if (!DataPreference.instance.logged) return null;
try { try {
final response = await client.getHomeHeaderData(); final response = await client.getHomeHeaderData();
......
...@@ -29,7 +29,6 @@ class AffiliateTabScreen extends BaseScreen { ...@@ -29,7 +29,6 @@ class AffiliateTabScreen extends BaseScreen {
class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with BasicState, PopupOnInit { class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with BasicState, PopupOnInit {
final AffiliateTabViewModel viewModel = Get.put(AffiliateTabViewModel()); final AffiliateTabViewModel viewModel = Get.put(AffiliateTabViewModel());
final _headerHomeVM = Get.find<HeaderHomeViewModel>();
late var _canBackButton = false; late var _canBackButton = false;
@override @override
......
// import 'dart:math';
// import 'package:fl_chart/fl_chart.dart';
// import 'package:flutter/material.dart';
//
// /// Dữ liệu 30 ngày, mỗi phần tử là kWh (double).
// class EnergyMonthBarChart extends StatefulWidget {
// final List<double> values; // length 28-31 (tháng)
// final DateTime startDate; // ngày đầu (ví dụ: 1/9/2025)
// final Color barColor;
// final String unit; // "kWh"
// final double? maxY; // nếu null sẽ auto
// final bool showGrid;
//
// const EnergyMonthBarChart({
// super.key,
// required this.values,
// required this.startDate,
// this.barColor = const Color(0xFF3B5AFB),
// this.unit = 'kWh',
// this.maxY,
// this.showGrid = true,
// });
//
// @override
// State<EnergyMonthBarChart> createState() => _EnergyMonthBarChartState();
// }
//
// class _EnergyMonthBarChartState extends State<EnergyMonthBarChart> {
// int? _touchedIndex;
//
// int get length => widget.values.length;
// double get _computedMaxY {
// final m = widget.values.fold<double>(0, (p, v) => max(p, v));
// if (m == 0) return 10;
// final step = 4; // nấc hiển thị
// return (m / step).ceil() * step.toDouble() + 2; // dư chút cho đẹp
// }
//
// // Ngày hiện tại nằm trong dải?
// int? get _todayIndex {
// final today = DateTime.now();
// final s = DateUtils.dateOnly(widget.startDate);
// for (int i = 0; i < length; i++) {
// final d = DateUtils.addDaysToDate(s, i);
// if (DateUtils.isSameDay(d, today)) return i;
// }
// return null;
// }
//
// String _weekdayLabel(DateTime d) {
// const labels = ['CN','T2','T3','T4','T5','T6','T7'];
// return labels[d.weekday % 7];
// }
//
// @override
// Widget build(BuildContext context) {
// final theme = Theme.of(context);
// final maxY = widget.maxY ?? _computedMaxY;
// final sDate = DateUtils.dateOnly(widget.startDate);
//
// // barWidth để fit 30 cột gọn trong thẻ:
// final barWidth = 12.0;
// final groups = List.generate(length, (i) {
// final v = widget.values[i];
// return BarChartGroupData(
// x: i,
// barRods: [
// BarChartRodData(
// toY: v,
// width: barWidth,
// borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
// gradient: LinearGradient(
// begin: Alignment.bottomCenter,
// end: Alignment.topCenter,
// colors: [
// widget.barColor.withOpacity(0.25),
// widget.barColor,
// ],
// ),
// ),
// ],
// );
// });
//
// // trục dưới: hiển thị thứ & ngày (thưa để đỡ rối)
// Widget bottomTitles(double value, TitleMeta meta) {
// final i = value.toInt();
// if (i < 0 || i >= length) return const SizedBox.shrink();
// final d = DateUtils.addDaysToDate(sDate, i);
// // hiển thị cách 2 ngày 1 lần cho gọn
// if (i % 2 != 0) return const SizedBox.shrink();
// final isToday = _todayIndex == i;
// return Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// Text(
// _weekdayLabel(d),
// style: theme.textTheme.labelSmall?.copyWith(
// color: isToday ? widget.barColor : Colors.black54,
// fontWeight: isToday ? FontWeight.w600 : FontWeight.w400,
// ),
// ),
// const SizedBox(height: 2),
// Text(
// '${d.day}',
// style: theme.textTheme.labelSmall?.copyWith(
// color: isToday ? widget.barColor : Colors.black54,
// fontWeight: isToday ? FontWeight.w600 : FontWeight.w400,
// ),
// ),
// ],
// );
// }
//
// // trục trái: 0,4,8,...
// Widget leftTitles(double value, TitleMeta meta) {
// if (value % 4 != 0) return const SizedBox.shrink();
// return Text(
// value.toInt().toString(),
// style: theme.textTheme.labelSmall?.copyWith(color: Colors.black45),
// );
// }
//
// final chart = BarChart(
// BarChartData(
// maxY: maxY,
// minY: 0,
// barGroups: groups,
// gridData: FlGridData(
// show: widget.showGrid,
// horizontalInterval: 4,
// getDrawingHorizontalLine: (v) => FlLine(
// color: Colors.black12,
// strokeWidth: 1,
// dashArray: [4, 4],
// ),
// drawVerticalLine: false,
// ),
// borderData: FlBorderData(show: false),
// titlesData: FlTitlesData(
// leftTitles: AxisTitles(
// sideTitles: SideTitles(showTitles: true, reservedSize: 28, getTitlesWidget: leftTitles),
// ),
// rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
// topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
// bottomTitles: AxisTitles(
// sideTitles: SideTitles(
// showTitles: true,
// reservedSize: 34,
// getTitlesWidget: bottomTitles,
// ),
// ),
// ),
// barTouchData: BarTouchData(
// enabled: true,
// handleBuiltInTouches: false, // tự custom tooltip
// touchCallback: (evt, resp) {
// setState(() {
// _touchedIndex = resp?.spot?.touchedBarGroupIndex;
// });
// if (resp?.spot != null && evt.isInterestedForInteractions) {
// final i = resp!.spot!.touchedBarGroupIndex;
// final v = widget.values[i];
// final d = DateUtils.addDaysToDate(sDate, i);
// final text = 'Số điện: ${v.toStringAsFixed(v % 1 == 0 ? 0 : 1)} ${widget.unit}';
// final overlay = Overlay.of(context);
// final entry = OverlayEntry(
// builder: (_) => _TooltipBubble(
// text: text,
// anchor: evt.localPosition,
// ),
// );
// overlay.insert(entry);
// Future.delayed(const Duration(milliseconds: 900), entry.remove);
// }
// },
// ),
// ),
// swapAnimationDuration: const Duration(milliseconds: 300),
// );
//
// // Vạch dọc nét đứt tại ngày hiện tại (nếu nằm trong dải)
// final todayIndex = _todayIndex;
//
// return Container(
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(24),
// boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 12, offset: Offset(0, 4))],
// ),
// padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
// child: SizedBox(
// height: 260,
// child: LayoutBuilder(
// builder: (ctx, cons) {
// final chartWidget = Padding(
// padding: const EdgeInsets.only(right: 8),
// child: chart,
// );
//
// if (todayIndex == null) return chartWidget;
//
// // Tính vị trí X ước lượng của cột today để vẽ vạch (theo tổng chiều rộng)
// // fl_chart không expose trực tiếp nên ta ước lượng theo spacing mặc định:
// final groupSpace = 8.0;
// final totalW = length * barWidth + (length - 1) * groupSpace;
// final usableW = cons.maxWidth - 28 /*left titles approx*/ - 8 /*right pad*/;
// final scale = usableW / totalW;
// final x = 28 + (todayIndex * (barWidth + groupSpace) + barWidth / 2) * scale;
//
// return Stack(
// children: [
// chartWidget,
// // vạch dọc nét đứt
// Positioned.fill(
// child: IgnorePointer(
// child: CustomPaint(
// painter: _DashedVerticalLinePainter(
// x: x,
// color: widget.barColor.withOpacity(0.5),
// ),
// ),
// ),
// ),
// ],
// );
// },
// ),
// ),
// );
// }
// }
//
// /// Tooltip đơn giản đặt gần vị trí chạm
// class _TooltipBubble extends StatelessWidget {
// final String text;
// final Offset anchor;
// const _TooltipBubble({required this.text, required this.anchor});
//
// @override
// Widget build(BuildContext context) {
// final theme = Theme.of(context);
// return Positioned(
// left: anchor.dx + 8,
// top: max(8, anchor.dy - 36),
// child: Material(
// color: Colors.transparent,
// child: Container(
// padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(10),
// boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 8)],
// ),
// child: Text(
// text,
// style: theme.textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w600),
// ),
// ),
// ),
// );
// }
// }
//
// class _DashedVerticalLinePainter extends CustomPainter {
// final double x;
// final Color color;
// const _DashedVerticalLinePainter({required this.x, required this.color});
// @override
// void paint(Canvas canvas, Size size) {
// final paint = Paint()
// ..color = color
// ..strokeWidth = 2;
// const dash = 6.0;
// const gap = 6.0;
// double y = 0;
// while (y < size.height) {
// canvas.drawLine(Offset(x, y), Offset(x, min(y + dash, size.height)), paint);
// y += dash + gap;
// }
// }
// @override
// bool shouldRepaint(covariant _DashedVerticalLinePainter old) => old.x != x || old.color != color;
// }
...@@ -4,6 +4,7 @@ import 'package:get/get.dart'; ...@@ -4,6 +4,7 @@ import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../base/base_response_model.dart'; import '../../base/base_response_model.dart';
import '../../networking/restful_api_viewmodel.dart'; import '../../networking/restful_api_viewmodel.dart';
import '../../firebase/push_token_service.dart';
import '../../permission/biometric_manager.dart'; import '../../permission/biometric_manager.dart';
import '../../preference/data_preference.dart'; import '../../preference/data_preference.dart';
import '../../shared/router_gage.dart'; import '../../shared/router_gage.dart';
...@@ -41,6 +42,7 @@ class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICre ...@@ -41,6 +42,7 @@ class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICre
hideLoading(); hideLoading();
if (response.isSuccess && response.data != null) { if (response.isSuccess && response.data != null) {
await DataPreference.instance.saveLoginToken(response.data!); await DataPreference.instance.saveLoginToken(response.data!);
await PushTokenService.uploadIfLogged();
_getUserProfile(); _getUserProfile();
} else { } else {
Get.offNamed(loginScreen, arguments: {'phone': phoneNumber}); Get.offNamed(loginScreen, arguments: {'phone': phoneNumber});
......
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_core/src/get_main.dart';
import '../../../base/base_screen.dart';
import '../../../base/basic_state.dart';
import '../../../main.dart';
import '../../../widgets/back_button.dart'; import '../../../widgets/back_button.dart';
import '../models/game_bundle_item_model.dart'; import '../models/game_bundle_item_model.dart';
import '../models/game_card_item_model.dart'; import '../models/game_card_item_model.dart';
import 'game_card_viewmodel.dart';
class GameCardScreen extends StatefulWidget { class GameCardScreen extends BaseScreen {
const GameCardScreen({super.key}); const GameCardScreen({super.key});
@override @override
State<GameCardScreen> createState() => _GameCardScreenState(); State<GameCardScreen> createState() => _GameCardScreenState();
} }
class _GameCardScreenState extends State<GameCardScreen> {
late final GameBundleItemModel data; class _GameCardScreenState extends BaseState<GameCardScreen> with BasicState, RouteAware {
final GameCardViewModel _viewModel = Get.put(GameCardViewModel());
@override @override
void initState() { void initState() {
super.initState(); super.initState();
String gameId = '';
GameBundleItemModel? data;
final args = Get.arguments; final args = Get.arguments;
if (args is GameBundleItemModel) { if (args is Map) {
data = args; data = args['data'] as GameBundleItemModel?;
gameId = args['gameId'] as String? ?? '';
}
if (data == null && gameId.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.back();
});
return;
} }
if (data != null) {
_viewModel.data.value = data;
}
if (gameId.isNotEmpty) {
_viewModel.getGameDetail(id: gameId);
}
_viewModel.onShowAlertError = (message) {
if (message.isEmpty) return;
showAlertError(content: message);
};
_viewModel.submitGameCardSuccess = (popup) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopup(data: popup);
});
};
_viewModel.getGameDetailSuccess = () {
WidgetsBinding.instance.addPostFrameCallback((_) {
final popup = _viewModel.data?.value?.popup;
if (popup == null) return;
showPopup(data: popup);
});
};
} }
@override @override
Widget build(BuildContext context) { void didChangeDependencies() {
final cards = data.options ?? []; super.didChangeDependencies();
final screenHeight = MediaQuery.of(context).size.height; final route = ModalRoute.of(context);
final startTop = screenHeight * 560 / 1920; if (route is PageRoute) {
routeObserver.subscribe(this, route);
}
}
@override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
@override
void didPopNext() {
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(const Duration(milliseconds: 16), () {
_viewModel.getGameDetail();
});
});
}
@override
Widget createBody() {
return Scaffold( return Scaffold(
body: Stack( body: Obx(() {
children: [ final data = _viewModel.data.value;
// Background full màn if (data == null) {
Container( return const SizedBox();
decoration: BoxDecoration( }
image: data.background != null final cards = data?.options ?? [];
? DecorationImage(image: NetworkImage(data.background!), fit: BoxFit.cover) final screenHeight = MediaQuery.of(context).size.height;
: null, final startTop = screenHeight * 560 / 1920;
color: Colors.green[100],
), return Stack(
), children: [
// Button Back Container(
SafeArea( decoration: BoxDecoration(
child: Padding( image:
padding: const EdgeInsets.all(8), data?.background != null
child: CustomBackButton(), ? DecorationImage(image: NetworkImage(data?.background ?? ''), fit: BoxFit.cover)
: null,
color: Colors.green[100],
),
), ),
), // Button Back
Positioned( SafeArea(child: Padding(padding: const EdgeInsets.all(8), child: CustomBackButton())),
top: startTop, Positioned(
left: 16, top: startTop,
right: 16, left: 16,
bottom: 0, right: 16,
child: GridView.builder( bottom: 0,
physics: const NeverScrollableScrollPhysics(), child: GridView.builder(
itemCount: cards.length, physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( itemCount: cards.length,
crossAxisCount: 2, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
mainAxisSpacing: 20, crossAxisCount: 2,
crossAxisSpacing: 20, mainAxisSpacing: 20,
childAspectRatio: 3 / 4, crossAxisSpacing: 20,
childAspectRatio: 3 / 4,
),
itemBuilder: (context, index) {
final card = cards[index];
return GameCardItem(card: card, onTapCard: () {
_viewModel.submitGameCard(data?.id ?? "", card.id ?? 0);
});
},
), ),
itemBuilder: (context, index) {
final card = cards[index];
return GameCardItem(card: card);
},
), ),
), ],
], );
), }),
); );
} }
} }
class GameCardItem extends StatelessWidget { class GameCardItem extends StatelessWidget {
final GameCardItemModel card; final GameCardItemModel card;
final VoidCallback? onTapCard;
const GameCardItem({super.key, required this.card}); const GameCardItem({super.key, required this.card, this.onTapCard});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: onTapCard,
print(card.id);
},
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
......
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../../configs/constants.dart';
import '../../../networking/restful_api_viewmodel.dart';
import '../../../widgets/alert/popup_data_model.dart';
import '../models/game_bundle_item_model.dart';
class GameCardViewModel extends RestfulApiViewModel {
var data = Rxn<GameBundleItemModel>();
void Function(String message)? onShowAlertError;
void Function(PopupDataModel popup)? submitGameCardSuccess;
void Function()? getGameDetailSuccess;
Future<void> submitGameCard(String gameId, int itemId) async {
showProgressIndicator();
final response = await client.submitGameCard(gameId, itemId.toString());
hideProgressIndicator();
final popupData = response.data?.popup;
if (response.isSuccess && popupData != null) {
submitGameCardSuccess?.call(popupData);
} else {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
}
}
Future<void> getGameDetail({String? id}) async {
showLoading();
final response = await client.getGameDetail(id ?? data.value?.id ?? '');
hideLoading();
if (response.data != null) {
data.value = response.data;
getGameDetailSuccess?.call();
} else {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
}
}
}
\ No newline at end of file
...@@ -8,9 +8,6 @@ import '../../shared/router_gage.dart'; ...@@ -8,9 +8,6 @@ import '../../shared/router_gage.dart';
import '../../widgets/back_button.dart'; import '../../widgets/back_button.dart';
import '../../widgets/custom_empty_widget.dart'; import '../../widgets/custom_empty_widget.dart';
import '../../widgets/custom_navigation_bar.dart'; import '../../widgets/custom_navigation_bar.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 '../popup_manager/popup_runner_helper.dart';
import 'game_tab_viewmodel.dart'; import 'game_tab_viewmodel.dart';
...@@ -22,7 +19,6 @@ class GameTabScreen extends BaseScreen { ...@@ -22,7 +19,6 @@ class GameTabScreen extends BaseScreen {
} }
class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState, PopupOnInit { class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState, PopupOnInit {
final _headerHomeVM = Get.find<HeaderHomeViewModel>();
final GameTabViewModel _viewModel = Get.put(GameTabViewModel()); final GameTabViewModel _viewModel = Get.put(GameTabViewModel());
final LayerLink _layerLink = LayerLink(); final LayerLink _layerLink = LayerLink();
final GlobalKey _infoKey = GlobalKey(); final GlobalKey _infoKey = GlobalKey();
...@@ -47,7 +43,7 @@ class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState, Popu ...@@ -47,7 +43,7 @@ class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState, Popu
if (data.popup != null) { if (data.popup != null) {
showPopup(data: data.popup!); showPopup(data: data.popup!);
} else { } else {
Get.toNamed(gameCardScreen, arguments: data); Get.toNamed(gameCardScreen, arguments: {'gameId': data.id});//'data': data,
} }
}; };
runPopupCheck(DirectionalScreenName.productVoucher); runPopupCheck(DirectionalScreenName.productVoucher);
......
...@@ -10,6 +10,7 @@ import '../../networking/restful_api_viewmodel.dart'; ...@@ -10,6 +10,7 @@ import '../../networking/restful_api_viewmodel.dart';
import '../../model/auth/login_token_response_model.dart'; import '../../model/auth/login_token_response_model.dart';
import '../../permission/biometric_manager.dart'; import '../../permission/biometric_manager.dart';
import '../../preference/data_preference.dart'; import '../../preference/data_preference.dart';
import '../../firebase/push_token_service.dart';
import '../main_tab_screen/main_tab_screen.dart'; import '../main_tab_screen/main_tab_screen.dart';
// login_state_enum.dart // login_state_enum.dart
...@@ -101,6 +102,8 @@ class LoginViewModel extends RestfulApiViewModel { ...@@ -101,6 +102,8 @@ class LoginViewModel extends RestfulApiViewModel {
Future<void> _handleLoginResponse(BaseResponseModel<LoginTokenResponseModel> response, String phone) async { Future<void> _handleLoginResponse(BaseResponseModel<LoginTokenResponseModel> response, String phone) async {
if (response.isSuccess && response.data != null) { if (response.isSuccess && response.data != null) {
await DataPreference.instance.saveLoginToken(response.data!); await DataPreference.instance.saveLoginToken(response.data!);
// Upload FCM token after login
await PushTokenService.uploadIfLogged();
_getUserProfile(); _getUserProfile();
return; return;
} }
......
...@@ -19,12 +19,11 @@ class ProductMobileCardScreen extends BaseScreen { ...@@ -19,12 +19,11 @@ class ProductMobileCardScreen extends BaseScreen {
} }
class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> with BasicState { class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> with BasicState {
late final ProductMobileCardViewModel _viewModel; late final ProductMobileCardViewModel _viewModel = Get.put(ProductMobileCardViewModel());
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_viewModel = ProductMobileCardViewModel();
_viewModel.getProductMobileCard(); _viewModel.getProductMobileCard();
_viewModel.onShowAlertError = (message) { _viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) { if (message.isNotEmpty) {
...@@ -32,11 +31,7 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w ...@@ -32,11 +31,7 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w
} }
}; };
_viewModel.onRedeemProductMobileSuccess = (data) { _viewModel.onRedeemProductMobileSuccess = (data) {
if (data != null) { showVoucherPopup(context, data);
showVoucherPopup(context, data);
} else {
showAlertError(content: "Đổi mã thẻ nạp thất bại, vui lòng thử lại sau!");
}
}; };
} }
...@@ -90,8 +85,8 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w ...@@ -90,8 +85,8 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w
child: ListView.separated( child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: _viewModel.mobileCardSections.value.length, itemCount: _viewModel.mobileCardSections.length,
separatorBuilder: (_, __) => const SizedBox(width: 12), separatorBuilder: (_, _) => const SizedBox(width: 12),
itemBuilder: (_, index) { itemBuilder: (_, index) {
final mobileCard = _viewModel.mobileCardSections.value[index]; final mobileCard = _viewModel.mobileCardSections.value[index];
final isSelected = mobileCard.brandCode == _viewModel.selectedBrandCode.value; final isSelected = mobileCard.brandCode == _viewModel.selectedBrandCode.value;
...@@ -187,7 +182,7 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w ...@@ -187,7 +182,7 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w
); );
} }
_redeemProductMobileCard() { void _redeemProductMobileCard() {
if (_viewModel.selectedProduct == null) return; if (_viewModel.selectedProduct == null) return;
if (!_viewModel.isValidBalance) { if (!_viewModel.isValidBalance) {
showAlertError(content: "Bạn chưa đủ điểm để đổi ưu đãi này, vui lòng tích lũy thêm điểm nhé!"); showAlertError(content: "Bạn chưa đủ điểm để đổi ưu đãi này, vui lòng tích lũy thêm điểm nhé!");
...@@ -196,7 +191,7 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w ...@@ -196,7 +191,7 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w
_showAlertConfirmRedeemProduct(); _showAlertConfirmRedeemProduct();
} }
_showAlertConfirmRedeemProduct() { void _showAlertConfirmRedeemProduct() {
final dataAlert = DataAlertModel( final dataAlert = DataAlertModel(
title: "Xác nhận", title: "Xác nhận",
description: "Bạn có muốn sử dụng ${_viewModel.payPoint.money(CurrencyUnit.point)} MyPoint để đổi lấy mã thẻ điện thoại này không?", description: "Bạn có muốn sử dụng ${_viewModel.payPoint.money(CurrencyUnit.point)} MyPoint để đổi lấy mã thẻ điện thoại này không?",
......
...@@ -31,14 +31,13 @@ class ProductMobileCardViewModel extends RestfulApiViewModel { ...@@ -31,14 +31,13 @@ class ProductMobileCardViewModel extends RestfulApiViewModel {
UserPointManager().fetchUserPoint(); UserPointManager().fetchUserPoint();
} }
redeemProductMobileCard() async { Future<void> redeemProductMobileCard() async {
showLoading(); showLoading();
try { try {
final response = await client.redeemMobileCard((selectedProduct?.id ?? 0).toString()); final response = await client.redeemMobileCard((selectedProduct?.id ?? 0).toString());
final itemId = response.data?.itemId ?? ""; final itemId = response.data?.itemId ?? "";
hideLoading(); hideLoading();
if (itemId.isEmpty) { if (itemId.isEmpty) {
hideLoading();
print("redeemMobileCard failed: ${response.errorMessage}"); print("redeemMobileCard failed: ${response.errorMessage}");
onShowAlertError?.call(response.errorMessage ?? Constants.commonError); onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
return; return;
...@@ -51,7 +50,7 @@ class ProductMobileCardViewModel extends RestfulApiViewModel { ...@@ -51,7 +50,7 @@ class ProductMobileCardViewModel extends RestfulApiViewModel {
} }
} }
_getMobileCardCode(String itemId) async { Future<void> _getMobileCardCode(String itemId) async {
showLoading(); showLoading();
try { try {
final response = await client.getMobileCardCode(itemId); final response = await client.getMobileCardCode(itemId);
...@@ -69,7 +68,7 @@ class ProductMobileCardViewModel extends RestfulApiViewModel { ...@@ -69,7 +68,7 @@ class ProductMobileCardViewModel extends RestfulApiViewModel {
} }
} }
getProductMobileCard() async { Future<void> getProductMobileCard() async {
showLoading(); showLoading();
try { try {
final response = await client.productMobileCardGetList(); final response = await client.productMobileCardGetList();
......
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../networking/restful_api_viewmodel.dart'; import '../../networking/restful_api_viewmodel.dart';
import '../../preference/data_preference.dart'; import '../../preference/data_preference.dart';
...@@ -72,19 +71,19 @@ class NotificationViewModel extends RestfulApiViewModel { ...@@ -72,19 +71,19 @@ class NotificationViewModel extends RestfulApiViewModel {
fetchNotifications(refresh: true); fetchNotifications(refresh: true);
} }
notificationMarkAsSeen() { void notificationMarkAsSeen() {
client.notificationMarkAsSeen().then((value) { client.notificationMarkAsSeen().then((value) {
_fetchCategories(); _fetchCategories();
}); });
} }
deleteAllNotifications() { void deleteAllNotifications() {
client.deleteAllNotifications().then((value) { client.deleteAllNotifications().then((value) {
_fetchCategories(); _fetchCategories();
}); });
} }
handleRemoveNotification(NotificationItemModel item) { void handleRemoveNotification(NotificationItemModel item) {
if (item.notificationId == null) { return; } if (item.notificationId == null) { return; }
client.deleteNotification(item.notificationId ?? "").then((value) { client.deleteNotification(item.notificationId ?? "").then((value) {
notifications.remove(item); notifications.remove(item);
...@@ -94,7 +93,7 @@ class NotificationViewModel extends RestfulApiViewModel { ...@@ -94,7 +93,7 @@ class NotificationViewModel extends RestfulApiViewModel {
}); });
} }
handleClickNotification(NotificationItemModel item) { void handleClickNotification(NotificationItemModel item) {
showLoading(); showLoading();
client.getNotificationDetail(item.notificationId ?? "").then((value) { client.getNotificationDetail(item.notificationId ?? "").then((value) {
hideLoading(); hideLoading();
......
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class RechargeSheet extends StatelessWidget {
final String code;
const RechargeSheet({super.key, required this.code});
String _buildUssd(String prefix) => '$prefix*$code#';
Future<void> _dialUssd(String ussd) async {
final uri = Uri(scheme: 'tel', path: ussd.replaceAll('#', Uri.encodeComponent('#')));
print('Dialing USSD: $uri');
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
}
}
@override
Widget build(BuildContext context) {
final pre = _buildUssd('*100');
final post = _buildUssd('*199');
return SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Spacer(),
const Expanded(
flex: 6,
child: Text(
'Nạp ngay',
textAlign: TextAlign.center, // ✅ căn giữa
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22),
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
)
],
),
const SizedBox(height: 4),
_RechargeTile(title: 'Cú pháp nạp thẻ trả trước', subtitle: pre, onTap: () => _dialUssd(pre)),
const Divider(height: 1),
_RechargeTile(title: 'Cú pháp nạp thẻ trả sau', subtitle: post, onTap: () => _dialUssd(post)),
const SizedBox(height: 8),
],
),
),
);
}
}
class _RechargeTile extends StatelessWidget {
final String title;
final String subtitle;
final VoidCallback onTap;
const _RechargeTile({required this.title, required this.subtitle, required this.onTap});
@override
Widget build(BuildContext context) {
return ListTile(
contentPadding: EdgeInsets.zero,
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w600)),
subtitle: Text(subtitle, style: const TextStyle(color: Colors.black54)),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
);
}
}
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../../configs/constants.dart';
import '../../../networking/restful_api_viewmodel.dart';
import '../../mobile_card/models/usable_voucher_model.dart';
class MyMobileCardDetailViewModel extends RestfulApiViewModel {
String itemId;
var dataCard = Rxn<UsableVoucherModel>();
void Function(String message)? onShowAlertError;
MyMobileCardDetailViewModel({required this.itemId});
RxBool isUsed = true.obs;
String get brandName => dataCard.value?.brand?.brandName ?? dataCard.value?.voucherTypeName ?? '';
String get code => dataCard.value?.codeSecret ?? '';
String get serial => dataCard.value?.serial ?? '';
String get valueText => (int.tryParse(dataCard.value?.prices?.firstOrNull?.originalPrice ?? '') ?? 0).money(CurrencyUnit.vnd);
Future<void> getMobileCardDetail() async {
showLoading();
final response = await client.getMobileCardCode(itemId);
final data = response.data?.item;
if (response.isSuccess && data != null) {
hideLoading();
dataCard.value = data;
isUsed.value = makeUsedCardDetail();
return;
}
hideLoading();
onShowAlertError?.call(response.message ?? Constants.commonError);
}
bool makeUsedCardDetail() {
final s = (dataCard.value?.statusCode ?? '').toUpperCase();
if (s == 'USED' || s == 'CONSUMED') return true;
final t = (dataCard.value?.status ?? '').toLowerCase();
return t.contains('đã sử dụng') || t.contains('used');
}
Future<void> onChangeCardStatus() async {
final newState = !isUsed.value;
showProgressIndicator();
try {
final response = newState ? await client.myProductMarkAsUsed(itemId) : await client.myProductMarkAsNotUsedYet(itemId);
if (response.isSuccess) {
isUsed.value = newState;
}
} catch (_) {
} finally {
hideProgressIndicator();
}
}
}
\ No newline at end of file
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/custom_toast_message.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../../resources/base_color.dart';
import '../../../widgets/bottom_sheet_helper.dart';
import '../../../widgets/custom_navigation_bar.dart';
import 'card_recharge_sheet.dart';
import 'my_mobile_card_detail_viewmodel.dart';
class MyMobileCardDetailScreen extends StatefulWidget {
const MyMobileCardDetailScreen({super.key});
@override
State<MyMobileCardDetailScreen> createState() => _MyMobileCardDetailScreenState();
}
class _MyMobileCardDetailScreenState extends State<MyMobileCardDetailScreen> {
late final MyMobileCardDetailViewModel _viewModel;
@override
initState() {
super.initState();
String? itemId;
final args = Get.arguments;
if (args is String) {
itemId = args;
} else if (args is Map) {
itemId = args['itemId'];
}
if (itemId == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.back();
});
return;
}
_viewModel = Get.put(MyMobileCardDetailViewModel(itemId: itemId));
_viewModel.getMobileCardDetail();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF3F5FA),
appBar: CustomNavigationBar(title: 'Chi tiết thẻ nạp'),
body: SafeArea(
child: LayoutBuilder(
builder: (ctx, cons) {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
child: Obx(
() => ConstrainedBox(
constraints: BoxConstraints(minHeight: cons.maxHeight - 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_MobileDetailCard(vm: _viewModel),
const SizedBox(height: 36),
SizedBox(
height: 52,
child: ElevatedButton(
onPressed: _viewModel.isUsed.value ? null : _onRechargeCard,
style: ElevatedButton.styleFrom(
backgroundColor: _viewModel.isUsed.value ? BaseColor.second500 : BaseColor.primary500,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(26)),
elevation: 0,
),
child: Text(
'Nạp ngay',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: _viewModel.isUsed.value ? BaseColor.primary500 : Colors.white,
)),
),
),
],
),
),
),
);
},
),
),
);
}
void _onRechargeCard() {
BottomSheetHelper.showBottomSheetPopup(
child: RechargeSheet(
code: _viewModel.code,
),
);
}
}
class _MobileDetailCard extends StatelessWidget {
final MyMobileCardDetailViewModel vm;
const _MobileDetailCard({required this.vm});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: const [BoxShadow(color: Color(0x22000000), blurRadius: 12, offset: Offset(0, 4))],
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
color: Colors.white,
child: loadNetworkImage(url: vm.dataCard.value?.brand?.logo, width: 48, height: 48),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
vm.valueText.isEmpty ? (vm.dataCard.value?.voucherValue ?? '') : vm.valueText,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(vm.brandName),
],
),
),
const SizedBox(width: 8),
// status
GestureDetector(
onTap: () {
vm.onChangeCardStatus();
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('Đã sử dụng'),
const SizedBox(width: 6),
Icon(Icons.fiber_manual_record, size: 18, color: vm.isUsed.value ? const Color(0xFF24C26A) : Colors.grey),
],
),
),
],
),
const SizedBox(height: 24),
Text('Mã thẻ'),
const SizedBox(height: 8),
_buildCodeRow(),
const SizedBox(height: 16),
_buildSerialRow(),
],
),
);
}
Widget _buildSerialRow() {
if (vm.serial.isEmpty) return const SizedBox.shrink();
return Row(
children: [
Text('Số seri: '),
Expanded(
child: AutoSizeText(
vm.serial,
style: TextStyle(color: Colors.black),
maxLines: 1,
minFontSize: 10,
),
),
const SizedBox(width: 6),
InkWell(
onTap: () async {
if (vm.serial.isEmpty) return;
await Clipboard.setData(ClipboardData(text: vm.serial));
showToastMessage('Đã sao chép số seri');
},
borderRadius: BorderRadius.circular(6),
child: const Padding(
padding: EdgeInsets.all(2.0),
child: Icon(Icons.copy, size: 16, color: Colors.black45),
),
),
],
);
}
Widget _buildCodeRow() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decoration: BoxDecoration(color: const Color(0xFFF4F6F8), borderRadius: BorderRadius.circular(12)),
child: Row(
children: [
Expanded(
child: SelectableText(
vm.code.isEmpty ? '—' : vm.code,
style: TextStyle(
fontSize: 16,
color: Colors.black,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 4),
IconButton(
onPressed: () async {
if (vm.code.isEmpty) return;
await Clipboard.setData(ClipboardData(text: vm.code));
showToastMessage('Đã sao chép mã thẻ');
},
icon: const Icon(Icons.copy, size: 20, color: Colors.black45),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
splashRadius: 18,
),
],
),
);
}
}
\ No newline at end of file
...@@ -2,13 +2,14 @@ import 'package:get/get_rx/src/rx_types/rx_types.dart'; ...@@ -2,13 +2,14 @@ import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/configs/constants.dart'; import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../../networking/restful_api_viewmodel.dart'; import '../../../networking/restful_api_viewmodel.dart';
import '../../mobile_card/models/product_mobile_card_model.dart'; import '../../mobile_card/models/usable_voucher_model.dart';
import '../models/my_product_status_type.dart'; import '../models/my_product_status_type.dart';
class MyMobileCardListViewModel extends RestfulApiViewModel { class MyMobileCardListViewModel extends RestfulApiViewModel {
final RxInt selectedTabIndex = 0.obs; final RxInt selectedTabIndex = 0.obs;
var myProducts = <ProductMobileCardModel>[].obs; var myCardModels = <UsableVoucherModel>[].obs;
void Function(String message)? onShowAlertError; void Function(String message)? onShowAlertError;
void Function(UsableVoucherModel data)? onRedeemProductMobileSuccess;
@override @override
void onInit() { void onInit() {
...@@ -21,24 +22,37 @@ class MyMobileCardListViewModel extends RestfulApiViewModel { ...@@ -21,24 +22,37 @@ class MyMobileCardListViewModel extends RestfulApiViewModel {
freshData(isRefresh: true); freshData(isRefresh: true);
} }
void freshData({bool isRefresh = false}) { Future<void> freshData({bool isRefresh = false}) async {
if (isRefresh) {
showLoading();
}
final body = { final body = {
"index": isRefresh ? 0 : myProducts.length, "index": (isRefresh ? 0 : myCardModels.length).toString(),
"size": 20, "size": '20',
}; };
final status = selectedTabIndex.value == 0 ? MyProductStatusType.waiting : MyProductStatusType.used; final status = selectedTabIndex.value == 0 ? MyProductStatusType.waiting : MyProductStatusType.used;
client.getMyMobileCards(status, body).then((response) { final response = await client.getMyMobileCards(status, body);
if (!response.isSuccess) { if (!response.isSuccess) {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError); onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
} }
final result = response.data?.listItems ?? []; final result = response.data?.listItems ?? [];
if (isRefresh) { if (isRefresh) {
myProducts.clear(); hideLoading();
} myCardModels.clear();
myProducts.addAll(result); }
}).catchError((error) { myCardModels.addAll(result);
myProducts.clear(); }
print('Error fetching products: $error');
}); Future<void> getMobileCardDetail(String itemId) async {
showLoading();
final response = await client.getMobileCardCode(itemId);
final data = response.data?.item;
if (response.isSuccess && data != null) {
hideLoading();
onRedeemProductMobileSuccess?.call(data);
return;
}
hideLoading();
onShowAlertError?.call(response.message ?? Constants.commonError);
} }
} }
\ No newline at end of file
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../base/base_screen.dart';
import '../../../base/basic_state.dart';
import '../../../shared/router_gage.dart';
import '../../../widgets/custom_empty_widget.dart'; import '../../../widgets/custom_empty_widget.dart';
import '../../../widgets/custom_navigation_bar.dart'; import '../../../widgets/custom_navigation_bar.dart';
import '../../../widgets/image_loader.dart'; import '../../../widgets/image_loader.dart';
import '../../mobile_card/models/product_mobile_card_model.dart'; import '../../mobile_card/models/usable_voucher_model.dart';
import '../../mobile_card/usable_mobile_card_popup.dart';
import 'my_mobile_card_list_viewmodel.dart'; import 'my_mobile_card_list_viewmodel.dart';
import 'package:dotted_border/dotted_border.dart'; import 'package:dotted_border/dotted_border.dart';
class MyMobileCardListScreen extends StatefulWidget { class MyMobileCardListScreen extends BaseScreen {
const MyMobileCardListScreen({super.key}); const MyMobileCardListScreen({super.key});
@override @override
State<MyMobileCardListScreen> createState() => _MyMobileCardListScreenState(); State<MyMobileCardListScreen> createState() => _MyMobileCardListScreenState();
} }
class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> { class _MyMobileCardListScreenState extends BaseState<MyMobileCardListScreen> with BasicState {
late final MyMobileCardListViewModel _viewModel = Get.put(MyMobileCardListViewModel()); late final MyMobileCardListViewModel _viewModel = Get.put(MyMobileCardListViewModel());
@override @override
void initState() { initState() {
super.initState(); super.initState();
// _viewModel = Get.put(MyProductListViewModel()); _viewModel.onShowAlertError = (message) {
if (message.isEmpty) return;
showAlertError(content: message);
};
_viewModel.onRedeemProductMobileSuccess = (data) {
showVoucherPopup(context, data);
};
} }
@override @override
Widget build(BuildContext context) { Widget createBody() {
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
return Scaffold( return Scaffold(
appBar: CustomNavigationBar(title: 'Thẻ nạp của tôi',), appBar: CustomNavigationBar(title: 'Thẻ nạp của tôi',),
...@@ -40,7 +51,7 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> { ...@@ -40,7 +51,7 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
), ),
), ),
const Divider(height: 1), const Divider(height: 1),
if (_viewModel.myProducts.isEmpty) if (_viewModel.myCardModels.isEmpty)
Expanded(child: EmptyWidget(size: Size(screenWidth / 2, screenWidth / 2))) Expanded(child: EmptyWidget(size: Size(screenWidth / 2, screenWidth / 2)))
else else
Expanded( Expanded(
...@@ -50,15 +61,15 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> { ...@@ -50,15 +61,15 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
}, },
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
itemCount: _viewModel.myProducts.length, itemCount: _viewModel.myCardModels.length,
itemBuilder: (_, index) { itemBuilder: (_, index) {
if (index >= _viewModel.myProducts.length) { if (index >= _viewModel.myCardModels.length) {
_viewModel.freshData(isRefresh: false); _viewModel.freshData(isRefresh: false);
return const Center( return const Center(
child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator()), child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator()),
); );
} }
final product = _viewModel.myProducts[index]; final product = _viewModel.myCardModels[index];
return _buildVoucherItem(product); return _buildVoucherItem(product);
}, },
), ),
...@@ -92,9 +103,11 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> { ...@@ -92,9 +103,11 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
); );
} }
Widget _buildVoucherItem(ProductMobileCardModel product) { Widget _buildVoucherItem(UsableVoucherModel product) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Get.toNamed(myMobileCardDetailScreen, arguments: {"itemId": product.voucherItemID ?? ""});
// _viewModel.getMobileCardCode(product.voucherItemID ?? "");
// Get.toNamed(voucherDetailScreen, arguments: {"customerProductId": product.id}); // Get.toNamed(voucherDetailScreen, arguments: {"customerProductId": product.id});
}, },
child: Container( child: Container(
...@@ -102,20 +115,14 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> { ...@@ -102,20 +115,14 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
child: DottedBorder( child: DottedBorder(
options: RoundedRectDottedBorderOptions( options: RoundedRectDottedBorderOptions(
// borderType: BorderType.RRect,
color: Colors.redAccent.withOpacity(0.3), color: Colors.redAccent.withOpacity(0.3),
radius: const Radius.circular(12), radius: const Radius.circular(12),
dashPattern: const [3, 3], dashPattern: const [3, 3],
strokeWidth: 1, strokeWidth: 1,
), ),
// color: Colors.redAccent.withOpacity(0.3),
// borderType: BorderType.RRect,
// radius: const Radius.circular(12),
// dashPattern: const [3, 3],
// strokeWidth: 1,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(10),
color: Colors.redAccent.withOpacity(0.03), color: Colors.redAccent.withOpacity(0.03),
), ),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
...@@ -124,7 +131,7 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> { ...@@ -124,7 +131,7 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: loadNetworkImage( child: loadNetworkImage(
url: product.brandLogo ?? '', url: product.brand?.logo ?? '',
width: 64, width: 64,
height: 64, height: 64,
fit: BoxFit.cover, fit: BoxFit.cover,
...@@ -136,14 +143,14 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> { ...@@ -136,14 +143,14 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( if ((product.brand?.brandName ?? '').isNotEmpty)
product.brandName ?? '', Text(
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.black54, fontSize: 12), product.brand?.brandName ?? '',
), style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.black54, fontSize: 12),
),
const SizedBox(height: 6), const SizedBox(height: 6),
Text(product.name ?? '', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), Text(product.name ?? '', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
const SizedBox(height: 6), const SizedBox(height: 6),
Text('HSD: ${product.expire}', style: const TextStyle(color: Colors.black54, fontSize: 12)),
], ],
), ),
), ),
......
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