point record
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
/// Remote Data Source: Points Record
|
||||
///
|
||||
/// Handles API communication for points records.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
|
||||
|
||||
/// Points Record Remote Data Source Interface
|
||||
abstract class PointsRecordRemoteDataSource {
|
||||
/// Get all points records for current user
|
||||
Future<List<PointsRecord>> getPointsRecords();
|
||||
|
||||
/// Get single points record by ID
|
||||
Future<PointsRecord> getPointsRecordById(String recordId);
|
||||
|
||||
/// Submit new points record
|
||||
Future<PointsRecord> submitPointsRecord(PointsRecord record);
|
||||
}
|
||||
|
||||
/// Points Record Remote Data Source Implementation (Mock)
|
||||
class PointsRecordRemoteDataSourceImpl
|
||||
implements PointsRecordRemoteDataSource {
|
||||
@override
|
||||
Future<List<PointsRecord>> getPointsRecords() async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
return [
|
||||
PointsRecord(
|
||||
recordId: 'PRR001',
|
||||
userId: 'user123',
|
||||
invoiceNumber: 'INV-VG-001',
|
||||
storeName: 'Công ty TNHH Vingroup',
|
||||
transactionDate: DateTime(2023, 11, 15),
|
||||
invoiceAmount: 2500000,
|
||||
notes: 'Gạch granite cao cấp cho khu vực lobby và hành lang',
|
||||
attachments: const [
|
||||
'https://example.com/invoice1.jpg',
|
||||
'https://example.com/invoice2.jpg',
|
||||
],
|
||||
status: PointsStatus.approved,
|
||||
pointsEarned: 250,
|
||||
submittedAt: DateTime(2023, 11, 15, 10, 0),
|
||||
processedAt: DateTime(2023, 11, 20, 14, 30),
|
||||
processedBy: 'admin001',
|
||||
),
|
||||
PointsRecord(
|
||||
recordId: 'PRR002',
|
||||
userId: 'user123',
|
||||
invoiceNumber: 'INV-BTX-002',
|
||||
storeName: 'Tập đoàn Bitexco',
|
||||
transactionDate: DateTime(2023, 11, 25),
|
||||
invoiceAmount: 1250000,
|
||||
notes: 'Gạch porcelain 80x80 cho sảnh chính và khu mua sắm',
|
||||
attachments: const [
|
||||
'https://example.com/invoice3.jpg',
|
||||
],
|
||||
status: PointsStatus.pending,
|
||||
submittedAt: DateTime(2023, 11, 25, 9, 15),
|
||||
),
|
||||
PointsRecord(
|
||||
recordId: 'PRR003',
|
||||
userId: 'user123',
|
||||
invoiceNumber: 'INV-ABC-003',
|
||||
storeName: 'Công ty TNHH ABC Manufacturing',
|
||||
transactionDate: DateTime(2023, 11, 20),
|
||||
invoiceAmount: 4200000,
|
||||
notes: 'Gạch chống trơn cho khu vực sản xuất và kho bãi',
|
||||
attachments: const [
|
||||
'https://example.com/invoice4.jpg',
|
||||
'https://example.com/invoice5.jpg',
|
||||
],
|
||||
status: PointsStatus.rejected,
|
||||
rejectReason: 'Hình ảnh minh chứng không hợp lệ',
|
||||
submittedAt: DateTime(2023, 11, 20, 11, 0),
|
||||
processedAt: DateTime(2023, 11, 28, 16, 45),
|
||||
processedBy: 'admin002',
|
||||
),
|
||||
PointsRecord(
|
||||
recordId: 'PRR004',
|
||||
userId: 'user123',
|
||||
invoiceNumber: 'INV-ECO-004',
|
||||
storeName: 'Ecopark Group',
|
||||
transactionDate: DateTime(2023, 10, 10),
|
||||
invoiceAmount: 3700000,
|
||||
notes: 'Gạch ceramic vân gỗ cho khu vực phòng khách và sân vườn',
|
||||
attachments: const [
|
||||
'https://example.com/invoice6.jpg',
|
||||
],
|
||||
status: PointsStatus.approved,
|
||||
pointsEarned: 370,
|
||||
submittedAt: DateTime(2023, 10, 10, 8, 30),
|
||||
processedAt: DateTime(2023, 10, 15, 10, 20),
|
||||
processedBy: 'admin001',
|
||||
),
|
||||
PointsRecord(
|
||||
recordId: 'PRR005',
|
||||
userId: 'user123',
|
||||
invoiceNumber: 'INV-DMD-005',
|
||||
storeName: 'Diamond Hospitality Group',
|
||||
transactionDate: DateTime(2023, 12, 1),
|
||||
invoiceAmount: 8600000,
|
||||
notes: 'Gạch marble tự nhiên cho lobby và phòng suite',
|
||||
attachments: const [
|
||||
'https://example.com/invoice7.jpg',
|
||||
'https://example.com/invoice8.jpg',
|
||||
'https://example.com/invoice9.jpg',
|
||||
],
|
||||
status: PointsStatus.pending,
|
||||
submittedAt: DateTime(2023, 12, 1, 13, 0),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PointsRecord> getPointsRecordById(String recordId) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
final records = await getPointsRecords();
|
||||
return records.firstWhere(
|
||||
(record) => record.recordId == recordId,
|
||||
orElse: () => throw Exception('Points record not found'),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PointsRecord> submitPointsRecord(PointsRecord record) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 800));
|
||||
|
||||
// Simulate successful submission
|
||||
return record.copyWith(
|
||||
recordId: 'PRR${DateTime.now().millisecondsSinceEpoch}',
|
||||
status: PointsStatus.pending,
|
||||
submittedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/// Repository Implementation: Points Record
|
||||
///
|
||||
/// Implements points record repository interface.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/loyalty/data/datasources/points_record_remote_datasource.dart';
|
||||
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
|
||||
import 'package:worker/features/loyalty/domain/repositories/points_record_repository.dart';
|
||||
|
||||
/// Points Record Repository Implementation
|
||||
class PointsRecordRepositoryImpl implements PointsRecordRepository {
|
||||
const PointsRecordRepositoryImpl(this._remoteDataSource);
|
||||
|
||||
final PointsRecordRemoteDataSource _remoteDataSource;
|
||||
|
||||
@override
|
||||
Future<List<PointsRecord>> getPointsRecords() async {
|
||||
try {
|
||||
return await _remoteDataSource.getPointsRecords();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PointsRecord> getPointsRecordById(String recordId) async {
|
||||
try {
|
||||
return await _remoteDataSource.getPointsRecordById(recordId);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PointsRecord> submitPointsRecord(PointsRecord record) async {
|
||||
try {
|
||||
return await _remoteDataSource.submitPointsRecord(record);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,11 @@ enum PointsStatus {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case PointsStatus.pending:
|
||||
return 'Pending';
|
||||
return 'Chờ duyệt';
|
||||
case PointsStatus.approved:
|
||||
return 'Approved';
|
||||
return 'Đã duyệt';
|
||||
case PointsStatus.rejected:
|
||||
return 'Rejected';
|
||||
return 'Bị từ chối';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/// Repository Interface: Points Record
|
||||
///
|
||||
/// Defines contract for points record operations.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
|
||||
|
||||
/// Points Record Repository Interface
|
||||
abstract class PointsRecordRepository {
|
||||
/// Get all points records for current user
|
||||
Future<List<PointsRecord>> getPointsRecords();
|
||||
|
||||
/// Get single points record by ID
|
||||
Future<PointsRecord> getPointsRecordById(String recordId);
|
||||
|
||||
/// Submit new points record
|
||||
Future<PointsRecord> submitPointsRecord(PointsRecord record);
|
||||
}
|
||||
19
lib/features/loyalty/domain/usecases/get_points_records.dart
Normal file
19
lib/features/loyalty/domain/usecases/get_points_records.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
/// Use Case: Get Points Records
|
||||
///
|
||||
/// Retrieves all points records for the current user.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
|
||||
import 'package:worker/features/loyalty/domain/repositories/points_record_repository.dart';
|
||||
|
||||
/// Get Points Records Use Case
|
||||
class GetPointsRecords {
|
||||
const GetPointsRecords(this._repository);
|
||||
|
||||
final PointsRecordRepository _repository;
|
||||
|
||||
/// Execute use case
|
||||
Future<List<PointsRecord>> call() async {
|
||||
return await _repository.getPointsRecords();
|
||||
}
|
||||
}
|
||||
386
lib/features/loyalty/presentation/pages/points_records_page.dart
Normal file
386
lib/features/loyalty/presentation/pages/points_records_page.dart
Normal file
@@ -0,0 +1,386 @@
|
||||
/// Page: Points Records List
|
||||
///
|
||||
/// Displays list of user's points records with filters.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.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/theme/colors.dart';
|
||||
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
|
||||
import 'package:worker/features/loyalty/presentation/providers/points_records_provider.dart';
|
||||
|
||||
/// Points Records Page
|
||||
class PointsRecordsPage extends ConsumerWidget {
|
||||
const PointsRecordsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final recordsAsync = ref.watch(filteredPointsRecordsProvider);
|
||||
final filter = ref.watch(pointsRecordsFilterProvider);
|
||||
final selectedStatus = filter.selectedStatus;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.arrowLeft,
|
||||
color: Colors.black,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: const Text(
|
||||
'Danh sách Ghi nhận điểm',
|
||||
style: TextStyle(color: Colors.black),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.plus,
|
||||
color: Colors.black,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
// TODO: Navigate to points record create page
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Tính năng tạo ghi nhận điểm sẽ được cập nhật'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
],
|
||||
elevation: AppBarSpecs.elevation,
|
||||
backgroundColor: AppColors.white,
|
||||
centerTitle: false,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Search Bar
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Mã yêu cầu',
|
||||
prefixIcon: const Icon(Icons.search, color: AppColors.grey500),
|
||||
filled: true,
|
||||
fillColor: AppColors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100),
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
ref.read(pointsRecordsFilterProvider.notifier).updateSearchQuery(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Status Filter Tabs
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildFilterChip(
|
||||
context,
|
||||
ref,
|
||||
label: 'Tất cả',
|
||||
isSelected: selectedStatus == null,
|
||||
onTap: () =>
|
||||
ref.read(pointsRecordsFilterProvider.notifier).clearStatusFilter(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
...PointsStatus.values.map(
|
||||
(status) => Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: _buildFilterChip(
|
||||
context,
|
||||
ref,
|
||||
label: status.displayName,
|
||||
isSelected: selectedStatus == status,
|
||||
onTap: () =>
|
||||
ref.read(pointsRecordsFilterProvider.notifier).selectStatus(status),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Points Records List
|
||||
Expanded(
|
||||
child: recordsAsync.when(
|
||||
data: (records) {
|
||||
if (records.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.read(allPointsRecordsProvider.notifier).refresh();
|
||||
},
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FaIcon(
|
||||
FontAwesomeIcons.folderOpen,
|
||||
size: 64,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Không có ghi nhận điểm nào',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Không tìm thấy ghi nhận điểm phù hợp',
|
||||
style: TextStyle(color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.read(allPointsRecordsProvider.notifier).refresh();
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: records.length,
|
||||
itemBuilder: (context, index) {
|
||||
final record = records[index];
|
||||
return _buildRecordCard(context, record);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
error: (error, stack) => RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.read(allPointsRecordsProvider.notifier).refresh();
|
||||
},
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Có lỗi xảy ra',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
error.toString(),
|
||||
style: const TextStyle(color: AppColors.grey500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Kéo xuống để thử lại',
|
||||
style: TextStyle(color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChip(
|
||||
BuildContext context,
|
||||
WidgetRef ref, {
|
||||
required String label,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.primaryBlue : AppColors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.primaryBlue : AppColors.grey100,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? AppColors.white : AppColors.grey900,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordCard(BuildContext context, PointsRecord record) {
|
||||
final currencyFormat = NumberFormat.currency(
|
||||
locale: 'vi_VN',
|
||||
symbol: '₫',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// TODO: Navigate to points record detail
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Chi tiết ghi nhận ${record.recordId}')),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'#${record.recordId}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
_buildStatusBadge(record.status),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Ngày gửi: ${DateFormat('dd/MM/yyyy').format(record.submittedAt)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Giá trị đơn hàng: ${currencyFormat.format(record.invoiceAmount)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
if (record.rejectReason != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFEF2F2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.triangleExclamation,
|
||||
size: 14,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
record.rejectReason!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusBadge(PointsStatus status) {
|
||||
final color = _getStatusColor(status);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
status.displayName,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatusColor(PointsStatus status) {
|
||||
switch (status) {
|
||||
case PointsStatus.pending:
|
||||
return AppColors.warning;
|
||||
case PointsStatus.approved:
|
||||
return AppColors.success;
|
||||
case PointsStatus.rejected:
|
||||
return AppColors.danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/// Providers: Points Records
|
||||
///
|
||||
/// State management for points records feature.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/features/loyalty/data/datasources/points_record_remote_datasource.dart';
|
||||
import 'package:worker/features/loyalty/data/repositories/points_record_repository_impl.dart';
|
||||
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
|
||||
import 'package:worker/features/loyalty/domain/repositories/points_record_repository.dart';
|
||||
import 'package:worker/features/loyalty/domain/usecases/get_points_records.dart';
|
||||
|
||||
part 'points_records_provider.g.dart';
|
||||
|
||||
// ============================================================================
|
||||
// Data Layer Providers
|
||||
// ============================================================================
|
||||
|
||||
/// Points Record Remote Data Source Provider
|
||||
@riverpod
|
||||
PointsRecordRemoteDataSource pointsRecordRemoteDataSource(Ref ref) {
|
||||
return PointsRecordRemoteDataSourceImpl();
|
||||
}
|
||||
|
||||
/// Points Record Repository Provider
|
||||
@riverpod
|
||||
PointsRecordRepository pointsRecordRepository(Ref ref) {
|
||||
final remoteDataSource = ref.watch(pointsRecordRemoteDataSourceProvider);
|
||||
return PointsRecordRepositoryImpl(remoteDataSource);
|
||||
}
|
||||
|
||||
/// Get Points Records Use Case Provider
|
||||
@riverpod
|
||||
GetPointsRecords getPointsRecords(Ref ref) {
|
||||
final repository = ref.watch(pointsRecordRepositoryProvider);
|
||||
return GetPointsRecords(repository);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Presentation Layer Providers
|
||||
// ============================================================================
|
||||
|
||||
/// All Points Records Provider (AsyncNotifier)
|
||||
@riverpod
|
||||
class AllPointsRecords extends _$AllPointsRecords {
|
||||
@override
|
||||
Future<List<PointsRecord>> build() async {
|
||||
final useCase = ref.watch(getPointsRecordsProvider);
|
||||
return await useCase();
|
||||
}
|
||||
|
||||
/// Refresh points records
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final useCase = ref.read(getPointsRecordsProvider);
|
||||
return await useCase();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Points Records Filter State Provider
|
||||
@riverpod
|
||||
class PointsRecordsFilter extends _$PointsRecordsFilter {
|
||||
@override
|
||||
({String searchQuery, PointsStatus? selectedStatus}) build() {
|
||||
return (searchQuery: '', selectedStatus: null);
|
||||
}
|
||||
|
||||
/// Update search query
|
||||
void updateSearchQuery(String query) {
|
||||
state = (searchQuery: query, selectedStatus: state.selectedStatus);
|
||||
}
|
||||
|
||||
/// Select status filter
|
||||
void selectStatus(PointsStatus? status) {
|
||||
state = (searchQuery: state.searchQuery, selectedStatus: status);
|
||||
}
|
||||
|
||||
/// Clear status filter
|
||||
void clearStatusFilter() {
|
||||
state = (searchQuery: state.searchQuery, selectedStatus: null);
|
||||
}
|
||||
}
|
||||
|
||||
/// Filtered Points Records Provider
|
||||
@riverpod
|
||||
AsyncValue<List<PointsRecord>> filteredPointsRecords(Ref ref) {
|
||||
final dataAsync = ref.watch(allPointsRecordsProvider);
|
||||
final filter = ref.watch(pointsRecordsFilterProvider);
|
||||
|
||||
return dataAsync.whenData((records) {
|
||||
var filtered = records;
|
||||
|
||||
// Apply status filter
|
||||
if (filter.selectedStatus != null) {
|
||||
filtered = filtered
|
||||
.where((record) => record.status == filter.selectedStatus)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Apply search filter
|
||||
if (filter.searchQuery.isNotEmpty) {
|
||||
final query = filter.searchQuery.toLowerCase();
|
||||
filtered = filtered.where((record) {
|
||||
final idMatch = record.recordId.toLowerCase().contains(query);
|
||||
final invoiceMatch = record.invoiceNumber.toLowerCase().contains(query);
|
||||
final storeMatch = record.storeName.toLowerCase().contains(query);
|
||||
return idMatch || invoiceMatch || storeMatch;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Sort by submission date (newest first)
|
||||
filtered.sort((a, b) => b.submittedAt.compareTo(a.submittedAt));
|
||||
|
||||
return filtered;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'points_records_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Points Record Remote Data Source Provider
|
||||
|
||||
@ProviderFor(pointsRecordRemoteDataSource)
|
||||
const pointsRecordRemoteDataSourceProvider =
|
||||
PointsRecordRemoteDataSourceProvider._();
|
||||
|
||||
/// Points Record Remote Data Source Provider
|
||||
|
||||
final class PointsRecordRemoteDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
PointsRecordRemoteDataSource,
|
||||
PointsRecordRemoteDataSource,
|
||||
PointsRecordRemoteDataSource
|
||||
>
|
||||
with $Provider<PointsRecordRemoteDataSource> {
|
||||
/// Points Record Remote Data Source Provider
|
||||
const PointsRecordRemoteDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'pointsRecordRemoteDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$pointsRecordRemoteDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<PointsRecordRemoteDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
PointsRecordRemoteDataSource create(Ref ref) {
|
||||
return pointsRecordRemoteDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(PointsRecordRemoteDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<PointsRecordRemoteDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$pointsRecordRemoteDataSourceHash() =>
|
||||
r'b0a68a4bb8f05281afa84885a916611c131e172a';
|
||||
|
||||
/// Points Record Repository Provider
|
||||
|
||||
@ProviderFor(pointsRecordRepository)
|
||||
const pointsRecordRepositoryProvider = PointsRecordRepositoryProvider._();
|
||||
|
||||
/// Points Record Repository Provider
|
||||
|
||||
final class PointsRecordRepositoryProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
PointsRecordRepository,
|
||||
PointsRecordRepository,
|
||||
PointsRecordRepository
|
||||
>
|
||||
with $Provider<PointsRecordRepository> {
|
||||
/// Points Record Repository Provider
|
||||
const PointsRecordRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'pointsRecordRepositoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$pointsRecordRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<PointsRecordRepository> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
PointsRecordRepository create(Ref ref) {
|
||||
return pointsRecordRepository(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(PointsRecordRepository value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<PointsRecordRepository>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$pointsRecordRepositoryHash() =>
|
||||
r'892ad8ca77bd5c59dba2e285163ef5e016c0ca0d';
|
||||
|
||||
/// Get Points Records Use Case Provider
|
||||
|
||||
@ProviderFor(getPointsRecords)
|
||||
const getPointsRecordsProvider = GetPointsRecordsProvider._();
|
||||
|
||||
/// Get Points Records Use Case Provider
|
||||
|
||||
final class GetPointsRecordsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
GetPointsRecords,
|
||||
GetPointsRecords,
|
||||
GetPointsRecords
|
||||
>
|
||||
with $Provider<GetPointsRecords> {
|
||||
/// Get Points Records Use Case Provider
|
||||
const GetPointsRecordsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'getPointsRecordsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$getPointsRecordsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<GetPointsRecords> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
GetPointsRecords create(Ref ref) {
|
||||
return getPointsRecords(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(GetPointsRecords value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<GetPointsRecords>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$getPointsRecordsHash() => r'f7ea3c5c9675878967cc34b18416adb3a665ccf8';
|
||||
|
||||
/// All Points Records Provider (AsyncNotifier)
|
||||
|
||||
@ProviderFor(AllPointsRecords)
|
||||
const allPointsRecordsProvider = AllPointsRecordsProvider._();
|
||||
|
||||
/// All Points Records Provider (AsyncNotifier)
|
||||
final class AllPointsRecordsProvider
|
||||
extends $AsyncNotifierProvider<AllPointsRecords, List<PointsRecord>> {
|
||||
/// All Points Records Provider (AsyncNotifier)
|
||||
const AllPointsRecordsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'allPointsRecordsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$allPointsRecordsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
AllPointsRecords create() => AllPointsRecords();
|
||||
}
|
||||
|
||||
String _$allPointsRecordsHash() => r'cd64b6952f9abfe1142773b4b88a051b74e8d763';
|
||||
|
||||
/// All Points Records Provider (AsyncNotifier)
|
||||
|
||||
abstract class _$AllPointsRecords extends $AsyncNotifier<List<PointsRecord>> {
|
||||
FutureOr<List<PointsRecord>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref =
|
||||
this.ref as $Ref<AsyncValue<List<PointsRecord>>, List<PointsRecord>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<List<PointsRecord>>, List<PointsRecord>>,
|
||||
AsyncValue<List<PointsRecord>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Points Records Filter State Provider
|
||||
|
||||
@ProviderFor(PointsRecordsFilter)
|
||||
const pointsRecordsFilterProvider = PointsRecordsFilterProvider._();
|
||||
|
||||
/// Points Records Filter State Provider
|
||||
final class PointsRecordsFilterProvider
|
||||
extends
|
||||
$NotifierProvider<
|
||||
PointsRecordsFilter,
|
||||
({String searchQuery, PointsStatus? selectedStatus})
|
||||
> {
|
||||
/// Points Records Filter State Provider
|
||||
const PointsRecordsFilterProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'pointsRecordsFilterProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$pointsRecordsFilterHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
PointsRecordsFilter create() => PointsRecordsFilter();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(
|
||||
({String searchQuery, PointsStatus? selectedStatus}) value,
|
||||
) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride:
|
||||
$SyncValueProvider<
|
||||
({String searchQuery, PointsStatus? selectedStatus})
|
||||
>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$pointsRecordsFilterHash() =>
|
||||
r'ae81040dff096894330a2a744959190545435c48';
|
||||
|
||||
/// Points Records Filter State Provider
|
||||
|
||||
abstract class _$PointsRecordsFilter
|
||||
extends $Notifier<({String searchQuery, PointsStatus? selectedStatus})> {
|
||||
({String searchQuery, PointsStatus? selectedStatus}) build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<
|
||||
({String searchQuery, PointsStatus? selectedStatus}),
|
||||
({String searchQuery, PointsStatus? selectedStatus})
|
||||
>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
({String searchQuery, PointsStatus? selectedStatus}),
|
||||
({String searchQuery, PointsStatus? selectedStatus})
|
||||
>,
|
||||
({String searchQuery, PointsStatus? selectedStatus}),
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Filtered Points Records Provider
|
||||
|
||||
@ProviderFor(filteredPointsRecords)
|
||||
const filteredPointsRecordsProvider = FilteredPointsRecordsProvider._();
|
||||
|
||||
/// Filtered Points Records Provider
|
||||
|
||||
final class FilteredPointsRecordsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<PointsRecord>>,
|
||||
AsyncValue<List<PointsRecord>>,
|
||||
AsyncValue<List<PointsRecord>>
|
||||
>
|
||||
with $Provider<AsyncValue<List<PointsRecord>>> {
|
||||
/// Filtered Points Records Provider
|
||||
const FilteredPointsRecordsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'filteredPointsRecordsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$filteredPointsRecordsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<AsyncValue<List<PointsRecord>>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
AsyncValue<List<PointsRecord>> create(Ref ref) {
|
||||
return filteredPointsRecords(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(AsyncValue<List<PointsRecord>> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<AsyncValue<List<PointsRecord>>>(
|
||||
value,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$filteredPointsRecordsHash() =>
|
||||
r'afb0691b799f053b5c7fff2f8b64065917b5cd33';
|
||||
Reference in New Issue
Block a user