Commit 6b980613 authored by DatHV's avatar DatHV
Browse files

update project structure

parent bfff9e47
import 'package:flutter/cupertino.dart';
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/api/product_api.dart' deferred as product_api;
import '../../../networking/restful_api_viewmodel.dart';
import '../../home/models/my_product_model.dart';
import '../../../base/base_response_model.dart';
import 'package:mypoint_flutter_app/core/network/api/product_api.dart' deferred as product_api;
import '../../../core/network/restful_api_viewmodel.dart';
import '../models/my_product_model.dart';
import '../../../shared/widgets/base_view/base_response_model.dart';
class MyProductListViewModel extends RestfulApiViewModel {
final RxInt selectedTabIndex = 0.obs;
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../shared/router_gage.dart';
import '../../../widgets/custom_empty_widget.dart';
import '../../../widgets/custom_navigation_bar.dart';
import '../../../widgets/image_loader.dart';
import '../../home/models/my_product_model.dart';
import '../../../shared/widgets/custom_empty_widget.dart';
import '../../../shared/widgets/custom_navigation_bar.dart';
import '../../../shared/widgets/image_loader.dart';
import '../models/my_product_model.dart';
import 'my_product_list_viewmodel.dart';
import 'package:dotted_border/dotted_border.dart';
......@@ -26,7 +26,6 @@ class _MyVoucherListScreenState extends State<MyVoucherListScreen> {
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
appBar: CustomNavigationBar(title: 'Ưu đãi của tôi'),
body: Obx(
......@@ -42,7 +41,7 @@ class _MyVoucherListScreenState extends State<MyVoucherListScreen> {
),
const Divider(height: 1),
if (_viewModel.myProducts.isEmpty)
Expanded(child: EmptyWidget(size: Size(screenWidth / 2, screenWidth / 2)))
Expanded(child: EmptyWidget(isLoading: _viewModel.isLoading.value))
else
Expanded(
child: RefreshIndicator(
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import '../../../directional/directional_action_type.dart';
import '../../../directional/directional_screen.dart';
import '../../../resources/base_color.dart';
import '../../../shared/router_gage.dart';
import '../../../app/routing/directional_action_type.dart';
import '../../../shared/navigation/directional_screen.dart';
class VoucherActionMenu extends StatelessWidget {
const VoucherActionMenu({super.key});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final itemWidth = screenWidth / 4;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import '../../../shared/router_gage.dart';
import '../../../widgets/custom_price_tag.dart';
import '../../../widgets/image_loader.dart';
import '../../../shared/widgets/custom_price_tag.dart';
import '../../../shared/widgets/image_loader.dart';
import '../models/product_model.dart';
class VoucherItemGrid extends StatelessWidget {
......@@ -46,7 +45,6 @@ class _VoucherGridItem extends StatelessWidget {
final double itemWidth;
const _VoucherGridItem({
super.key,
required this.product,
required this.itemWidth,
});
......
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
import '../../../resources/base_color.dart';
import '../../../widgets/custom_price_tag.dart';
import '../../../widgets/image_loader.dart';
import '../../../core/theme/base_color.dart';
import '../../../shared/widgets/custom_price_tag.dart';
import '../../../shared/widgets/image_loader.dart';
import '../models/product_model.dart';
class VoucherItemList extends StatelessWidget {
......
import 'package:barcode_widget/barcode_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mypoint_flutter_app/widgets/back_button.dart';
import 'package:mypoint_flutter_app/widgets/custom_toast_message.dart';
import 'package:mypoint_flutter_app/widgets/dashed_line.dart';
import 'package:mypoint_flutter_app/shared/widgets/back_button.dart';
import 'package:mypoint_flutter_app/shared/widgets/custom_toast_message.dart';
import 'package:mypoint_flutter_app/shared/widgets/dashed_line.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../../resources/base_color.dart';
import '../../widgets/custom_point_text_tag.dart';
import '../../widgets/image_loader.dart';
import '../../core/theme/base_color.dart';
import '../../shared/widgets/custom_point_text_tag.dart';
import '../../shared/widgets/image_loader.dart';
import 'models/product_model.dart';
class VoucherCodeCardScreen extends StatelessWidget {
......
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../base/base_screen.dart';
import '../../../base/basic_state.dart';
import '../../../configs/constants.dart';
import '../../../shared/widgets/base_view/base_screen.dart';
import '../../../shared/widgets/base_view/basic_state.dart';
import '../../../app/config/constants.dart';
import '../../../shared/router_gage.dart';
import '../../../widgets/custom_empty_widget.dart';
import '../../../widgets/custom_navigation_bar.dart';
import '../../../widgets/custom_search_navigation_bar.dart';
import '../../../shared/widgets/custom_empty_widget.dart';
import '../../../shared/widgets/custom_navigation_bar.dart';
import '../../../shared/widgets/custom_search_navigation_bar.dart';
import '../sub_widget/voucher_item_list.dart';
import 'voucher_list_viewmodel.dart';
......@@ -142,17 +142,18 @@ class _VoucherListScreenState extends BaseState<VoucherListScreen> with BasicSta
Expanded(
child: Obx(() {
if (_viewModel.products.isEmpty) {
return const Center(child: EmptyWidget());
return Center(child: EmptyWidget(isLoading: _viewModel.isLoading.value));
}
// Countdown start được điều phối ở initState qua ever(isLoading)
return RefreshIndicator(
onRefresh: () => _viewModel.loadData(reset: true),
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: _viewModel.products.length + (_viewModel.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= _viewModel.products.length) {
if (index >= _viewModel.products.length && _viewModel.products.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_viewModel.loadData(reset: false);
});
return const Center(
child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator()),
);
......
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/api/product_api.dart' deferred as product_api;
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 'package:mypoint_flutter_app/core/network/api/product_api.dart' deferred as product_api;
import 'package:mypoint_flutter_app/core/network/restful_api_client_all_request.dart';
import '../../../shared/widgets/base_view/base_response_model.dart';
import '../../../core/network/restful_api_viewmodel.dart';
import '../models/product_model.dart';
import '../models/product_type.dart';
import '../models/search_product_response_model.dart';
class VoucherListViewModel extends RestfulApiViewModel {
VoucherListViewModel({required this.isHotProduct, this.isFavorite = false});
......@@ -14,16 +14,15 @@ class VoucherListViewModel extends RestfulApiViewModel {
final bool isHotProduct;
Timer? _debounce;
final RxList<ProductModel> products = <ProductModel>[].obs;
var isLoading = false.obs;
var isLoadMore = false.obs;
int _currentPage = 0;
final int _pageSize = 20;
@override
final RxBool isLoading = false.obs;
bool _hasMore = true;
bool get hasMore => _hasMore;
String _searchQuery = '';
String get searchQuery => _searchQuery;
var totalResult = 0.obs;
/// Đánh dấu đã hoàn tất lần tải đầu tiên (có dữ liệu) để UI có thể bắt đầu countdown
final firstLoadDone = false.obs;
void Function(BaseResponseModel<SubmitViewVoucherCompletedResponse> response)? submitCampaignViewVoucherResponse;
bool _productApiLoaded = false;
......@@ -77,30 +76,28 @@ class VoucherListViewModel extends RestfulApiViewModel {
products.clear();
} else {
_currentPage = products.length;
}
if (!_hasMore) return;
if (reset) {
showLoading();
}
final body = {"size": _pageSize, "index": _currentPage};
try {
isLoading.value = true;
isLoadMore.value = true;
final result = await _callProductApi((api) => api.productsCustomerLikes(body));
final fetchedData = result.data ?? [];
if (fetchedData.isEmpty || fetchedData.length < _pageSize) {
final body = {"size": _pageSize, "index": _currentPage};
await callApi<List<ProductModel>>(
request: () => _callProductApi((api) => api.productsCustomerLikes(body)),
onSuccess: (data, _) {
if (data.isEmpty || data.length < _pageSize) {
_hasMore = false;
}
products.addAll(fetchedData);
} catch (error) {
debugPrint("Error fetching products: $error");
} finally {
hideLoading();
products.addAll(data);
},
onFailure: (message, _, _) {
_hasMore = false;
// onShowAlertError?.call(message);
},
onComplete: () {
isLoading.value = false;
isLoadMore.value = false;
// Khi lần đầu có dữ liệu, đánh dấu để UI start countdown
if (products.isNotEmpty) firstLoadDone.value = true;
}
},
withLoading: reset,
);
}
Future<void> _getProducts({bool reset = false}) async {
......@@ -113,9 +110,6 @@ class VoucherListViewModel extends RestfulApiViewModel {
_currentPage = products.length;
}
if (!_hasMore) return;
if (reset) {
showLoading();
}
final body = {
"type": ProductType.voucher.value,
"size": _pageSize,
......@@ -124,26 +118,27 @@ class VoucherListViewModel extends RestfulApiViewModel {
if (_searchQuery.isNotEmpty) "keywords": _searchQuery,
if (_searchQuery.isNotEmpty) "keyword": _searchQuery,
};
try {
isLoading.value = true;
isLoadMore.value = true;
final result = await _callProductApi((api) => api.getSearchProducts(body));
final fetchedData = result.data?.products ?? [];
totalResult.value = result.data?.total ?? 0;
await callApi<SearchProductResponseModel>(
request: () => _callProductApi((api) => api.getSearchProducts(body)),
onSuccess: (data, _) {
final fetchedData = data.products ?? [];
totalResult.value = data.total ?? 0;
if (fetchedData.isEmpty || fetchedData.length < _pageSize) {
_hasMore = false;
}
products.addAll(fetchedData);
} catch (error) {
debugPrint("Error fetching products: $error");
} finally {
hideLoading();
},
onFailure: (message, _, _) {
_hasMore = false;
// onShowAlertError?.call(message);
},
onComplete: () {
isLoading.value = false;
isLoadMore.value = false;
// Khi lần đầu có dữ liệu, đánh dấu để UI start countdown
if (products.isNotEmpty) firstLoadDone.value = true;
}
},
withLoading: reset,
);
}
void submitCampaignViewVoucherComplete() async {
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/screen/voucher/voucher_list/voucher_list_screen.dart';
import '../../directional/directional_action_type.dart';
import '../../app/routing/directional_action_type.dart';
import '../../shared/router_gage.dart';
import '../home/header_home_viewmodel.dart';
import '../popup_manager/popup_manager_screen.dart';
import '../popup_manager/popup_manager_viewmodel.dart';
import '../popup_manager/popup_runner_helper.dart';
import 'voucher_tab_viewmodel.dart';
import 'sub_widget/voucher_action_menu.dart';
import 'sub_widget/voucher_item_grid.dart';
import 'sub_widget/voucher_item_list.dart';
import 'sub_widget/voucher_section_title.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../../shared/widgets/custom_navigation_bar.dart';
class VoucherTabScreen extends StatefulWidget {
const VoucherTabScreen({super.key});
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/api/product_api.dart' deferred as product_api;
import 'package:mypoint_flutter_app/screen/voucher/models/product_type.dart';
import '../../base/base_response_model.dart';
import '../../networking/restful_api_viewmodel.dart';
import 'package:mypoint_flutter_app/core/network/api/product_api.dart' deferred as product_api;
import 'package:mypoint_flutter_app/features/voucher/models/product_type.dart';
import '../../shared/widgets/base_view/base_response_model.dart';
import '../../core/network/restful_api_viewmodel.dart';
import 'models/product_model.dart';
class VoucherTabViewModel extends RestfulApiViewModel {
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import 'package:mypoint_flutter_app/shared/widgets/image_loader.dart';
import '../../shared/router_gage.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../../shared/widgets/custom_navigation_bar.dart';
import '../faqs/faqs_model.dart';
import '../news/news_list_viewmodel.dart';
......@@ -15,6 +15,7 @@ class VplayGameCenterScreen extends StatelessWidget {
final width = MediaQuery.of(context).size.width;
final space = 12.0;
final itemWidth = (width - space * 3) / 2;
return Scaffold(
appBar: CustomNavigationBar(title: "Trung tâm trò chơi"),
body: Obx(() {
......
......@@ -4,15 +4,14 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../directional/directional_screen.dart';
import '../../resources/base_color.dart';
import '../../shared/widgets/base_view/base_screen.dart';
import '../../shared/widgets/base_view/basic_state.dart';
import '../../shared/navigation/directional_screen.dart';
import '../../core/theme/base_color.dart';
import '../../shared/router_gage.dart';
import '../../widgets/alert/data_alert_model.dart';
import '../../widgets/back_button.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../../shared/widgets/alert/data_alert_model.dart';
import '../../shared/widgets/back_button.dart';
import '../../shared/widgets/custom_navigation_bar.dart';
enum PaymentProcess {
begin,
......
......@@ -4,19 +4,19 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/back_button.dart';
import 'package:mypoint_flutter_app/widgets/custom_toast_message.dart';
import 'package:mypoint_flutter_app/shared/widgets/back_button.dart';
import 'package:mypoint_flutter_app/shared/widgets/custom_toast_message.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../base/app_loading.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../directional/directional_screen.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../../preference/data_preference.dart';
import '../../preference/package_info.dart';
import '../../shared/widgets/loading/app_loading.dart';
import '../../shared/widgets/base_view/base_screen.dart';
import '../../shared/widgets/base_view/basic_state.dart';
import '../../shared/navigation/directional_screen.dart';
import '../../shared/widgets/custom_navigation_bar.dart';
import '../../shared/preferences/data_preference.dart';
import '../../core/services/package_info.dart';
/// Payload for launching [BaseWebViewScreen].
class BaseWebViewInput {
......
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/base/app_navigator.dart';
import 'package:mypoint_flutter_app/resources/base_color.dart';
import 'package:mypoint_flutter_app/screen/splash/splash_screen.dart';
import 'package:mypoint_flutter_app/app/routing/app_navigator.dart';
import 'package:mypoint_flutter_app/core/theme/base_color.dart';
import 'package:mypoint_flutter_app/features/splash/splash_screen.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
import 'package:mypoint_flutter_app/core/app_initializer.dart';
import 'package:mypoint_flutter_app/app/app_initializer.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
......@@ -14,6 +15,7 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
if (kIsWeb) {
setUrlStrategy(PathUrlStrategy());
await _precacheWebSplashImage();
}
// Initialize all app features
await AppInitializer.initialize();
......@@ -23,7 +25,6 @@ void main() async {
AppInitializer.setupPostInitCallbacks();
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
......@@ -44,3 +45,30 @@ class MyApp extends StatelessWidget {
);
}
}
Future<void> _precacheWebSplashImage() async {
if (!kIsWeb) return;
try {
final imageProvider = const AssetImage('assets/images/splash_screen.webp');
final stream = imageProvider.resolve(ImageConfiguration.empty);
final completer = Completer<void>();
late final ImageStreamListener listener;
listener = ImageStreamListener(
(info, synchronousCall) {
stream.removeListener(listener);
if (!completer.isCompleted) completer.complete();
},
onError: (error, stackTrace) {
stream.removeListener(listener);
if (!completer.isCompleted) completer.completeError(error, stackTrace);
},
);
stream.addListener(listener);
await completer.future.timeout(const Duration(seconds: 2), onTimeout: () {
stream.removeListener(listener);
});
} catch (error, stackTrace) {
debugPrint('Failed to precache splash image: $error');
debugPrintStack(stackTrace: stackTrace);
}
}
import 'package:mypoint_flutter_app/base/base_response_model.dart';
import 'package:mypoint_flutter_app/configs/api_paths.dart';
import 'package:mypoint_flutter_app/configs/callbacks.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import '../../screen/affiliate/model/affiliate_brand_model.dart';
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';
class AffiliateApi {
AffiliateApi(this.client);
final RestfulAPIClient client;
Future<BaseResponseModel<List<AffiliateCategoryModel>>>
affiliateCategoryGetList() async {
final token = DataPreference.instance.token ?? "";
final body = {"access_token": token};
return client.requestNormal(
APIPaths.affiliateCategoryGetList,
Method.POST,
body,
(data) {
final list = data as List<dynamic>;
return list.map((e) => AffiliateCategoryModel.fromJson(e)).toList();
},
);
}
Future<BaseResponseModel<List<AffiliateBrandModel>>> affiliateBrandGetList({
String? categoryCode,
}) async {
final token = DataPreference.instance.token ?? "";
final body = {"access_token": token};
if ((categoryCode ?? '').isNotEmpty) {
body['category_code'] = categoryCode!;
}
return client.requestNormal(
APIPaths.affiliateBrandGetList,
Method.POST,
body,
(data) {
final list = data as List<dynamic>;
return list.map((e) => AffiliateBrandModel.fromJson(e)).toList();
},
);
}
Future<BaseResponseModel<List<AffiliateProductTopSaleModel>>>
affiliateProductTopSale() async {
final token = DataPreference.instance.token ?? "";
final body = {"access_token": token};
return client.requestNormal(
APIPaths.affiliateProductTopSale,
Method.POST,
body,
(data) {
final list = data as List<dynamic>;
return list
.map((e) => AffiliateProductTopSaleModel.fromJson(e))
.toList();
},
);
}
Future<BaseResponseModel<CashbackOverviewModel>> getCashBackOverview() async {
return client.requestNormal(APIPaths.getCashbackOverview, Method.GET, {}, (
data,
) {
return CashbackOverviewModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<AffiliateBrandDetailModel>> getAffiliateBrandDetail(
String brandId,
) async {
final token = DataPreference.instance.token ?? "";
final body = {"access_token": token, "brand_id": brandId};
return client.requestNormal(
APIPaths.affiliateBrandGetDetail,
Method.POST,
body,
(data) {
return AffiliateBrandDetailModel.fromJson(data as Json);
},
);
}
}
import 'package:json_annotation/json_annotation.dart';
part 'header_home_model.g.dart';
@JsonSerializable()
class HeaderHomeModel {
final String? greeting;
@JsonKey(name: 'total_voucher')
final int? totalVoucher;
@JsonKey(name: 'total_point_active')
final int? totalPointActive;
final String? background;
HeaderHomeModel({
this.greeting,
this.totalVoucher,
this.totalPointActive,
this.background,
});
factory HeaderHomeModel.fromJson(Map<String, dynamic> json) => _$HeaderHomeModelFromJson(json);
Map<String, dynamic> toJson() => _$HeaderHomeModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'header_home_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
HeaderHomeModel _$HeaderHomeModelFromJson(Map<String, dynamic> json) =>
HeaderHomeModel(
greeting: json['greeting'] as String?,
totalVoucher: (json['total_voucher'] as num?)?.toInt(),
totalPointActive: (json['total_point_active'] as num?)?.toInt(),
background: json['background'] as String?,
);
Map<String, dynamic> _$HeaderHomeModelToJson(HeaderHomeModel instance) =>
<String, dynamic>{
'greeting': instance.greeting,
'total_voucher': instance.totalVoucher,
'total_point_active': instance.totalPointActive,
'background': instance.background,
};
class PreviewFlashSale {
final int? id;
final int? countdownSecond;
final String? startTime;
final String? endTime;
final int? fsQuantityTotal;
final int? fsQuantitySold;
final int? percentTag;
final String? rewardType;
final String? rewardContent;
final String? openingContent;
final String? name;
final String? rewardPopup;
final int? price;
final bool? isFlashSale;
final bool? isFlashSalePrice;
final String? headerImg;
final int? fsQuantityPerPersonTotal;
final int? fsQuantityPerPersonBought;
const PreviewFlashSale({
this.id,
this.countdownSecond,
this.startTime,
this.endTime,
this.fsQuantityTotal,
this.fsQuantitySold,
this.percentTag,
this.rewardType,
this.rewardContent,
this.openingContent,
this.name,
this.rewardPopup,
this.price,
this.isFlashSale,
this.isFlashSalePrice,
this.headerImg,
this.fsQuantityPerPersonTotal,
this.fsQuantityPerPersonBought,
});
factory PreviewFlashSale.fromJson(Map<String, dynamic> json) {
return PreviewFlashSale(
id: json['id'] as int?,
countdownSecond: json['countdown_second'] as int?,
startTime: json['start_time'] as String?,
endTime: json['end_time'] as String?,
fsQuantityTotal: json['fs_quantity_total'] as int?,
fsQuantitySold: json['fs_quantity_sold'] as int?,
percentTag: json['percent_tag'] as int?,
rewardType: json['reward_type'] as String?,
rewardContent: json['reward_content'] as String?,
openingContent: json['opening_content'] as String?,
name: json['name'] as String?,
rewardPopup: json['reward_popup'] as String?,
price: json['price'] as int?,
isFlashSale: json['is_flash_sale'] as bool?,
isFlashSalePrice: json['is_flash_sale_price'] as bool?,
headerImg: json['header_img'] as String?,
fsQuantityPerPersonTotal: json['fs_quantity_per_person_total'] as int?,
fsQuantityPerPersonBought: json['fs_quantity_per_person_bought'] as int?,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'countdown_second': countdownSecond,
'start_time': startTime,
'end_time': endTime,
'fs_quantity_total': fsQuantityTotal,
'fs_quantity_sold': fsQuantitySold,
'percent_tag': percentTag,
'reward_type': rewardType,
'reward_content': rewardContent,
'opening_content': openingContent,
'name': name,
'reward_popup': rewardPopup,
'price': price,
'is_flash_sale': isFlashSale,
'is_flash_sale_price': isFlashSalePrice,
'header_img': headerImg,
'fs_quantity_per_person_total': fsQuantityPerPersonTotal,
'fs_quantity_per_person_bought': fsQuantityPerPersonBought,
};
double? get progress {
if (fsQuantityTotal != null && fsQuantitySold != null && fsQuantityTotal! > 0) {
return fsQuantitySold! / fsQuantityTotal!;
}
return null;
}
bool get isSoldOut => fsQuantitySold == fsQuantityTotal;
String get textQuantitySold => isSoldOut ? "Đã bán hết" : "Đã bán ${fsQuantitySold ?? 0}";
DateTime? get startDate => _parseDate(startTime)?.subtract(Duration(seconds: 1));
DateTime? get endDate => _parseDate(endTime)?.add(Duration(seconds: 1));
bool? get isGoingOn {
final now = DateTime.now();
if (startDate != null && endDate != null && now.isBefore(endDate!)) {
return now.isAfter(startDate!);
}
return null;
}
String? get desTime {
final go = isGoingOn;
if (go == null) return null;
return go ? "Kết thúc trong" : "Bắt đầu sau";
}
Duration? get countdownLocal {
final now = DateTime.now();
if (isGoingOn == true) {
return endDate?.difference(now);
} else {
return startDate?.difference(now);
}
}
int? get maximumQuantityPurchased {
if (fsQuantityPerPersonTotal != null) {
final bought = fsQuantityPerPersonBought ?? 0;
return (fsQuantityPerPersonTotal! - bought).clamp(0, fsQuantityPerPersonTotal!);
}
return null;
}
bool get isShowProgressSoldItemCell => isFlashSale == true && (fsQuantityTotal ?? 0) > 0;
bool get isHidenOpeningContent =>
openingContent == null || openingContent!.isEmpty || isGoingOn == true || isFlashSalePrice == true;
bool get isHasReward => rewardContent != null && rewardContent!.isNotEmpty;
String? get rewardImageAsset {
return rewardType == "point" ? "assets/icons/ic_point.png" : "assets/icons/ic_gift_flash_sale.png";
}
DateTime? _parseDate(String? str) {
if (str == null) return null;
return DateTime.tryParse(str);
}
}
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import 'package:mypoint_flutter_app/screen/popup_manager/popup_manager_model.dart';
import '../../networking/restful_api_viewmodel.dart';
class PopupManagerViewModel extends RestfulApiViewModel {
PopupManagerViewModel._();
static final PopupManagerViewModel instance = PopupManagerViewModel._();
final Set<String> _shownIds = {};
List<PopupManagerModel>? _popupData;
bool _loaded = false;
Future<void>? _loadingFuture;
Future<void> ensureLoaded() async {
if (_loaded) return;
if (_loadingFuture != null) {
return _loadingFuture;
}
_loadingFuture = _getPopupManagerDataInternal();
await _loadingFuture;
}
Future<void> _getPopupManagerDataInternal() async {
try {
final response = await client.getPopupManagerCommonScreen();
_popupData = response.data ?? [];
// _popupData = [
// PopupManagerModel(
// id: '1',
// screenToShow: 'APP_SCREEN_HOME',
// clickActionType: 'VIEW_PRODUCT_VOUCHER',
// clickActionParam: '50760',
// posActionID: 'action1',
// posActionCode: 'code1',
// timeToShow: '10:00-18:00',
// timeCountDown: '30',
// hourStartInDay: '9',
// hourStopInDay: '17',
// afterPosID: 'pos1',
// afterPosCode: 'posCode1',
// afterPosName: 'POS Name 1',
// marketingRequestDescription: 'Marketing description here.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'daily',
// scheduleRunTypeName: 'Daily Schedule',
// scheduleAtTime: '12:00',
// popupTitleTemplate: 'APP_SCREEN_HOME',
// popupBodyTemplate: 'Enjoy your stay and check out our features.',
// imageID: 'image123',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '2',
// screenToShow: 'APP_SCREEN_POINTBACK',
// clickActionType: 'APP_SCREEN_SIM_SERVICE',
// clickActionParam: 'https://example.com/settings',
// posActionID: 'action2',
// posActionCode: 'code2',
// timeToShow: '08:00-20:00',
// timeCountDown: '60',
// hourStartInDay: '8',
// hourStopInDay: '20',
// afterPosID: 'pos2',
// afterPosCode: 'posCode2',
// afterPosName: 'POS Name 2',
// marketingRequestDescription: 'Settings popup description.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'weekly',
// scheduleRunTypeName: 'Weekly Schedule',
// scheduleAtTime: '10:00',
// popupTitleTemplate: 'APP_SCREEN_POINTBACK',
// popupBodyTemplate: 'Check out the new settings options.',
// imageID: 'image456',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '3',
// screenToShow: 'APP_SCREEN_PRODUCT_VOUCHER',
// clickActionType: 'APP_SCREEN_GIFTS',
// clickActionParam: 'Profile updated successfully.',
// posActionID: 'action3',
// posActionCode: 'code3',
// timeToShow: '09:00-21:00',
// timeCountDown: '45',
// hourStartInDay: '9',
// hourStopInDay: '21',
// afterPosID: 'pos3',
// afterPosCode: 'posCode3',
// afterPosName: 'POS Name 3',
// marketingRequestDescription: 'Profile update alert.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'monthly',
// scheduleRunTypeName: 'Monthly Schedule',
// scheduleAtTime: '15:00',
// popupTitleTemplate: 'APP_SCREEN_PRODUCT_VOUCHER',
// popupBodyTemplate: 'Your profile has been updated successfully.',
// imageID: 'image789',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '4',
// screenToShow: 'APP_SCREEN_GAME_BUNDLE',
// clickActionType: 'APP_SCREEN_CAMPAIGN_WALKING',
// clickActionParam: '1',
// posActionID: 'action3',
// posActionCode: 'code3',
// timeToShow: '09:00-21:00',
// timeCountDown: '45',
// hourStartInDay: '9',
// hourStopInDay: '21',
// afterPosID: 'pos3',
// afterPosCode: 'posCode3',
// afterPosName: 'POS Name 3',
// marketingRequestDescription: 'Profile update alert.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'monthly',
// scheduleRunTypeName: 'Monthly Schedule',
// scheduleAtTime: '15:00',
// popupTitleTemplate: 'APP_SCREEN_GAME_BUNDLE',
// popupBodyTemplate: 'Your profile has been updated successfully.',
// imageID: 'image789',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// PopupManagerModel(
// id: '5',
// screenToShow: 'APP_SCREEN_PERSONAL',
// clickActionType: 'APP_SCREEN_SIM_SERVICE',
// clickActionParam: 'Profile updated successfully.',
// posActionID: 'action3',
// posActionCode: 'code3',
// timeToShow: '09:00-21:00',
// timeCountDown: '45',
// hourStartInDay: '9',
// hourStopInDay: '21',
// afterPosID: 'pos3',
// afterPosCode: 'posCode3',
// afterPosName: 'POS Name 3',
// marketingRequestDescription: 'Profile update alert.',
// effectiveFromDate: '2023-01-01',
// effectiveToDate: '2023-12-31',
// scheduleRunTypeCode: 'monthly',
// scheduleRunTypeName: 'Monthly Schedule',
// scheduleAtTime: '15:00',
// popupTitleTemplate: 'APP_SCREEN_PERSONAL',
// popupBodyTemplate: 'Your profile has been updated successfully.',
// imageID: 'image789',
// imageURL: 'https://picsum.photos/1200/800',
// ),
// ];
_loaded = true;
} catch (e) {
_popupData = [];
_loaded = true;
rethrow;
} finally {
_loadingFuture = null;
}
}
PopupManagerModel? getForScreen(String screenName) {
if (_popupData == null || _popupData!.isEmpty) return null;
final idx = _popupData!.indexWhere(
(e) => (e.screenToShow ?? '').trim().toUpperCase() == screenName.trim().toUpperCase(),
);
if (idx < 0) return null;
final found = _popupData![idx];
if (_shownIds.contains(found.id)) return null;
return found;
}
Future<void> markShownOnce(String popupId) async {
_shownIds.add(popupId);
}
Future<void> reset() async {
_shownIds.clear();
_popupData = [];
_loaded = false;
_loadingFuture = null;
}
}
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