Compare commits

..

2 Commits

Author SHA1 Message Date
Phuoc Nguyen
c0527a086c fix loyalty 2025-10-27 16:06:31 +07:00
Phuoc Nguyen
8eae79f72b loyalty 2025-10-27 15:47:03 +07:00
9 changed files with 1195 additions and 6 deletions

View File

@@ -8,6 +8,8 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:worker/features/cart/presentation/pages/cart_page.dart'; import 'package:worker/features/cart/presentation/pages/cart_page.dart';
import 'package:worker/features/favorites/presentation/pages/favorites_page.dart'; import 'package:worker/features/favorites/presentation/pages/favorites_page.dart';
import 'package:worker/features/loyalty/presentation/pages/loyalty_page.dart';
import 'package:worker/features/loyalty/presentation/pages/points_history_page.dart';
import 'package:worker/features/loyalty/presentation/pages/rewards_page.dart'; import 'package:worker/features/loyalty/presentation/pages/rewards_page.dart';
import 'package:worker/features/main/presentation/pages/main_scaffold.dart'; import 'package:worker/features/main/presentation/pages/main_scaffold.dart';
import 'package:worker/features/orders/presentation/pages/order_detail_page.dart'; import 'package:worker/features/orders/presentation/pages/order_detail_page.dart';
@@ -101,6 +103,16 @@ class AppRouter {
), ),
), ),
// Loyalty Route
GoRoute(
path: RouteNames.loyalty,
name: RouteNames.loyalty,
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
child: const LoyaltyPage(),
),
),
// Loyalty Rewards Route // Loyalty Rewards Route
GoRoute( GoRoute(
path: '/loyalty/rewards', path: '/loyalty/rewards',
@@ -111,6 +123,16 @@ class AppRouter {
), ),
), ),
// Points History Route
GoRoute(
path: RouteNames.pointsHistory,
name: 'loyalty_points_history',
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
child: const PointsHistoryPage(),
),
),
// Orders Route // Orders Route
GoRoute( GoRoute(
path: RouteNames.orders, path: RouteNames.orders,

View File

@@ -169,8 +169,7 @@ class HomePage extends ConsumerWidget {
QuickAction( QuickAction(
icon: Icons.history, icon: Icons.history,
label: 'Lịch sử điểm', label: 'Lịch sử điểm',
onTap: () => onTap: () => context.push(RouteNames.pointsHistory),
_showComingSoon(context, 'Lịch sử điểm', l10n),
), ),
], ],
), ),

View File

