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

update voucher tab

parent d86c3328
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProductModel _$ProductModelFromJson(Map<String, dynamic> json) => ProductModel(
quantityAvailable: (json['quantity_available'] as num?)?.toInt(),
content:
json['content'] == null
? null
: ProductContentModel.fromJson(
json['content'] as Map<String, dynamic>,
),
price:
json['price'] == null
? null
: ProductPriceModel.fromJson(json['price'] as Map<String, dynamic>),
brand:
json['brand'] == null
? null
: ProductBrandModel.fromJson(json['brand'] as Map<String, dynamic>),
properties:
json['voucher_properties'] == null
? null
: ProductPropertiesModel.fromJson(
json['voucher_properties'] as Map<String, dynamic>,
),
media:
(json['media'] as List<dynamic>?)
?.map((e) => ProductMediaItem.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$ProductModelToJson(ProductModel instance) =>
<String, dynamic>{
'quantity_available': instance.quantityAvailable,
'content': instance.content,
'price': instance.price,
'brand': instance.brand,
'voucher_properties': instance.properties,
'media': instance.media,
};
import 'package:json_annotation/json_annotation.dart';
import 'cash_type.dart'; // Enum CashType
part 'product_price_model.g.dart';
@JsonSerializable()
class ProductPriceModel {
@JsonKey(name: "payment_method")
final String? paymentMethod;
@JsonKey(name: "sale_price")
final int? salePrice;
@JsonKey(name: "last_price")
final int? lastPrice;
ProductPriceModel({
this.paymentMethod,
this.salePrice,
this.lastPrice,
});
CashType get method => CashTypeExt.from(paymentMethod);
int? get value => lastPrice ?? salePrice;
String? get displayPriceType {
if (value == null) return null;
return value!.makeDisplayPrice(method);
}
String? get displayPriceCommon {
if (value == null) return null;
return "${value!.toString()} đ"; // Replace with your number formatting
}
factory ProductPriceModel.fromJson(Map<String, dynamic> json) => _$ProductPriceModelFromJson(json);
Map<String, dynamic> toJson() => _$ProductPriceModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_price_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProductPriceModel _$ProductPriceModelFromJson(Map<String, dynamic> json) =>
ProductPriceModel(
paymentMethod: json['payment_method'] as String?,
salePrice: (json['sale_price'] as num?)?.toInt(),
lastPrice: (json['last_price'] as num?)?.toInt(),
);
Map<String, dynamic> _$ProductPriceModelToJson(ProductPriceModel instance) =>
<String, dynamic>{
'payment_method': instance.paymentMethod,
'sale_price': instance.salePrice,
'last_price': instance.lastPrice,
};
import 'package:json_annotation/json_annotation.dart';
part 'product_properties_model.g.dart';
@JsonSerializable()
class ProductPropertiesModel {
final String? voucherType;
final double? voucherValue;
ProductPropertiesModel({this.voucherType, this.voucherValue});
String? get title {
if (voucherValue == null || voucherValue == 0) return null;
if (voucherType == "VOUCHER_TYPE_DISCOUNT") {
return "${voucherValue!.toStringAsFixed(0)}%";
}
return "$voucherValue đ";
}
factory ProductPropertiesModel.fromJson(Map<String, dynamic> json) => _$ProductPropertiesModelFromJson(json);
Map<String, dynamic> toJson() => _$ProductPropertiesModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_properties_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProductPropertiesModel _$ProductPropertiesModelFromJson(
Map<String, dynamic> json,
) => ProductPropertiesModel(
voucherType: json['voucherType'] as String?,
voucherValue: (json['voucherValue'] as num?)?.toDouble(),
);
Map<String, dynamic> _$ProductPropertiesModelToJson(
ProductPropertiesModel instance,
) => <String, dynamic>{
'voucherType': instance.voucherType,
'voucherValue': instance.voucherValue,
};
enum ProductType {
voucher,
topupMobile,
topupData,
mobileCard,
offline,
typeCard,
vnTra,
}
extension ProductTypeExt on ProductType {
String get value {
switch (this) {
case ProductType.voucher:
return 'PRODUCT_TYPE_VOUCHER';
case ProductType.topupMobile:
return 'PRODUCT_TYPE_TOPUP_MOBILE';
case ProductType.topupData:
return 'PRODUCT_TYPE_TOPUP_DATA';
case ProductType.mobileCard:
return 'PRODUCT_TYPE_MOBILE_CARD';
case ProductType.offline:
return 'PRODUCT_TYPE_OFFLINE';
case ProductType.typeCard:
return 'PRODUCT_TYPE_CARD';
case ProductType.vnTra:
return 'PRODUCT_TYPE_VNTRA_PACKAGE';
}
}
/// Parse từ String về enum
static ProductType? from(String? raw) {
switch (raw) {
case 'PRODUCT_TYPE_VOUCHER':
return ProductType.voucher;
case 'PRODUCT_TYPE_TOPUP_MOBILE':
return ProductType.topupMobile;
case 'PRODUCT_TYPE_TOPUP_DATA':
return ProductType.topupData;
case 'PRODUCT_TYPE_MOBILE_CARD':
return ProductType.mobileCard;
case 'PRODUCT_TYPE_OFFLINE':
return ProductType.offline;
case 'PRODUCT_TYPE_CARD':
return ProductType.typeCard;
case 'PRODUCT_TYPE_VNTRA_PACKAGE':
return ProductType.vnTra;
default:
return null;
}
}
}
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_model.dart';
part 'search_product_response_model.g.dart';
@JsonSerializable()
class SearchProductResponseModel {
final List<ProductModel>? products;
final int? total;
SearchProductResponseModel({this.products, this.total});
factory SearchProductResponseModel.fromJson(Map<String, dynamic> json) => _$SearchProductResponseModelFromJson(json);
Map<String, dynamic> toJson() => _$SearchProductResponseModelToJson(this);
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'search_product_response_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SearchProductResponseModel _$SearchProductResponseModelFromJson(
Map<String, dynamic> json,
) => SearchProductResponseModel(
products:
(json['products'] as List<dynamic>?)
?.map((e) => ProductModel.fromJson(e as Map<String, dynamic>))
.toList(),
total: (json['total'] as num?)?.toInt(),
);
Map<String, dynamic> _$SearchProductResponseModelToJson(
SearchProductResponseModel instance,
) => <String, dynamic>{'products': instance.products, 'total': instance.total};
import 'package:flutter/material.dart';
import '../../../resouce/base_color.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(
children: const [
_ActionItem(icon: Icons.phone_android, label: 'Nạp tiền\ndiện thoại'),
_ActionItem(icon: Icons.credit_card, label: 'Đổi mã\nthẻ nạp'),
_ActionItem(icon: Icons.wifi, label: 'Gói cước\nnhà mạng'),
_ActionItem(icon: Icons.card_giftcard, label: 'Ưu đãi\nData'),
],
),
);
}
}
class _ActionItem extends StatelessWidget {
final IconData icon;
final String label;
const _ActionItem({required this.icon, required this.label});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final itemWidth = screenWidth / 4;
return SizedBox(
width: itemWidth,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 40, color: BaseColor.primary400),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}
import 'package:flutter/material.dart';
import '../../../widgets/image_loader.dart';
import '../models/product_model.dart';
class VoucherItemGrid extends StatelessWidget {
final List<ProductModel> items;
const VoucherItemGrid({super.key, required this.items});
@override
Widget build(BuildContext context) {
final double itemWidth = MediaQuery.of(context).size.width * 0.7;
final double imageHeight = itemWidth / (16 / 9);
final double totalHeight = imageHeight + 80;
return SizedBox(
height: totalHeight,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: items.length,
separatorBuilder: (context, index) => const SizedBox(width: 12),
itemBuilder: (context, index) =>
_VoucherGridItem(product: items[index], itemWidth: itemWidth),
),
);
}
}
class _VoucherGridItem extends StatelessWidget {
final ProductModel product;
final double itemWidth;
const _VoucherGridItem({
super.key,
required this.product,
required this.itemWidth,
});
@override
Widget build(BuildContext context) {
final hasDiscount = product.properties?.title != null;
final priceText = product.price?.displayPriceType ?? '';
final brandName = product.brand?.name ?? '';
final brandLogo = product.brand?.logo ?? "";
final String? bgImage = product.banner?.url;
return Container(
width: itemWidth,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade200),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: SizedBox(
width: itemWidth,
height: itemWidth / (16 / 9),
child: loadNetworkImage(url: bgImage, placeholderAsset: "assets/images/sample.png"),
),
),
if (hasDiscount)
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12),
),
child: Text(
product.properties?.title ?? "",
style: const TextStyle(color: Colors.white, fontSize: 12),
),
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 70), // ✅ Không cứng height, mà giới hạn max
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title: auto co giãn nhưng không tràn
Text(
product.content?.name ?? '',
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(), // ✅ Spacer đẩy info xuống dưới nhưng không gây overflow
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircleAvatar(
radius: 10,
backgroundColor: Colors.transparent,
child: ClipOval(
child: loadNetworkImage(
url: brandLogo,
width: 20,
height: 20,
fit: BoxFit.cover,
placeholderAsset: 'assets/images/sample.png', // ⚠️ SVG dùng wrong tại đây!
),
),
),
const SizedBox(width: 4),
Expanded(
child: Text(
brandName,
style: const TextStyle(fontSize: 11),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Image.asset('assets/images/ic_point.png', width: 12, height: 12),
const SizedBox(width: 4),
Text(
priceText,
style: const TextStyle(color: Colors.orange, fontSize: 12),
),
],
),
),
],
),
],
),
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import '../../../resouce/base_color.dart';
import '../../../widgets/image_loader.dart';
import '../models/product_model.dart';
class VoucherItemList extends StatelessWidget {
final List<ProductModel> items;
const VoucherItemList({super.key, required this.items});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final product = items[index];
return VoucherListItem(product: product);
},
);
}
}
class VoucherListItem extends StatelessWidget {
final ProductModel product;
const VoucherListItem({super.key, required this.product});
@override
Widget build(BuildContext context) {
final productName = product.content?.name ?? '';
final brandName = product.brand?.name ?? '';
final brandLogo = product.brand?.logo ?? '';
final priceText = product.price?.displayPriceType ?? 'Miễn phí';
final isFree = priceText.contains("Miễn phí");
final String? bgImage = product.banner?.url;
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: SizedBox(
height: 112,
child: Row(
children: [
// Ảnh banner (16:9)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: AspectRatio(
aspectRatio: 16 / 9,
child: loadNetworkImage(
url: bgImage,
fit: BoxFit.cover,
placeholderAsset: 'assets/images/sample.png',
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
productName,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Logo thương hiệu
CircleAvatar(
radius: 10,
backgroundColor: Colors.transparent,
child: ClipOval(
child: loadNetworkImage(
url: brandLogo,
width: 16,
height: 16,
fit: BoxFit.cover,
placeholderAsset: 'assets/images/ic_logo.png',
),
),
),
const SizedBox(width: 4),
// Tên thương hiệu
Expanded(
child: Text(
brandName,
style: const TextStyle(fontSize: 11),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: isFree ? Colors.orange.shade50 : Colors.red.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset('assets/images/ic_point.png', width: 20, height: 20),
const SizedBox(width: 4),
Text(
priceText,
style: TextStyle(
fontSize: 12,
color: isFree ? Colors.orange : Colors.red,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Divider(
height: 1,
thickness: 1,
color: BaseColor.second200,
),
),
],
);
}
}
import 'package:flutter/material.dart';
class VoucherSectionTitle extends StatelessWidget {
final String title;
final VoidCallback? onViewAll; // 👈 Optional
const VoucherSectionTitle({
super.key,
required this.title,
this.onViewAll, // 👈 Nếu null thì không hiển thị button
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
if (onViewAll != null)
GestureDetector(
onTap: onViewAll,
child: const Text(
'Xem tất cả',
style: TextStyle(color: Colors.blue),
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../widgets/custom_navigation_bar.dart';
import '../../../widgets/custom_search_navigation_bar.dart';
import '../sub_widget/voucher_item_list.dart';
import 'voucher_list_viewmodel.dart';
class VoucherListScreen extends StatefulWidget {
const VoucherListScreen({super.key});
@override
_VoucherListScreenState createState() => _VoucherListScreenState();
}
class _VoucherListScreenState extends State<VoucherListScreen> {
late final Map<String, dynamic> args;
late final bool enableSearch;
late final bool isHotProduct;
late final VoucherListViewModel _viewModel;
@override
void initState() {
super.initState();
args = Get.arguments ?? {};
enableSearch = args['enableSearch'] ?? false;
isHotProduct = args['isHotProduct'] ?? false;
_viewModel = Get.put(VoucherListViewModel(isHotProduct: isHotProduct));
}
@override
Widget build(BuildContext context) {
final String title = isHotProduct ? 'Săn ưu đãi' : 'Tất cả ưu đãi';
return Scaffold(
appBar:
enableSearch
? CustomSearchNavigationBar(onSearchChanged: _viewModel.onSearchChanged,)
: CustomNavigationBar(title: title),
body: Column(
children: [
Expanded(
child: Obx(
() => RefreshIndicator(
onRefresh: () => _viewModel.getProducts(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);
return const Center(
child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator()),
);
}
final product = _viewModel.products[index];
return VoucherListItem(product: product);
},
),
),
),
),
],
),
);
}
}
import 'dart:async';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../../base/restful_api_viewmodel.dart';
import '../models/product_model.dart';
import '../models/product_type.dart';
class VoucherListViewModel extends RestfulApiViewModel {
VoucherListViewModel({required this.isHotProduct});
final bool isHotProduct;
Timer? _debounce;
var products = <ProductModel>[].obs;
var isLoading = false.obs;
var isLoadMore = false.obs;
int _currentPage = 0;
final int _pageSize = 20;
bool _hasMore = true;
bool get hasMore => _hasMore;
String _searchQuery = '';
var totalResult = 0.obs;
@override
void onInit() {
super.onInit();
getProducts(reset: true);
}
@override
void onClose() {
_debounce?.cancel();
super.onClose();
}
void onSearchChanged(String value) {
if (_searchQuery == value) return;
_searchQuery = value;
_debounce?.cancel();
_debounce = Timer(const Duration(seconds: 1), () {
getProducts(reset: true);
});
}
Future<void> getProducts({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 = {
"type": ProductType.voucher.value,
"size": _pageSize,
"index": _currentPage,
if (isHotProduct) "catalog_code": "HOT",
if (_searchQuery.isNotEmpty) "keywords": _searchQuery,
if (_searchQuery.isNotEmpty) "keyword": _searchQuery,
};
try {
isLoading.value = true;
isLoadMore.value = true;
final result = await client.getSearchProducts(body);
final fetchedData = result.data?.products ?? [];
totalResult.value = result.data?.total ?? 0;
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;
}
}
}
import 'package:flutter/material.dart';
class VoucherScreen extends StatelessWidget {
const VoucherScreen({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('Ưu đãi'));
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/screen/voucher/voucher_list/voucher_list_screen.dart';
import '../../shared/router_gage.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';
class VoucherTabScreen extends StatelessWidget {
const VoucherTabScreen({super.key});
@override
Widget build(BuildContext context) {
final VoucherTabViewModel viewModel = Get.put(VoucherTabViewModel());
return Scaffold(
appBar: CustomNavigationBar(
title: "Ưu đãi",
showBackButton: false,
rightButtons: [
IconButton(
icon: const Icon(Icons.search, color: Colors.white),
onPressed: () {
Get.toNamed(vouchersScreen, arguments: {"enableSearch": true});
},
),
],
),
body: Obx(() {
if (viewModel.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: viewModel.refreshData,
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo is ScrollUpdateNotification &&
scrollInfo.metrics.axis == Axis.vertical && // ✅ Chỉ check vertical
!viewModel.isLoadMore.value &&
viewModel.hasMore &&
scrollInfo.metrics.pixels >=
scrollInfo.metrics.maxScrollExtent - 200) {
viewModel.getAllProducts();
}
return false;
},
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [
const VoucherSectionTitle(title: 'Ưu đãi từ nhà mạng'),
const VoucherActionMenu(),
VoucherSectionTitle(
title: 'Săn ưu đãi',
onViewAll: () {
Get.toNamed(vouchersScreen, arguments: {"isHotProduct": true});
},
),
VoucherItemGrid(items: viewModel.hotProducts),
const VoucherSectionTitle(title: 'Tất cả ưu đãi'),
VoucherItemList(items: viewModel.allProducts),
if (viewModel.isLoadMore.value)
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
),
],
),
),
);
}),
);
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_type.dart';
import '../../base/restful_api_viewmodel.dart';
import 'models/product_model.dart';
class VoucherTabViewModel extends RestfulApiViewModel {
final RxList<ProductModel> hotProducts = <ProductModel>[].obs;
final RxList<ProductModel> allProducts = <ProductModel>[].obs;
final RxBool isLoading = false.obs;
final RxBool isLoadMore = false.obs;
int _currentPage = 0;
final int _pageSize = 20;
bool _hasMore = true;
bool get hasMore => _hasMore;
@override
void onInit() {
super.onInit();
refreshData();
}
Future<void> refreshData() async {
isLoading.value = true;
await Future.wait([
getHotProducts(),
getAllProducts(reset: true),
]);
isLoading.value = false;
}
Future<void> getHotProducts() async {
final body = {
"type": ProductType.voucher.value,
"size": 10,
"index": 0,
"catalog_code": "HOT",
};
try {
final result = await client.getProducts(body);
hotProducts.value = result.data ?? [];
} catch (error) {
print("Error fetching hot products: $error");
}
}
Future<void> getAllProducts({bool reset = false}) async {
if (reset) {
_currentPage = 0;
_hasMore = true;
allProducts.clear();
} else {
_currentPage = allProducts.length;
}
if (!_hasMore) return;
final body = {
"type": ProductType.voucher.value,
"size": _pageSize,
"index": _currentPage,
};
try {
isLoadMore.value = true;
final result = await client.getProducts(body);
final fetchedData = result.data ?? [];
if (fetchedData.isEmpty || fetchedData.length < _pageSize) {
_hasMore = false;
}
allProducts.addAll(fetchedData);
} catch (error) {
print("Error fetching all products: $error");
} finally {
isLoadMore.value = false;
}
}
}
\ No newline at end of file
......@@ -4,12 +4,14 @@ import '../screen/main_tab_screen/main_tab_screen.dart';
import '../screen/onboarding/onboarding_screen.dart';
import '../screen/setting/setting_screen.dart';
import '../screen/splash/splash_screen.dart';
import '../screen/voucher/voucher_list/voucher_list_screen.dart';
const splashScreen = '/splash';
const onboardingScreen = '/onboarding';
const loginScreen = '/login';
const mainScreen = '/main';
const settingScreen = '/setting';
const vouchersScreen = '/vouchers';
class RouterPage {
static List<GetPage> pages() {
......@@ -25,6 +27,7 @@ class RouterPage {
GetPage(name: loginScreen, page: () => const LoginScreen()),
GetPage(name: mainScreen, page: () => const MainTabScreen()),
GetPage(name: settingScreen, page: () => const SettingScreen()),
GetPage(name: vouchersScreen, page: () => VoucherListScreen(),),
];
}
}
import 'package:flutter/material.dart';
import 'back_button.dart';
class CustomNavigationBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final String? backgroundImage;
final bool showBackButton;
final List<Widget> rightButtons;
const CustomNavigationBar({
super.key,
required this.title,
this.backgroundImage = "assets/images/bg_header_navi.png",
this.showBackButton = true,
this.rightButtons = const [],
});
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
@override
Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top;
return Container(
height: statusBarHeight + kToolbarHeight,
decoration: BoxDecoration(
image: backgroundImage != null
? DecorationImage(
image: AssetImage(backgroundImage!),
fit: BoxFit.cover,
)
: null,
color: backgroundImage == null ? Colors.white : null,
),
child: SafeArea(
bottom: false,
child: Stack(
alignment: Alignment.center,
children: [
// Title ở giữa
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
// Back button bên trái
if (showBackButton)
Positioned(
left: 12,
child: CustomBackButton(),
),
// Buttons bên phải
if (rightButtons != null)
Positioned(
right: 12,
child: Row(
mainAxisSize: MainAxisSize.min,
children: rightButtons!,
),
),
],
),
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'back_button.dart';
class CustomSearchNavigationBar extends StatefulWidget implements PreferredSizeWidget {
final ValueChanged<String>? onSearchChanged;
final String? hintText;
final String? backgroundImage;
final bool showBackButton;
final List<Widget> rightButtons;
const CustomSearchNavigationBar({
super.key,
this.onSearchChanged,
this.hintText = 'Tìm kiếm...',
this.backgroundImage = "assets/images/bg_header_navi.png",
this.showBackButton = true,
this.rightButtons = const [],
});
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
@override
_CustomSearchNavigationBarState createState() => _CustomSearchNavigationBarState();
}
class _CustomSearchNavigationBarState extends State<CustomSearchNavigationBar> {
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top;
return Container(
height: statusBarHeight + kToolbarHeight,
decoration: BoxDecoration(
image: widget.backgroundImage != null
? DecorationImage(
image: AssetImage(widget.backgroundImage!),
fit: BoxFit.cover,
)
: null,
color: widget.backgroundImage == null ? Colors.white : null,
),
child: SafeArea(
bottom: false,
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
left: widget.showBackButton ? 68 : 16,
right: widget.rightButtons.isNotEmpty ? 60 : 16,
child: Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Icon(Icons.search, size: 20),
const SizedBox(width: 4),
Expanded(
child: TextField(
controller: _controller,
onChanged: (value) {
setState(() {}); // Update UI for suffix icon
widget.onSearchChanged?.call(value);
},
decoration: InputDecoration(
border: InputBorder.none,
hintText: widget.hintText,
isDense: true,
contentPadding: EdgeInsets.zero,
),
),
),
if (_controller.text.isNotEmpty)
GestureDetector(
onTap: () {
_controller.clear();
widget.onSearchChanged?.call('');
setState(() {});
},
child: const Icon(Icons.close, size: 20),
),
],
),
),
),
if (widget.showBackButton)
Positioned(
left: 12,
child: CustomBackButton(),
),
if (widget.rightButtons.isNotEmpty)
Positioned(
right: 12,
child: Row(
mainAxisSize: MainAxisSize.min,
children: widget.rightButtons,
),
),
],
),
),
);
}
}
\ No newline at end of file
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