Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Hoàng Văn Đạt
mypoint_flutter_app
Commits
4bd580fa
Commit
4bd580fa
authored
Sep 26, 2025
by
DatHV
Browse files
update game card detail, mobile card
parent
b75a9279
Changes
43
Hide whitespace changes
Inline
Side-by-side
lib/push_setup.dart
→
lib/
firebase/
push_setup.dart
View file @
4bd580fa
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
)
{
final
n
=
message
.
notification
;
if
(
n
!=
null
)
{
_flnp
.
show
(
n
.
hashCode
,
n
.
title
,
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
(
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
;
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
// 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
();
print
(
'FCM token:
$token
'
);
// if (kDebugMode) {
print
(
'FCM token:
$token
'
);
// }
}
lib/firebase/push_token_service.dart
0 → 100644
View file @
4bd580fa
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
);
}
}
lib/main.dart
View file @
4bd580fa
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
());
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
{
...
...
@@ -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
(
...
...
@@ -50,4 +60,17 @@ class MyApp extends StatelessWidget {
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
lib/networking/restful_api_client_all_request.dart
View file @
4bd580fa
...
...
@@ -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
.
redeem
MobileCard
,
Method
.
POST
,
body
,
(
data
)
{
return
requestNormal
(
APIPaths
.
get
MobileCard
Code
,
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
lib/preference/point/point_manager.dart
View file @
4bd580fa
...
...
@@ -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
();
...
...
lib/screen/affiliate/affiliate_tab_screen.dart
View file @
4bd580fa
...
...
@@ -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
...
...
lib/screen/chart/energy_month_bar_chart.dart
0 → 100644
View file @
4bd580fa
// 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;
// }
lib/screen/create_pass/signup_create_password_repository.dart
View file @
4bd580fa
...
...
@@ -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
});
...
...
lib/screen/game/game_cards/game_card_screen.dart
View file @
4bd580fa
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
??
[];
final
screenHeight
=
MediaQuery
.
of
(
context
).
size
.
height
;
final
startTop
=
screenHeight
*
560
/
1920
;
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:
Stack
(
children:
[
// Background full màn
Container
(
decoration:
BoxDecoration
(
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
(),
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
Stack
(
children:
[
Container
(
decoration:
BoxDecoration
(
image:
data
?.
background
!=
null
?
DecorationImage
(
image:
NetworkImage
(
data
?.
background
??
''
),
fit:
BoxFit
.
cover
)
:
null
,
color:
Colors
.
green
[
100
],
),
),
),
Positioned
(
top:
startTop
,
left:
16
,
right:
16
,
bottom:
0
,
child:
GridView
.
builder
(
physics:
const
NeverScrollableScrollPhysics
(),
itemCount:
cards
.
length
,
gridDelegate:
const
SliverGridDelegateWithFixedCrossAxisCount
(
crossAxisCount:
2
,
mainAxisSpacing:
20
,
crossAxisSpacing:
20
,
childAspectRatio:
3
/
4
,
// Button Back
SafeArea
(
child:
Padding
(
padding:
const
EdgeInsets
.
all
(
8
),
child:
CustomBackButton
())),
Positioned
(
top:
startTop
,
left:
16
,
right:
16
,
bottom:
0
,
child:
GridView
.
builder
(
physics:
const
NeverScrollableScrollPhysics
(),
itemCount:
cards
.
length
,
gridDelegate:
const
SliverGridDelegateWithFixedCrossAxisCount
(
crossAxisCount:
2
,
mainAxisSpacing:
20
,
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
{
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
,
...
...
lib/screen/game/game_cards/game_card_viewmodel.dart
View file @
4bd580fa
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
lib/screen/game/game_tab_screen.dart
View file @
4bd580fa
...
...
@@ -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
);
...
...
lib/screen/login/login_viewmodel.dart
View file @
4bd580fa
...
...
@@ -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
;
}
...
...
lib/screen/mobile_card/product_mobile_card_screen.dart
View file @
4bd580fa
...
...
@@ -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!"
);
}
showVoucherPopup
(
context
,
data
);
};
}
...
...
@@ -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?"
,
...
...
lib/screen/mobile_card/product_mobile_card_viewmodel.dart
View file @
4bd580fa
...
...
@@ -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
();
...
...
lib/screen/notification/notification_viewmodel.dart
View file @
4bd580fa
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
();
...
...
lib/screen/voucher/mobile_card/card_recharge_sheet.dart
0 → 100644
View file @
4bd580fa
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
,
);
}
}
lib/screen/voucher/mobile_card/my_mobile_card_detail_viewmodel.dart
0 → 100644
View file @
4bd580fa
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
lib/screen/voucher/mobile_card/my_mobile_card_detail_widget.dart
0 → 100644
View file @
4bd580fa
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
lib/screen/voucher/m
y_voucher
/my_mobile_card_list_viewmodel.dart
→
lib/screen/voucher/m
obile_card
/my_mobile_card_list_viewmodel.dart
View file @
4bd580fa
...
...
@@ -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
my
Products
=
<
ProductMobileCard
Model
>[].
obs
;
var
my
CardModels
=
<
UsableVoucher
Model
>[].
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
:
my
Products
.
length
,
"size"
:
20
,
"index"
:
(
isRefresh
?
0
:
my
CardModels
.
length
).
toString
()
,
"size"
:
'
20
'
,
};
final
status
=
selectedTabIndex
.
value
==
0
?
MyProductStatusType
.
waiting
:
MyProductStatusType
.
used
;
client
.
getMyMobileCards
(
status
,
body
).
then
((
response
)
{
if
(!
response
.
isSuccess
)
{
onShowAlertError
?.
call
(
response
.
errorMessage
??
Constants
.
commonError
);
}
final
result
=
response
.
data
?.
listItems
??
[];
if
(
isRefresh
)
{
myProducts
.
clear
();
}
myProducts
.
addAll
(
result
);
}).
catchError
((
error
)
{
myProducts
.
clear
();
print
(
'Error fetching products:
$error
'
);
});
final
response
=
await
client
.
getMyMobileCards
(
status
,
body
);
if
(!
response
.
isSuccess
)
{
onShowAlertError
?.
call
(
response
.
errorMessage
??
Constants
.
commonError
);
}
final
result
=
response
.
data
?.
listItems
??
[];
if
(
isRefresh
)
{
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
;
}
hideLoading
();
onShowAlertError
?.
call
(
response
.
message
??
Constants
.
commonError
);
}
}
\ No newline at end of file
lib/screen/voucher/m
y_voucher
/my_mobile_card_list_widget.dart
→
lib/screen/voucher/m
obile_card
/my_mobile_card_list_widget.dart
View file @
4bd580fa
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
Base
State
<
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
.
my
Product
s
.
isEmpty
)
if
(
_viewModel
.
my
CardModel
s
.
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
.
my
Product
s
.
length
,
itemCount:
_viewModel
.
my
CardModel
s
.
length
,
itemBuilder:
(
_
,
index
)
{
if
(
index
>=
_viewModel
.
my
Product
s
.
length
)
{
if
(
index
>=
_viewModel
.
my
CardModel
s
.
length
)
{
_viewModel
.
freshData
(
isRefresh:
false
);
return
const
Center
(
child:
Padding
(
padding:
EdgeInsets
.
all
(
16
),
child:
CircularProgressIndicator
()),
);
}
final
product
=
_viewModel
.
my
Product
s
[
index
];
final
product
=
_viewModel
.
my
CardModel
s
[
index
];
return
_buildVoucherItem
(
product
);
},
),
...
...
@@ -92,9 +103,11 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
);
}
Widget
_buildVoucherItem
(
ProductMobileCard
Model
product
)
{
Widget
_buildVoucherItem
(
UsableVoucher
Model
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
(
1
2
),
borderRadius:
BorderRadius
.
circular
(
1
0
),
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
.
brand
L
ogo
??
''
,
url:
product
.
brand
?.
l
ogo
??
''
,
width:
64
,
height:
64
,
fit:
BoxFit
.
cover
,
...
...
@@ -136,14 +143,14 @@ class _MyMobileCardListScreenState extends State<MyMobileCardListScreen> {
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
product
.
brandName
??
''
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
bold
,
color:
Colors
.
black54
,
fontSize:
12
),
),
if
((
product
.
brand
?.
brandName
??
''
).
isNotEmpty
)
Text
(
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
)),
],
),
),
...
...
Prev
1
2
3
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment