Commit efb4662c authored by DatHV's avatar DatHV
Browse files

update campaign 7day

parent 4c376d38
......@@ -65,7 +65,7 @@ class VoucherListItem extends StatelessWidget {
if (!product.inStock)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.5),
color: Colors.black.withOpacity(0.3),
alignment: Alignment.center,
child: const Text(
'Tạm hết',
......
......@@ -19,6 +19,7 @@ class _VoucherListScreenState extends State<VoucherListScreen> {
late final Map<String, dynamic> args;
late final bool enableSearch;
late final bool isHotProduct;
late final bool isFavorite;
late final VoucherListViewModel _viewModel;
@override
......@@ -27,12 +28,13 @@ class _VoucherListScreenState extends State<VoucherListScreen> {
args = Get.arguments ?? {};
enableSearch = args['enableSearch'] ?? false;
isHotProduct = args['isHotProduct'] ?? false;
_viewModel = Get.put(VoucherListViewModel(isHotProduct: isHotProduct));
isFavorite = args['favorite'] ?? false;
_viewModel = Get.put(VoucherListViewModel(isHotProduct: isHotProduct, isFavorite: isFavorite));
}
@override
Widget build(BuildContext context) {
final String title = isHotProduct ? 'Săn ưu đãi' : 'Tất cả ưu đãi';
final String title = isFavorite ? 'Yêu thích' : (isHotProduct ? 'Săn ưu đãi' : 'Tất cả ưu đãi');
return Scaffold(
appBar:
enableSearch
......@@ -69,21 +71,22 @@ class _VoucherListScreenState extends State<VoucherListScreen> {
);
}
return RefreshIndicator(
onRefresh: () => _viewModel.getProducts(reset: true),
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) {
_viewModel.getProducts(reset: false);
_viewModel.loadData(reset: false);
return const Center(
child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator()),
);
}
final product = _viewModel.products[index];
return GestureDetector(
onTap: () {
Get.toNamed(voucherDetailScreen, arguments: {"productId": product.id});
onTap: () async {
await Get.toNamed(voucherDetailScreen, arguments: {"productId": product.id});
_viewModel.loadData(reset: true);
},
child: VoucherListItem(product: product),
);
......
......@@ -6,8 +6,8 @@ import '../models/product_model.dart';
import '../models/product_type.dart';
class VoucherListViewModel extends RestfulApiViewModel {
VoucherListViewModel({required this.isHotProduct});
VoucherListViewModel({required this.isHotProduct, this.isFavorite = false});
final bool isFavorite;
final bool isHotProduct;
Timer? _debounce;
var products = <ProductModel>[].obs;
......@@ -24,7 +24,7 @@ class VoucherListViewModel extends RestfulApiViewModel {
@override
void onInit() {
super.onInit();
getProducts(reset: true);
loadData(reset: true);
}
@override
......@@ -38,11 +38,47 @@ class VoucherListViewModel extends RestfulApiViewModel {
_searchQuery = value;
_debounce?.cancel();
_debounce = Timer(const Duration(seconds: 1), () {
getProducts(reset: true);
loadData(reset: true);
});
}
Future<void> getProducts({bool reset = false}) async {
Future<void> loadData({bool reset = false}) async {
if (isFavorite) {
await _getFavoriteProducts(reset: reset);
} else {
await _getProducts(reset: reset);
}
}
Future<void> _getFavoriteProducts({bool reset = false}) async {
if (isLoading.value) return;
if (reset) {
_currentPage = 0;
_hasMore = true;
products.clear();
} else {
_currentPage = products.length;
}
if (!_hasMore) return;
final body = {"size": _pageSize, "index": _currentPage};
try {
isLoading.value = true;
isLoadMore.value = true;
final result = await client.productsCustomerLikes(body);
final fetchedData = result.data ?? [];
if (fetchedData.isEmpty || fetchedData.length < _pageSize) {
_hasMore = false;
}
products.addAll(fetchedData);
} catch (error) {
print("Error fetching products: $error");
} finally {
isLoading.value = false;
isLoadMore.value = false;
}
}
Future<void> _getProducts({bool reset = false}) async {
if (isLoading.value) return;
if (reset) {
_currentPage = 0;
......
......@@ -41,6 +41,7 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
});
return;
}
showLoading();
_controller =
WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
......@@ -48,12 +49,14 @@ class _BaseWebViewScreenState extends BaseState<BaseWebViewScreen> with BasicSta
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (_) async {
hideLoading();
final title = await _controller.getTitle();
setState(() {
_dynamicTitle = title;
});
},
onWebResourceError: (error) {
hideLoading();
if (error.description != 'about:blank') {
showAlertError(content: error.description);
}
......
......@@ -6,9 +6,12 @@ import '../screen/affiliate/affiliate_tab_screen.dart';
import '../screen/affiliate_brand_detail/affiliate_brand_detail_screen.dart';
import '../screen/affiliate_brand_detail/affiliate_brand_list_screen.dart';
import '../screen/affiliate_brand_detail/affiliate_category_grid_screen.dart';
import '../screen/campaign7day/campaign_7day_screen.dart';
import '../screen/contacts/contacts_list_screen.dart';
import '../screen/daily_checkin/daily_checkin_screen.dart';
import '../screen/data_network_service/data_network_service_screen.dart';
import '../screen/electric_payment/electric_payment_history_screen.dart';
import '../screen/electric_payment/electric_payment_screen.dart';
import '../screen/game/game_cards/game_card_screen.dart';
import '../screen/game/game_tab_screen.dart';
import '../screen/history_point_cashback/history_point_cashback_screen.dart';
......@@ -23,13 +26,17 @@ import '../screen/onboarding/onboarding_screen.dart';
import '../screen/order_menu/order_menu_screen.dart';
import '../screen/pageDetail/campaign_detail_screen.dart';
import '../screen/personal/personal_edit_screen.dart';
import '../screen/quiz_campaign/quiz_campaign_screen.dart';
import '../screen/register_campaign/register_form_input_screen.dart';
import '../screen/setting/setting_screen.dart';
import '../screen/splash/splash_screen.dart';
import '../screen/support/support_screen.dart';
import '../screen/topup/topup_screen.dart';
import '../screen/traffic_service/traffic_service_detail_screen.dart';
import '../screen/traffic_service/traffic_service_screen.dart';
import '../screen/transaction/history/transaction_history_detail_screen.dart';
import '../screen/transaction/transaction_detail_screen.dart';
import '../screen/transaction/transactions_history_screen.dart';
import '../screen/voucher/detail/voucher_detail_screen.dart';
import '../screen/voucher/my_voucher/my_product_list_widget.dart';
import '../screen/voucher/voucher_list/voucher_list_screen.dart';
......@@ -73,6 +80,13 @@ const affiliateCategoryGridScreen = '/affiliateCategoryGridScreen';
const inviteFriendCampaignScreen = '/inviteFriendCampaignScreen';
const contactsListScreen = '/contactsListScreen';
const dailyCheckInScreen = '/dailyCheckInScreen';
const transactionHistoryScreen = '/transactionHistoryScreen';
const electricPaymentScreen = '/electricPaymentScreen';
const electricPaymentHistoryScreen = '/electricPaymentHistoryScreen';
const trafficServiceScreen = '/trafficServiceScreen';
const trafficServiceDetailScreen = '/trafficServiceDetailScreen';
const campaignSevenDayScreen = '/campaignSevenDayScreen';
const surveyQuestionScreen = '/surveyQuestionScreen';
class RouterPage {
static List<GetPage> pages() {
......@@ -86,7 +100,13 @@ class RouterPage {
GetPage(name: splashScreen, page: () => SplashScreen()),
GetPage(name: onboardingScreen, page: () => OnboardingScreen()),
GetPage(name: loginScreen, page: () => LoginScreen()),
GetPage(name: mainScreen, page: () => MainTabScreen(), customTransition: NoSwipeBackTransition()),
GetPage(
name: mainScreen,
page: () => MainTabScreen(),
participatesInRootNavigator: true,
fullscreenDialog: true,
binding: BindingsBuilder(() {}),
),
GetPage(name: settingScreen, page: () => SettingScreen()),
GetPage(name: vouchersScreen, page: () => VoucherListScreen()),
GetPage(name: voucherDetailScreen, page: () => VoucherDetailScreen()),
......@@ -119,20 +139,13 @@ class RouterPage {
GetPage(name: inviteFriendCampaignScreen, page: () => InviteFriendCampaignScreen()),
GetPage(name: contactsListScreen, page: () => ContactsListScreen()),
GetPage(name: dailyCheckInScreen, page: () => DailyCheckInScreen()),
GetPage(name: transactionHistoryScreen, page: () => TransactionHistoryScreen()),
GetPage(name: electricPaymentScreen, page: () => ElectricPaymentScreen()),
GetPage(name: electricPaymentHistoryScreen, page: () => ElectricPaymentHistoryScreen()),
GetPage(name: trafficServiceScreen, page: () => TrafficServiceScreen()),
GetPage(name: trafficServiceDetailScreen, page: () => TrafficServiceDetailScreen()),
GetPage(name: campaignSevenDayScreen, page: () => Campaign7DayScreen()),
GetPage(name: surveyQuestionScreen, page: () => SurveyQuestionScreen()),
];
}
}
\ No newline at end of file
class NoSwipeBackTransition extends CustomTransition {
@override
Widget buildTransition(
BuildContext context,
Curve? curve,
Alignment? alignment,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return child;
}
}
\ No newline at end of file
......@@ -24,7 +24,7 @@ class ButtonConfigModel {
final bgColor = color?.toColor() ?? Colors.white;
return AlertButton(
text: text ?? "",
textColor: bgColor.invert,
textColor: bgColor.contrastTextColor,
bgColor: bgColor,
onPressed: () async {
DirectionalScreen? directional = DirectionalScreen.build(
......
......@@ -24,7 +24,7 @@ class CustomAlertDialog extends StatelessWidget {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(0),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: Colors.white),
child: Stack(
children: [
......@@ -32,8 +32,10 @@ class CustomAlertDialog extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
_buildHeaderImage(),
const SizedBox(height: 2),
// Title
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
if ((alertData.title ?? "").isNotEmpty)
Text(
alertData.title!,
......@@ -54,16 +56,18 @@ class CustomAlertDialog extends StatelessWidget {
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: BaseColor.primary500),
textAlign: TextAlign.center,
),
// Buttons
const SizedBox(height: 8),
_buildButtons(),
],
),
),
],
),
// Close Button (X) ở góc phải trên
if (showCloseButton)
Positioned(
top: 0,
right: 0,
top: 8,
right: 8,
child: GestureDetector(
onTap: () => Get.back(),
child: const Icon(Icons.close, color: Colors.black, size: 24),
......@@ -79,11 +83,14 @@ class CustomAlertDialog extends StatelessWidget {
if ((alertData.urlHeaderImage ?? "").isNotEmpty) {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Container(
// color: Colors.grey,
child: loadNetworkImage(
url: alertData.urlHeaderImage,
fit: BoxFit.cover,
fit: BoxFit.fill,
placeholderAsset: "assets/images/ic_pipi_06.png",
),
),
);
}
final localHeaderImage = (alertData.localHeaderImage ?? "");
......
......@@ -5,6 +5,9 @@ import 'package:get/get.dart';
class BottomSheetHelper {
static void showBottomSheetPopup({
required Widget child,
Color backgroundContainerColor = Colors.white,
double horizontalContainerPadding = 8.0,
double bottomContainerPadding = 16.0,
bool isDismissible = true,
}) {
showModalBottomSheet(
......@@ -12,11 +15,12 @@ class BottomSheetHelper {
isScrollControlled: true,
isDismissible: isDismissible,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withOpacity(0.5),
barrierColor: Colors.black.withOpacity(0.7),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
final bottom = MediaQuery.of(context).padding.bottom;
return Padding(
padding: MediaQuery.of(context).viewInsets.add(
const EdgeInsets.only(bottom: 0), // 👈 Safe area bottom
......@@ -24,11 +28,15 @@ class BottomSheetHelper {
child: Wrap(
children: [
Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
decoration: BoxDecoration(
color: backgroundContainerColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
padding: EdgeInsets.only(
left: horizontalContainerPadding,
right: horizontalContainerPadding,
bottom: bottomContainerPadding,
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
child: child,
),
SizedBox(height: 32,),
......
......@@ -16,7 +16,6 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
this.height = 56,
});
/// 🔥 AppBar mặc định với nút back và title
static CustomAppBar back({required String title}) {
return CustomAppBar(title: title, leftButtons: [CustomBackButton()]);
}
......
......@@ -9,7 +9,7 @@ class EmptyWidget extends StatelessWidget {
super.key,
this.imageAsset = 'assets/images/ic_pipi_06.png',
this.content = 'Không có dữ liệu hiển thị',
this.size = const Size(120, 120),
this.size = const Size(200, 200),
});
@override
......
......@@ -54,7 +54,7 @@ class CustomNavigationBar extends StatelessWidget implements PreferredSizeWidget
Text(
title,
maxLines: 1,
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold, color: Colors.white),
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w800, color: Colors.white),
textAlign: TextAlign.center,
),
// Back button bên trái
......
......@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
Widget loadNetworkImage({
required String? url,
BoxFit fit = BoxFit.cover,
BoxFit fit = BoxFit.contain,
double? width,
double? height,
String placeholderAsset = 'assets/images/ic_logo.png',
......
import 'package:contacts_service/contacts_service.dart';
import 'package:permission_handler/permission_handler.dart';
// Future<void> pickContact(BuildContext context) async {
// // Yêu cầu quyền truy cập danh bạ
// final status = await Permission.contacts.request();
// if (!status.isGranted) {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(content: Text('Bạn cần cấp quyền truy cập danh bạ')),
// );
// return;
// }
//
// // Mở danh sách liên hệ và chọn
// try {
// final Contact? contact = await ContactsService.openDeviceContactPicker();
// if (contact != null && contact.phones != null && contact.phones!.isNotEmpty) {
// final phoneNumber = contact.phones!.first.value;
// print("Số điện thoại được chọn: $phoneNumber");
//
// // TODO: bạn có thể gán vào TextEditingController ở đây
// _phoneController.text = phoneNumber ?? '';
// }
// } catch (e) {
// print("Lỗi khi chọn danh bạ: $e");
// }
// }
......@@ -30,6 +30,8 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
dio: ^5.8.0+1
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
......@@ -47,7 +49,7 @@ dependencies:
flutter_svg:
local_auth:
pin_code_fields:
intl: ^0.18.1
intl: ^0.19.0
webview_flutter: ^4.2.2
webview_flutter_wkwebview: ^3.9.4
qr_flutter: ^4.0.0
......@@ -59,6 +61,10 @@ dependencies:
flutter_contacts: ^1.1.6
permission_handler: ^11.0.0
share_plus: ^7.2.1
file_saver: ^0.2.2
month_picker_dialog:
marquee: ^2.2.3
game_miniapp:
path: ../mini_app/game_miniapp
dev_dependencies:
......
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