Commit efb4662c authored by DatHV's avatar DatHV
Browse files

update campaign 7day

parent 4c376d38
......@@ -78,4 +78,19 @@ class APIPaths {
static const String inviteFriendCampaigns = "/campaign/api/v3.0/invite-friend/campaigns";
static const String phoneInviteFriend = "/campaign/api/v3.0/invite-friend/invite";
static const String rewardOpportunityGetList = "/rewardOpportunityGetList/1.0.0";
static const String rewardOpportunityOpenRequest = "/rewardOpportunityOpenRequest/1.0.0";
static const String productsCustomerLikes = "/product/api/v2.0/customer/likes";
static const String orderHistoryCategories = "/order/api/v1.0/orders/categories";
static const String getTransactionOrderHistory = "/order/api/v1.0/my-orders";
static const String customerContractRequestSearch = "/customerContractRequestSearch/1.0.0";
static const String customerContractSearchHistoryGetList = "/customerContractSearchHistoryGetList/1.0.0";
static const String customerContractDelete = "/customerContractDelete/1.0.0";
static const String getProductVnTraSold = "/product/api/v2.0/vntra_sold";
static const String detailMyPackageVnTra = "/product/api/v2.0/vntra_sold/%@";
static const String getCampaignLiveTransactions = "/campaign/api/v3.0/%@/live-transactions";
static const String getCampaignMissions = "/campaign/api/v3.0/%@/missions";
static const String getCampaignReward = "/campaign/api/v3.0/%@/reward";
static const String submitCampaignMission = "/campaign/api/v3.0/%@/mission/%@/submit";
static const String getQuizCampaign = "/quiz/api/v1.0/quiz/%@/";
static const String quizSubmitCampaign = "/quiz/api/v1.0/quiz/%@/submit/";
}
\ No newline at end of file
......@@ -43,7 +43,7 @@ enum DirectionalScreenName {
memberShip,
ranking,
mobileTopup,
carrierPackage,
mobileTopupData,
topup,
register,
gifts,
......@@ -96,6 +96,7 @@ enum DirectionalScreenName {
pointHunting,
orderMenu,
unknown,
transactionHistories,
}
extension DirectionalScreenRouterExtension on DirectionalScreenName {
......@@ -208,7 +209,7 @@ extension DirectionalScreenNameExtension on DirectionalScreenName {
return "APP_SCREEN_RANKING_PROGRAM";
case DirectionalScreenName.mobileTopup:
return "APP_SCREEN_PRODUCT_MOBILE_TOPUP";
case DirectionalScreenName.carrierPackage:
case DirectionalScreenName.mobileTopupData:
return "APP_SCREEN_MOBILE_TOPUP_DATA";
case DirectionalScreenName.topup:
return "APP_SCREEN_TOPUP";
......@@ -314,6 +315,8 @@ extension DirectionalScreenNameExtension on DirectionalScreenName {
return "UNKNOWN";
case DirectionalScreenName.orderMenu:
return "APP_SCREEN_ORDER_MENU";
case DirectionalScreenName.transactionHistories:
return "APP_SCREEN_TRANSACTION_HISTORIES";
}
}
......
......@@ -82,14 +82,14 @@ class DirectionalScreen {
case DirectionalScreenName.topup || DirectionalScreenName.mobileTopup:
Get.toNamed(phoneTopUpScreen);
return true;
case DirectionalScreenName.carrierPackage:
BaseWebViewInput input = BaseWebViewInput(url: clickActionParam ?? "");
Get.toNamed(baseWebViewScreen, arguments: input);
return true;
case DirectionalScreenName.productMobileCard:
Get.toNamed(productMobileCardScreen);
return true;
case DirectionalScreenName.simService:
BaseWebViewInput input = BaseWebViewInput(url: "https://mypoint.uudaigoicuoc.com/");
Get.toNamed(baseWebViewScreen, arguments: input);
return true;
case DirectionalScreenName.mobileTopupData:
Get.toNamed(dataNetworkServiceScreen);
return true;
case DirectionalScreenName.pointBack:
......@@ -98,14 +98,13 @@ class DirectionalScreen {
case DirectionalScreenName.gamesBundle:
Get.toNamed(gameTabScreen, arguments: {"can_back_button": true});
return true;
case DirectionalScreenName.vnTraPackage || DirectionalScreenName.familyMedon:
case DirectionalScreenName.vnTraPackage || DirectionalScreenName.familyMedon || DirectionalScreenName.voucher:
Get.toNamed(voucherDetailScreen, arguments: {"productId": int.parse(clickActionParam ?? "")});
return true;
case DirectionalScreenName.viewDeepLink || DirectionalScreenName.viewDeepLinkInApp:
if ((clickActionParam ?? "").isEmpty) return true;
const replaceId1 = '{{customerId}}';
const replaceId2 = '{customerId}';
final customerId = DataPreference.instance.profile?.workerSite?.customerId ?? "";
String urlString = clickActionParam!.urlDecoded;
if (urlString.contains(replaceId1)) {
......@@ -118,11 +117,9 @@ class DirectionalScreen {
Uri? uri = Uri.tryParse(urlString);
if (uri == null) return true;
final requestId = const Uuid().v4(); // Cần package `uuid`
final updatedUri = uri.replace(queryParameters: {
...uri.queryParameters,
'aff_sub3': requestId,
});
LaunchMode mode = type == DirectionalScreenName.viewDeepLink ? LaunchMode.externalApplication : LaunchMode.platformDefault;
final updatedUri = uri.replace(queryParameters: {...uri.queryParameters, 'aff_sub3': requestId});
LaunchMode mode =
type == DirectionalScreenName.viewDeepLink ? LaunchMode.externalApplication : LaunchMode.platformDefault;
forceOpen(url: updatedUri, mode: mode);
return true;
case DirectionalScreenName.refundHistory:
......@@ -131,9 +128,34 @@ class DirectionalScreen {
case DirectionalScreenName.inviteFriend:
Get.toNamed(inviteFriendCampaignScreen);
return true;
case DirectionalScreenName.dailyCheckin:
case DirectionalScreenName.dailyCheckin || DirectionalScreenName.dailyCheckinScreen:
Get.toNamed(dailyCheckInScreen);
return true;
case DirectionalScreenName.favorite:
Get.toNamed(vouchersScreen, arguments: {"favorite": true});
return true;
case DirectionalScreenName.transactionHistories:
Get.toNamed(transactionHistoryScreen);
return true;
case DirectionalScreenName.electricBill:
Get.toNamed(electricPaymentScreen);
return true;
case DirectionalScreenName.listPaymentOfElectric:
Get.toNamed(electricPaymentHistoryScreen);
return true;
case DirectionalScreenName.myVnTraPackage:
Get.toNamed(trafficServiceScreen);
return true;
case DirectionalScreenName.campaignSevenDayScreen:
Get.toNamed(campaignSevenDayScreen);
return true;
case DirectionalScreenName.workerProfile:
Get.toNamed(personalEditScreen);
return true;
case DirectionalScreenName.surveyCampaign:
// if ((clickActionParam ?? '').isEmpty) return false;
Get.toNamed(surveyQuestionScreen, arguments: {"quizId": "1"});
return true;
default:
print("Không nhận diện được action type: $clickActionType");
return false;
......@@ -147,10 +169,7 @@ Future<bool> forceOpen({required Uri url, LaunchMode mode = LaunchMode.platformD
await launchUrl(
url,
mode: LaunchMode.externalApplication,
webViewConfiguration: const WebViewConfiguration(
enableJavaScript: true,
headers: <String, String>{},
),
webViewConfiguration: const WebViewConfiguration(enableJavaScript: true, headers: <String, String>{}),
);
return true;
}
......@@ -162,7 +181,7 @@ Future<void> openAppStore(String url) async {
if (await canLaunchUrl(uri)) {
await launchUrl(
uri,
mode: LaunchMode.externalApplication, // ⚠️ Phải dùng mode này
mode: LaunchMode.externalApplication,
);
} else {
debugPrint("⚠️ Không thể mở URL: $url");
......
import 'dart:ui';
import 'package:flutter/material.dart';
extension ColorExtension on Color {
Color get invert => Color.fromARGB(
alpha,
......@@ -7,4 +9,10 @@ extension ColorExtension on Color {
255 - green,
255 - blue,
);
Color get contrastTextColor {
//(luminance perceived brightness)
final brightness = (0.299 * red + 0.587 * green + 0.114 * blue);
return brightness > 186 ? Colors.black : Colors.white;
}
}
\ No newline at end of file
......@@ -2,4 +2,5 @@ class DateFormat {
static const String serverTimezone = "yyyy-MM-dd'T'HH:mm:ssZ";
static const String server = "yyyy-MM-dd HH:mm:ss";
static const String ddMMyyyy = "dd/MM/yyyy";
static const String ddMMyyyyhhmm = "dd/MM/yyyy hh:mm";
}
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import 'package:mypoint_flutter_app/preference/point/point_manager.dart';
import 'package:mypoint_flutter_app/resouce/base_color.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
void main() async {
......@@ -20,9 +22,18 @@ class MyApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
initialRoute: '/splash',
theme: ThemeData(
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.deepPurple),
primaryColor: Colors.deepPurple,
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.brown),
primaryColor: BaseColor.primary500,
),
locale: const Locale('vi'),
supportedLocales: const [
Locale('vi', 'VN'), // Vietnamese
],
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
// home: SplashScreen(),
getPages: RouterPage.pages(),
);
......
......@@ -18,8 +18,12 @@ import '../screen/affiliate/model/affiliate_category_model.dart';
import '../screen/affiliate/model/affiliate_product_top_sale_model.dart';
import '../screen/affiliate/model/cashback_overview_model.dart';
import '../screen/affiliate_brand_detail/models/affiliate_brand_detail_model.dart';
import '../screen/campaign7day/models/campaign_7day_info_model.dart';
import '../screen/campaign7day/models/campaign_7day_mission_model.dart';
import '../screen/campaign7day/models/campaign_7day_reward_model.dart';
import '../screen/daily_checkin/daily_checkin_models.dart';
import '../screen/data_network_service/product_network_data_model.dart';
import '../screen/electric_payment/customer_contract_object_model.dart';
import '../screen/faqs/faqs_model.dart';
import '../screen/game/models/game_bundle_item_model.dart';
import '../screen/history_point_cashback/models/history_point_cashback_model.dart';
......@@ -45,10 +49,14 @@ import '../screen/otp/model/create_otp_response_model.dart';
import '../screen/otp/model/otp_verify_response_model.dart';
import '../screen/pageDetail/model/campaign_detail_model.dart';
import '../screen/pageDetail/model/detail_page_rule_type.dart';
import '../screen/quiz_campaign/quiz_campaign_model.dart';
import '../screen/register_campaign/model/registration_form_package_model.dart';
import '../screen/splash/splash_screen_viewmodel.dart';
import '../screen/topup/models/brand_network_model.dart';
import '../screen/traffic_service/traffic_service_model.dart';
import '../screen/transaction/history/transaction_category_model.dart';
import '../screen/transaction/history/transaction_history_model.dart';
import '../screen/transaction/history/transaction_history_response_model.dart';
import '../screen/transaction/model/order_product_payment_response_model.dart';
import '../screen/transaction/model/payment_bank_account_info_model.dart';
import '../screen/transaction/model/payment_method_model.dart';
......@@ -299,6 +307,13 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
);
}
Future<BaseResponseModel<List<ProductModel>>> productsCustomerLikes(Json body) async {
return requestNormal(APIPaths.productsCustomerLikes, Method.GET, body, (data) {
final list = data as List<dynamic>;
return list.map((e) => ProductModel.fromJson(e)).toList();
});
}
Future<BaseResponseModel<ProductModel>> getProduct(int id) async {
final path = APIPaths.getProductDetail.replaceAll("%@", id.toString());
return requestNormal(path, Method.GET, {}, (data) => ProductModel.fromJson(data as Json));
......@@ -722,4 +737,122 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
return CheckInDataModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<SubmitCheckInData>> submitCheckIn() async {
String? token = DataPreference.instance.token ?? "";
final body = {
"access_token": token,
};
return requestNormal(APIPaths.rewardOpportunityOpenRequest, Method.POST, body, (data) {
return SubmitCheckInData.fromJson(data as Json);
});
}
Future<BaseResponseModel<TransactionHistoryResponse>> getTransactionHistoryResponse(Json body) async {
return requestNormal(APIPaths.getTransactionOrderHistory, Method.GET, body, (data) {
return TransactionHistoryResponse.fromJson(data as Json);
});
}
Future<BaseResponseModel<List<TransactionCategoryModel>>> getTransactionHistoryCategories() async {
return requestNormal(APIPaths.orderHistoryCategories, Method.GET, {}, (data) {
final list = data as List<dynamic>;
return list.map((e) => TransactionCategoryModel.fromJson(e)).toList();
});
}
Future<BaseResponseModel<CustomerContractModel>> customerContractRequestSearch(String maKH) async {
String? token = DataPreference.instance.token ?? "";
final body = {
"access_token": token,
'ma_khang': maKH,
};
return requestNormal(APIPaths.customerContractRequestSearch, Method.POST, body, (data) {
return CustomerContractModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<List<CustomerContractModel>>> customerContractSearchHistoryGetList() async {
String? token = DataPreference.instance.token ?? "";
final body = {
"access_token": token,
};
return requestNormal(APIPaths.customerContractSearchHistoryGetList, Method.POST, body, (data) {
final list = data as List<dynamic>;
return list.map((e) => CustomerContractModel.fromJson(e)).toList();
});
}
Future<BaseResponseModel<bool>> customerContractDelete(String maKHs) async {
String? token = DataPreference.instance.token ?? "";
final body = {
"access_token": token,
'ma_khang': maKHs,
};
return requestNormal(APIPaths.customerContractDelete, Method.POST, body, (data) {
return data == true;
});
}
Future<BaseResponseModel<TrafficServiceResponseModel>> getProductVnTraSold(Json body) async {
return requestNormal(APIPaths.getProductVnTraSold, Method.GET, body, (data) {
return TrafficServiceResponseModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<TrafficServiceDetailModel>> getDetailMyPackageVnTra(String id) async {
final path = APIPaths.detailMyPackageVnTra.replaceAll("%@", id);
return requestNormal(path, Method.GET, {}, (data) {
return TrafficServiceDetailModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<EmptyCodable>> submitPerformMission(Campaign7DayMissionModel mission, String id) async {
final path = APIPaths.submitCampaignMission
.replaceFirst('%@', id)
.replaceFirst('%@', mission.id.toString());
return requestNormal(path, Method.POST, {}, (data) {
return EmptyCodable.fromJson(data as Json);
});
}
Future<BaseResponseModel<List<Campaign7DayRewardModel>>> getCampaignRewards(String id) async {
final path = APIPaths.getCampaignReward.replaceFirst('%@', id);
return requestNormal(path, Method.GET, {}, (data) {
final list = data as List<dynamic>;
return list.map((e) => Campaign7DayRewardModel.fromJson(e)).toList();
});
}
Future<BaseResponseModel<List<String>>> getCampaignLiveTransactions(String id) async {
final path = APIPaths.getCampaignLiveTransactions.replaceFirst('%@', id);
return requestNormal(path, Method.GET, {}, (data) {
if (data is List) {
return data.map((e) => e.toString()).toList();
}
return <String>[];
});
}
Future<BaseResponseModel<Campaign7DayInfoModel>> getCampaignMissions(String id) async {
final path = APIPaths.getCampaignMissions.replaceFirst('%@', id);
return requestNormal(path, Method.GET, {}, (data) {
return Campaign7DayInfoModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<SurveyCampaignInfoModel>> getCampaignQuizSurvey(String id) async {
final path = APIPaths.getQuizCampaign.replaceFirst('%@', id);
return requestNormal(path, Method.GET, {}, (data) {
return SurveyCampaignInfoModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<QuizCampaignSubmitResponseModel>> quizSubmitCampaign(String id, Json body) async {
final path = APIPaths.quizSubmitCampaign.replaceFirst('%@', id);
return requestNormal(path, Method.POST, body, (data) {
return QuizCampaignSubmitResponseModel.fromJson(data as Json);
});
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import '../../resouce/base_color.dart';
class Campaign7DayGuideDialog extends StatelessWidget {
final String title;
final String htmlContent;
final VoidCallback onClose;
const Campaign7DayGuideDialog({super.key, required this.title, required this.htmlContent, required this.onClose});
@override
Widget build(BuildContext context) {
final widthScreen = MediaQuery.of(context).size.width;
final heightScreen = MediaQuery.of(context).size.height;
return Stack(
children: [
Container(
padding: const EdgeInsets.only(top: 32),
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
decoration: BoxDecoration(color: const Color(0xFFFCECEF), borderRadius: BorderRadius.circular(24)),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 24, left: 16, right: 16, bottom: 16),
child: Row(
children: [
Expanded(
child: Text(
title,
style: TextStyle(color: BaseColor.primary400, fontSize: 24, fontWeight: FontWeight.w800),
),
),
SizedBox(width: widthScreen / 2.5),
],
),
),
const SizedBox(height: 8),
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)),
constraints: BoxConstraints(maxHeight: heightScreen*0.6),
child: SingleChildScrollView(
child: HtmlWidget(htmlContent, textStyle: const TextStyle(fontSize: 16, height: 1.5)),
),
),
const SizedBox(height: 24),
],
),
),
Positioned(
top: -30,
right: 0,
child: Image.asset('assets/images/bg_header_campaign_info.png', width: widthScreen / 1.6),
),
],
),
),
Positioned(
top: 0,
right: 8,
child: GestureDetector(
onTap: onClose,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
shape: BoxShape.circle,
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 1.5)),
),
child: const Icon(Icons.close_outlined, color: Colors.white, size: 20),
),
),
),
],
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import '../../resouce/base_color.dart';
import 'models/campaign_7day_mission_model.dart';
class Campaign7DayMissionInfoDialog extends StatelessWidget {
final Campaign7DayMissionModel mission;
final VoidCallback onConfirm;
final VoidCallback onClose;
const Campaign7DayMissionInfoDialog({
super.key,
required this.mission,
required this.onConfirm,
required this.onClose,
});
@override
Widget build(BuildContext context) {
return Stack(children: [_buildContentBox(), _buildCloseButton()]);
}
Widget _buildContentBox() {
return Container(
padding: const EdgeInsets.only(top: 24),
child: Stack(clipBehavior: Clip.none, children: [_buildInnerContainer(), _buildHeaderBanner()]),
);
}
Widget _buildInnerContainer() {
return Container(
margin: const EdgeInsets.only(top: 9, left: 8, right: 8),
padding: const EdgeInsets.only(top: 16, left: 16, right: 16, bottom: 32),
decoration: BoxDecoration(color: const Color(0xFFFEF1F2), borderRadius: BorderRadius.circular(16)),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 36),
HtmlWidget(mission.info ?? '', textStyle: const TextStyle(fontSize: 16, color: Colors.black87)),
const SizedBox(height: 36),
if (mission.isReady) _buildConfirmButton(),
],
),
);
}
Widget _buildHeaderBanner() {
return Positioned(
top: 0,
left: 0,
right: 0,
child: Center(
child: Stack(
alignment: Alignment.center,
children: [
Image.asset('assets/images/ic_header_alert_campaign.png', height: 48, fit: BoxFit.contain),
const Text(
'Thông tin nhiệm vụ',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 17,
shadows: [Shadow(color: Colors.black26, offset: Offset(2, 2), blurRadius: 2)],
),
),
],
),
),
);
}
Widget _buildCloseButton() {
return Positioned(
top: 0,
right: 8,
child: GestureDetector(
onTap: onClose,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
shape: BoxShape.circle,
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 1.5)),
),
child: const Icon(Icons.close_outlined, color: Colors.white, size: 20),
),
),
);
}
Widget _buildConfirmButton() {
return ElevatedButton(
onPressed: onConfirm,
style: ElevatedButton.styleFrom(
backgroundColor: BaseColor.primary400,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
),
child: const Text('THỰC HIỆN', style: TextStyle(fontWeight: FontWeight.w800, color: Colors.white, fontSize: 18)),
);
}
}
import 'package:flutter/material.dart';
import 'custom_widgets/campaign_7day_reward_item.dart';
import 'models/campaign_7day_reward_model.dart';
class Campaign7DayRewardDialog extends StatelessWidget {
final List<Campaign7DayRewardModel> rewards;
final VoidCallback onClose;
const Campaign7DayRewardDialog({super.key, required this.rewards, required this.onClose});
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(16),
child: Stack(
children: [
Container(
padding: const EdgeInsets.only(top: 32),
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
decoration: BoxDecoration(color: const Color(0xFFFCECEF), borderRadius: BorderRadius.circular(24)),
padding: const EdgeInsets.only(top: 48, left: 16, right: 16, bottom: 24),
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7, // 70% chiều cao màn hình
),
child: ListView.separated(
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
itemCount: rewards.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return Campaign7DayRewardItem(model: rewards[index]);
},
),
),
),
Positioned(
top: -9,
left: 0,
right: 0,
child: Center(
child: Stack(
alignment: Alignment.center,
children: [
Image.asset('assets/images/ic_header_alert_campaign.png', height: 48, fit: BoxFit.contain),
const Text(
'Lịch sử phần thưởng',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 17),
),
],
),
),
),
],
),
),
Positioned(
top: 0,
right: 8,
child: GestureDetector(
onTap: onClose,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
shape: BoxShape.circle,
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 1.5)),
),
child: const Icon(Icons.close_outlined, color: Colors.white, size: 20),
),
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/custom_empty_widget.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../resouce/base_color.dart';
import '../../widgets/bottom_sheet_helper.dart';
import '../home/custom_widget/hover_view.dart';
import 'campaign_7day_guide_dialog.dart';
import 'campaign_7day_mission_info_dialog.dart';
import 'campaign_7day_reward_dialog.dart';
import 'campaign_7day_viewmodel.dart';
import 'custom_widgets/campaign_7day_marguee_bar.dart';
import 'custom_widgets/campaign_7day_mission_card_item.dart';
import 'custom_widgets/campaign_7day_progress_box.dart';
import 'custom_widgets/campaign_7day_special_misssion_card.dart';
import 'custom_widgets/campaign_7day_top_buttons.dart';
class Campaign7DayScreen extends BaseScreen {
const Campaign7DayScreen({super.key});
@override
State<Campaign7DayScreen> createState() => _Campaign7DayScreenState();
}
class _Campaign7DayScreenState extends BaseState<Campaign7DayScreen> with BasicState {
late final Campaign7DayViewModel _viewModel;
@override
void initState() {
super.initState();
String? campaignId = '1';
final args = Get.arguments;
if (args is String) {
campaignId = args;
}
if (campaignId == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.back();
});
return;
}
_viewModel = Get.put(Campaign7DayViewModel(campaignId: campaignId));
_viewModel.getLiveTransactions();
_viewModel.getCampaign7DayInfo();
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
showAlertError(content: message);
}
};
_viewModel.submitPerformMissionResponse = (mission) {
final popup = mission.popup;
if (popup != null) {
showAlert(data: popup.dataAlertModel);
}
};
_viewModel.getCampaignRewardsResponse = (rewards) {
BottomSheetHelper.showBottomSheetPopup(
backgroundContainerColor: Colors.transparent,
horizontalContainerPadding: 0,
child: Campaign7DayRewardDialog(
rewards: rewards,
onClose: () {
Get.back();
},
),
);
};
}
@override
Widget createBody() {
return Scaffold(
body: Stack(
children: [
RefreshIndicator(
onRefresh: () async {
_viewModel.getLiveTransactions();
_viewModel.getCampaign7DayInfo();
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
_buildHeaderWithInfo(),
const SizedBox(height: 88),
Obx(() => _buildMissionList()),
const SizedBox(height: 24),
],
),
),
),
Campaign7dayTopButtons(
onBack: () {
Get.back();
},
onInfo: () {
BottomSheetHelper.showBottomSheetPopup(
backgroundContainerColor: Colors.transparent,
horizontalContainerPadding: 0,
bottomContainerPadding: 0,
child: Campaign7DayGuideDialog(
title: _viewModel.campaign7DayInfo.value?.name ?? 'Chiến dịch 7 ngày',
htmlContent: _viewModel.campaign7DayInfo.value?.description ?? '',
onClose: () {
Get.back();
},
),
);
},
onGift: () {
_viewModel.getCampaignRewards();
},
),
Positioned.fill(
child: Obx(()
// print('_viewModel.campaign7DayInfo.value?.countDownTime: ${_viewModel.campaign7DayInfo.value?.countDownTime}');
=> HoverView(
imagePath: _viewModel.campaign7DayInfo.value?.config?.mainIcon ?? 'assets/images/ic_pipi_04.png',
onTap: null,
onClose: null,
backgroundColor: Colors.transparent,
size: 80,
countDownTime: _viewModel.campaign7DayInfo.value?.countDownTime ?? 0.0,
),
),
),
],
),
backgroundColor: BaseColor.primary400,
);
}
Widget _buildHeaderWithInfo() {
final heightImageBG = MediaQuery.of(context).size.width * 1589 / 1125;
return Stack(
clipBehavior: Clip.none,
children: [
Image.asset(
'assets/images/bg_campaign_7day.png',
fit: BoxFit.fill,
width: double.infinity,
height: heightImageBG,
),
Positioned(
left: 16,
right: 16,
child: Transform.translate(
offset: Offset(0, heightImageBG - 90),
child: Obx(() {
final text = _viewModel.liveTransactions.value;
final missions = _viewModel.campaign7DayInfo.value?.missions ?? [];
return Column(
children: [
if (text.isNotEmpty) CampaignMarqueeNoticeBar(text: text.join(' • ')),
const SizedBox(height: 14),
if (missions.isNotEmpty) Campaign7DayProgressBox(missions: missions),
const SizedBox(height: 16),
],
);
}),
),
),
],
);
}
Widget _buildMissionList() {
final missions = _viewModel.campaign7DayInfo.value?.missions ?? [];
if (missions.isEmpty) {
return const Center(child: EmptyWidget(content: 'Không có nhiệm vụ nào'));
}
final normalMissions = missions.where((mission) => !mission.isSpecial).toList();
final specialMissions = missions.where((mission) => mission.isSpecial).toList();
return Stack(
clipBehavior: Clip.none,
children: [
Container(
margin: const EdgeInsets.only(top: 9, left: 16, right: 16), // để chừa chỗ ảnh header
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Color(0xFFFEF1F2), borderRadius: BorderRadius.circular(16)),
child: Column(
children: [
const SizedBox(height: 30),
...normalMissions.map(
(mission) => Campaign7DayMissionCardItem(
mission: mission,
onTapInfo: () {
BottomSheetHelper.showBottomSheetPopup(
backgroundContainerColor: Colors.transparent,
child: Campaign7DayMissionInfoDialog(
mission: mission,
onConfirm: () {
Get.back();
_viewModel.submitPerformMission(mission);
},
onClose: () {
Get.back();
},
),
);
},
onTapClaimReward: () {
_viewModel.submitPerformMission(mission);
},
),
),
if (specialMissions.isNotEmpty)
...specialMissions.map(
(mission) => Campaign7DaySpecialMissionCard(
mission: mission,
onTap: () {
_viewModel.submitPerformMission(mission);
},
),
),
],
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: Center(
child: Stack(
alignment: Alignment.center,
children: [
Image.asset('assets/images/ic_header_alert_campaign.png', height: 48, fit: BoxFit.contain),
const Text(
'Danh sách nhiệm vụ',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 17,
shadows: [Shadow(color: Colors.black26, offset: Offset(2, 2), blurRadius: 2)],
),
),
],
),
),
),
],
);
}
}
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../configs/constants.dart';
import 'models/campaign_7day_info_model.dart';
import 'models/campaign_7day_mission_model.dart';
import 'models/campaign_7day_reward_model.dart';
class Campaign7DayViewModel extends RestfulApiViewModel {
String campaignId;
var liveTransactions = RxList<String>();
var campaign7DayInfo = Rxn<Campaign7DayInfoModel>();
void Function(String message)? onShowAlertError;
void Function(Campaign7DayMissionModel mission)? submitPerformMissionResponse;
void Function(List<Campaign7DayRewardModel> rewards)? getCampaignRewardsResponse;
Campaign7DayViewModel({required this.campaignId});
void getLiveTransactions() {
client.getCampaignLiveTransactions(campaignId).then((value) {
liveTransactions.value = value.data ?? [];
});
}
void getCampaignRewards() {
showLoading();
client.getCampaignRewards(campaignId).then((value) {
hideLoading();
final data = value.data ?? [];
if (!value.isSuccess) {
onShowAlertError?.call(value.errorMessage ?? Constants.commonError);
return;
}
if (data.isEmpty) {
onShowAlertError?.call("Bạn chưa có phần thưởng nào. Vui lòng hoàn thành các nhiệm vụ để nhận thưởng!");
} else {
getCampaignRewardsResponse?.call(data);
}
});
}
void getCampaign7DayInfo() {
client.getCampaignMissions(campaignId).then((value) {
if (!value.isSuccess) {
onShowAlertError?.call(value.errorMessage ?? Constants.commonError);
}
campaign7DayInfo.value = value.data;
});
}
void submitPerformMission(Campaign7DayMissionModel mission) {
if (!mission.isReady) return;
showLoading();
client.submitPerformMission(mission, campaignId).then((value) {
hideLoading();
if (value.isSuccess) {
// getCampaign7DayInfo();
if (mission.popup != null) {
submitPerformMissionResponse?.call(mission);
} else {
mission.directionScreen?.begin();
}
} else {
onShowAlertError?.call(value.errorMessage ?? Constants.commonError);
}
});
}
}
import 'package:flutter/material.dart';
import 'package:marquee/marquee.dart';
class CampaignMarqueeNoticeBar extends StatelessWidget {
final String text;
final String iconAsset; // ví dụ: assets/icons/ic_speaker.png
const CampaignMarqueeNoticeBar({
super.key,
required this.text,
this.iconAsset = 'assets/images/ic_speaker_toast.png',
});
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
Container(
height: 28,
margin: const EdgeInsets.only(left: 8, right: 8, top: 6),
padding: const EdgeInsets.only(left: 36, right: 12),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: Marquee(
text: text,
style: const TextStyle(color: Colors.white, fontSize: 14),
scrollAxis: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.center,
blankSpace: 60.0,
velocity: 40.0,
pauseAfterRound: Duration.zero,
startPadding: 0.0,
accelerationDuration: Duration(seconds: 1),
accelerationCurve: Curves.linear,
decelerationDuration: Duration(seconds: 1),
decelerationCurve: Curves.linear,
),
),
),
// Icon loa nằm ngoài trái
Positioned(
left: 8,
top: 0,
child: Image.asset(
iconAsset,
width: 40,
height: 40,
),
),
],
);
}
}
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../../resouce/base_color.dart';
import '../models/campaign_7day_mission_model.dart';
class Campaign7DayMissionCardItem extends StatelessWidget {
final Campaign7DayMissionModel mission;
final VoidCallback? onTapInfo;
final VoidCallback? onTapClaimReward;
const Campaign7DayMissionCardItem({super.key, required this.mission, this.onTapInfo, this.onTapClaimReward});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
loadNetworkImage(
url: mission.avatar,
width: 48,
height: 48,
fit: BoxFit.cover,
placeholderAsset: 'assets/images/ic_point.png',
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(mission.title ?? '', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
const SizedBox(height: 16),
Row(
children: [
Image.asset(
mission.reward?.imageAsset ?? 'assets/images/ic_campaign_gift.png',
width: 24,
height: 24,
),
const SizedBox(width: 4),
Text(
mission.reward?.value ?? '',
style: const TextStyle(fontWeight: FontWeight.w700, color: Colors.orange),
),
],
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.start,
children: [
GestureDetector(onTap: onTapInfo, child: Icon(Icons.info_outline, size: 20, color: BaseColor.primary400)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: mission.isReady ? onTapClaimReward : null,
style: ElevatedButton.styleFrom(
backgroundColor: mission.isReady ? BaseColor.primary400 : BaseColor.second500,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
),
child: const Text(
'Thực hiện',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: Colors.white),
),
),
],
),
],
),
);
}
}
import 'package:flutter/material.dart';
import '../models/campaign_7day_config_model.dart';
import '../models/campaign_7day_mission_model.dart';
class Campaign7DayProgressBox extends StatelessWidget {
final List<Campaign7DayMissionModel>? missions;
const Campaign7DayProgressBox({super.key, required this.missions});
@override
Widget build(BuildContext context) {
final normalMissions = missions?.where((mission) => !mission.isSpecial).toList() ?? [];
final completedCount = (missions?.where((mission) => mission.mStatus == MissionStatus.completed).toList() ?? []).length;
final specialMission = missions?.lastWhere((mission) => mission.isSpecial);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Hoàn thành ${normalMissions.length} nhiệm vụ để mở thưởng lớn',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(child: _buildLine(0 < completedCount)),
for (int i = 0; i < (missions?.length ?? 0); i++) ...[
_buildIcon(i < completedCount),
Expanded(child: _buildLine(i < completedCount)),
],
if (specialMission != null)
Image.asset(
specialMission.mStatus == MissionStatus.completed
? 'assets/images/ic_campaign_mission_gift_open.png'
: 'assets/images/ic_campaign_mission.png',
width: 48,
height: 48,
),
],
),
],
),
);
}
Widget _buildIcon(bool isDone) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Image.asset(
isDone ? 'assets/images/ic_point.png' : 'assets/images/ic_point_gray.png',
width: 24,
height: 24,
),
);
}
Widget _buildLine(bool isDone) {
return Container(height: 3, color: isDone ? Colors.orange : Colors.grey.shade300);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:get/get.dart';
import '../../../resouce/base_color.dart';
import '../models/campaign_7day_reward_model.dart';
class Campaign7DayRewardItem extends StatelessWidget {
final Campaign7DayRewardModel model;
const Campaign7DayRewardItem({super.key, required this.model});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08), // màu bóng
blurRadius: 8, // độ mờ
offset: const Offset(0, 4), // hướng đổ bóng
),
],
),
padding: const EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(model.imageAsset, width: 48, height: 48),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
HtmlWidget(model.textDisplay ?? '', textStyle: const TextStyle(fontSize: 15, color: Colors.black)),
const SizedBox(height: 8),
Divider(color: Colors.grey.shade200, thickness: 1, height: 2),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(child: Text(model.createdAt ?? '', style: TextStyle(fontSize: 12, color: Colors.black54))),
ElevatedButton(
onPressed: () {
Get.back();
model.direction?.begin();
},
style: ElevatedButton.styleFrom(
backgroundColor: BaseColor.primary400,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
minimumSize: const Size(0, 32),
),
child: Text(
model.buttonText ?? 'Sử dụng ngay',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white),
),
),
],
),
],
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import '../models/campaign_7day_config_model.dart';
import '../models/campaign_7day_mission_model.dart';
class Campaign7DaySpecialMissionCard extends StatelessWidget {
final Campaign7DayMissionModel mission;
final VoidCallback? onTap;
const Campaign7DaySpecialMissionCard({
super.key,
required this.mission,
this.onTap,
});
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final height = (width)*300/957;
final imagePath = makeImageBGSubmitMission(mission.mStatus);
return GestureDetector(
onTap: onTap,
child: Container(
width: width,
height: height,
margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 4),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(imagePath),
fit: BoxFit.contain,
),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
mission.title ?? '',
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w800,
color: Colors.white,
shadows: [
Shadow(
offset: Offset(2, 2),
blurRadius: 0.1,
color: Colors.black54,
),
],
),
),
const SizedBox(height: 2),
Text(
mission.description ?? '',
style: const TextStyle(
fontSize: 14,
color: Colors.white,
),
),
],
),
),
SizedBox(width: width/4.5,),
],
),
),
);
}
String makeImageBGSubmitMission(MissionStatus status) {
late final String imageName;
switch (status) {
case MissionStatus.ready:
imageName = 'assets/images/bg_misstion_submit_campaign_7day_ready.png';
break;
case MissionStatus.pending:
imageName = 'assets/images/bg_misstion_submit_campaign_7day_disable.png';
break;
case MissionStatus.completed:
imageName = 'assets/images/bg_misstion_submit_campaign_7day_complete.png';
break;
}
return imageName;
}
}
import 'package:flutter/material.dart';
class Campaign7dayTopButtons extends StatelessWidget {
final VoidCallback? onBack;
final VoidCallback? onInfo;
final VoidCallback? onGift;
const Campaign7dayTopButtons({
super.key,
this.onBack,
this.onInfo,
this.onGift,
});
@override
Widget build(BuildContext context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildIconButton("assets/images/ic_campaign_back.png", onBack),
Row(
children: [
_buildIconButton("assets/images/ic_campaign_info.png", onInfo),
const SizedBox(width: 12),
_buildIconButton("assets/images/ic_campaign_gift.png", onGift),
],
),
],
),
),
);
}
Widget _buildIconButton(String assetName, VoidCallback? onPressed) {
return GestureDetector(
onTap: onPressed,
child: SizedBox(
width: 40,
height: 40,
child: Image.asset(
assetName,
// color: Colors.white,
width: 22,
height: 22,
),
),
);
}
}
import 'dart:ui';
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/resouce/base_color.dart';
part 'campaign_7day_config_model.g.dart';
@JsonSerializable()
class Campaign7dayConfigModel {
@JsonKey(name: 'main_icon')
final String? mainIcon;
@JsonKey(name: 'progress_bar_text')
final String? progressBarText;
@JsonKey(name: 'header_img')
final String? headerImg;
@JsonKey(name: 'main_color')
final String? mainColorHex;
@JsonKey(name: 'main_img_type')
final String? mainImgType;
Campaign7dayConfigModel({
this.mainIcon,
this.progressBarText,
this.headerImg,
this.mainColorHex,
this.mainImgType,
});
factory Campaign7dayConfigModel.fromJson(Map<String, dynamic> json) =>
_$Campaign7dayConfigModelFromJson(json);
Map<String, dynamic> toJson() => _$Campaign7dayConfigModelToJson(this);
String get suffixImageName {
final style = CampaignStyleImage.values.firstWhere(
(e) => e.name.toUpperCase() == (mainImgType ?? '').toUpperCase(),
orElse: () => CampaignStyleImage.RED,
);
return style.name;
}
Color get mainColor {
if ((mainColorHex ?? '').isEmpty) {
return BaseColor.primary400;
}
return _parseHexColor(mainColorHex!) ?? BaseColor.primary400;
}
String get headerMissionBGAsset =>
'assets/images/bg_header_alert_campaign_7day_$suffixImageName.png';
Color? _parseHexColor(String hex) {
try {
final buffer = StringBuffer();
if (hex.startsWith('#')) hex = hex.substring(1);
if (hex.length == 6) buffer.write('ff');
buffer.write(hex);
return Color(int.parse(buffer.toString(), radix: 16));
} catch (_) {
return null;
}
}
}
enum MissionStatus { ready, pending, completed }
enum TypeReward { point, voucher }
enum CampaignStyleImage { RED, ORANGE }
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'campaign_7day_config_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Campaign7dayConfigModel _$Campaign7dayConfigModelFromJson(
Map<String, dynamic> json,
) => Campaign7dayConfigModel(
mainIcon: json['main_icon'] as String?,
progressBarText: json['progress_bar_text'] as String?,
headerImg: json['header_img'] as String?,
mainColorHex: json['main_color'] as String?,
mainImgType: json['main_img_type'] as String?,
);
Map<String, dynamic> _$Campaign7dayConfigModelToJson(
Campaign7dayConfigModel instance,
) => <String, dynamic>{
'main_icon': instance.mainIcon,
'progress_bar_text': instance.progressBarText,
'header_img': instance.headerImg,
'main_color': instance.mainColorHex,
'main_img_type': instance.mainImgType,
};
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