@@ -0,0 +1,189 @@
/// Data Source: Points History Local Data Source
///
/// Handles local storage operations for loyalty points history using Hive.
library;
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
import 'package:worker/features/loyalty/data/models/loyalty_point_entry_model.dart';
/// Points History Local Data Source
///
/// Provides methods to interact with locally stored points history data.
class PointsHistoryLocalDataSource {
Box? _entriesBox;
/// Get Hive box for points entries
Future<Box> get entriesBox async {
if (_entriesBox != null) return _entriesBox!;
// Use the box already opened by HiveService
if (Hive.isBoxOpen(HiveBoxNames.loyaltyBox)) {
_entriesBox = Hive.box(HiveBoxNames.loyaltyBox);
} else {
_entriesBox = await Hive.openBox(HiveBoxNames.loyaltyBox);
}
return _entriesBox!;
}
/// Get all points entries
Future<List<LoyaltyPointEntryModel>> getAllEntries() async {
final box = await entriesBox;
final entries = box.values
.whereType<LoyaltyPointEntryModel>()
.toList();
entries.sort((a, b) => b.timestamp.compareTo(a.timestamp)); // Newest first
return entries;
}
/// Get entry by ID
Future<LoyaltyPointEntryModel?> getEntryById(String entryId) async {
final box = await entriesBox;
try {
return box.values
.whereType<LoyaltyPointEntryModel>()
.firstWhere((entry) => entry.entryId == entryId);
} catch (e) {
throw Exception('Entry not found');
}
}
/// Save entry
Future<void> saveEntry(LoyaltyPointEntryModel entry) async {
final box = await entriesBox;
await box.put(entry.entryId, entry);
}
/// Save multiple entries
Future<void> saveEntries(List<LoyaltyPointEntryModel> entries) async {
final box = await entriesBox;
final Map<String, LoyaltyPointEntryModel> entriesMap = {
for (var entry in entries) entry.entryId: entry,
};
await box.putAll(entriesMap);
}
/// Delete entry
Future<void> deleteEntry(String entryId) async {
final box = await entriesBox;
await box.delete(entryId);
}
/// Clear all entries
Future<void> clearEntries() async {
final box = await entriesBox;
await box.clear();
}
/// Seed mock data for development
Future<void> seedMockEntries() async {
final now = DateTime.now();
final mockEntries = [
LoyaltyPointEntryModel(
entryId: 'entry_001',
userId: 'user_001',
points: 3,
entryType: EntryType.earn,
source: EntrySource.purchase,
description: 'Giao dịch mua hàng 00083',
referenceId: 'order_00083',
referenceType: 'order',
complaint: null,
complaintStatus: ComplaintStatus.none,
balanceAfter: 604,
expiryDate: now.add(const Duration(days: 365)),
timestamp: DateTime(2023, 9, 28, 17, 23, 18),
),
LoyaltyPointEntryModel(
entryId: 'entry_002',
userId: 'user_001',
points: 0,
entryType: EntryType.earn,
source: EntrySource.purchase,
description: 'Giao dịch mua hàng 00081',
referenceId: 'order_00081',
referenceType: 'order',
complaint: null,
complaintStatus: ComplaintStatus.none,
balanceAfter: 604,
expiryDate: now.add(const Duration(days: 365)),
timestamp: DateTime(2023, 9, 27, 17, 23, 18),
),
LoyaltyPointEntryModel(
entryId: 'entry_003',
userId: 'user_001',
points: -5,
entryType: EntryType.expiry,
source: EntrySource.promotion,
description: 'Điểm thưởng hết hạn',
referenceId: null,
referenceType: null,
complaint: null,
complaintStatus: ComplaintStatus.none,
balanceAfter: 604,
expiryDate: null,
timestamp: DateTime(2023, 9, 20, 17, 23, 18),
),
LoyaltyPointEntryModel(
entryId: 'entry_004',
userId: 'user_001',
points: -500,
entryType: EntryType.redeem,
source: EntrySource.giftRedemption,
description: 'Đổi Voucher HSG',
referenceId: 'gift_001',
referenceType: 'gift',
complaint: null,
complaintStatus: ComplaintStatus.none,
balanceAfter: 604,
expiryDate: null,
timestamp: DateTime(2023, 9, 19, 17, 23, 18),
),
LoyaltyPointEntryModel(
entryId: 'entry_005',
userId: 'user_001',
points: 5,
entryType: EntryType.earn,
source: EntrySource.referral,
description: 'Giới thiệu người dùng',
referenceId: null,
referenceType: null,
complaint: null,
complaintStatus: ComplaintStatus.none,
balanceAfter: 604,
expiryDate: now.add(const Duration(days: 365)),
timestamp: DateTime(2023, 9, 10, 17, 23, 18),
),
LoyaltyPointEntryModel(
entryId: 'entry_006',
userId: 'user_001',
points: -200,
entryType: EntryType.redeem,
source: EntrySource.giftRedemption,
description: 'Đổi quà',
referenceId: 'gift_002',
referenceType: 'gift',
complaint: null,
complaintStatus: ComplaintStatus.none,
balanceAfter: 604,
expiryDate: null,
timestamp: DateTime(2023, 9, 5, 17, 23, 18),
),
];
await saveEntries(mockEntries);
}
/// Get transaction amount from description (for purchase entries)
int? getTransactionAmount(String description) {
if (description.contains('Giao dịch mua hàng 00083')) {
return 100000000; // 100M VND
} else if (description.contains('Giao dịch mua hàng 00081')) {
return 200000000; // 200M VND
}
return null;
}
}

View File

