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

update voucher tab

parent d86c3328
<svg width="138" height="138" viewBox="0 0 138 138" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="138" height="138" rx="10" fill="#D42230"/>
<path d="M102.755 0.446289V42.2101C102.755 57.5434 90.3969 70.041 75.2347 70.041H61.4571C46.2948 70.041 33.9365 57.5434 33.9365 42.2101V0.446289H47.7141V42.2101C47.7141 49.8767 53.8759 56.1431 61.4571 56.1431H75.2347C82.8158 56.1431 89.0123 49.9117 89.0123 42.2101V0.446289H102.755Z" fill="white"/>
<path d="M102.751 83.9731C102.751 101.232 88.8694 115.27 71.7685 115.27H64.8797C47.8135 115.27 33.8975 101.232 33.8975 83.9731H47.675C47.675 93.5651 55.3947 101.372 64.8797 101.372H71.7685C81.2536 101.372 88.9732 93.5651 88.9732 83.9731H102.751Z" fill="white"/>
</svg>
......@@ -25,4 +25,6 @@ class APIPaths {
static const String headerHome = "/dynamic-home/api/v1.0/header-home";
static const String otpDeleteAccountRequest = "/user/api/v2.0/me/delete/request";
static const String verifyDeleteAccount = "/user/api/v2.0/me/delete/verify";
static const String getProducts = "/product/api/v2.0/products";
static const String getSearchProducts = "/product/api/v2.0/products/search";
}
......@@ -5,6 +5,7 @@ import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import 'package:mypoint_flutter_app/networking/restful_api.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_model.dart';
import '../configs/device_info.dart';
import '../model/auth/biometric_register_response_model.dart';
import '../model/auth/login_token_response_model.dart';
......@@ -19,6 +20,7 @@ import '../screen/otp/model/otp_verify_response_model.dart';
import '../screen/pageDetail/model/campaign_detail_model.dart';
import '../screen/pageDetail/model/detail_page_rule_type.dart';
import '../screen/splash/splash_screen_viewmodel.dart';
import '../screen/voucher/models/search_product_response_model.dart';
import 'model_maker.dart';
extension RestfullAPIClientAllApi on RestfulAPIClient {
......@@ -259,4 +261,20 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
(data) => HeaderHomeModel.fromJson(data as Json),
);
}
Future<BaseResponseModel<List<ProductModel>>> getProducts(Json body) async {
return requestNormal(APIPaths.getProducts, Method.GET, body, (data) {
final list = data as List<dynamic>;
return list.map((e) => ProductModel.fromJson(e)).toList();
});
}
Future<BaseResponseModel<SearchProductResponseModel>> getSearchProducts(Json body) async {
return requestNormal(
APIPaths.getSearchProducts,
Method.POST,
body,
(data) =>SearchProductResponseModel.fromJson(data as Json),
);
}
}
const module = 'assets/images';
const String icClose = '$module/ic_close.svg';
const String icLogo = '$module/ic_logo.svg';
const String icLogo = '$module/ic_logo.png';
const String bgAlertHeader = '$module/bg_alert_header.svg';
\ No newline at end of file
......@@ -3,7 +3,8 @@ import '../game/games_screen.dart';
import '../home/home_screen.dart';
import '../personal/personal_screen.dart';
import '../shopping/shopping_screen.dart';
import '../voucher/voucher_screen.dart';
import '../support/transaction_history_screen.dart';
import '../voucher/voucher_tab_screen.dart';
class MainTabScreen extends StatefulWidget {
const MainTabScreen({super.key});
......@@ -17,9 +18,9 @@ class _MainTabScreenState extends State<MainTabScreen> {
final List<Widget> _pages = const [
HomeScreen(),
VoucherScreen(),
VoucherTabScreen(),
GameScreen(),
ShoppingScreen(),
TransactionHistoryScreen(),
PersonalScreen(),
];
......
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'transaction_service.dart';
class TransactionHistoryScreen extends StatefulWidget {
const TransactionHistoryScreen({super.key});
@override
State<TransactionHistoryScreen> createState() => _TransactionHistoryScreenState();
}
class _TransactionHistoryScreenState extends State<TransactionHistoryScreen> {
final List<String> categories = [
'Tất cả',
'Viện thông',
'Mua sắm',
'Ưu đãi',
'Hóa đơn',
];
int selectedCategoryIndex = 0;
DateTime selectedMonth = DateTime.now();
bool isLoading = false;
List<Transaction> transactions = [];
final TransactionService _transactionService = TransactionService();
@override
void initState() {
super.initState();
fetchTransactions();
}
Future<void> fetchTransactions() async {
setState(() {
isLoading = true;
});
try {
final result = await _transactionService.getTransactions(
category: categories[selectedCategoryIndex],
month: selectedMonth,
);
setState(() {
transactions = result;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Không thể tải dữ liệu: $e')),
);
}
}
void selectCategory(int index) {
setState(() {
selectedCategoryIndex = index;
});
fetchTransactions();
}
void selectMonth() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: selectedMonth,
firstDate: DateTime(2020),
lastDate: DateTime(2026),
initialDatePickerMode: DatePickerMode.year,
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.light(
primary: Colors.red.shade400,
onPrimary: Colors.white,
onSurface: Colors.black,
),
),
child: child!,
);
},
);
if (picked != null) {
setState(() {
selectedMonth = picked;
});
fetchTransactions();
}
}
@override
Widget build(BuildContext context) {
final transactionCount = transactions.length;
final hasTransactions = transactionCount > 0;
// Tính tổng tiền và điểm
int totalAmount = 0;
int totalPoints = 0;
for (var transaction in transactions) {
totalAmount += transaction.amount;
totalPoints += transaction.points;
}
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
title: const Text(
'Lịch sử giao dịch',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, size: 20, color: Colors.black54),
onPressed: () => Navigator.of(context).pop(),
),
),
body: Column(
children: [
// Danh mục
Container(
height: 60,
color: Colors.white,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: categories.length,
itemBuilder: (context, index) {
final isSelected = selectedCategoryIndex == index;
return GestureDetector(
onTap: () => selectCategory(index),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: isSelected ? Colors.red.shade50 : Colors.grey.shade100,
borderRadius: BorderRadius.circular(20),
border: isSelected
? Border.all(color: Colors.red.shade100)
: null,
),
alignment: Alignment.center,
child: Text(
categories[index],
style: TextStyle(
color: isSelected ? Colors.red : Colors.grey.shade700,
fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal,
),
),
),
);
},
),
),
const SizedBox(height: 8),
// Tháng và số giao dịch
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Tháng ${DateFormat('MM/yyyy').format(selectedMonth)} (${transactionCount} giao dịch)',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
InkWell(
onTap: selectMonth,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
const Text(
'Tháng',
style: TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
const SizedBox(width: 4),
Icon(Icons.keyboard_arrow_down, size: 18, color: Colors.grey.shade700),
],
),
),
),
],
),
),
// Tổng tiền và điểm
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${totalAmount}đ',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Giao dịch bằng tiền',
style: TextStyle(
color: Colors.black54,
fontSize: 14,
),
),
Text(
'${totalAmount}đ',
style: const TextStyle(
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Giao dịch bằng điểm',
style: TextStyle(
color: Colors.black54,
fontSize: 14,
),
),
Row(
children: [
Container(
width: 18,
height: 18,
decoration: const BoxDecoration(
color: Colors.amber,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.currency_exchange,
size: 12,
color: Colors.white,
),
),
),
const SizedBox(width: 4),
Text(
'$totalPoints',
style: const TextStyle(
fontWeight: FontWeight.w500,
),
),
],
),
],
),
],
),
),
// Danh sách giao dịch hoặc trạng thái trống
Expanded(
child: isLoading
? const Center(child: CircularProgressIndicator())
: hasTransactions
? ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: transactions.length,
itemBuilder: (context, index) {
final transaction = transactions[index];
return TransactionItem(transaction: transaction);
},
)
: const EmptyTransactionState(),
),
],
),
);
}
}
class TransactionItem extends StatelessWidget {
final Transaction transaction;
const TransactionItem({
super.key,
required this.transaction,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
// Icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Stack(
children: [
Center(
child: Icon(
transaction.icon,
color: Colors.red,
size: 20,
),
),
if (transaction.status == TransactionStatus.completed)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: const Center(
child: Icon(
Icons.check,
size: 8,
color: Colors.white,
),
),
),
),
],
),
),
const SizedBox(width: 12),
// Thông tin giao dịch
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
transaction.title,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 15,
),
),
const SizedBox(height: 2),
Text(
'${DateFormat('HH:mm').format(transaction.date)} - ${DateFormat('dd/MM/yyyy').format(transaction.date)}',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 13,
),
),
],
),
),
// Điểm
Row(
children: [
Container(
width: 20,
height: 20,
decoration: const BoxDecoration(
color: Colors.amber,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.currency_exchange,
size: 12,
color: Colors.white,
),
),
),
const SizedBox(width: 4),
Text(
'${transaction.points}',
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 15,
),
),
],
),
],
),
);
}
}
class EmptyTransactionState extends StatelessWidget {
const EmptyTransactionState({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Colors.grey.shade100,
shape: BoxShape.circle,
),
child: Center(
child: Icon(
Icons.receipt_long,
size: 50,
color: Colors.grey.shade400,
),
),
),
const SizedBox(height: 16),
const Text(
'Bạn hiện chưa có giao dịch nào',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
],
),
);
}
}
\ No newline at end of file
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
enum TransactionStatus {
pending,
completed,
failed,
}
class Transaction {
final String id;
final String title;
final int amount;
final int points;
final DateTime date;
final String category;
final IconData icon;
final TransactionStatus status;
Transaction({
required this.id,
required this.title,
required this.amount,
required this.points,
required this.date,
required this.category,
required this.icon,
required this.status,
});
factory Transaction.fromJson(Map<String, dynamic> json) {
IconData getIconForCategory(String category) {
switch (category) {
case 'Viện thông':
return Icons.phone_android;
case 'Mua sắm':
return Icons.shopping_bag;
case 'Ưu đãi':
return Icons.card_giftcard;
case 'Hóa đơn':
return Icons.receipt;
default:
return Icons.receipt_long;
}
}
TransactionStatus getStatus(String statusStr) {
switch (statusStr) {
case 'completed':
return TransactionStatus.completed;
case 'pending':
return TransactionStatus.pending;
case 'failed':
return TransactionStatus.failed;
default:
return TransactionStatus.pending;
}
}
return Transaction(
id: json['id'],
title: json['title'],
amount: json['amount'],
points: json['points'],
date: DateTime.parse(json['date']),
category: json['category'],
icon: getIconForCategory(json['category']),
status: getStatus(json['status']),
);
}
}
class TransactionService {
static const String baseUrl = 'https://api.example.com';
Future<List<Transaction>> getTransactions({
required String category,
required DateTime month,
}) async {
try {
final response = await http.get(
Uri.parse(
'$baseUrl/transactions?category=${Uri.encodeComponent(category)}&month=${month.year}-${month.month}',
),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((json) => Transaction.fromJson(json)).toList();
} else {
throw Exception('Failed to load transactions: ${response.statusCode}');
}
} catch (e) {
// Trong môi trường thực tế, bạn nên xử lý lỗi một cách phù hợp
print('Error fetching transactions: $e');
// Trả về dữ liệu mẫu cho mục đích demo
if (category == 'Ưu đãi') {
return [
Transaction(
id: '1',
title: 'Thanh toán mua ưu đãi',
amount: 0,
points: 0,
date: DateTime.now(),
category: 'Ưu đãi',
icon: Icons.card_giftcard,
status: TransactionStatus.completed,
),
Transaction(
id: '2',
title: 'Thanh toán mua ưu đãi',
amount: 0,
points: 0,
date: DateTime.now(),
category: 'Ưu đãi',
icon: Icons.card_giftcard,
status: TransactionStatus.completed,
),
];
}
return [];
}
}
}
\ No newline at end of file
enum CashType {
all,
point,
cash,
}
extension CashTypeExt on CashType {
static CashType from(String? value) {
switch (value) {
case "PAYMENT_METHOD_POINT":
return CashType.point;
case "PAYMENT_METHOD_CASH":
return CashType.cash;
default:
return CashType.all;
}
}
}
extension IntExtension on int {
String makeDisplayPrice(CashType type) {
if (this == 0) return "Miễn phí";
if (type == CashType.point) return "$this điểm";
return "$this đ";
}
}
enum MediaType {
video,
avatar,
banner16_9,
banner1_1,
gallery,
}
extension MediaTypeExtension on MediaType {
static MediaType? fromRawValue(String? rawValue) {
switch (rawValue) {
case 'MEDIA_TYPE_VIDEO':
return MediaType.video;
case 'MEDIA_TYPE_AVATAR':
return MediaType.avatar;
case 'MEDIA_TYPE_BANNER_16_9':
return MediaType.banner16_9;
case 'MEDIA_TYPE_BANNER_1_1':
return MediaType.banner1_1;
case 'MEDIA_TYPE_GALLERY':
return MediaType.gallery;
default:
return null;
}
}
String get rawValue {
switch (this) {
case MediaType.video:
return 'MEDIA_TYPE_VIDEO';
case MediaType.avatar:
return 'MEDIA_TYPE_AVATAR';
case MediaType.banner16_9:
return 'MEDIA_TYPE_BANNER_16_9';
case MediaType.banner1_1:
return 'MEDIA_TYPE_BANNER_1_1';
case MediaType.gallery:
return 'MEDIA_TYPE_GALLERY';
}
}
}
import 'package:json_annotation/json_annotation.dart';
part 'product_brand_model.g.dart';
@JsonSerializable()
class ProductBrandModel {
final int? id;
final String? logo;
final String? name;
final String? email;
final String? phone;
final String? website;
final String? code;
ProductBrandModel({
this.id,
this.logo,
this.name,
this.email,
this.phone,
this.website,
this.code,
});
factory ProductBrandModel.fromJson(Map<String, dynamic> json) => _$ProductBrandModelFromJson(json);
Map<String, dynamic> toJson() => _$ProductBrandModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_brand_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProductBrandModel _$ProductBrandModelFromJson(Map<String, dynamic> json) =>
ProductBrandModel(
id: (json['id'] as num?)?.toInt(),
logo: json['logo'] as String?,
name: json['name'] as String?,
email: json['email'] as String?,
phone: json['phone'] as String?,
website: json['website'] as String?,
code: json['code'] as String?,
);
Map<String, dynamic> _$ProductBrandModelToJson(ProductBrandModel instance) =>
<String, dynamic>{
'id': instance.id,
'logo': instance.logo,
'name': instance.name,
'email': instance.email,
'phone': instance.phone,
'website': instance.website,
'code': instance.code,
};
import 'package:json_annotation/json_annotation.dart';
part 'product_content_model.g.dart';
@JsonSerializable()
class ProductContentModel {
final String? language;
final String? name;
final String? detail;
final String? termAndCondition;
ProductContentModel({
this.language,
this.name,
this.detail,
this.termAndCondition,
});
factory ProductContentModel.fromJson(Map<String, dynamic> json) => _$ProductContentModelFromJson(json);
get imageUrl => null;
Map<String, dynamic> toJson() => _$ProductContentModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_content_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProductContentModel _$ProductContentModelFromJson(Map<String, dynamic> json) =>
ProductContentModel(
language: json['language'] as String?,
name: json['name'] as String?,
detail: json['detail'] as String?,
termAndCondition: json['termAndCondition'] as String?,
);
Map<String, dynamic> _$ProductContentModelToJson(
ProductContentModel instance,
) => <String, dynamic>{
'language': instance.language,
'name': instance.name,
'detail': instance.detail,
'termAndCondition': instance.termAndCondition,
};
import 'package:json_annotation/json_annotation.dart';
import 'media_type.dart';
part 'product_media_item.g.dart';
@JsonSerializable()
class ProductMediaItem {
final String? name;
final String? url;
@JsonKey(name: 'type')
final String? rawType;
ProductMediaItem({
this.name,
this.url,
this.rawType,
});
MediaType? get type => MediaTypeExtension.fromRawValue(rawType);
factory ProductMediaItem.fromJson(Map<String, dynamic> json) =>
_$ProductMediaItemFromJson(json);
Map<String, dynamic> toJson() => _$ProductMediaItemToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_media_item.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProductMediaItem _$ProductMediaItemFromJson(Map<String, dynamic> json) =>
ProductMediaItem(
name: json['name'] as String?,
url: json['url'] as String?,
rawType: json['type'] as String?,
);
Map<String, dynamic> _$ProductMediaItemToJson(ProductMediaItem instance) =>
<String, dynamic>{
'name': instance.name,
'url': instance.url,
'type': instance.rawType,
};
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_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 'media_type.dart';
part 'product_model.g.dart';
@JsonSerializable()
class ProductModel {
@JsonKey(name: 'quantity_available')
final int? quantityAvailable;
final ProductContentModel? content;
final ProductPriceModel? price;
final ProductBrandModel? brand;
@JsonKey(name: 'voucher_properties')
final ProductPropertiesModel? properties;
final List<ProductMediaItem>? media;
ProductModel({
this.quantityAvailable,
this.content,
this.price,
this.brand,
this.properties,
this.media,
});
String? get name {
if (content == null) return null;
return content!.name;
}
ProductMediaItem? get banner {
if (media == null) return null;
return media!.firstWhere((item) => item.type == MediaType.banner16_9, orElse: () => media!.first);
}
factory ProductModel.fromJson(Map<String, dynamic> json) => _$ProductModelFromJson(json);
Map<String, dynamic> toJson() => _$ProductModelToJson(this);
}
\ 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