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

update game card detail, mobile card

parent b75a9279
import 'dart:io';
import 'dart:convert';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:mypoint_flutter_app/firebase/push_notification.dart';
import 'firebase_options.dart';
import 'notification_parse_payload.dart';
@pragma('vm:entry-point') // bắt buộc cho background isolate
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
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();
bool _bgLocalInit = false;
Future<void> _initLocalNotifications() async {
Future<void> _ensureBgLocalNotifications() async {
if (_bgLocalInit) return;
const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosInit = DarwinInitializationSettings();
const init = InitializationSettings(android: androidInit, iOS: iosInit);
await _flnp.initialize(init);
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',
importance: Importance.high,
);
......@@ -28,11 +84,26 @@ Future<void> _initLocalNotifications() async {
?.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 {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
final messaging = FirebaseMessaging.instance;
// Quyền iOS / Android 13+
......@@ -41,39 +112,46 @@ Future<void> initFirebaseAndFcm() async {
} else {
await messaging.requestPermission(); // Android 13+ POST_NOTIFICATIONS
}
await _initLocalNotifications();
// Foreground: tự hiện local notification
// Foreground: Android không tự hiển thị -> ta show local notification
FirebaseMessaging.onMessage.listen((message) {
if (kDebugMode) {
print('=== FOREGROUND MESSAGE RECEIVED ===');
print('Message: ${message.messageId}');
print('Data: ${message.data}');
print('Notification: ${message.notification?.title} - ${message.notification?.body}');
}
// if (Platform.isAndroid) {
final n = message.notification;
if (n != null) {
final title = n?.title ?? (message.data['title']?.toString());
final body = n?.body ?? (message.data['body']?.toString());
if ((title ?? body) != null) {
_flnp.show(
n.hashCode,
n.title,
n.body,
message.hashCode,
title,
body,
const NotificationDetails(
android: AndroidNotificationDetails('default_channel', 'General',
importance: Importance.high, priority: Priority.high),
iOS: DarwinNotificationDetails(),
android: AndroidNotificationDetails(
'default_channel',
'General',
importance: Importance.high,
priority: Priority.high,
),
),
payload: message.data.isNotEmpty ? message.data.toString() : null,
payload: message.data.isNotEmpty ? jsonEncode(message.data) : null,
);
}
// }
});
// User click notification mở app
// User click notification mở app (khi app đang chạy ở background)
FirebaseMessaging.onMessageOpenedApp.listen((message) {
// TODO: điều hướng theo message.data['screen'] (nếu có)
NotificationRouter.handleRemoteMessage(message);
});
// 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
}
// Initial message sẽ được xử lý sau khi runApp trong main.dart
// Lấy token để test gửi
final token = await messaging.getToken();
// 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:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/app_navigator.dart';
......@@ -10,18 +11,26 @@ import 'package:mypoint_flutter_app/shared/router_gage.dart';
import 'base/app_loading.dart';
import 'env_loader.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 {
WidgetsFlutterBinding.ensureInitialized();
await loadEnv();
await initFirebaseAndFcm();
await DataPreference.instance.init();
DioHttpService();
Get.put(HeaderThemeController(), permanent: true);
await DataPreference.instance.init();
await initFirebaseAndFcm();
await UserPointManager().fetchUserPoint();
runApp(const MyApp());
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 {
......@@ -31,6 +40,7 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return GetMaterialApp(
navigatorKey: AppNavigator.key,
navigatorObservers: [routeObserver],
debugShowCheckedModeBanner: false,
initialRoute: '/splash',
theme: ThemeData(
......@@ -51,3 +61,16 @@ class MyApp extends StatelessWidget {
);
}
}
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 {
var deviceKey = await DeviceInfo.getDeviceId();
var key = "$phone+_=$deviceKey/*8854";
final body = {"device_key": deviceKey, "phone_number": phone, "key": key.toSha256()};
print('body: $body');
return requestNormal(
APIPaths.checkPhoneNumber,
Method.POST,
......@@ -367,12 +368,20 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
}
Future<BaseResponseModel<GameBundleItemModel>> getGameDetail(String id) async {
print("RestfulAPIClientAllRequest getGameDetail - id: $id");
final path = APIPaths.getGameDetail.replaceAll("%@", id);
return requestNormal(path, Method.POST, {}, (data) {
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 {
String? token = DataPreference.instance.token ?? "";
final body = {"access_token": token};
......@@ -660,7 +669,7 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
Future<BaseResponseModel<RedeemProductResponseModel>> getMobileCardCode(String itemId) async {
String? token = DataPreference.instance.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);
});
}
......@@ -916,10 +925,8 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
switch (status) {
case MyProductStatusType.waiting:
path = APIPaths.getMyProductGetWaitingList;
break;
case MyProductStatusType.used:
path = APIPaths.getMyProductGetUsedList;
break;
case MyProductStatusType.expired:
path = APIPaths.getMyProductGetExpiredList;
}
......@@ -968,4 +975,45 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
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 {
int get point => _userPoint.value;
Future<int?> fetchUserPoint() async {
print("fetchUserPoint");
if (!DataPreference.instance.logged) return null;
try {
final response = await client.getHomeHeaderData();
......
......@@ -29,7 +29,6 @@ class AffiliateTabScreen extends BaseScreen {
class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with BasicState, PopupOnInit {
final AffiliateTabViewModel viewModel = Get.put(AffiliateTabViewModel());
final _headerHomeVM = Get.find<HeaderHomeViewModel>();
late var _canBackButton = false;
@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';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../base/base_response_model.dart';
import '../../networking/restful_api_viewmodel.dart';
import '../../firebase/push_token_service.dart';
import '../../permission/biometric_manager.dart';
import '../../preference/data_preference.dart';
import '../../shared/router_gage.dart';
......@@ -41,6 +42,7 @@ class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICre
hideLoading();
if (response.isSuccess && response.data != null) {
await DataPreference.instance.saveLoginToken(response.data!);
await PushTokenService.uploadIfLogged();
_getUserProfile();
} else {
Get.offNamed(loginScreen, arguments: {'phone': phoneNumber});
......
import 'package:flutter/material.dart';
import 'package:get/get.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 '../models/game_bundle_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});
@override
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
void initState() {
super.initState();
String gameId = '';
GameBundleItemModel? data;
final args = Get.arguments;
if (args is GameBundleItemModel) {
data = args;
if (args is Map) {
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
Widget build(BuildContext context) {
final cards = data.options ?? [];
void didChangeDependencies() {
super.didChangeDependencies();
final route = ModalRoute.of(context);
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(
body: Obx(() {
final data = _viewModel.data.value;
if (data == null) {
return const SizedBox();
}
final cards = data?.options ?? [];
final screenHeight = MediaQuery.of(context).size.height;
final startTop = screenHeight * 560 / 1920;
return Scaffold(
body: Stack(
return Stack(
children: [
// Background full màn
Container(
decoration: BoxDecoration(
image: data.background != null
? DecorationImage(image: NetworkImage(data.background!), fit: BoxFit.cover)
image:
data?.background != null
? DecorationImage(image: NetworkImage(data?.background ?? ''), fit: BoxFit.cover)
: null,
color: Colors.green[100],
),
),
// Button Back
SafeArea(
child: Padding(
padding: const EdgeInsets.all(8),
child: CustomBackButton(),
),
),
SafeArea(child: Padding(padding: const EdgeInsets.all(8), child: CustomBackButton())),
Positioned(
top: startTop,
left: 16,
......@@ -63,27 +124,28 @@ class _GameCardScreenState extends State<GameCardScreen> {
),
itemBuilder: (context, index) {
final card = cards[index];
return GameCardItem(card: card);
return GameCardItem(card: card, onTapCard: () {
_viewModel.submitGameCard(data?.id ?? "", card.id ?? 0);
});
},
),
),
],
),
);
}),
);
}
}
class GameCardItem extends StatelessWidget {
final GameCardItemModel card;
const GameCardItem({super.key, required this.card});
final VoidCallback? onTapCard;
const GameCardItem({super.key, required this.card, this.onTapCard});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
print(card.id);
},
onTap: onTapCard,
child: Container(
decoration: BoxDecoration(
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';
import '../../widgets/back_button.dart';
import '../../widgets/custom_empty_widget.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 'game_tab_viewmodel.dart';
......@@ -22,7 +19,6 @@ class GameTabScreen extends BaseScreen {
}
class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState, PopupOnInit {
final _headerHomeVM = Get.find<HeaderHomeViewModel>();
final GameTabViewModel _viewModel = Get.put(GameTabViewModel());
final LayerLink _layerLink = LayerLink();
final GlobalKey _infoKey = GlobalKey();
......@@ -47,7 +43,7 @@ class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState, Popu
if (data.popup != null) {
showPopup(data: data.popup!);
} else {
Get.toNamed(gameCardScreen, arguments: data);
Get.toNamed(gameCardScreen, arguments: {'gameId': data.id});//'data': data,
}
};
runPopupCheck(DirectionalScreenName.productVoucher);
......
......@@ -10,6 +10,7 @@ import '../../networking/restful_api_viewmodel.dart';
import '../../model/auth/login_token_response_model.dart';
import '../../permission/biometric_manager.dart';
import '../../preference/data_preference.dart';
import '../../firebase/push_token_service.dart';
import '../main_tab_screen/main_tab_screen.dart';
// login_state_enum.dart
......@@ -101,6 +102,8 @@ class LoginViewModel extends RestfulApiViewModel {
Future<void> _handleLoginResponse(BaseResponseModel<LoginTokenResponseModel> response, String phone) async {
if (response.isSuccess && response.data != null) {
await DataPreference.instance.saveLoginToken(response.data!);
// Upload FCM token after login
await PushTokenService.uploadIfLogged();
_getUserProfile();
return;
}
......
......@@ -19,12 +19,11 @@ class ProductMobileCardScreen extends BaseScreen {
}
class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> with BasicState {
late final ProductMobileCardViewModel _viewModel;
late final ProductMobileCardViewModel _viewModel = Get.put(ProductMobileCardViewModel());
@override
void initState() {
super.initState();
_viewModel = ProductMobileCardViewModel();
_viewModel.getProductMobileCard();
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
......@@ -32,11 +31,7 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w
}
};
_viewModel.onRedeemProductMobileSuccess = (data) {
if (data != null) {
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
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
itemCount: _viewModel.mobileCardSections.value.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemCount: _viewModel.mobileCardSections.length,
separatorBuilder: (_, _) => const SizedBox(width: 12),
itemBuilder: (_, index) {
final mobileCard = _viewModel.mobileCardSections.value[index];
final isSelected = mobileCard.brandCode == _viewModel.selectedBrandCode.value;
......@@ -187,7 +182,7 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w
);
}
_redeemProductMobileCard() {
void _redeemProductMobileCard() {
if (_viewModel.selectedProduct == null) return;
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é!");
......@@ -196,7 +191,7 @@ class _ProductMobileCardScreenState extends BaseState<ProductMobileCardScreen> w
_showAlertConfirmRedeemProduct();
}
_showAlertConfirmRedeemProduct() {
void _showAlertConfirmRedeemProduct() {
final dataAlert = DataAlertModel(
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?",
......
......@@ -31,14 +31,13 @@ class ProductMobileCardViewModel extends RestfulApiViewModel {
UserPointManager().fetchUserPoint();
}
redeemProductMobileCard() async {
Future<void> redeemProductMobileCard() async {
showLoading();
try {
final response = await client.redeemMobileCard((selectedProduct?.id ?? 0).toString());
final itemId = response.data?.itemId ?? "";
hideLoading();
if (itemId.isEmpty) {
hideLoading();
print("redeemMobileCard failed: ${response.errorMessage}");
onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
return;
......@@ -51,7 +50,7 @@ class ProductMobileCardViewModel extends RestfulApiViewModel {
}
}
_getMobileCardCode(String itemId) async {
Future<void> _getMobileCardCode(String itemId) async {
showLoading();
try {
final response = await client.getMobileCardCode(itemId);
......@@ -69,7 +68,7 @@ class ProductMobileCardViewModel extends RestfulApiViewModel {
}
}
getProductMobileCard() async {
Future<void> getProductMobileCard() async {
showLoading();
try {
final response = await client.productMobileCardGetList();
......
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 '../../networking/restful_api_viewmodel.dart';
import '../../preference/data_preference.dart';
......@@ -72,19 +71,19 @@ class NotificationViewModel extends RestfulApiViewModel {
fetchNotifications(refresh: true);
}
notificationMarkAsSeen() {
void notificationMarkAsSeen() {
client.notificationMarkAsSeen().then((value) {
_fetchCategories();
});
}
deleteAllNotifications() {
void deleteAllNotifications() {
client.deleteAllNotifications().then((value) {
_fetchCategories();
});
}
handleRemoveNotification(NotificationItemModel item) {
void handleRemoveNotification(NotificationItemModel item) {
if (item.notificationId == null) { return; }
client.deleteNotification(item.notificationId ?? "").then((value) {
notifications.remove(item);
......@@ -94,7 +93,7 @@ class NotificationViewModel extends RestfulApiViewModel {
});
}
handleClickNotification(NotificationItemModel item) {
void handleClickNotification(NotificationItemModel item) {
showLoading();
client.getNotificationDetail(item.notificationId ?? "").then((value) {
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';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../../networking/restful_api_viewmodel.dart';
import '../../mobile_card/models/product_mobile_card_model.dart';
import '../../mobile_card/models/usable_voucher_model.dart';
import '../models/my_product_status_type.dart';
class MyMobileCardListViewModel extends RestfulApiViewModel {
final RxInt selectedTabIndex = 0.obs;
var myProducts = <ProductMobileCardModel>[].obs;
var myCardModels = <UsableVoucherModel>[].obs;
void Function(String message)? onShowAlertError;
void Function(UsableVoucherModel data)? onRedeemProductMobileSuccess;
@override
void onInit() {
......@@ -21,24 +22,37 @@ class MyMobileCardListViewModel extends RestfulApiViewModel {
freshData(isRefresh: true);
}
void freshData({bool isRefresh = false}) {
Future<void> freshData({bool isRefresh = false}) async {
if (isRefresh) {
showLoading();
}
final body = {
"index": isRefresh ? 0 : myProducts.length,
"size": 20,
"index": (isRefresh ? 0 : myCardModels.length).toString(),
"size": '20',
};
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) {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
}
final result = response.data?.listItems ?? [];
if (isRefresh) {
myProducts.clear();
hideLoading();
myCardModels.clear();
}
myCardModels.addAll(result);
}
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;
}
myProducts.addAll(result);
}).catchError((error) {
myProducts.clear();
print('Error fetching products: $error');
});
hideLoading();
onShowAlertError?.call(response.message ?? Constants.commonError);
}
}
\ No newline at end of file
import 'package:flutter/material.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_navigation_bar.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 'package:dotted_border/dotted_border.dart';
class MyMobileCardListScreen extends StatefulWidget {
class MyMobileCardListScreen extends BaseScreen {
const MyMobileCardListScreen({super.key});
@override
State<MyMobileCardListScreen> createState() => _MyMobileCardListScreenState();
}
class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
class _MyMobileCardListScreenState extends BaseState<MyMobileCardListScreen> with BasicState {
late final MyMobileCardListViewModel _viewModel = Get.put(MyMobileCardListViewModel());
@override
void initState() {
initState() {
super.initState();
// _viewModel = Get.put(MyProductListViewModel());
_viewModel.onShowAlertError = (message) {
if (message.isEmpty) return;
showAlertError(content: message);
};
_viewModel.onRedeemProductMobileSuccess = (data) {
showVoucherPopup(context, data);
};
}
@override
Widget build(BuildContext context) {
Widget createBody() {
final screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
appBar: CustomNavigationBar(title: 'Thẻ nạp của tôi',),
......@@ -40,7 +51,7 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
),
),
const Divider(height: 1),
if (_viewModel.myProducts.isEmpty)
if (_viewModel.myCardModels.isEmpty)
Expanded(child: EmptyWidget(size: Size(screenWidth / 2, screenWidth / 2)))
else
Expanded(
......@@ -50,15 +61,15 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
},
child: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _viewModel.myProducts.length,
itemCount: _viewModel.myCardModels.length,
itemBuilder: (_, index) {
if (index >= _viewModel.myProducts.length) {
if (index >= _viewModel.myCardModels.length) {
_viewModel.freshData(isRefresh: false);
return const Center(
child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator()),
);
}
final product = _viewModel.myProducts[index];
final product = _viewModel.myCardModels[index];
return _buildVoucherItem(product);
},
),
......@@ -92,9 +103,11 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
);
}
Widget _buildVoucherItem(ProductMobileCardModel product) {
Widget _buildVoucherItem(UsableVoucherModel product) {
return GestureDetector(
onTap: () {
Get.toNamed(myMobileCardDetailScreen, arguments: {"itemId": product.voucherItemID ?? ""});
// _viewModel.getMobileCardCode(product.voucherItemID ?? "");
// Get.toNamed(voucherDetailScreen, arguments: {"customerProductId": product.id});
},
child: Container(
......@@ -102,20 +115,14 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
padding: const EdgeInsets.all(0),
child: DottedBorder(
options: RoundedRectDottedBorderOptions(
// borderType: BorderType.RRect,
color: Colors.redAccent.withOpacity(0.3),
radius: const Radius.circular(12),
dashPattern: const [3, 3],
strokeWidth: 1,
),
// 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),
borderRadius: BorderRadius.circular(10),
color: Colors.redAccent.withOpacity(0.03),
),
padding: const EdgeInsets.all(12),
......@@ -124,7 +131,7 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: loadNetworkImage(
url: product.brandLogo ?? '',
url: product.brand?.logo ?? '',
width: 64,
height: 64,
fit: BoxFit.cover,
......@@ -136,14 +143,14 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if ((product.brand?.brandName ?? '').isNotEmpty)
Text(
product.brandName ?? '',
product.brand?.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)),
],
),
),
......
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