@@ -0,0 +1,473 @@
/// Page: Loyalty Page
///
/// Main loyalty program page displaying member card, progress, and features.
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/loyalty/presentation/providers/loyalty_points_provider.dart';
/// Loyalty Page
///
/// Features:
/// - Diamond member card with QR code
/// - Progress bar to next tier
/// - Quick action menu items
/// - Current tier benefits
class LoyaltyPage extends ConsumerWidget {
const LoyaltyPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final loyaltyPoints = ref.watch(loyaltyPointsProvider);
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
title: const Text(
'Hội viên thân thiết',
style: TextStyle(color: Colors.black),
),
elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white,
foregroundColor: AppColors.grey900,
centerTitle: false,
automaticallyImplyLeading: false,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Member Card
_buildMemberCard(loyaltyPoints),
const SizedBox(height: 16),
// Progress Card
_buildProgressCard(),
const SizedBox(height: 16),
// Loyalty Features Menu
..._buildLoyaltyMenu(context),
const SizedBox(height: 16),
// Current Benefits Card
_buildBenefitsCard(),
],
),
),
);
}
/// Build Diamond Member Card
Widget _buildMemberCard(LoyaltyPointsState loyaltyPoints) {
return Container(
height: 200,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF4A00E0), Color(0xFF8E2DE2)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: const Color(0xFF4A00E0).withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top Row: Brand and Valid Through
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'EUROTILE',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
),
),
const SizedBox(height: 4),
Text(
'ARCHITECT MEMBERSHIP',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 11,
letterSpacing: 0.5,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'Valid through',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 11,
),
),
const SizedBox(height: 2),
const Text(
'31/12/2025',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
const Spacer(),
// Bottom Row: User Info and QR Code
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'La Nguyen Quynh',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
'CLASS: DIAMOND',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
Text(
'Points: ${loyaltyPoints.availablePoints}',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: QrImageView(
data: '0983441099',
version: QrVersions.auto,
size: 60,
backgroundColor: Colors.white,
),
),
],
),
],
),
);
}
/// Build Progress Card
Widget _buildProgressCard() {
return Card(
elevation: 5,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Tiến trình lên hạng',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 16),
// Current and Next Tier
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Hạng hiện tại: DIAMOND',
style: TextStyle(
fontSize: 13,
color: AppColors.grey500,
),
),
Text(
'Hạng kế tiếp: PLATINUM',
style: TextStyle(
fontSize: 13,
color: AppColors.grey500,
),
),
],
),
const SizedBox(height: 12),
// Progress Bar
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: 0.65,
minHeight: 8,
backgroundColor: AppColors.grey100,
valueColor: const AlwaysStoppedAnimation<Color>(
Color(0xFF4A00E0),
),
),
),
const SizedBox(height: 12),
// Points to Next Tier
Center(
child: RichText(
textAlign: TextAlign.center,
text: const TextSpan(
style: TextStyle(
fontSize: 13,
color: AppColors.grey500,
),
children: [
TextSpan(text: 'Còn '),
TextSpan(
text: '2,250 điểm',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
TextSpan(text: ' nữa để lên hạng Platinum'),
],
),
),
),
],
),
),
);
}
/// Build Loyalty Menu Items
List<Widget> _buildLoyaltyMenu(BuildContext context) {
final menuItems = [
{
'icon': Icons.card_giftcard,
'title': 'Đổi quà tặng',
'subtitle': 'Sử dụng điểm để đổi quà hấp dẫn',
'route': '/loyalty/rewards',
},
{
'icon': Icons.add_circle_outline,
'title': 'Ghi nhận điểm',
'subtitle': 'Gửi hóa đơn để nhận điểm thưởng',
'route': null,
},
{
'icon': Icons.history,
'title': 'Lịch sử điểm',
'subtitle': 'Xem chi tiết cộng/trừ điểm',
'route': '/loyalty/points-history',
},
{
'icon': Icons.person_add,
'title': 'Giới thiệu bạn bè',
'subtitle': 'Nhận thưởng khi giới thiệu thành công',
'route': null,
},
{
'icon': Icons.inventory_2_outlined,
'title': 'Quà của tôi',
'subtitle': 'Xem voucher và quà tặng đã đổi',
'route': null,
},
{
'icon': Icons.diamond_outlined,
'title': 'Quyền lợi hội viên',
'subtitle': 'Xem các ưu đãi dành cho hạng của bạn',
'route': null,
},
];
return menuItems.map((item) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: AppColors.grey100),
),
child: InkWell(
onTap: () {
if (item['route'] != null) {
context.push(item['route'] as String);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${item['title']} - Đang phát triển')),
);
}
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Icon
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.primaryBlue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
item['icon'] as IconData,
color: AppColors.primaryBlue,
size: 24,
),
),
const SizedBox(width: 16),
// Title and Subtitle
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['title'] as String,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 2),
Text(
item['subtitle'] as String,
style: const TextStyle(
fontSize: 13,
color: AppColors.grey500,
),
),
],
),
),
// Arrow
const Icon(
Icons.chevron_right,
color: AppColors.grey500,
size: 20,
),
],
),
),
),
);
}).toList();
}
/// Build Benefits Card
Widget _buildBenefitsCard() {
return Card(
elevation: 1,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Quyền lợi hạng Diamond',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 16),
_buildBenefitItem('Chiết khấu 15% cho tất cả sản phẩm'),
_buildBenefitItem('Giao hàng miễn phí cho đơn từ 5 triệu'),
_buildBenefitItem('Ưu tiên xử lý đơn hàng'),
_buildBenefitItem('Tặng 500 điểm vào ngày sinh nhật'),
_buildBenefitItem('Tư vấn thiết kế miễn phí'),
_buildBenefitItem('Mời tham gia sự kiện VIP độc quyền', isLast: true),
],
),
),
);
}
/// Build Benefit Item
Widget _buildBenefitItem(String text, {bool isLast = false}) {
return Padding(
padding: EdgeInsets.only(bottom: isLast ? 0 : 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icons.check_circle,
size: 20,
color: Color(0xFF4A00E0),
),
const SizedBox(width: 12),
Expanded(
child: Text(
text,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey900,
height: 1.4,
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,329 @@
/// Page: Points History Page
///
/// Displays history of points earned and spent transactions.
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/database/models/enums.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/loyalty/data/datasources/points_history_local_datasource.dart';
import 'package:worker/features/loyalty/data/models/loyalty_point_entry_model.dart';
import 'package:worker/features/loyalty/presentation/providers/points_history_provider.dart';
/// Points History Page
///
/// Features:
/// - Filter section with date range
/// - List of transaction cards
/// - Each card shows: description, date, amount, points change, new balance
/// - Complaint button for each transaction
class PointsHistoryPage extends ConsumerWidget {
const PointsHistoryPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final historyAsync = ref.watch(pointsHistoryProvider);
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => context.pop(),
),
title: const Text(
'Lịch sử điểm',
style: TextStyle(color: Colors.black),
),
elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white,
foregroundColor: AppColors.grey900,
centerTitle: false,
),
body: RefreshIndicator(
onRefresh: () async {
await ref.read(pointsHistoryProvider.notifier).refresh();
},
child: historyAsync.when(
data: (entries) {
if (entries.isEmpty) {
return _buildEmptyState();
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Filter Section
_buildFilterSection(),
const SizedBox(height: 16),
// Transaction List
...entries.map((entry) => _buildTransactionCard(context, ref, entry)),
],
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => _buildErrorState(error),
),
),
);
}
/// Build filter section
Widget _buildFilterSection() {
return Card(
elevation: 1,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Bộ lọc',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
Icon(
Icons.filter_list,
color: AppColors.primaryBlue,
size: 20,
),
],
),
const SizedBox(height: 8),
const Text(
'Thời gian hiệu lực: 01/01/2023 - 31/12/2023',
style: TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
],
),
),
);
}
/// Build transaction card
Widget _buildTransactionCard(
BuildContext context,
WidgetRef ref,
LoyaltyPointEntryModel entry,
) {
final dateFormatter = DateFormat('dd/MM/yyyy HH:mm:ss');
final currencyFormatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'VND',
decimalDigits: 0,
);
// Get transaction amount if it's a purchase
final datasource = ref.read(pointsHistoryLocalDataSourceProvider);
final transactionAmount = datasource.getTransactionAmount(entry.description);
return Card(
elevation: 1,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top row: Description and Complaint button
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Description
Text(
entry.description,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppColors.primaryBlue,
),
),
const SizedBox(height: 4),
// Timestamp
Text(
'Thời gian: ${dateFormatter.format(entry.timestamp)}',
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
// Transaction amount (if purchase)
if (transactionAmount != null) ...[
const SizedBox(height: 2),
Text(
'Giao dịch: ${currencyFormatter.format(transactionAmount)}',
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
],
],
),
),
const SizedBox(width: 12),
// Complaint button
OutlinedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chức năng khiếu nại đang phát triển')),
);
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
side: const BorderSide(color: AppColors.grey500),
foregroundColor: AppColors.grey900,
textStyle: const TextStyle(fontSize: 12),
minimumSize: const Size(0, 32),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
child: const Text('Khiếu nại'),
),
],
),
const SizedBox(height: 12),
// Bottom row: Points change and new balance
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// Points change
Text(
entry.points > 0 ? '+${entry.points}' : '${entry.points}',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: entry.points > 0
? AppColors.success
: entry.points < 0
? AppColors.danger
: AppColors.grey900,
),
),
const SizedBox(height: 2),
// New balance
Text(
'Điểm mới: ${entry.balanceAfter}',
style: const TextStyle(
fontSize: 12,
color: AppColors.primaryBlue,
),
),
],
),
],
),
],
),
),
);
}
/// Build empty state
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.history,
size: 80,
color: AppColors.grey500.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
const Text(
'Chưa có lịch sử điểm',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.grey500,
),
),
const SizedBox(height: 8),
const Text(
'Kéo xuống để làm mới',
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
),
);
}
/// Build error state
Widget _buildErrorState(Object error) {
print(error.toString());
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 80,
color: AppColors.danger.withValues(alpha: 0.7),
),
const SizedBox(height: 16),
const Text(
'Có lỗi xảy ra',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 8),
Text(
error.toString(),
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
textAlign: TextAlign.center,
),
],
),
);
}
}

