Commit 29b7f923 authored by DatHV's avatar DatHV
Browse files

update voucher detail

parent 6fcbfba8
import 'package:json_annotation/json_annotation.dart';
import 'my_product_status_type.dart';
part 'product_customer_info_model.g.dart';
@JsonSerializable()
class ProductCustomerInfoModel {
final int id;
@JsonKey(name: 'status')
final int? rawStatus;
ProductCustomerInfoModel({
required this.id,
this.rawStatus,
});
MyProductStatusType get status =>
MyProductStatusType.fromRaw(rawStatus ?? 0);
factory ProductCustomerInfoModel.fromJson(Map<String, dynamic> json) =>
_$ProductCustomerInfoModelFromJson(json);
Map<String, dynamic> toJson() => _$ProductCustomerInfoModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_customer_info_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProductCustomerInfoModel _$ProductCustomerInfoModelFromJson(
Map<String, dynamic> json,
) => ProductCustomerInfoModel(
id: (json['id'] as num).toInt(),
rawStatus: (json['status'] as num?)?.toInt(),
);
Map<String, dynamic> _$ProductCustomerInfoModelToJson(
ProductCustomerInfoModel instance,
) => <String, dynamic>{'id': instance.id, 'status': instance.rawStatus};
import 'package:json_annotation/json_annotation.dart';
part 'product_item_model.g.dart';
@JsonSerializable()
class ProductItemModel {
@JsonKey(name: 'expire_time')
final String? expireTime;
@JsonKey(name: 'code_secret')
final String? codeSecret;
final String? password;
ProductItemModel({
this.expireTime,
this.codeSecret,
this.password,
});
factory ProductItemModel.fromJson(Map<String, dynamic> json) =>
_$ProductItemModelFromJson(json);
Map<String, dynamic> toJson() => _$ProductItemModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_item_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProductItemModel _$ProductItemModelFromJson(Map<String, dynamic> json) =>
ProductItemModel(
expireTime: json['expire_time'] as String?,
codeSecret: json['code_secret'] as String?,
password: json['password'] as String?,
);
Map<String, dynamic> _$ProductItemModelToJson(ProductItemModel instance) =>
<String, dynamic>{
'expire_time': instance.expireTime,
'code_secret': instance.codeSecret,
'password': instance.password,
};
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_brand_model.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_content_model.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_customer_info_model.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_item_model.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_media_item.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_price_model.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_properties_model.dart';
import '../../flash_sale/preview_flash_sale_model.dart';
import 'media_type.dart';
import 'my_product_status_type.dart';
part 'product_model.g.dart';
@JsonSerializable()
class ProductModel {
final int? id;
@JsonKey(name: 'quantity_available')
final int? quantityAvailable;
final ProductContentModel? content;
......@@ -17,19 +22,57 @@ class ProductModel {
@JsonKey(name: 'voucher_properties')
final ProductPropertiesModel? properties;
final List<ProductMediaItem>? media;
@JsonKey(name: 'preview_campaign_flash_sale')
final PreviewFlashSale? previewFlashSale;
@JsonKey(name: 'customer_product_info')
final ProductCustomerInfoModel? customerInfoModel;
@JsonKey(name: 'product_item')
final ProductItemModel? itemModel;
@JsonKey(name: 'expire_time')
final String? expireTime;
ProductModel({
this.id,
this.quantityAvailable,
this.content,
this.price,
this.brand,
this.properties,
this.media,
this.previewFlashSale,
this.customerInfoModel,
this.itemModel,
this.expireTime,
});
String? get name {
if (content == null) return null;
return content!.name;
return content?.name;
}
String? get expire {
return isMyProduct ? itemModel?.expireTime : expireTime;
}
int? get amountToBePaid {
if (previewFlashSale?.isFlashSalePrice == true) {
return previewFlashSale?.price;
}
return price?.value;
}
bool get isMyProduct {
return customerInfoModel != null;
}
bool get inStock {
return (quantityAvailable ?? 1) != 0;
}
bool get expired {
if (customerInfoModel != null) {
return customerInfoModel?.status == MyProductStatusType.expired;
}
return (quantityAvailable ?? 1) != 0;
}
ProductMediaItem? get banner {
......
......@@ -7,6 +7,7 @@ part of 'product_model.dart';
// **************************************************************************
ProductModel _$ProductModelFromJson(Map<String, dynamic> json) => ProductModel(
id: (json['id'] as num?)?.toInt(),
quantityAvailable: (json['quantity_available'] as num?)?.toInt(),
content:
json['content'] == null
......@@ -32,14 +33,38 @@ ProductModel _$ProductModelFromJson(Map<String, dynamic> json) => ProductModel(
(json['media'] as List<dynamic>?)
?.map((e) => ProductMediaItem.fromJson(e as Map<String, dynamic>))
.toList(),
previewFlashSale:
json['preview_campaign_flash_sale'] == null
? null
: PreviewFlashSale.fromJson(
json['preview_campaign_flash_sale'] as Map<String, dynamic>,
),
customerInfoModel:
json['customer_product_info'] == null
? null
: ProductCustomerInfoModel.fromJson(
json['customer_product_info'] as Map<String, dynamic>,
),
itemModel:
json['product_item'] == null
? null
: ProductItemModel.fromJson(
json['product_item'] as Map<String, dynamic>,
),
expireTime: json['expire_time'] as String?,
);
Map<String, dynamic> _$ProductModelToJson(ProductModel instance) =>
<String, dynamic>{
'id': instance.id,
'quantity_available': instance.quantityAvailable,
'content': instance.content,
'price': instance.price,
'brand': instance.brand,
'voucher_properties': instance.properties,
'media': instance.media,
'preview_campaign_flash_sale': instance.previewFlashSale,
'customer_product_info': instance.customerInfoModel,
'product_item': instance.itemModel,
'expire_time': instance.expireTime,
};
......@@ -19,19 +19,8 @@ class ProductPriceModel {
});
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);
}
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
part 'product_properties_model.g.dart';
@JsonSerializable()
class ProductPropertiesModel {
@JsonKey(name: 'voucher_type')
final String? voucherType;
@JsonKey(name: 'voucher_value')
final double? voucherValue;
ProductPropertiesModel({this.voucherType, this.voucherValue});
......@@ -13,7 +16,7 @@ class ProductPropertiesModel {
if (voucherType == "VOUCHER_TYPE_DISCOUNT") {
return "${voucherValue!.toStringAsFixed(0)}%";
}
return "$voucherValue đ";
return voucherValue!.money(CurrencyUnit.vnd);
}
factory ProductPropertiesModel.fromJson(Map<String, dynamic> json) => _$ProductPropertiesModelFromJson(json);
......
......@@ -9,13 +9,13 @@ part of 'product_properties_model.dart';
ProductPropertiesModel _$ProductPropertiesModelFromJson(
Map<String, dynamic> json,
) => ProductPropertiesModel(
voucherType: json['voucherType'] as String?,
voucherValue: (json['voucherValue'] as num?)?.toDouble(),
voucherType: json['voucher_type'] as String?,
voucherValue: (json['voucher_value'] as num?)?.toDouble(),
);
Map<String, dynamic> _$ProductPropertiesModelToJson(
ProductPropertiesModel instance,
) => <String, dynamic>{
'voucherType': instance.voucherType,
'voucherValue': instance.voucherValue,
'voucher_type': instance.voucherType,
'voucher_value': instance.voucherValue,
};
......@@ -14,10 +14,10 @@ class VoucherActionMenu extends StatelessWidget {
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'),
_ActionItem(icon: "assets/images/ic_topup.png", label: 'Nạp tiền\ndiện thoại'),
_ActionItem(icon: "assets/images/ic_card_code.png", label: 'Đổi mã\nthẻ nạp'),
_ActionItem(icon: "assets/images/ic_sim_service.png", label: 'Gói cước\nnhà mạng'),
_ActionItem(icon: "assets/images/ic_topup_data.png", label: 'Ưu đãi\nData'),
],
),
);
......@@ -25,7 +25,7 @@ class VoucherActionMenu extends StatelessWidget {
}
class _ActionItem extends StatelessWidget {
final IconData icon;
final String icon;
final String label;
const _ActionItem({required this.icon, required this.label});
......@@ -40,7 +40,12 @@ class _ActionItem extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 40, color: BaseColor.primary400),
Image.asset(
icon,
fit: BoxFit.cover,
width: 40,
height: 40,
),
const SizedBox(height: 8),
Text(
label,
......
import 'package:flutter/material.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import '../../../widgets/custom_price_tag.dart';
import '../../../widgets/image_loader.dart';
import '../models/product_model.dart';
......@@ -39,7 +41,6 @@ class _VoucherGridItem extends StatelessWidget {
@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;
......@@ -63,22 +64,51 @@ class _VoucherGridItem extends StatelessWidget {
child: SizedBox(
width: itemWidth,
height: itemWidth / (16 / 9),
child: loadNetworkImage(url: bgImage, placeholderAsset: "assets/images/sample.png"),
child: loadNetworkImage(
url: bgImage,
placeholderAsset: "assets/images/ic_logo.png"),
),
),
if (hasDiscount)
Positioned(
top: 8,
right: 8,
top: 0,
right: 0,
child: Container(
height: 30,
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: Center(
child: Text(
product.properties?.title ?? "",
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold),
),
),
),
),
if (!product.inStock)
Positioned(
left: 0,
bottom: 0,
child: Container(
height: 30,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
borderRadius: const BorderRadius.only(
topRight: Radius.circular(8),
),
),
child: Text(
product.properties?.title ?? "",
style: const TextStyle(color: Colors.white, fontSize: 12),
child: Center(
child: const Text(
'Tạm hết',
style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
),
),
),
),
......@@ -91,6 +121,7 @@ class _VoucherGridItem extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
// Title: auto co giãn nhưng không tràn
Text(
product.content?.name ?? '',
......@@ -111,7 +142,7 @@ class _VoucherGridItem extends StatelessWidget {
width: 20,
height: 20,
fit: BoxFit.cover,
placeholderAsset: 'assets/images/sample.png', // ⚠️ SVG dùng wrong tại đây!
placeholderAsset: 'assets/images/ic_logo.png',
),
),
),
......@@ -123,23 +154,7 @@ class _VoucherGridItem extends StatelessWidget {
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),
),
],
),
),
PriceTagWidget(point: product.amountToBePaid ?? 0,),
],
),
],
......
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 '../../../resouce/base_color.dart';
import '../../../widgets/custom_price_tag.dart';
import '../../../widgets/image_loader.dart';
import '../models/product_model.dart';
......@@ -15,7 +20,12 @@ class VoucherItemList extends StatelessWidget {
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final product = items[index];
return VoucherListItem(product: product);
return GestureDetector(
onTap: () {
Get.toNamed(voucherDetailScreen, arguments: product.id);
},
child: VoucherListItem(product: product),
);
},
);
}
......@@ -30,8 +40,6 @@ class VoucherListItem extends StatelessWidget {
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(
......@@ -42,16 +50,34 @@ class VoucherListItem extends StatelessWidget {
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',
),
child: Stack(
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: loadNetworkImage(
url: bgImage,
fit: BoxFit.cover,
placeholderAsset: 'assets/images/sample.png',
),
),
if (!product.inStock)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.5),
alignment: Alignment.center,
child: const Text(
'Tạm hết',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
const SizedBox(width: 12),
......@@ -70,7 +96,6 @@ class VoucherListItem extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Logo thương hiệu
CircleAvatar(
radius: 10,
backgroundColor: Colors.transparent,
......@@ -85,7 +110,6 @@ class VoucherListItem extends StatelessWidget {
),
),
const SizedBox(width: 4),
// Tên thương hiệu
Expanded(
child: Text(
brandName,
......@@ -96,28 +120,7 @@ class VoucherListItem extends StatelessWidget {
],
),
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,
),
),
],
),
),
PriceTagWidget(point: product.amountToBePaid ?? 0,),
],
),
),
......@@ -127,11 +130,7 @@ class VoucherListItem extends StatelessWidget {
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Divider(
height: 1,
thickness: 1,
color: BaseColor.second200,
),
child: Divider(height: 1, thickness: 1, color: BaseColor.second200),
),
],
);
......
......@@ -24,9 +24,9 @@ class VoucherSectionTitle extends StatelessWidget {
if (onViewAll != null)
GestureDetector(
onTap: onViewAll,
child: const Text(
child: Text(
'Xem tất cả',
style: TextStyle(color: Colors.blue),
style: TextStyle(color: Colors.blue[700], fontWeight: FontWeight.bold),
),
),
],
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../widgets/custom_empty_widget.dart';
import '../../../widgets/custom_navigation_bar.dart';
import '../../../widgets/custom_search_navigation_bar.dart';
import '../sub_widget/voucher_item_list.dart';
......@@ -37,25 +38,52 @@ class _VoucherListScreenState extends State<VoucherListScreen> {
: CustomNavigationBar(title: title),
body: Column(
children: [
if (enableSearch)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Obx(() {
final resultCount = _viewModel.totalResult.value;
final displayText = _viewModel.searchQuery.isNotEmpty
? '$title ($resultCount kết quả)'
: title;
return Align(
alignment: Alignment.centerLeft,
child: Text(
displayText,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
);
}),
),
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);
},
),
),
() {
if (_viewModel.products.isEmpty) {
return const Center(
child: EmptyWidget(),
);
}
return 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);
},
),
);
}
),
),
],
......
......@@ -18,6 +18,7 @@ class VoucherListViewModel extends RestfulApiViewModel {
bool _hasMore = true;
bool get hasMore => _hasMore;
String _searchQuery = '';
String get searchQuery => _searchQuery;
var totalResult = 0.obs;
@override
......
......@@ -4,6 +4,7 @@ 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/detail/voucher_detail_screen.dart';
import '../screen/voucher/voucher_list/voucher_list_screen.dart';
const splashScreen = '/splash';
......@@ -12,6 +13,7 @@ const loginScreen = '/login';
const mainScreen = '/main';
const settingScreen = '/setting';
const vouchersScreen = '/vouchers';
const voucherDetailScreen = '/voucherDetail';
class RouterPage {
static List<GetPage> pages() {
......@@ -28,6 +30,7 @@ class RouterPage {
GetPage(name: mainScreen, page: () => const MainTabScreen()),
GetPage(name: settingScreen, page: () => const SettingScreen()),
GetPage(name: vouchersScreen, page: () => VoucherListScreen(),),
GetPage(name: voucherDetailScreen, page: () => VoucherDetailScreen(),),
];
}
}
......@@ -16,14 +16,14 @@ class CustomBackButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
height: 48,
width: 48,
height: 42,
width: 42,
child: Stack(
children: [
Center(
child: Container(
height: 32,
width: 32,
height: 28,
width: 28,
decoration: BoxDecoration(
border: Border.all(color: BaseColor.second300, width: 1),
borderRadius: BorderRadius.circular(8),
......
import 'package:flutter/material.dart';
class EmptyWidget extends StatelessWidget {
final String imageAsset;
final String content;
const EmptyWidget({
super.key,
this.imageAsset = 'assets/images/ic_pipi_06.png',
this.content = 'Không có dữ liệu hiển thị',
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
imageAsset,
width: 120,
height: 120,
fit: BoxFit.contain,
),
const SizedBox(height: 16),
Text(
content,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import '../extensions/num_extension.dart';
import '../resouce/base_color.dart';
class PriceTagWidget extends StatelessWidget {
final int point;
const PriceTagWidget({super.key, required this.point});
bool get isFree => point == 0;
@override
Widget build(BuildContext context) {
return Container(
height: 26,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: BaseColor.primary150,
borderRadius: BorderRadius.circular(13),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset('assets/images/ic_point.png', width: 16, height: 14),
const SizedBox(width: 4),
Text(
isFree ? 'Miễn phí' : point.money(CurrencyUnit.none),
style: TextStyle(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}
......@@ -5,7 +5,7 @@ Widget loadNetworkImage({
BoxFit fit = BoxFit.cover,
double? width,
double? height,
String placeholderAsset = 'assets/images/sample.png',
String placeholderAsset = 'assets/images/ic_logo.png',
}) {
if (url == null || url.isEmpty) {
return Image.asset(
......
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