point
This commit is contained in:
@@ -93,7 +93,7 @@ class PointsHistoryLocalDataSource {
|
|||||||
complaintStatus: ComplaintStatus.none,
|
complaintStatus: ComplaintStatus.none,
|
||||||
balanceAfter: 604,
|
balanceAfter: 604,
|
||||||
expiryDate: now.add(const Duration(days: 365)),
|
expiryDate: now.add(const Duration(days: 365)),
|
||||||
timestamp: DateTime(2023, 9, 28, 17, 23, 18),
|
timestamp: DateTime(2025, 9, 28, 17, 23, 18),
|
||||||
),
|
),
|
||||||
LoyaltyPointEntryModel(
|
LoyaltyPointEntryModel(
|
||||||
entryId: 'entry_002',
|
entryId: 'entry_002',
|
||||||
@@ -108,7 +108,7 @@ class PointsHistoryLocalDataSource {
|
|||||||
complaintStatus: ComplaintStatus.none,
|
complaintStatus: ComplaintStatus.none,
|
||||||
balanceAfter: 604,
|
balanceAfter: 604,
|
||||||
expiryDate: now.add(const Duration(days: 365)),
|
expiryDate: now.add(const Duration(days: 365)),
|
||||||
timestamp: DateTime(2023, 9, 27, 17, 23, 18),
|
timestamp: DateTime(2025, 9, 27, 17, 23, 18),
|
||||||
),
|
),
|
||||||
LoyaltyPointEntryModel(
|
LoyaltyPointEntryModel(
|
||||||
entryId: 'entry_003',
|
entryId: 'entry_003',
|
||||||
@@ -123,7 +123,7 @@ class PointsHistoryLocalDataSource {
|
|||||||
complaintStatus: ComplaintStatus.none,
|
complaintStatus: ComplaintStatus.none,
|
||||||
balanceAfter: 604,
|
balanceAfter: 604,
|
||||||
expiryDate: null,
|
expiryDate: null,
|
||||||
timestamp: DateTime(2023, 9, 20, 17, 23, 18),
|
timestamp: DateTime(2025, 9, 20, 17, 23, 18),
|
||||||
),
|
),
|
||||||
LoyaltyPointEntryModel(
|
LoyaltyPointEntryModel(
|
||||||
entryId: 'entry_004',
|
entryId: 'entry_004',
|
||||||
@@ -138,7 +138,7 @@ class PointsHistoryLocalDataSource {
|
|||||||
complaintStatus: ComplaintStatus.none,
|
complaintStatus: ComplaintStatus.none,
|
||||||
balanceAfter: 604,
|
balanceAfter: 604,
|
||||||
expiryDate: null,
|
expiryDate: null,
|
||||||
timestamp: DateTime(2023, 9, 19, 17, 23, 18),
|
timestamp: DateTime(2025, 9, 19, 17, 23, 18),
|
||||||
),
|
),
|
||||||
LoyaltyPointEntryModel(
|
LoyaltyPointEntryModel(
|
||||||
entryId: 'entry_005',
|
entryId: 'entry_005',
|
||||||
@@ -153,7 +153,7 @@ class PointsHistoryLocalDataSource {
|
|||||||
complaintStatus: ComplaintStatus.none,
|
complaintStatus: ComplaintStatus.none,
|
||||||
balanceAfter: 604,
|
balanceAfter: 604,
|
||||||
expiryDate: now.add(const Duration(days: 365)),
|
expiryDate: now.add(const Duration(days: 365)),
|
||||||
timestamp: DateTime(2023, 9, 10, 17, 23, 18),
|
timestamp: DateTime(2025, 9, 10, 17, 23, 18),
|
||||||
),
|
),
|
||||||
LoyaltyPointEntryModel(
|
LoyaltyPointEntryModel(
|
||||||
entryId: 'entry_006',
|
entryId: 'entry_006',
|
||||||
@@ -168,7 +168,7 @@ class PointsHistoryLocalDataSource {
|
|||||||
complaintStatus: ComplaintStatus.none,
|
complaintStatus: ComplaintStatus.none,
|
||||||
balanceAfter: 604,
|
balanceAfter: 604,
|
||||||
expiryDate: null,
|
expiryDate: null,
|
||||||
timestamp: DateTime(2023, 9, 5, 17, 23, 18),
|
timestamp: DateTime(2025, 9, 5, 17, 23, 18),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
userId: 'user123',
|
userId: 'user123',
|
||||||
invoiceNumber: 'INV-VG-001',
|
invoiceNumber: 'INV-VG-001',
|
||||||
storeName: 'Công ty TNHH Vingroup',
|
storeName: 'Công ty TNHH Vingroup',
|
||||||
transactionDate: DateTime(2023, 11, 15),
|
transactionDate: DateTime(2025, 11, 15),
|
||||||
invoiceAmount: 2500000,
|
invoiceAmount: 2500000,
|
||||||
notes: 'Gạch granite cao cấp cho khu vực lobby và hành lang',
|
notes: 'Gạch granite cao cấp cho khu vực lobby và hành lang',
|
||||||
attachments: const [
|
attachments: const [
|
||||||
@@ -39,8 +39,8 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
],
|
],
|
||||||
status: PointsStatus.approved,
|
status: PointsStatus.approved,
|
||||||
pointsEarned: 250,
|
pointsEarned: 250,
|
||||||
submittedAt: DateTime(2023, 11, 15, 10, 0),
|
submittedAt: DateTime(2025, 11, 15, 10, 0),
|
||||||
processedAt: DateTime(2023, 11, 20, 14, 30),
|
processedAt: DateTime(2025, 11, 20, 14, 30),
|
||||||
processedBy: 'admin001',
|
processedBy: 'admin001',
|
||||||
),
|
),
|
||||||
PointsRecord(
|
PointsRecord(
|
||||||
@@ -48,21 +48,21 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
userId: 'user123',
|
userId: 'user123',
|
||||||
invoiceNumber: 'INV-BTX-002',
|
invoiceNumber: 'INV-BTX-002',
|
||||||
storeName: 'Tập đoàn Bitexco',
|
storeName: 'Tập đoàn Bitexco',
|
||||||
transactionDate: DateTime(2023, 11, 25),
|
transactionDate: DateTime(2025, 11, 25),
|
||||||
invoiceAmount: 1250000,
|
invoiceAmount: 1250000,
|
||||||
notes: 'Gạch porcelain 80x80 cho sảnh chính và khu mua sắm',
|
notes: 'Gạch porcelain 80x80 cho sảnh chính và khu mua sắm',
|
||||||
attachments: const [
|
attachments: const [
|
||||||
'https://example.com/invoice3.jpg',
|
'https://example.com/invoice3.jpg',
|
||||||
],
|
],
|
||||||
status: PointsStatus.pending,
|
status: PointsStatus.pending,
|
||||||
submittedAt: DateTime(2023, 11, 25, 9, 15),
|
submittedAt: DateTime(2025, 11, 25, 9, 15),
|
||||||
),
|
),
|
||||||
PointsRecord(
|
PointsRecord(
|
||||||
recordId: 'PRR003',
|
recordId: 'PRR003',
|
||||||
userId: 'user123',
|
userId: 'user123',
|
||||||
invoiceNumber: 'INV-ABC-003',
|
invoiceNumber: 'INV-ABC-003',
|
||||||
storeName: 'Công ty TNHH ABC Manufacturing',
|
storeName: 'Công ty TNHH ABC Manufacturing',
|
||||||
transactionDate: DateTime(2023, 11, 20),
|
transactionDate: DateTime(2025, 11, 20),
|
||||||
invoiceAmount: 4200000,
|
invoiceAmount: 4200000,
|
||||||
notes: 'Gạch chống trơn cho khu vực sản xuất và kho bãi',
|
notes: 'Gạch chống trơn cho khu vực sản xuất và kho bãi',
|
||||||
attachments: const [
|
attachments: const [
|
||||||
@@ -71,8 +71,8 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
],
|
],
|
||||||
status: PointsStatus.rejected,
|
status: PointsStatus.rejected,
|
||||||
rejectReason: 'Hình ảnh minh chứng không hợp lệ',
|
rejectReason: 'Hình ảnh minh chứng không hợp lệ',
|
||||||
submittedAt: DateTime(2023, 11, 20, 11, 0),
|
submittedAt: DateTime(2025, 11, 20, 11, 0),
|
||||||
processedAt: DateTime(2023, 11, 28, 16, 45),
|
processedAt: DateTime(2025, 11, 28, 16, 45),
|
||||||
processedBy: 'admin002',
|
processedBy: 'admin002',
|
||||||
),
|
),
|
||||||
PointsRecord(
|
PointsRecord(
|
||||||
@@ -80,7 +80,7 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
userId: 'user123',
|
userId: 'user123',
|
||||||
invoiceNumber: 'INV-ECO-004',
|
invoiceNumber: 'INV-ECO-004',
|
||||||
storeName: 'Ecopark Group',
|
storeName: 'Ecopark Group',
|
||||||
transactionDate: DateTime(2023, 10, 10),
|
transactionDate: DateTime(2025, 10, 10),
|
||||||
invoiceAmount: 3700000,
|
invoiceAmount: 3700000,
|
||||||
notes: 'Gạch ceramic vân gỗ cho khu vực phòng khách và sân vườn',
|
notes: 'Gạch ceramic vân gỗ cho khu vực phòng khách và sân vườn',
|
||||||
attachments: const [
|
attachments: const [
|
||||||
@@ -88,8 +88,8 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
],
|
],
|
||||||
status: PointsStatus.approved,
|
status: PointsStatus.approved,
|
||||||
pointsEarned: 370,
|
pointsEarned: 370,
|
||||||
submittedAt: DateTime(2023, 10, 10, 8, 30),
|
submittedAt: DateTime(2025, 10, 10, 8, 30),
|
||||||
processedAt: DateTime(2023, 10, 15, 10, 20),
|
processedAt: DateTime(2025, 10, 15, 10, 20),
|
||||||
processedBy: 'admin001',
|
processedBy: 'admin001',
|
||||||
),
|
),
|
||||||
PointsRecord(
|
PointsRecord(
|
||||||
@@ -97,7 +97,7 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
userId: 'user123',
|
userId: 'user123',
|
||||||
invoiceNumber: 'INV-DMD-005',
|
invoiceNumber: 'INV-DMD-005',
|
||||||
storeName: 'Diamond Hospitality Group',
|
storeName: 'Diamond Hospitality Group',
|
||||||
transactionDate: DateTime(2023, 12, 1),
|
transactionDate: DateTime(2025, 12, 1),
|
||||||
invoiceAmount: 8600000,
|
invoiceAmount: 8600000,
|
||||||
notes: 'Gạch marble tự nhiên cho lobby và phòng suite',
|
notes: 'Gạch marble tự nhiên cho lobby và phòng suite',
|
||||||
attachments: const [
|
attachments: const [
|
||||||
@@ -106,7 +106,7 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
'https://example.com/invoice9.jpg',
|
'https://example.com/invoice9.jpg',
|
||||||
],
|
],
|
||||||
status: PointsStatus.pending,
|
status: PointsStatus.pending,
|
||||||
submittedAt: DateTime(2023, 12, 1, 13, 0),
|
submittedAt: DateTime(2025, 12, 1, 13, 0),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import 'package:worker/features/loyalty/presentation/providers/points_history_pr
|
|||||||
/// Points History Page
|
/// Points History Page
|
||||||
///
|
///
|
||||||
/// Features:
|
/// Features:
|
||||||
/// - Filter section with date range
|
/// - Filter section with date range picker
|
||||||
/// - List of transaction cards with new design
|
/// - List of transaction cards with new design
|
||||||
/// - Each card shows: code, date, description, reference, points change, balance after
|
/// - Each card shows: code, date, description, reference, points change, balance after
|
||||||
class PointsHistoryPage extends ConsumerWidget {
|
class PointsHistoryPage extends ConsumerWidget {
|
||||||
@@ -26,7 +26,11 @@ class PointsHistoryPage extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
// Use unfiltered data for now (mock data)
|
||||||
final historyAsync = ref.watch(pointsHistoryProvider);
|
final historyAsync = ref.watch(pointsHistoryProvider);
|
||||||
|
final filter = ref.watch(pointsHistoryFilterProvider);
|
||||||
|
//todo: implement filtering logic in provider later
|
||||||
|
//:note: final historyAsync = ref.watch(filteredPointsHistoryProvider);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: colorScheme.surfaceContainerLowest,
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
@@ -50,19 +54,18 @@ class PointsHistoryPage extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: historyAsync.when(
|
child: historyAsync.when(
|
||||||
data: (entries) {
|
data: (entries) {
|
||||||
if (entries.isEmpty) {
|
|
||||||
return _buildEmptyState(colorScheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
// Filter Section
|
// Filter Section
|
||||||
_buildFilterSection(colorScheme),
|
_buildFilterSection(context, ref, colorScheme, filter),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Transaction List
|
// Empty state or Transaction List
|
||||||
|
if (entries.isEmpty)
|
||||||
|
_buildEmptyStateInline(colorScheme)
|
||||||
|
else
|
||||||
...entries.map(
|
...entries.map(
|
||||||
(entry) => _buildTransactionCard(context, entry, colorScheme),
|
(entry) => _buildTransactionCard(context, entry, colorScheme),
|
||||||
),
|
),
|
||||||
@@ -76,19 +79,26 @@ class PointsHistoryPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build filter section
|
/// Build filter section with date pickers
|
||||||
Widget _buildFilterSection(ColorScheme colorScheme) {
|
Widget _buildFilterSection(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
ColorScheme colorScheme,
|
||||||
|
PointsHistoryFilter filter,
|
||||||
|
) {
|
||||||
|
final dateFormatter = DateFormat('dd/MM/yyyy');
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Bộ lọc',
|
'Bộ lọc',
|
||||||
@@ -98,16 +108,212 @@ class PointsHistoryPage extends ConsumerWidget {
|
|||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'Thời gian hiệu lực: 01/01/2023 - 31/12/2023',
|
|
||||||
style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
FaIcon(FontAwesomeIcons.filter, color: colorScheme.primary, size: 18),
|
FaIcon(FontAwesomeIcons.filter, color: colorScheme.primary, size: 18),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Date range row
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Start date
|
||||||
|
Expanded(
|
||||||
|
child: _buildDateField(
|
||||||
|
context: context,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
label: 'Từ ngày',
|
||||||
|
value: filter.startDate,
|
||||||
|
dateFormatter: dateFormatter,
|
||||||
|
onTap: () async {
|
||||||
|
final date = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: filter.startDate ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: filter.endDate ?? DateTime.now(),
|
||||||
|
);
|
||||||
|
if (date != null) {
|
||||||
|
ref.read(pointsHistoryFilterProvider.notifier).setStartDate(date);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
// End date
|
||||||
|
Expanded(
|
||||||
|
child: _buildDateField(
|
||||||
|
context: context,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
label: 'Đến ngày',
|
||||||
|
value: filter.endDate,
|
||||||
|
dateFormatter: dateFormatter,
|
||||||
|
onTap: () async {
|
||||||
|
final date = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: filter.endDate ?? DateTime.now(),
|
||||||
|
firstDate: filter.startDate ?? DateTime(2020),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
);
|
||||||
|
if (date != null) {
|
||||||
|
ref.read(pointsHistoryFilterProvider.notifier).setEndDate(date);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Quick filter chips
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildQuickFilterChip(
|
||||||
|
context: context,
|
||||||
|
ref: ref,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
label: 'Hôm nay',
|
||||||
|
onTap: () {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
ref.read(pointsHistoryFilterProvider.notifier).setDateRange(today, today);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildQuickFilterChip(
|
||||||
|
context: context,
|
||||||
|
ref: ref,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
label: '7 ngày',
|
||||||
|
onTap: () {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
final weekAgo = today.subtract(const Duration(days: 7));
|
||||||
|
ref.read(pointsHistoryFilterProvider.notifier).setDateRange(weekAgo, today);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildQuickFilterChip(
|
||||||
|
context: context,
|
||||||
|
ref: ref,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
label: '30 ngày',
|
||||||
|
onTap: () {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
final monthAgo = today.subtract(const Duration(days: 30));
|
||||||
|
ref.read(pointsHistoryFilterProvider.notifier).setDateRange(monthAgo, today);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildQuickFilterChip(
|
||||||
|
context: context,
|
||||||
|
ref: ref,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
label: 'Năm nay',
|
||||||
|
onTap: () {
|
||||||
|
final now = DateTime.now();
|
||||||
|
ref.read(pointsHistoryFilterProvider.notifier).setDateRange(
|
||||||
|
DateTime(now.year, 1, 1),
|
||||||
|
DateTime(now.year, 12, 31),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildQuickFilterChip(
|
||||||
|
context: context,
|
||||||
|
ref: ref,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
label: 'Tất cả',
|
||||||
|
onTap: () {
|
||||||
|
ref.read(pointsHistoryFilterProvider.notifier).clearFilter();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDateField({
|
||||||
|
required BuildContext context,
|
||||||
|
required ColorScheme colorScheme,
|
||||||
|
required String label,
|
||||||
|
required DateTime? value,
|
||||||
|
required DateFormat dateFormatter,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value != null ? dateFormatter.format(value) : 'Chọn',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: value != null ? colorScheme.onSurface : colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FaIcon(
|
||||||
|
FontAwesomeIcons.calendar,
|
||||||
|
size: 14,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildQuickFilterChip({
|
||||||
|
required BuildContext context,
|
||||||
|
required WidgetRef ref,
|
||||||
|
required ColorScheme colorScheme,
|
||||||
|
required String label,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -258,30 +464,32 @@ class PointsHistoryPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build empty state
|
/// Build inline empty state (inside scrollable list)
|
||||||
Widget _buildEmptyState(ColorScheme colorScheme) {
|
Widget _buildEmptyStateInline(ColorScheme colorScheme) {
|
||||||
return Center(
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 60),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
FaIcon(
|
FaIcon(
|
||||||
FontAwesomeIcons.clockRotateLeft,
|
FontAwesomeIcons.clockRotateLeft,
|
||||||
size: 80,
|
size: 64,
|
||||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Chưa có lịch sử điểm',
|
'Không có giao dịch',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Kéo xuống để làm mới',
|
'Không tìm thấy giao dịch trong khoảng thời gian này',
|
||||||
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
|
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,6 +9,59 @@ import 'package:worker/features/loyalty/data/models/loyalty_point_entry_model.da
|
|||||||
|
|
||||||
part 'points_history_provider.g.dart';
|
part 'points_history_provider.g.dart';
|
||||||
|
|
||||||
|
/// Points History Filter State
|
||||||
|
class PointsHistoryFilter {
|
||||||
|
const PointsHistoryFilter({
|
||||||
|
this.startDate,
|
||||||
|
this.endDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateTime? startDate;
|
||||||
|
final DateTime? endDate;
|
||||||
|
|
||||||
|
PointsHistoryFilter copyWith({
|
||||||
|
DateTime? startDate,
|
||||||
|
DateTime? endDate,
|
||||||
|
bool clearStartDate = false,
|
||||||
|
bool clearEndDate = false,
|
||||||
|
}) {
|
||||||
|
return PointsHistoryFilter(
|
||||||
|
startDate: clearStartDate ? null : (startDate ?? this.startDate),
|
||||||
|
endDate: clearEndDate ? null : (endDate ?? this.endDate),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Points History Filter Provider
|
||||||
|
@riverpod
|
||||||
|
class PointsHistoryFilterNotifier extends _$PointsHistoryFilterNotifier {
|
||||||
|
@override
|
||||||
|
PointsHistoryFilter build() {
|
||||||
|
// Default: current year
|
||||||
|
final now = DateTime.now();
|
||||||
|
return PointsHistoryFilter(
|
||||||
|
startDate: DateTime(now.year, 1, 1),
|
||||||
|
endDate: DateTime(now.year, 12, 31),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDateRange(DateTime? start, DateTime? end) {
|
||||||
|
state = PointsHistoryFilter(startDate: start, endDate: end);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStartDate(DateTime? date) {
|
||||||
|
state = state.copyWith(startDate: date);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setEndDate(DateTime? date) {
|
||||||
|
state = state.copyWith(endDate: date);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearFilter() {
|
||||||
|
state = const PointsHistoryFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Points History Local Data Source Provider
|
/// Points History Local Data Source Provider
|
||||||
@riverpod
|
@riverpod
|
||||||
PointsHistoryLocalDataSource pointsHistoryLocalDataSource(Ref ref) {
|
PointsHistoryLocalDataSource pointsHistoryLocalDataSource(Ref ref) {
|
||||||
@@ -44,3 +97,41 @@ class PointsHistory extends _$PointsHistory {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Filtered Points History Provider
|
||||||
|
@riverpod
|
||||||
|
Future<List<LoyaltyPointEntryModel>> filteredPointsHistory(Ref ref) async {
|
||||||
|
final entries = await ref.watch(pointsHistoryProvider.future);
|
||||||
|
final filter = ref.watch(pointsHistoryFilterProvider);
|
||||||
|
|
||||||
|
return entries.where((entry) {
|
||||||
|
// Filter by start date
|
||||||
|
if (filter.startDate != null) {
|
||||||
|
final startOfDay = DateTime(
|
||||||
|
filter.startDate!.year,
|
||||||
|
filter.startDate!.month,
|
||||||
|
filter.startDate!.day,
|
||||||
|
);
|
||||||
|
if (entry.timestamp.isBefore(startOfDay)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by end date
|
||||||
|
if (filter.endDate != null) {
|
||||||
|
final endOfDay = DateTime(
|
||||||
|
filter.endDate!.year,
|
||||||
|
filter.endDate!.month,
|
||||||
|
filter.endDate!.day,
|
||||||
|
23,
|
||||||
|
59,
|
||||||
|
59,
|
||||||
|
);
|
||||||
|
if (entry.timestamp.isAfter(endOfDay)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,68 @@ part of 'points_history_provider.dart';
|
|||||||
|
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
// ignore_for_file: type=lint, type=warning
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
/// Points History Filter Provider
|
||||||
|
|
||||||
|
@ProviderFor(PointsHistoryFilterNotifier)
|
||||||
|
const pointsHistoryFilterProvider = PointsHistoryFilterNotifierProvider._();
|
||||||
|
|
||||||
|
/// Points History Filter Provider
|
||||||
|
final class PointsHistoryFilterNotifierProvider
|
||||||
|
extends
|
||||||
|
$NotifierProvider<PointsHistoryFilterNotifier, PointsHistoryFilter> {
|
||||||
|
/// Points History Filter Provider
|
||||||
|
const PointsHistoryFilterNotifierProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'pointsHistoryFilterProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$pointsHistoryFilterNotifierHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
PointsHistoryFilterNotifier create() => PointsHistoryFilterNotifier();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(PointsHistoryFilter value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<PointsHistoryFilter>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$pointsHistoryFilterNotifierHash() =>
|
||||||
|
r'ef2587f4461c9488d9b15ed033e1d362042795f8';
|
||||||
|
|
||||||
|
/// Points History Filter Provider
|
||||||
|
|
||||||
|
abstract class _$PointsHistoryFilterNotifier
|
||||||
|
extends $Notifier<PointsHistoryFilter> {
|
||||||
|
PointsHistoryFilter build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref = this.ref as $Ref<PointsHistoryFilter, PointsHistoryFilter>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<PointsHistoryFilter, PointsHistoryFilter>,
|
||||||
|
PointsHistoryFilter,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Points History Local Data Source Provider
|
/// Points History Local Data Source Provider
|
||||||
|
|
||||||
@ProviderFor(pointsHistoryLocalDataSource)
|
@ProviderFor(pointsHistoryLocalDataSource)
|
||||||
@@ -130,3 +192,50 @@ abstract class _$PointsHistory
|
|||||||
element.handleValue(ref, created);
|
element.handleValue(ref, created);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Filtered Points History Provider
|
||||||
|
|
||||||
|
@ProviderFor(filteredPointsHistory)
|
||||||
|
const filteredPointsHistoryProvider = FilteredPointsHistoryProvider._();
|
||||||
|
|
||||||
|
/// Filtered Points History Provider
|
||||||
|
|
||||||
|
final class FilteredPointsHistoryProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<List<LoyaltyPointEntryModel>>,
|
||||||
|
List<LoyaltyPointEntryModel>,
|
||||||
|
FutureOr<List<LoyaltyPointEntryModel>>
|
||||||
|
>
|
||||||
|
with
|
||||||
|
$FutureModifier<List<LoyaltyPointEntryModel>>,
|
||||||
|
$FutureProvider<List<LoyaltyPointEntryModel>> {
|
||||||
|
/// Filtered Points History Provider
|
||||||
|
const FilteredPointsHistoryProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'filteredPointsHistoryProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$filteredPointsHistoryHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$FutureProviderElement<List<LoyaltyPointEntryModel>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $FutureProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<List<LoyaltyPointEntryModel>> create(Ref ref) {
|
||||||
|
return filteredPointsHistory(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$filteredPointsHistoryHash() =>
|
||||||
|
r'989e2bf824eeb161b44b67d9ee81b713444a6e87';
|
||||||
|
|||||||
Reference in New Issue
Block a user