View File

@@ -0,0 +1,44 @@
/// Providers: Points History
///
/// Riverpod providers for managing points history state.
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/loyalty/data/datasources/points_history_local_datasource.dart';
import 'package:worker/features/loyalty/data/models/loyalty_point_entry_model.dart';
part 'points_history_provider.g.dart';
/// Points History Local Data Source Provider
@riverpod
PointsHistoryLocalDataSource pointsHistoryLocalDataSource(Ref ref) {
return PointsHistoryLocalDataSource();
}
/// Points History Provider
///
/// Provides list of all points history entries from local data source.
@riverpod
class PointsHistory extends _$PointsHistory {
@override
Future<List<LoyaltyPointEntryModel>> build() async {
final datasource = ref.read(pointsHistoryLocalDataSourceProvider);
// Seed mock data on first load
final entries = await datasource.getAllEntries();
if (entries.isEmpty) {
await datasource.seedMockEntries();
return await datasource.getAllEntries();
}
return entries;
}
/// Refresh points history
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return await ref.read(pointsHistoryLocalDataSourceProvider).getAllEntries();
});
}
}

View File

@@ -0,0 +1,132 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'points_history_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Points History Local Data Source Provider
@ProviderFor(pointsHistoryLocalDataSource)
const pointsHistoryLocalDataSourceProvider =
PointsHistoryLocalDataSourceProvider._();
/// Points History Local Data Source Provider
final class PointsHistoryLocalDataSourceProvider
extends
$FunctionalProvider<
PointsHistoryLocalDataSource,
PointsHistoryLocalDataSource,
PointsHistoryLocalDataSource
>
with $Provider<PointsHistoryLocalDataSource> {
/// Points History Local Data Source Provider
const PointsHistoryLocalDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'pointsHistoryLocalDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$pointsHistoryLocalDataSourceHash();
@$internal
@override
$ProviderElement<PointsHistoryLocalDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
PointsHistoryLocalDataSource create(Ref ref) {
return pointsHistoryLocalDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(PointsHistoryLocalDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<PointsHistoryLocalDataSource>(value),
);
}
}
String _$pointsHistoryLocalDataSourceHash() =>
r'324e4d6d12e0f1ec3f77de8e0fd60f69eaa8c7ce';
/// Points History Provider
///
/// Provides list of all points history entries from local data source.
@ProviderFor(PointsHistory)
const pointsHistoryProvider = PointsHistoryProvider._();
/// Points History Provider
///
/// Provides list of all points history entries from local data source.
final class PointsHistoryProvider
extends
$AsyncNotifierProvider<PointsHistory, List<LoyaltyPointEntryModel>> {
/// Points History Provider
///
/// Provides list of all points history entries from local data source.
const PointsHistoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'pointsHistoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$pointsHistoryHash();
@$internal
@override
PointsHistory create() => PointsHistory();
}
String _$pointsHistoryHash() => r'e812d6f6707f02c15a1263ae5b5ee912f269358d';
/// Points History Provider
///
/// Provides list of all points history entries from local data source.
abstract class _$PointsHistory
extends $AsyncNotifier<List<LoyaltyPointEntryModel>> {
FutureOr<List<LoyaltyPointEntryModel>> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref =
this.ref
as $Ref<
AsyncValue<List<LoyaltyPointEntryModel>>,
List<LoyaltyPointEntryModel>
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
AsyncValue<List<LoyaltyPointEntryModel>>,
List<LoyaltyPointEntryModel>
>,
AsyncValue<List<LoyaltyPointEntryModel>>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/home/presentation/pages/home_page.dart'; import 'package:worker/features/home/presentation/pages/home_page.dart';
import 'package:worker/features/loyalty/presentation/pages/loyalty_page.dart';
import 'package:worker/features/main/presentation/providers/current_page_provider.dart'; import 'package:worker/features/main/presentation/providers/current_page_provider.dart';
import 'package:worker/features/promotions/presentation/pages/promotions_page.dart'; import 'package:worker/features/promotions/presentation/pages/promotions_page.dart';
@@ -29,7 +30,7 @@ class MainScaffold extends ConsumerWidget {
// Define pages // Define pages
final pages = [ final pages = [
const HomePage(), const HomePage(),
_buildComingSoonPage('Hội viên'), // Loyalty const LoyaltyPage(), // Loyalty
const PromotionsPage(), const PromotionsPage(),
_buildComingSoonPage('Thông báo'), // Notifications _buildComingSoonPage('Thông báo'), // Notifications
_buildComingSoonPage('Cài đặt'), // Account _buildComingSoonPage('Cài đặt'), // Account

View File

@@ -59,7 +59,7 @@ final class QuotesLocalDataSourceProvider
} }
String _$quotesLocalDataSourceHash() => String _$quotesLocalDataSourceHash() =>
r'02a822db926d8d80460bcc27a08ea494dff6c441'; r'675d9ae15bf5e3dcbb12c1a893c8a73fbfb8c2ee';
/// Quotes Provider /// Quotes Provider
/// ///
@@ -288,7 +288,7 @@ final class FilteredQuotesProvider
} }
} }
String _$filteredQuotesHash() => r'77076cfa483cb81cc56972bca6a3c1e97861165c'; String _$filteredQuotesHash() => r'ce6fc7db1d4f2e90431e7258a3faef2c55db80d5';
/// Quotes Count by Status Provider /// Quotes Count by Status Provider
@@ -335,4 +335,4 @@ final class QuotesCountByStatusProvider
} }
String _$quotesCountByStatusHash() => String _$quotesCountByStatusHash() =>
r'474b62ad0ccf890df1c33c64a17f9a0f428f676e'; r'9a2f2f10dd392505d0d51428f45390f16952763d';